Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 5 additions & 11 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1912,9 +1912,6 @@ export default function ChatView(props: ChatViewProps) {
const isRevertingCheckpoint = useComposerDraftStore((store) =>
store.rewindingThreadKeys.has(routeThreadKey),
);
const [maximizedRightPanelThreadKey, setMaximizedRightPanelThreadKey] = useState<string | null>(
null,
);
const userInputResponsesInFlight = useRef(new Set<string>());
const [respondingRequestIds, setRespondingRequestIds] = useState<RuntimeRequestId[]>([]);

Expand Down Expand Up @@ -2390,8 +2387,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<ThreadPanelPresentation>("inline");
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -6057,11 +6053,9 @@ export default function ChatView(props: ChatViewProps) {
useRightPanelStore.getState().toggleThreadPanel(activeThreadRef, threadPanelPresentation);
}, [activeThreadRef, threadPanelPresentation]);
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;
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/rightPanelStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/rightPanelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -177,6 +179,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,
Expand Down Expand Up @@ -563,6 +566,7 @@ export function migratePersistedRightPanelState(persistedState: unknown): {
),
}
: {}),
...(validThreadState?.maximized === true ? { maximized: true as const } : {}),
},
];
}),
Expand Down Expand Up @@ -943,6 +947,15 @@ export const useRightPanelStore = create<RightPanelStoreState>()(
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) => ({
Expand Down
Loading