diff --git a/src/browser/components/AIView.tsx b/src/browser/components/AIView.tsx index f88c0ce5c4..69796a6ebe 100644 --- a/src/browser/components/AIView.tsx +++ b/src/browser/components/AIView.tsx @@ -14,10 +14,16 @@ import { EditCutoffBarrier } from "./Messages/ChatBarrier/EditCutoffBarrier"; import { StreamingBarrier } from "./Messages/ChatBarrier/StreamingBarrier"; import { RetryBarrier } from "./Messages/ChatBarrier/RetryBarrier"; import { PinnedTodoList } from "./PinnedTodoList"; -import { getAutoRetryKey, VIM_ENABLED_KEY } from "@/common/constants/storage"; +import { + getAutoRetryKey, + VIM_ENABLED_KEY, + RIGHT_SIDEBAR_TAB_KEY, + RIGHT_SIDEBAR_COSTS_WIDTH_KEY, + RIGHT_SIDEBAR_REVIEW_WIDTH_KEY, +} from "@/common/constants/storage"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; import { ChatInput, type ChatInputAPI } from "./ChatInput/index"; -import { RightSidebar } from "./RightSidebar"; +import { RightSidebar, type TabType } from "./RightSidebar"; import { useResizableSidebar } from "@/browser/hooks/useResizableSidebar"; import { shouldShowInterruptedBarrier, @@ -92,18 +98,33 @@ const AIViewInner: React.FC = ({ const { api } = useAPI(); const chatAreaRef = useRef(null); - // Resizable RightSidebar width (used for both Review + Costs to avoid tab-switch jank) - const { - width: sidebarWidth, - isResizing, - startResize, - } = useResizableSidebar({ - enabled: true, + // Track which right sidebar tab is selected (listener: true to sync with RightSidebar changes) + const [selectedRightTab] = usePersistedState(RIGHT_SIDEBAR_TAB_KEY, "costs", { + listener: true, + }); + + // Resizable RightSidebar width - separate hooks per tab for independent persistence + const costsSidebar = useResizableSidebar({ + enabled: selectedRightTab === "costs", defaultWidth: 300, minWidth: 300, maxWidth: 1200, - storageKey: "review-sidebar-width", + storageKey: RIGHT_SIDEBAR_COSTS_WIDTH_KEY, }); + const reviewSidebar = useResizableSidebar({ + enabled: selectedRightTab === "review", + defaultWidth: 600, + minWidth: 300, + maxWidth: 1200, + storageKey: RIGHT_SIDEBAR_REVIEW_WIDTH_KEY, + }); + + // Derive active sidebar props based on selected tab + const sidebarWidth = selectedRightTab === "review" ? reviewSidebar.width : costsSidebar.width; + const isResizing = + selectedRightTab === "review" ? reviewSidebar.isResizing : costsSidebar.isResizing; + const startResize = + selectedRightTab === "review" ? reviewSidebar.startResize : costsSidebar.startResize; const workspaceState = useWorkspaceState(workspaceId); const aggregator = useWorkspaceAggregator(workspaceId); diff --git a/src/browser/components/RightSidebar.tsx b/src/browser/components/RightSidebar.tsx index 4f09c08388..1d1535a284 100644 --- a/src/browser/components/RightSidebar.tsx +++ b/src/browser/components/RightSidebar.tsx @@ -24,8 +24,10 @@ export interface ReviewStats { interface SidebarContainerProps { collapsed: boolean; wide?: boolean; - /** Custom width from drag-resize (takes precedence over collapsed/wide) */ + /** Custom width from drag-resize (persisted per-tab by AIView) */ customWidth?: number; + /** Whether actively dragging resize handle (disables transition) */ + isResizing?: boolean; children: React.ReactNode; role: string; "aria-label": string; @@ -36,14 +38,15 @@ interface SidebarContainerProps { * * Width priority (first match wins): * 1. collapsed (20px) - Shows vertical token meter only - * 2. customWidth - From drag-resize on Review tab - * 3. wide - Auto-calculated max width for Review tab (when not resizing) - * 4. default (300px) - Costs/Tools tabs + * 2. customWidth - From drag-resize (persisted per-tab) + * 3. wide - Auto-calculated max width for Review tab (when not drag-resizing) + * 4. default (300px) - Costs tab when no customWidth saved */ const SidebarContainer: React.FC = ({ collapsed, wide, customWidth, + isResizing, children, role, "aria-label": ariaLabel, @@ -60,7 +63,7 @@ const SidebarContainer: React.FC = ({
; - /** Custom width in pixels (overrides default widths when Review tab is resizable) */ + /** Custom width in pixels (persisted per-tab, provided by AIView) */ width?: number; - /** Drag start handler for resize (Review tab only) */ + /** Drag start handler for resize */ onStartResize?: (e: React.MouseEvent) => void; /** Whether currently resizing */ isResizing?: boolean; @@ -243,7 +246,8 @@ const RightSidebarComponent: React.FC = ({ diff --git a/src/browser/stories/App.rightsidebar.stories.tsx b/src/browser/stories/App.rightsidebar.stories.tsx index 7e3962a8d7..836d89155a 100644 --- a/src/browser/stories/App.rightsidebar.stories.tsx +++ b/src/browser/stories/App.rightsidebar.stories.tsx @@ -8,7 +8,11 @@ import { appMeta, AppWithMocks, type AppStory } from "./meta.js"; import { setupSimpleChatStory } from "./storyHelpers"; import { createUserMessage, createAssistantMessage } from "./mockFactory"; import { within, userEvent, waitFor } from "@storybook/test"; -import { RIGHT_SIDEBAR_TAB_KEY } from "@/common/constants/storage"; +import { + RIGHT_SIDEBAR_TAB_KEY, + RIGHT_SIDEBAR_COSTS_WIDTH_KEY, + RIGHT_SIDEBAR_REVIEW_WIDTH_KEY, +} from "@/common/constants/storage"; import type { ComponentType } from "react"; import type { MockSessionUsage } from "../../../.storybook/mocks/orpc"; @@ -65,6 +69,9 @@ export const CostsTab: AppStory = { { localStorage.setItem(RIGHT_SIDEBAR_TAB_KEY, JSON.stringify("costs")); + // Set per-tab widths: costs at 350px, review at 700px + localStorage.setItem(RIGHT_SIDEBAR_COSTS_WIDTH_KEY, "350"); + localStorage.setItem(RIGHT_SIDEBAR_REVIEW_WIDTH_KEY, "700"); return setupSimpleChatStory({ workspaceId: "ws-costs", @@ -96,12 +103,16 @@ export const CostsTab: AppStory = { /** * Review tab selected - click switches from Costs to Review tab + * Verifies per-tab width persistence: starts at Costs width (350px), switches to Review width (700px) */ export const ReviewTab: AppStory = { render: () => ( { localStorage.setItem(RIGHT_SIDEBAR_TAB_KEY, JSON.stringify("costs")); + // Set distinct widths per tab to verify switching behavior + localStorage.setItem(RIGHT_SIDEBAR_COSTS_WIDTH_KEY, "350"); + localStorage.setItem(RIGHT_SIDEBAR_REVIEW_WIDTH_KEY, "700"); return setupSimpleChatStory({ workspaceId: "ws-review", diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index 08eb2e9689..222a7bb841 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -239,6 +239,19 @@ export const RIGHT_SIDEBAR_TAB_KEY = "right-sidebar-tab"; */ export const RIGHT_SIDEBAR_COLLAPSED_KEY = "right-sidebar:collapsed"; +/** + * Right sidebar width for Costs tab (global) + * Format: "right-sidebar:width:costs" + */ +export const RIGHT_SIDEBAR_COSTS_WIDTH_KEY = "right-sidebar:width:costs"; + +/** + * Right sidebar width for Review tab (global) + * Reuses legacy key to preserve existing user preferences + * Format: "review-sidebar-width" + */ +export const RIGHT_SIDEBAR_REVIEW_WIDTH_KEY = "review-sidebar-width"; + /** * Get the localStorage key for unified Review search state per workspace * Stores: { input: string, useRegex: boolean, matchCase: boolean }