Skip to content

Commit 42385f0

Browse files
authored
πŸ€– fix: avoid right sidebar width jank on tab switch (#1172)
Keep RightSidebar width stable when switching Costs ↔ Review (no collapse-to-0 flash). - Use a single persisted container width across both tabs. - Remove AIView ↔ RightSidebar tab-sync plumbing; switching tabs now only swaps content. - Avoid Costs/Context auto-collapse oscillation when the sidebar is wider than the default width. Validation: - make static-check --- _Generated with `mux` β€’ Model: `openai:gpt-5.2` β€’ Thinking: `xhigh`_
1 parent a92f3e8 commit 42385f0

File tree

2 files changed

+28
-40
lines changed

2 files changed

+28
-40
lines changed

β€Žsrc/browser/components/AIView.tsxβ€Ž

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,10 @@ import { EditCutoffBarrier } from "./Messages/ChatBarrier/EditCutoffBarrier";
1414
import { StreamingBarrier } from "./Messages/ChatBarrier/StreamingBarrier";
1515
import { RetryBarrier } from "./Messages/ChatBarrier/RetryBarrier";
1616
import { PinnedTodoList } from "./PinnedTodoList";
17-
import {
18-
getAutoRetryKey,
19-
VIM_ENABLED_KEY,
20-
RIGHT_SIDEBAR_TAB_KEY,
21-
} from "@/common/constants/storage";
17+
import { getAutoRetryKey, VIM_ENABLED_KEY } from "@/common/constants/storage";
2218
import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults";
2319
import { ChatInput, type ChatInputAPI } from "./ChatInput/index";
24-
import { RightSidebar, type TabType } from "./RightSidebar";
20+
import { RightSidebar } from "./RightSidebar";
2521
import { useResizableSidebar } from "@/browser/hooks/useResizableSidebar";
2622
import {
2723
shouldShowInterruptedBarrier,
@@ -38,7 +34,7 @@ import { formatKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
3834
import { useAutoScroll } from "@/browser/hooks/useAutoScroll";
3935
import { useOpenTerminal } from "@/browser/hooks/useOpenTerminal";
4036
import { useOpenInEditor } from "@/browser/hooks/useOpenInEditor";
41-
import { readPersistedState, usePersistedState } from "@/browser/hooks/usePersistedState";
37+
import { usePersistedState } from "@/browser/hooks/usePersistedState";
4238
import { useThinking } from "@/browser/contexts/ThinkingContext";
4339
import {
4440
useWorkspaceState,
@@ -95,28 +91,17 @@ const AIViewInner: React.FC<AIViewProps> = ({
9591
const { api } = useAPI();
9692
const chatAreaRef = useRef<HTMLDivElement>(null);
9793

98-
// Track active tab to conditionally enable resize functionality
99-
// Initialize from persisted value to avoid layout flash; RightSidebar owns the state
100-
// and notifies us of changes via onTabChange callback
101-
const [activeTab, setActiveTab] = useState<TabType>(() =>
102-
readPersistedState<TabType>(RIGHT_SIDEBAR_TAB_KEY, "costs")
103-
);
104-
105-
const isReviewTabActive = activeTab === "review";
106-
107-
// Resizable sidebar for Review tab only
108-
// Hook encapsulates all drag logic, persistence, and constraints
109-
// Returns width to apply to RightSidebar and startResize for handle's onMouseDown
94+
// Resizable RightSidebar width (used for both Review + Costs to avoid tab-switch jank)
11095
const {
11196
width: sidebarWidth,
11297
isResizing,
11398
startResize,
11499
} = useResizableSidebar({
115-
enabled: isReviewTabActive, // Only active on Review tab
116-
defaultWidth: 600, // Initial width or fallback
117-
minWidth: 300, // Can't shrink smaller
118-
maxWidth: 1200, // Can't grow larger
119-
storageKey: "review-sidebar-width", // Persists across sessions
100+
enabled: true,
101+
defaultWidth: 300,
102+
minWidth: 300,
103+
maxWidth: 1200,
104+
storageKey: "review-sidebar-width",
120105
});
121106

122107
const workspaceState = useWorkspaceState(workspaceId);
@@ -775,12 +760,11 @@ const AIViewInner: React.FC<AIViewProps> = ({
775760
workspaceId={workspaceId}
776761
workspacePath={namedWorkspacePath}
777762
chatAreaRef={chatAreaRef}
778-
onTabChange={setActiveTab} // Notifies us when tab changes
779-
width={isReviewTabActive ? sidebarWidth : undefined} // Custom width only on Review tab
780-
onStartResize={isReviewTabActive ? startResize : undefined} // Pass resize handler when Review active
781-
isResizing={isResizing} // Pass resizing state
782-
onReviewNote={handleReviewNote} // Pass review note handler to append to chat
783-
isCreating={status === "creating"} // Workspace still being set up
763+
width={sidebarWidth}
764+
onStartResize={startResize}
765+
isResizing={isResizing}
766+
onReviewNote={handleReviewNote}
767+
isCreating={status === "creating"}
784768
/>
785769

786770
<PopoverError

β€Žsrc/browser/components/RightSidebar.tsxβ€Ž

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,6 @@ interface RightSidebarProps {
8383
workspaceId: string;
8484
workspacePath: string;
8585
chatAreaRef: React.RefObject<HTMLDivElement>;
86-
/** Callback fired when tab selection changes (used for resize logic in AIView) */
87-
onTabChange?: (tab: TabType) => void;
8886
/** Custom width in pixels (overrides default widths when Review tab is resizable) */
8987
width?: number;
9088
/** Drag start handler for resize (Review tab only) */
@@ -101,7 +99,6 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
10199
workspaceId,
102100
workspacePath,
103101
chatAreaRef,
104-
onTabChange,
105102
width,
106103
onStartResize,
107104
isResizing = false,
@@ -117,11 +114,6 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
117114
// Review stats reported by ReviewPanel
118115
const [reviewStats, setReviewStats] = React.useState<ReviewStats | null>(null);
119116

120-
// Notify parent (AIView) of tab changes so it can enable/disable resize functionality
121-
React.useEffect(() => {
122-
onTabChange?.(selectedTab);
123-
}, [selectedTab, onTabChange]);
124-
125117
// Keyboard shortcuts for tab switching
126118
React.useEffect(() => {
127119
const handleKeyDown = (e: KeyboardEvent) => {
@@ -137,7 +129,7 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
137129

138130
window.addEventListener("keydown", handleKeyDown);
139131
return () => window.removeEventListener("keydown", handleKeyDown);
140-
}, [setSelectedTab, selectedTab]);
132+
}, [setSelectedTab]);
141133

142134
const usage = useWorkspaceUsage(workspaceId);
143135
const { options } = useProviderOptions();
@@ -212,14 +204,26 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
212204
return;
213205
}
214206

207+
// If the sidebar is custom-resized (wider than the default Costs width),
208+
// auto-collapse based on chatAreaWidth can oscillate between expanded and
209+
// collapsed states (because collapsed is 20px but expanded can be much wider),
210+
// which looks like a constant flash. In that case, keep it expanded and let
211+
// the user resize manually.
212+
if (width !== undefined && width > 300) {
213+
if (showCollapsed) {
214+
setShowCollapsed(false);
215+
}
216+
return;
217+
}
218+
215219
// Normal hysteresis for Costs/Tools tabs
216220
if (chatAreaWidth <= COLLAPSE_THRESHOLD) {
217221
setShowCollapsed(true);
218222
} else if (chatAreaWidth >= EXPAND_THRESHOLD) {
219223
setShowCollapsed(false);
220224
}
221225
// Between thresholds: maintain current state (no change)
222-
}, [chatAreaWidth, selectedTab, showCollapsed, setShowCollapsed]);
226+
}, [chatAreaWidth, selectedTab, showCollapsed, setShowCollapsed, width]);
223227

224228
// Single render point for VerticalTokenMeter
225229
// Shows when: (1) collapsed, OR (2) Review tab is active

0 commit comments

Comments
Β (0)