| 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 | 1914 | const isRevertingCheckpoint = useComposerDraftStore((store) => |
| 1915 | 1915 | store.rewindingThreadKeys.has(routeThreadKey), |
| 1916 | 1916 | ); |
| 1917 | const [maximizedRightPanelThreadKey, setMaximizedRightPanelThreadKey] = useState<string | null>( | |
| 1918 | null, | |
| 1919 | ); | |
| 1920 | 1917 | const userInputResponsesInFlight = useRef(new Set<string>()); |
| 1921 | 1918 | const [respondingRequestIds, setRespondingRequestIds] = useState<RuntimeRequestId[]>([]); |
| 1922 | 1919 | |
| ... | ... | @@ -2392,8 +2389,7 @@ export default function ChatView(props: ChatViewProps) { |
| 2392 | 2389 | renderedRightPanelSurface, |
| 2393 | 2390 | ); |
| 2394 | 2391 | const canMaximizeRightPanel = rightPanelOpen && !shouldUsePlanSidebarSheet; |
| 2395 | const rightPanelMaximized = | |
| 2396 | canMaximizeRightPanel && maximizedRightPanelThreadKey === routeThreadKey; | |
| 2392 | const rightPanelMaximized = canMaximizeRightPanel && rightPanelState.maximized === true; | |
| 2397 | 2393 | const inlineRightPanelOwnsTitleBar = rightPanelOpen && !shouldUsePlanSidebarSheet; |
| 2398 | 2394 | const [threadPanelPresentation, setThreadPanelPresentation] = |
| 2399 | 2395 | useState<ThreadPanelPresentation>("inline"); |
| ... | ... | @@ -5866,7 +5862,7 @@ export default function ChatView(props: ChatViewProps) { |
| 5866 | 5862 | .getState() |
| 5867 | 5863 | .open(activeThreadRef, { kind: "device", ...activeRightPanelSurface.target }); |
| 5868 | 5864 | } |
| 5869 | setMaximizedRightPanelThreadKey(null); | |
| 5865 | useRightPanelStore.getState().setMaximized(activeThreadRef, false); | |
| 5870 | 5866 | useRightPanelStore.getState().close(activeThreadRef); |
| 5871 | 5867 | } |
| 5872 | 5868 | }, [activeRightPanelSurface, activeThreadRef]); |
| ... | ... | @@ -6064,11 +6060,9 @@ export default function ChatView(props: ChatViewProps) { |
| 6064 | 6060 | } |
| 6065 | 6061 | }, [canMaximizeRightPanel, routeThreadKey, routeThreadRef]); |
| 6066 | 6062 | 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]); | |
| 6072 | 6066 | const cleanupRightPanelSurfaces = useCallback( |
| 6073 | 6067 | (surfaces: readonly RightPanelSurface[]) => { |
| 6074 | 6068 | if (!activeThreadRef) return; |
apps/web/src/rightPanelStore.test.ts+29| ... | ... | @@ -322,6 +322,35 @@ describe("rightPanelStore", () => { |
| 322 | 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 | 354 | it("drops the legacy singleton terminal surface during migration", () => { |
| 326 | 355 | expect( |
| 327 | 356 | migratePersistedRightPanelState({ |
apps/web/src/rightPanelStore.ts+13| ... | ... | @@ -112,6 +112,8 @@ export interface ThreadRightPanelState { |
| 112 | 112 | activeSurfaceId: string | null; |
| 113 | 113 | surfaces: RightPanelSurface[]; |
| 114 | 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 | 119 | export interface ThreadPanelVisibility { |
| ... | ... | @@ -185,6 +187,7 @@ interface RightPanelStoreState { |
| 185 | 187 | reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void; |
| 186 | 188 | show: (ref: ScopedThreadRef) => void; |
| 187 | 189 | close: (ref: ScopedThreadRef) => void; |
| 190 | setMaximized: (ref: ScopedThreadRef, maximized: boolean) => void; | |
| 188 | 191 | toggleVisibility: (ref: ScopedThreadRef) => void; |
| 189 | 192 | toggle: ( |
| 190 | 193 | ref: ScopedThreadRef, |
| ... | ... | @@ -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 | 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 | 985 | toggleVisibility: (ref) => |
| 973 | 986 | set((state) => |
| 974 | 987 | userAction(state, scopedThreadKey(ref), (current) => ({ |