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) {...@@ -1914,9 +1914,6 @@ export default function ChatView(props: ChatViewProps) {
1914 const isRevertingCheckpoint = useComposerDraftStore((store) =>1914 const isRevertingCheckpoint = useComposerDraftStore((store) =>
1915 store.rewindingThreadKeys.has(routeThreadKey),1915 store.rewindingThreadKeys.has(routeThreadKey),
1916 );1916 );
1917 const [maximizedRightPanelThreadKey, setMaximizedRightPanelThreadKey] = useState<string | null>(
1918 null,
1919 );
1920 const userInputResponsesInFlight = useRef(new Set<string>());1917 const userInputResponsesInFlight = useRef(new Set<string>());
1921 const [respondingRequestIds, setRespondingRequestIds] = useState<RuntimeRequestId[]>([]);1918 const [respondingRequestIds, setRespondingRequestIds] = useState<RuntimeRequestId[]>([]);
19221919
...@@ -2392,8 +2389,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -2392,8 +2389,7 @@ export default function ChatView(props: ChatViewProps) {
2392 renderedRightPanelSurface,2389 renderedRightPanelSurface,
2393 );2390 );
2394 const canMaximizeRightPanel = rightPanelOpen && !shouldUsePlanSidebarSheet;2391 const canMaximizeRightPanel = rightPanelOpen && !shouldUsePlanSidebarSheet;
2395 const rightPanelMaximized =2392 const rightPanelMaximized = canMaximizeRightPanel && rightPanelState.maximized === true;
2396 canMaximizeRightPanel && maximizedRightPanelThreadKey === routeThreadKey;
2397 const inlineRightPanelOwnsTitleBar = rightPanelOpen && !shouldUsePlanSidebarSheet;2393 const inlineRightPanelOwnsTitleBar = rightPanelOpen && !shouldUsePlanSidebarSheet;
2398 const [threadPanelPresentation, setThreadPanelPresentation] =2394 const [threadPanelPresentation, setThreadPanelPresentation] =
2399 useState<ThreadPanelPresentation>("inline");2395 useState<ThreadPanelPresentation>("inline");
...@@ -5866,7 +5862,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -5866,7 +5862,7 @@ export default function ChatView(props: ChatViewProps) {
5866 .getState()5862 .getState()
5867 .open(activeThreadRef, { kind: "device", ...activeRightPanelSurface.target });5863 .open(activeThreadRef, { kind: "device", ...activeRightPanelSurface.target });
5868 }5864 }
5869 setMaximizedRightPanelThreadKey(null);5865 useRightPanelStore.getState().setMaximized(activeThreadRef, false);
5870 useRightPanelStore.getState().close(activeThreadRef);5866 useRightPanelStore.getState().close(activeThreadRef);
5871 }5867 }
5872 }, [activeRightPanelSurface, activeThreadRef]);5868 }, [activeRightPanelSurface, activeThreadRef]);
...@@ -6064,11 +6060,9 @@ export default function ChatView(props: ChatViewProps) {...@@ -6064,11 +6060,9 @@ export default function ChatView(props: ChatViewProps) {
6064 }6060 }
6065 }, [canMaximizeRightPanel, routeThreadKey, routeThreadRef]);6061 }, [canMaximizeRightPanel, routeThreadKey, routeThreadRef]);
6066 const toggleRightPanelMaximized = useCallback(() => {6062 const toggleRightPanelMaximized = useCallback(() => {
6067 if (!canMaximizeRightPanel) return;6063 if (!canMaximizeRightPanel || !activeThreadRef) return;
6068 setMaximizedRightPanelThreadKey((threadKey) =>6064 useRightPanelStore.getState().setMaximized(activeThreadRef, !rightPanelMaximized);
6069 threadKey === routeThreadKey ? null : routeThreadKey,6065 }, [activeThreadRef, canMaximizeRightPanel, rightPanelMaximized]);
6070 );
6071 }, [canMaximizeRightPanel, routeThreadKey]);
6072 const cleanupRightPanelSurfaces = useCallback(6066 const cleanupRightPanelSurfaces = useCallback(
6073 (surfaces: readonly RightPanelSurface[]) => {6067 (surfaces: readonly RightPanelSurface[]) => {
6074 if (!activeThreadRef) return;6068 if (!activeThreadRef) return;
apps/web/src/rightPanelStore.test.ts+29
...@@ -322,6 +322,35 @@ describe("rightPanelStore", () => {...@@ -322,6 +322,35 @@ describe("rightPanelStore", () => {
322 expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff");322 expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff");
323 });323 });
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
325 it("drops the legacy singleton terminal surface during migration", () => {354 it("drops the legacy singleton terminal surface during migration", () => {
326 expect(355 expect(
327 migratePersistedRightPanelState({356 migratePersistedRightPanelState({
apps/web/src/rightPanelStore.ts+13
...@@ -112,6 +112,8 @@ export interface ThreadRightPanelState {...@@ -112,6 +112,8 @@ export interface ThreadRightPanelState {
112 activeSurfaceId: string | null;112 activeSurfaceId: string | null;
113 surfaces: RightPanelSurface[];113 surfaces: RightPanelSurface[];
114 dismissedDeviceSurfaceIds?: string[];114 dismissedDeviceSurfaceIds?: string[];
115 /** Kept with the thread so the layout survives a ChatView remount or a reload. */
116 maximized?: true;
115}117}
116118
117export interface ThreadPanelVisibility {119export interface ThreadPanelVisibility {
...@@ -185,6 +187,7 @@ interface RightPanelStoreState {...@@ -185,6 +187,7 @@ interface RightPanelStoreState {
185 reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void;187 reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void;
186 show: (ref: ScopedThreadRef) => void;188 show: (ref: ScopedThreadRef) => void;
187 close: (ref: ScopedThreadRef) => void;189 close: (ref: ScopedThreadRef) => void;
190 setMaximized: (ref: ScopedThreadRef, maximized: boolean) => void;
188 toggleVisibility: (ref: ScopedThreadRef) => void;191 toggleVisibility: (ref: ScopedThreadRef) => void;
189 toggle: (192 toggle: (
190 ref: ScopedThreadRef,193 ref: ScopedThreadRef,
...@@ -571,6 +574,7 @@ export function migratePersistedRightPanelState(persistedState: unknown): {...@@ -571,6 +574,7 @@ export function migratePersistedRightPanelState(persistedState: unknown): {
571 ),574 ),
572 }575 }
573 : {}),576 : {}),
577 ...(validThreadState?.maximized === true ? { maximized: true as const } : {}),
574 },578 },
575 ];579 ];
576 }),580 }),
...@@ -969,6 +973,15 @@ export const useRightPanelStore = create<RightPanelStoreState>()(...@@ -969,6 +973,15 @@ export const useRightPanelStore = create<RightPanelStoreState>()(
969 current.isOpen ? { ...current, isOpen: false } : current,973 current.isOpen ? { ...current, isOpen: false } : current,
970 ),974 ),
971 ),975 ),
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 ),
972 toggleVisibility: (ref) =>985 toggleVisibility: (ref) =>
973 set((state) =>986 set((state) =>
974 userAction(state, scopedThreadKey(ref), (current) => ({987 userAction(state, scopedThreadKey(ref), (current) => ({