From c58f0700cd86de3aff0bd748f1938b4c8f39a997 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:48:30 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20fix:=20avoid=20right=20sideb?= =?UTF-8?q?ar=20width=20jank=20on=20tab=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notify AIView of tab changes synchronously (click/keybind) so Review width doesn't momentarily compute to 0px. --- _Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh`_ --- src/browser/components/RightSidebar.tsx | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/browser/components/RightSidebar.tsx b/src/browser/components/RightSidebar.tsx index 25d39c2f59..2498120814 100644 --- a/src/browser/components/RightSidebar.tsx +++ b/src/browser/components/RightSidebar.tsx @@ -117,19 +117,19 @@ const RightSidebarComponent: React.FC = ({ // Review stats reported by ReviewPanel const [reviewStats, setReviewStats] = React.useState(null); - // Notify parent (AIView) of tab changes so it can enable/disable resize functionality - React.useEffect(() => { - onTabChange?.(selectedTab); - }, [selectedTab, onTabChange]); + // NOTE: We intentionally avoid mirroring `selectedTab` into AIView via `useEffect`. + // AIView needs tab changes in the same render to avoid sidebar width jank. // Keyboard shortcuts for tab switching React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (matchesKeybind(e, KEYBINDS.COSTS_TAB)) { e.preventDefault(); + onTabChange?.("costs"); setSelectedTab("costs"); } else if (matchesKeybind(e, KEYBINDS.REVIEW_TAB)) { e.preventDefault(); + onTabChange?.("review"); setSelectedTab("review"); setFocusTrigger((prev) => prev + 1); } @@ -137,7 +137,7 @@ const RightSidebarComponent: React.FC = ({ window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [setSelectedTab, selectedTab]); + }, [onTabChange, setSelectedTab]); const usage = useWorkspaceUsage(workspaceId); const { options } = useProviderOptions(); @@ -276,7 +276,10 @@ const RightSidebarComponent: React.FC = ({ ? "bg-hover text-foreground" : "bg-transparent text-muted hover:bg-hover/50 hover:text-foreground" )} - onClick={() => setSelectedTab("costs")} + onClick={() => { + onTabChange?.("costs"); + setSelectedTab("costs"); + }} id={costsTabId} role="tab" type="button" @@ -304,7 +307,10 @@ const RightSidebarComponent: React.FC = ({ ? "bg-hover text-foreground" : "bg-transparent text-muted hover:bg-hover/50 hover:text-foreground" )} - onClick={() => setSelectedTab("review")} + onClick={() => { + onTabChange?.("review"); + setSelectedTab("review"); + }} id={reviewTabId} role="tab" type="button" From a031169c62ee4c37e1e4a32ddfc9943c6d5f770a Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:12:16 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A4=96=20fix:=20keep=20right=20sideba?= =?UTF-8?q?r=20width=20stable=20across=20tabs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop changing RightSidebar width when switching Costs ↔ Review by using a single persisted width for the container. --- _Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh`_ --- src/browser/components/AIView.tsx | 44 ++++++++----------------- src/browser/components/RightSidebar.tsx | 20 ++--------- 2 files changed, 17 insertions(+), 47 deletions(-) diff --git a/src/browser/components/AIView.tsx b/src/browser/components/AIView.tsx index fe57cecf59..d00d631421 100644 --- a/src/browser/components/AIView.tsx +++ b/src/browser/components/AIView.tsx @@ -14,14 +14,10 @@ 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, - RIGHT_SIDEBAR_TAB_KEY, -} from "@/common/constants/storage"; +import { getAutoRetryKey, VIM_ENABLED_KEY } from "@/common/constants/storage"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; import { ChatInput, type ChatInputAPI } from "./ChatInput/index"; -import { RightSidebar, type TabType } from "./RightSidebar"; +import { RightSidebar } from "./RightSidebar"; import { useResizableSidebar } from "@/browser/hooks/useResizableSidebar"; import { shouldShowInterruptedBarrier, @@ -38,7 +34,7 @@ import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds"; import { useAutoScroll } from "@/browser/hooks/useAutoScroll"; import { useOpenTerminal } from "@/browser/hooks/useOpenTerminal"; import { useOpenInEditor } from "@/browser/hooks/useOpenInEditor"; -import { readPersistedState, usePersistedState } from "@/browser/hooks/usePersistedState"; +import { usePersistedState } from "@/browser/hooks/usePersistedState"; import { useThinking } from "@/browser/contexts/ThinkingContext"; import { useWorkspaceState, @@ -95,28 +91,17 @@ const AIViewInner: React.FC = ({ const { api } = useAPI(); const chatAreaRef = useRef(null); - // Track active tab to conditionally enable resize functionality - // Initialize from persisted value to avoid layout flash; RightSidebar owns the state - // and notifies us of changes via onTabChange callback - const [activeTab, setActiveTab] = useState(() => - readPersistedState(RIGHT_SIDEBAR_TAB_KEY, "costs") - ); - - const isReviewTabActive = activeTab === "review"; - - // Resizable sidebar for Review tab only - // Hook encapsulates all drag logic, persistence, and constraints - // Returns width to apply to RightSidebar and startResize for handle's onMouseDown + // Resizable RightSidebar width (used for both Review + Costs to avoid tab-switch jank) const { width: sidebarWidth, isResizing, startResize, } = useResizableSidebar({ - enabled: isReviewTabActive, // Only active on Review tab - defaultWidth: 600, // Initial width or fallback - minWidth: 300, // Can't shrink smaller - maxWidth: 1200, // Can't grow larger - storageKey: "review-sidebar-width", // Persists across sessions + enabled: true, + defaultWidth: 300, + minWidth: 300, + maxWidth: 1200, + storageKey: "review-sidebar-width", }); const workspaceState = useWorkspaceState(workspaceId); @@ -775,12 +760,11 @@ const AIViewInner: React.FC = ({ workspaceId={workspaceId} workspacePath={namedWorkspacePath} chatAreaRef={chatAreaRef} - onTabChange={setActiveTab} // Notifies us when tab changes - width={isReviewTabActive ? sidebarWidth : undefined} // Custom width only on Review tab - onStartResize={isReviewTabActive ? startResize : undefined} // Pass resize handler when Review active - isResizing={isResizing} // Pass resizing state - onReviewNote={handleReviewNote} // Pass review note handler to append to chat - isCreating={status === "creating"} // Workspace still being set up + width={sidebarWidth} + onStartResize={startResize} + isResizing={isResizing} + onReviewNote={handleReviewNote} + isCreating={status === "creating"} /> ; - /** Callback fired when tab selection changes (used for resize logic in AIView) */ - onTabChange?: (tab: TabType) => void; /** Custom width in pixels (overrides default widths when Review tab is resizable) */ width?: number; /** Drag start handler for resize (Review tab only) */ @@ -101,7 +99,6 @@ const RightSidebarComponent: React.FC = ({ workspaceId, workspacePath, chatAreaRef, - onTabChange, width, onStartResize, isResizing = false, @@ -117,19 +114,14 @@ const RightSidebarComponent: React.FC = ({ // Review stats reported by ReviewPanel const [reviewStats, setReviewStats] = React.useState(null); - // NOTE: We intentionally avoid mirroring `selectedTab` into AIView via `useEffect`. - // AIView needs tab changes in the same render to avoid sidebar width jank. - // Keyboard shortcuts for tab switching React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (matchesKeybind(e, KEYBINDS.COSTS_TAB)) { e.preventDefault(); - onTabChange?.("costs"); setSelectedTab("costs"); } else if (matchesKeybind(e, KEYBINDS.REVIEW_TAB)) { e.preventDefault(); - onTabChange?.("review"); setSelectedTab("review"); setFocusTrigger((prev) => prev + 1); } @@ -137,7 +129,7 @@ const RightSidebarComponent: React.FC = ({ window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [onTabChange, setSelectedTab]); + }, [setSelectedTab]); const usage = useWorkspaceUsage(workspaceId); const { options } = useProviderOptions(); @@ -276,10 +268,7 @@ const RightSidebarComponent: React.FC = ({ ? "bg-hover text-foreground" : "bg-transparent text-muted hover:bg-hover/50 hover:text-foreground" )} - onClick={() => { - onTabChange?.("costs"); - setSelectedTab("costs"); - }} + onClick={() => setSelectedTab("costs")} id={costsTabId} role="tab" type="button" @@ -307,10 +296,7 @@ const RightSidebarComponent: React.FC = ({ ? "bg-hover text-foreground" : "bg-transparent text-muted hover:bg-hover/50 hover:text-foreground" )} - onClick={() => { - onTabChange?.("review"); - setSelectedTab("review"); - }} + onClick={() => setSelectedTab("review")} id={reviewTabId} role="tab" type="button" From f3e295269aedf20d4f712ca1ea22a49354a91621 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:31:55 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A4=96=20fix:=20stop=20RightSidebar?= =?UTF-8?q?=20collapse=20flashing=20with=20custom=20width?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid auto-collapse oscillation on Costs/Context when the sidebar is wider than the default width. --- _Generated with `mux` • Model: `openai:gpt-5.2` • Thinking: `xhigh`_ --- src/browser/components/RightSidebar.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/browser/components/RightSidebar.tsx b/src/browser/components/RightSidebar.tsx index 525b47c637..ae102e3460 100644 --- a/src/browser/components/RightSidebar.tsx +++ b/src/browser/components/RightSidebar.tsx @@ -204,6 +204,18 @@ const RightSidebarComponent: React.FC = ({ return; } + // If the sidebar is custom-resized (wider than the default Costs width), + // auto-collapse based on chatAreaWidth can oscillate between expanded and + // collapsed states (because collapsed is 20px but expanded can be much wider), + // which looks like a constant flash. In that case, keep it expanded and let + // the user resize manually. + if (width !== undefined && width > 300) { + if (showCollapsed) { + setShowCollapsed(false); + } + return; + } + // Normal hysteresis for Costs/Tools tabs if (chatAreaWidth <= COLLAPSE_THRESHOLD) { setShowCollapsed(true); @@ -211,7 +223,7 @@ const RightSidebarComponent: React.FC = ({ setShowCollapsed(false); } // Between thresholds: maintain current state (no change) - }, [chatAreaWidth, selectedTab, showCollapsed, setShowCollapsed]); + }, [chatAreaWidth, selectedTab, showCollapsed, setShowCollapsed, width]); // Single render point for VerticalTokenMeter // Shows when: (1) collapsed, OR (2) Review tab is active