From 71f6f1db89a50f0108663ebf1724cf3a1a4134ed Mon Sep 17 00:00:00 2001 From: James Villarrubia <8172873+jamesvillarrubia@users.noreply.github.com> Date: Sat, 10 Oct 2026 07:37:00 -0400 Subject: [PATCH] fix(web): a maximized right panel stays maximized when you return to its thread (#17327) Co-authored-by: Claude Opus 5.5 --- apps/web/src/components/ChatView.tsx | 16 +++++---------- apps/web/src/rightPanelStore.test.ts | 29 ++++++++++++++++++++++++++++ apps/web/src/rightPanelStore.ts | 13 +++++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 2aeca85e6aa298a387f0665b7823e4ebad19533a..2620f363adc8a033feb2ac2096e4f9d582c948a2 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1914,9 +1914,6 @@ export default function ChatView(props: ChatViewProps) { const isRevertingCheckpoint = useComposerDraftStore((store) => store.rewindingThreadKeys.has(routeThreadKey), ); - const [maximizedRightPanelThreadKey, setMaximizedRightPanelThreadKey] = useState( - null, - ); const userInputResponsesInFlight = useRef(new Set()); const [respondingRequestIds, setRespondingRequestIds] = useState([]); @@ -2392,8 +2389,7 @@ export default function ChatView(props: ChatViewProps) { renderedRightPanelSurface, ); const canMaximizeRightPanel = rightPanelOpen && !shouldUsePlanSidebarSheet; - const rightPanelMaximized = - canMaximizeRightPanel && maximizedRightPanelThreadKey === routeThreadKey; + const rightPanelMaximized = canMaximizeRightPanel && rightPanelState.maximized === true; const inlineRightPanelOwnsTitleBar = rightPanelOpen && !shouldUsePlanSidebarSheet; const [threadPanelPresentation, setThreadPanelPresentation] = useState("inline"); @@ -5866,7 +5862,7 @@ export default function ChatView(props: ChatViewProps) { .getState() .open(activeThreadRef, { kind: "device", ...activeRightPanelSurface.target }); } - setMaximizedRightPanelThreadKey(null); + useRightPanelStore.getState().setMaximized(activeThreadRef, false); useRightPanelStore.getState().close(activeThreadRef); } }, [activeRightPanelSurface, activeThreadRef]); @@ -6064,11 +6060,9 @@ export default function ChatView(props: ChatViewProps) { } }, [canMaximizeRightPanel, routeThreadKey, routeThreadRef]); const toggleRightPanelMaximized = useCallback(() => { - if (!canMaximizeRightPanel) return; - setMaximizedRightPanelThreadKey((threadKey) => - threadKey === routeThreadKey ? null : routeThreadKey, - ); - }, [canMaximizeRightPanel, routeThreadKey]); + if (!canMaximizeRightPanel || !activeThreadRef) return; + useRightPanelStore.getState().setMaximized(activeThreadRef, !rightPanelMaximized); + }, [activeThreadRef, canMaximizeRightPanel, rightPanelMaximized]); const cleanupRightPanelSurfaces = useCallback( (surfaces: readonly RightPanelSurface[]) => { if (!activeThreadRef) return; diff --git a/apps/web/src/rightPanelStore.test.ts b/apps/web/src/rightPanelStore.test.ts index ed27a64edd6f21c9f5b93c0c55d8b727e4748f5a..961a4a178dac963de81002452c3820444a186956 100644 --- a/apps/web/src/rightPanelStore.test.ts +++ b/apps/web/src/rightPanelStore.test.ts @@ -322,6 +322,35 @@ describe("rightPanelStore", () => { expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff"); }); + it("keeps a maximized panel per thread without counting it as a manual choice", () => { + const store = useRightPanelStore.getState(); + store.open(refA, "preview"); + store.open(refB, "diff"); + const revision = store.getUserActionRevision(refA); + store.setMaximized(refA, true); + store.setMaximized(refB, true); + store.setMaximized(refB, false); + + const { byThreadKey } = useRightPanelStore.getState(); + expect(selectThreadRightPanelState(byThreadKey, refA).maximized).toBe(true); + expect(selectThreadRightPanelState(byThreadKey, refB).maximized).toBeUndefined(); + expect(store.getUserActionRevision(refA)).toBe(revision); + }); + + it("restores a saved maximized panel during migration", () => { + const migrated = migratePersistedRightPanelState({ + byThreadKey: { + "env-1:thread-A": { + isOpen: true, + activeSurfaceId: "browser:new", + surfaces: [{ id: "browser:new", kind: "preview", resourceId: null }], + maximized: true, + }, + }, + }); + expect(selectThreadRightPanelState(migrated.byThreadKey, refA).maximized).toBe(true); + }); + it("drops the legacy singleton terminal surface during migration", () => { expect( migratePersistedRightPanelState({ diff --git a/apps/web/src/rightPanelStore.ts b/apps/web/src/rightPanelStore.ts index 8afdbedfcc877221cebcc493e426eaeb38266e90..fa417786a019cf83a3b142b1a968df5adb83a47c 100644 --- a/apps/web/src/rightPanelStore.ts +++ b/apps/web/src/rightPanelStore.ts @@ -112,6 +112,8 @@ export interface ThreadRightPanelState { activeSurfaceId: string | null; surfaces: RightPanelSurface[]; dismissedDeviceSurfaceIds?: string[]; + /** Kept with the thread so the layout survives a ChatView remount or a reload. */ + maximized?: true; } export interface ThreadPanelVisibility { @@ -185,6 +187,7 @@ interface RightPanelStoreState { reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void; show: (ref: ScopedThreadRef) => void; close: (ref: ScopedThreadRef) => void; + setMaximized: (ref: ScopedThreadRef, maximized: boolean) => void; toggleVisibility: (ref: ScopedThreadRef) => void; toggle: ( ref: ScopedThreadRef, @@ -571,6 +574,7 @@ export function migratePersistedRightPanelState(persistedState: unknown): { ), } : {}), + ...(validThreadState?.maximized === true ? { maximized: true as const } : {}), }, ]; }), @@ -969,6 +973,15 @@ export const useRightPanelStore = create()( current.isOpen ? { ...current, isOpen: false } : current, ), ), + // Layout only: it changes no surface, so proactive panels still apply. + setMaximized: (ref, maximized) => + set((state) => + automaticUpdate(state, scopedThreadKey(ref), (current) => { + if ((current.maximized ?? false) === maximized) return current; + const { maximized: _maximized, ...rest } = current; + return maximized ? { ...rest, maximized: true } : rest; + }), + ), toggleVisibility: (ref) => set((state) => userAction(state, scopedThreadKey(ref), (current) => ({ -- 2.54.0