Skip to content

Commit cdf522c

Browse files
author
Test
committed
🤖 fix: maintain manual open state for mobile review panel
Prevent the mobile review panel from immediately auto-collapsing after the user taps the new header button: - Track manual expansions to disable hysteresis auto-collapse until the user dismisses the panel - Wire the mobile header button through AIView -> RightSidebar to open in manual mode - Reuse the same close helper across overlay, close button, and swipe gestures - Remove the bottom FAB entirely in favor of the header entrypoint This fixes the flicker where the panel opened and instantly closed on mobile devices. _Generated with `cmux`_ Change-Id: Id6fbe1a850808355c16a88c76bd43958c522f0dd Signed-off-by: Test <test@example.com>
1 parent 8a86163 commit cdf522c

File tree

1 file changed

+103
-71
lines changed

1 file changed

+103
-71
lines changed

src/components/RightSidebar.tsx

Lines changed: 103 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -167,35 +167,63 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
167167
false
168168
);
169169

170+
// Single render point for VerticalTokenMeter
171+
// Shows when: (1) collapsed, OR (2) Review tab is active
172+
const showMeter = showCollapsed || selectedTab === "review";
173+
const verticalMeter = showMeter ? <VerticalTokenMeter data={verticalMeterData} /> : null;
174+
175+
// Track manual expansion to prevent auto-collapse immediately after user opens sidebar
176+
const manualExpandRef = React.useRef(false);
177+
178+
const openSidebar = React.useCallback(
179+
(manual: boolean) => {
180+
manualExpandRef.current = manual;
181+
setShowCollapsed(false);
182+
},
183+
[setShowCollapsed]
184+
);
185+
186+
const closeSidebar = React.useCallback(() => {
187+
manualExpandRef.current = false;
188+
setShowCollapsed(true);
189+
}, [setShowCollapsed]);
190+
191+
// Expose open function to parent (for mobile header button)
192+
React.useEffect(() => {
193+
if (onMountOpenCallback) {
194+
onMountOpenCallback(() => openSidebar(true));
195+
}
196+
}, [onMountOpenCallback, openSidebar]);
197+
198+
const openSidebarAuto = React.useCallback(() => openSidebar(false), [openSidebar]);
199+
const openSidebarManual = React.useCallback(() => openSidebar(true), [openSidebar]);
200+
170201
React.useEffect(() => {
171202
// Never collapse when Review tab is active - code review needs space
172203
if (selectedTab === "review") {
173204
if (showCollapsed) {
174-
setShowCollapsed(false);
205+
openSidebarAuto();
175206
}
207+
manualExpandRef.current = false;
208+
return;
209+
}
210+
211+
// If user manually expanded on mobile, keep sidebar open until they close it
212+
if (manualExpandRef.current) {
176213
return;
177214
}
178215

179-
// Normal hysteresis for Costs/Tools tabs
180216
if (chatAreaWidth <= COLLAPSE_THRESHOLD) {
181-
setShowCollapsed(true);
217+
if (!showCollapsed) {
218+
closeSidebar();
219+
}
182220
} else if (chatAreaWidth >= EXPAND_THRESHOLD) {
183-
setShowCollapsed(false);
221+
if (showCollapsed) {
222+
openSidebarAuto();
223+
}
184224
}
185225
// Between thresholds: maintain current state (no change)
186-
}, [chatAreaWidth, selectedTab, showCollapsed, setShowCollapsed]);
187-
188-
// Single render point for VerticalTokenMeter
189-
// Shows when: (1) collapsed, OR (2) Review tab is active
190-
const showMeter = showCollapsed || selectedTab === "review";
191-
const verticalMeter = showMeter ? <VerticalTokenMeter data={verticalMeterData} /> : null;
192-
193-
// Expose open function to parent (for mobile header button)
194-
React.useEffect(() => {
195-
if (onMountOpenCallback) {
196-
onMountOpenCallback(() => setShowCollapsed(false));
197-
}
198-
}, [onMountOpenCallback, setShowCollapsed]);
226+
}, [chatAreaWidth, selectedTab, showCollapsed, closeSidebar, openSidebarAuto]);
199227

200228
// Swipe gesture detection for mobile - right-to-left swipe to open sidebar
201229
React.useEffect(() => {
@@ -243,11 +271,11 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
243271

244272
// Open sidebar on left swipe from right edge when collapsed
245273
if (isLeftSwipe && isHorizontal && isFastEnough && showCollapsed) {
246-
setShowCollapsed(false);
274+
openSidebarManual();
247275
}
248276
// Close sidebar on right swipe when open (from anywhere on screen)
249277
else if (isRightSwipe && isHorizontal && isFastEnough && !showCollapsed) {
250-
setShowCollapsed(true);
278+
closeSidebar();
251279
}
252280
};
253281

@@ -258,15 +286,15 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
258286
window.removeEventListener("touchstart", handleTouchStart);
259287
window.removeEventListener("touchend", handleTouchEnd);
260288
};
261-
}, [showCollapsed, setShowCollapsed]);
289+
}, [showCollapsed, closeSidebar, openSidebarManual]);
262290

263291
return (
264292
<>
265293
{/* Backdrop overlay - only on mobile when sidebar is expanded */}
266294
{!showCollapsed && (
267295
<div
268296
className="fixed inset-0 z-[998] hidden bg-black/50 backdrop-blur-sm max-md:block"
269-
onClick={() => setShowCollapsed(true)}
297+
onClick={closeSidebar}
270298
aria-hidden="true"
271299
/>
272300
)}
@@ -301,68 +329,72 @@ const RightSidebarComponent: React.FC<RightSidebarProps> = ({
301329

302330
<div className="flex min-w-0 flex-1 flex-col">
303331
<div
304-
className="bg-background-secondary border-border relative flex border-b [&>*]:flex-1"
332+
className="bg-background-secondary border-border flex items-center border-b px-2 py-2 max-md:gap-2"
305333
role="tablist"
306334
aria-label="Metadata views"
307335
>
308336
{/* Close button - only visible on mobile */}
309337
<button
310-
onClick={() => setShowCollapsed(true)}
338+
onClick={closeSidebar}
311339
title="Close panel"
312340
aria-label="Close panel"
313341
className={cn(
314-
"hidden max-md:flex absolute top-2 right-2 z-10",
315-
"w-8 h-8 bg-separator border border-border-light rounded cursor-pointer",
316-
"items-center justify-center text-foreground transition-all duration-200",
317-
"hover:bg-hover hover:border-bg-light",
318-
"active:scale-95"
342+
"hidden max-md:inline-flex h-7 w-7 items-center justify-center rounded-md",
343+
"border border-border-light/80 bg-separator/90 text-xs font-semibold text-foreground",
344+
"shadow-[0_1px_2px_rgba(0,0,0,0.2)] transition-all duration-200",
345+
"hover:bg-hover hover:border-bg-light active:scale-95"
319346
)}
320347
>
321-
348+
×
322349
</button>
323350

324-
<TooltipWrapper inline>
325-
<button
326-
className={cn(
327-
"w-full py-2.5 px-[15px] border-none border-solid cursor-pointer font-primary text-[13px] font-medium transition-all duration-200",
328-
selectedTab === "costs"
329-
? "text-white bg-separator border-b-2 border-b-plan-mode"
330-
: "bg-transparent text-secondary border-b-2 border-b-transparent hover:bg-background-secondary hover:text-foreground"
331-
)}
332-
onClick={() => setSelectedTab("costs")}
333-
id={costsTabId}
334-
role="tab"
335-
type="button"
336-
aria-selected={selectedTab === "costs"}
337-
aria-controls={costsPanelId}
338-
>
339-
Costs
340-
</button>
341-
<Tooltip className="tooltip" position="bottom" align="center">
342-
{formatKeybind(KEYBINDS.COSTS_TAB)}
343-
</Tooltip>
344-
</TooltipWrapper>
345-
<TooltipWrapper inline>
346-
<button
347-
className={cn(
348-
"w-full py-2.5 px-[15px] border-none border-solid cursor-pointer font-primary text-[13px] font-medium transition-all duration-200",
349-
selectedTab === "review"
350-
? "text-white bg-separator border-b-2 border-b-plan-mode"
351-
: "bg-transparent text-secondary border-b-2 border-b-transparent hover:bg-background-secondary hover:text-foreground"
352-
)}
353-
onClick={() => setSelectedTab("review")}
354-
id={reviewTabId}
355-
role="tab"
356-
type="button"
357-
aria-selected={selectedTab === "review"}
358-
aria-controls={reviewPanelId}
359-
>
360-
Review
361-
</button>
362-
<Tooltip className="tooltip" position="bottom" align="center">
363-
{formatKeybind(KEYBINDS.REVIEW_TAB)}
364-
</Tooltip>
365-
</TooltipWrapper>
351+
<div className="flex flex-1 gap-1 [&>*]:flex-1">
352+
<TooltipWrapper inline>
353+
<button
354+
className={cn(
355+
"w-full py-2 px-[15px] border-none border-solid cursor-pointer font-primary text-[13px] font-medium transition-all duration-200",
356+
selectedTab === "costs"
357+
? "text-white bg-separator border-b-2 border-b-plan-mode"
358+
: "bg-transparent text-secondary border-b-2 border-b-transparent hover:bg-background-secondary hover:text-foreground"
359+
)}
360+
onClick={() => setSelectedTab("costs")}
361+
id={costsTabId}
362+
role="tab"
363+
type="button"
364+
aria-selected={selectedTab === "costs"}
365+
aria-controls={costsPanelId}
366+
>
367+
Costs
368+
</button>
369+
<Tooltip className="tooltip" position="bottom" align="center">
370+
{formatKeybind(KEYBINDS.COSTS_TAB)}
371+
</Tooltip>
372+
</TooltipWrapper>
373+
<TooltipWrapper inline>
374+
<button
375+
className={cn(
376+
"w-full py-2 px-[15px] border-none border-solid cursor-pointer font-primary text-[13px] font-medium transition-all duration-200",
377+
selectedTab === "review"
378+
? "text-white bg-separator border-b-2 border-b-plan-mode"
379+
: "bg-transparent text-secondary border-b-2 border-b-transparent hover:bg-background-secondary hover:text-foreground"
380+
)}
381+
onClick={() => {
382+
setSelectedTab("review");
383+
setFocusTrigger((prev) => prev + 1);
384+
}}
385+
id={reviewTabId}
386+
role="tab"
387+
type="button"
388+
aria-selected={selectedTab === "review"}
389+
aria-controls={reviewPanelId}
390+
>
391+
Review
392+
</button>
393+
<Tooltip className="tooltip" position="bottom" align="center">
394+
{formatKeybind(KEYBINDS.REVIEW_TAB)}
395+
</Tooltip>
396+
</TooltipWrapper>
397+
</div>
366398
</div>
367399
<div
368400
className={cn(

0 commit comments

Comments
 (0)