authorgravatar for 8172873+jamesvillarrubia@users.noreply.github.comJames Villarrubia <8172873+jamesvillarrubia@users.noreply.github.com> 2026-10-10 07:37:00-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 06:37:00-05:00
log71f6f1db89a50f0108663ebf1724cf3a1a4134ed
treea7987131e33a00243b44b9af2e7aadbcb694c05a
parent33c1e36fd097fe4f8e4498db653ee97b17184791
signature Signed by PGP key B5690EEEBB952194

fix(web): a maximized right panel stays maximized when you return to its thread (#17327)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

3 files changed, 47 insertions(+), 11 deletions(-)

apps/web/src/components/ChatView.tsx+5-11
......@@ -1914,9 +1914,6 @@ export default function ChatView(props: ChatViewProps) {
19141914 const isRevertingCheckpoint = useComposerDraftStore((store) =>
19151915 store.rewindingThreadKeys.has(routeThreadKey),
19161916 );
1917 const [maximizedRightPanelThreadKey, setMaximizedRightPanelThreadKey] = useState<string | null>(
1918 null,
1919 );
19201917 const userInputResponsesInFlight = useRef(new Set<string>());
19211918 const [respondingRequestIds, setRespondingRequestIds] = useState<RuntimeRequestId[]>([]);
19221919
......@@ -2392,8 +2389,7 @@ export default function ChatView(props: ChatViewProps) {
23922389 renderedRightPanelSurface,
23932390 );
23942391 const canMaximizeRightPanel = rightPanelOpen && !shouldUsePlanSidebarSheet;
2395 const rightPanelMaximized =
2396 canMaximizeRightPanel && maximizedRightPanelThreadKey === routeThreadKey;
2392 const rightPanelMaximized = canMaximizeRightPanel && rightPanelState.maximized === true;
23972393 const inlineRightPanelOwnsTitleBar = rightPanelOpen && !shouldUsePlanSidebarSheet;
23982394 const [threadPanelPresentation, setThreadPanelPresentation] =
23992395 useState<ThreadPanelPresentation>("inline");
......@@ -5866,7 +5862,7 @@ export default function ChatView(props: ChatViewProps) {
58665862 .getState()
58675863 .open(activeThreadRef, { kind: "device", ...activeRightPanelSurface.target });
58685864 }
5869 setMaximizedRightPanelThreadKey(null);
5865 useRightPanelStore.getState().setMaximized(activeThreadRef, false);
58705866 useRightPanelStore.getState().close(activeThreadRef);
58715867 }
58725868 }, [activeRightPanelSurface, activeThreadRef]);
......@@ -6064,11 +6060,9 @@ export default function ChatView(props: ChatViewProps) {
60646060 }
60656061 }, [canMaximizeRightPanel, routeThreadKey, routeThreadRef]);
60666062 const toggleRightPanelMaximized = useCallback(() => {
6067 if (!canMaximizeRightPanel) return;
6068 setMaximizedRightPanelThreadKey((threadKey) =>
6069 threadKey === routeThreadKey ? null : routeThreadKey,
6070 );
6071 }, [canMaximizeRightPanel, routeThreadKey]);
6063 if (!canMaximizeRightPanel || !activeThreadRef) return;
6064 useRightPanelStore.getState().setMaximized(activeThreadRef, !rightPanelMaximized);
6065 }, [activeThreadRef, canMaximizeRightPanel, rightPanelMaximized]);
60726066 const cleanupRightPanelSurfaces = useCallback(
60736067 (surfaces: readonly RightPanelSurface[]) => {
60746068 if (!activeThreadRef) return;
apps/web/src/rightPanelStore.test.ts+29
......@@ -322,6 +322,35 @@ describe("rightPanelStore", () => {
322322 expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff");
323323 });
324324
325 it("keeps a maximized panel per thread without counting it as a manual choice", () => {
326 const store = useRightPanelStore.getState();
327 store.open(refA, "preview");
328 store.open(refB, "diff");
329 const revision = store.getUserActionRevision(refA);
330 store.setMaximized(refA, true);
331 store.setMaximized(refB, true);
332 store.setMaximized(refB, false);
333
334 const { byThreadKey } = useRightPanelStore.getState();
335 expect(selectThreadRightPanelState(byThreadKey, refA).maximized).toBe(true);
336 expect(selectThreadRightPanelState(byThreadKey, refB).maximized).toBeUndefined();
337 expect(store.getUserActionRevision(refA)).toBe(revision);
338 });
339
340 it("restores a saved maximized panel during migration", () => {
341 const migrated = migratePersistedRightPanelState({
342 byThreadKey: {
343 "env-1:thread-A": {
344 isOpen: true,
345 activeSurfaceId: "browser:new",
346 surfaces: [{ id: "browser:new", kind: "preview", resourceId: null }],
347 maximized: true,
348 },
349 },
350 });
351 expect(selectThreadRightPanelState(migrated.byThreadKey, refA).maximized).toBe(true);
352 });
353
325354 it("drops the legacy singleton terminal surface during migration", () => {
326355 expect(
327356 migratePersistedRightPanelState({
apps/web/src/rightPanelStore.ts+13
......@@ -112,6 +112,8 @@ export interface ThreadRightPanelState {
112112 activeSurfaceId: string | null;
113113 surfaces: RightPanelSurface[];
114114 dismissedDeviceSurfaceIds?: string[];
115 /** Kept with the thread so the layout survives a ChatView remount or a reload. */
116 maximized?: true;
115117}
116118
117119export interface ThreadPanelVisibility {
......@@ -185,6 +187,7 @@ interface RightPanelStoreState {
185187 reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void;
186188 show: (ref: ScopedThreadRef) => void;
187189 close: (ref: ScopedThreadRef) => void;
190 setMaximized: (ref: ScopedThreadRef, maximized: boolean) => void;
188191 toggleVisibility: (ref: ScopedThreadRef) => void;
189192 toggle: (
190193 ref: ScopedThreadRef,
......@@ -571,6 +574,7 @@ export function migratePersistedRightPanelState(persistedState: unknown): {
571574 ),
572575 }
573576 : {}),
577 ...(validThreadState?.maximized === true ? { maximized: true as const } : {}),
574578 },
575579 ];
576580 }),
......@@ -969,6 +973,15 @@ export const useRightPanelStore = create<RightPanelStoreState>()(
969973 current.isOpen ? { ...current, isOpen: false } : current,
970974 ),
971975 ),
976 // Layout only: it changes no surface, so proactive panels still apply.
977 setMaximized: (ref, maximized) =>
978 set((state) =>
979 automaticUpdate(state, scopedThreadKey(ref), (current) => {
980 if ((current.maximized ?? false) === maximized) return current;
981 const { maximized: _maximized, ...rest } = current;
982 return maximized ? { ...rest, maximized: true } : rest;
983 }),
984 ),
972985 toggleVisibility: (ref) =>
973986 set((state) =>
974987 userAction(state, scopedThreadKey(ref), (current) => ({