| author | |
| committer | |
| log | 71f6f1db89a50f0108663ebf1724cf3a1a4134ed |
| tree | a7987131e33a00243b44b9af2e7aadbcb694c05a |
| parent | 33c1e36fd097fe4f8e4498db653ee97b17184791 |
| signature | Signed by PGP key B5690EEEBB952194 |
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[]>([]); |
| 1922 | 1919 | ||
| ... | @@ -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 | }); |
| 324 | 324 | ||
| 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 | } |
| 116 | 118 | ||
| 117 | export interface ThreadPanelVisibility { | 119 | export 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) => ({ |