Skip to content

Commit 1741445

Browse files
committed
Scope preview state by project
- Persist preview open, layout, size, and preset per project - Update chat and preview panel wiring to use project IDs - Bump preview state storage version and cover the new behavior in tests
1 parent 5105833 commit 1741445

File tree

5 files changed

+151
-115
lines changed

5 files changed

+151
-115
lines changed

apps/web/src/components/ChatView.tsx

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -382,17 +382,24 @@ export default function ChatView({ threadId }: ChatViewProps) {
382382
const setStickyComposerModel = useComposerDraftStore((store) => store.setStickyModel);
383383
const timestampFormat = settings.timestampFormat;
384384
const navigate = useNavigate();
385-
const previewOpen = usePreviewStateStore((state) => state.globalOpen);
386-
const togglePreviewOpen = usePreviewStateStore((state) => state.toggleGlobalOpen);
387-
const setPreviewOpen = usePreviewStateStore((state) => state.setGlobalOpen);
388-
const previewDock = usePreviewStateStore((state) => state.dockByThreadId[threadId] ?? "right");
389-
const previewSize = usePreviewStateStore(
390-
(state) => state.sizeByThreadId[threadId] ?? PREVIEW_SPLIT_DEFAULT_SIZE_PX,
385+
const activeProjectId = threads.find((t) => t.id === threadId)?.projectId ?? null;
386+
const previewOpen = usePreviewStateStore((state) =>
387+
activeProjectId ? (state.openByProjectId[activeProjectId] ?? false) : false,
388+
);
389+
const togglePreviewOpen = usePreviewStateStore((state) => state.toggleProjectOpen);
390+
const setPreviewOpen = usePreviewStateStore((state) => state.setProjectOpen);
391+
const previewDock = usePreviewStateStore((state) =>
392+
activeProjectId ? (state.dockByProjectId[activeProjectId] ?? "right") : "right",
393+
);
394+
const previewSize = usePreviewStateStore((state) =>
395+
activeProjectId
396+
? (state.sizeByProjectId[activeProjectId] ?? PREVIEW_SPLIT_DEFAULT_SIZE_PX)
397+
: PREVIEW_SPLIT_DEFAULT_SIZE_PX,
391398
);
392399
const previewStacked = previewDock === "top" || previewDock === "bottom";
393-
const setPreviewDock = usePreviewStateStore((state) => state.setThreadDock);
394-
const togglePreviewLayout = usePreviewStateStore((state) => state.toggleThreadLayout);
395-
const setPreviewSize = usePreviewStateStore((state) => state.setThreadSize);
400+
const setPreviewDock = usePreviewStateStore((state) => state.setProjectDock);
401+
const togglePreviewLayout = usePreviewStateStore((state) => state.toggleProjectLayout);
402+
const setPreviewSize = usePreviewStateStore((state) => state.setProjectSize);
396403
const previewSplitRef = useRef<HTMLDivElement | null>(null);
397404
const previewResizeStateRef = useRef<{
398405
pointerId: number;
@@ -671,9 +678,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
671678
);
672679
const latestTurnSettled = isLatestTurnSettled(activeLatestTurn, activeThread?.session ?? null);
673680
const activeProject = projects.find((p) => p.id === activeThread?.projectId);
674-
const previewPanelKey = activeThread
675-
? `${activeThread.id}:${activeProject?.id ?? "no-project"}:${previewDock}`
676-
: null;
681+
const previewPanelKey = activeProject ? `${activeProject.id}:${previewDock}` : null;
677682

678683
const openPullRequestDialog = useCallback(
679684
(reference?: string) => {
@@ -1593,7 +1598,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
15931598
const handlePreviewUrl = useCallback(
15941599
(url: string) => {
15951600
if (!activeProject || !activeThread) return;
1596-
setPreviewOpen(true);
1601+
setPreviewOpen(activeProject.id, true);
15971602
void previewBridgeRef?.createTab({ url });
15981603
},
15991604
[activeProject, activeThread, setPreviewOpen, previewBridgeRef],
@@ -4549,9 +4554,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
45494554
PREVIEW_SPLIT_MIN_SIZE_PX,
45504555
Math.min(Math.round(nextSizeUnclamped), maxSize),
45514556
);
4552-
setPreviewSize(threadId, nextSize);
4557+
if (activeProjectId) setPreviewSize(activeProjectId, nextSize);
45534558
},
4554-
[previewDock, previewStacked, setPreviewSize, threadId],
4559+
[activeProjectId, previewDock, previewStacked, setPreviewSize],
45554560
);
45564561

45574562
const handlePreviewResizePointerEnd = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
@@ -4604,20 +4609,22 @@ export default function ChatView({ threadId }: ChatViewProps) {
46044609
}
46054610

46064611
event.preventDefault();
4612+
if (!activeProjectId) return;
4613+
46074614
if (previewOpen && previewDock === targetDock) {
4608-
setPreviewOpen(false);
4615+
setPreviewOpen(activeProjectId, false);
46094616
return;
46104617
}
46114618

4612-
setPreviewOpen(true);
4613-
setPreviewDock(threadId, targetDock);
4619+
setPreviewOpen(activeProjectId, true);
4620+
setPreviewDock(activeProjectId, targetDock);
46144621
};
46154622

46164623
window.addEventListener("keydown", onWindowKeyDown);
46174624
return () => {
46184625
window.removeEventListener("keydown", onWindowKeyDown);
46194626
};
4620-
}, [activeProject, previewDock, previewOpen, setPreviewDock, setPreviewOpen, threadId]);
4627+
}, [activeProject, activeProjectId, previewDock, previewOpen, setPreviewDock, setPreviewOpen]);
46214628

46224629
// Empty state: no active thread
46234630
if (!activeThread) {
@@ -4670,8 +4677,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
46704677
onImportProjectScripts={importProjectScripts}
46714678
onToggleTerminal={toggleTerminalVisibility}
46724679
onToggleDiff={onToggleDiff}
4673-
onTogglePreview={() => togglePreviewOpen()}
4674-
onTogglePreviewLayout={() => togglePreviewLayout(activeThread.id)}
4680+
onTogglePreview={() => activeProjectId && togglePreviewOpen(activeProjectId)}
4681+
onTogglePreviewLayout={() => activeProjectId && togglePreviewLayout(activeProjectId)}
46754682
/>
46764683
</header>
46774684

@@ -4706,8 +4713,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
47064713
>
47074714
<PreviewPanel
47084715
key={previewPanelKey ?? undefined}
4709-
threadId={activeThread.id}
4710-
onClose={() => setPreviewOpen(false)}
4716+
projectId={activeProject!.id}
4717+
onClose={() => setPreviewOpen(activeProject!.id, false)}
47114718
/>
47124719
</div>
47134720
<div
@@ -5527,8 +5534,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
55275534
>
55285535
<PreviewPanel
55295536
key={previewPanelKey ?? undefined}
5530-
threadId={activeThread.id}
5531-
onClose={() => setPreviewOpen(false)}
5537+
projectId={activeProject!.id}
5538+
onClose={() => setPreviewOpen(activeProject!.id, false)}
55325539
/>
55335540
</div>
55345541
</>

apps/web/src/components/PreviewPanel.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { PreviewTabsState, PreviewTabState, ThreadId } from "@okcode/contracts";
1+
import type { PreviewTabsState, PreviewTabState, ProjectId } from "@okcode/contracts";
22
import { type FormEvent, useEffect, useLayoutEffect, useRef, useState } from "react";
33
import {
44
ChevronLeftIcon,
@@ -99,17 +99,17 @@ function tabDisplayTitle(tab: PreviewTabState): string {
9999
}
100100

101101
interface PreviewPanelProps {
102-
threadId: ThreadId;
102+
projectId: ProjectId;
103103
onClose: () => void;
104104
}
105105

106-
export function PreviewPanel({ threadId, onClose }: PreviewPanelProps) {
106+
export function PreviewPanel({ projectId, onClose }: PreviewPanelProps) {
107107
const previewBridge = readDesktopPreviewBridge();
108-
const setGlobalOpen = usePreviewStateStore((state) => state.setGlobalOpen);
108+
const setProjectOpen = usePreviewStateStore((state) => state.setProjectOpen);
109109
const favoriteUrls = usePreviewStateStore((state) => state.favoriteUrls);
110110
const toggleFavoriteUrl = usePreviewStateStore((state) => state.toggleFavoriteUrl);
111-
const presetId = usePreviewStateStore((state) => state.presetByThreadId[threadId] ?? null);
112-
const setThreadPreset = usePreviewStateStore((state) => state.setThreadPreset);
111+
const presetId = usePreviewStateStore((state) => state.presetByProjectId[projectId] ?? null);
112+
const setProjectPreset = usePreviewStateStore((state) => state.setProjectPreset);
113113
const activePreset = presetId ? getBrowserPreset(presetId) : null;
114114
const PresetIcon = presetId ? PRESET_ICONS[presetId] : null;
115115

@@ -239,7 +239,7 @@ export function PreviewPanel({ threadId, onClose }: PreviewPanelProps) {
239239
visualViewport?.removeEventListener("scroll", invalidateBounds);
240240
void previewBridge.setBounds(HIDDEN_PREVIEW_BOUNDS);
241241
};
242-
}, [previewBridge, tabsState.tabs.length, threadId]);
242+
}, [previewBridge, tabsState.tabs.length, projectId]);
243243

244244
// Cleanup on unmount
245245
useEffect(() => {
@@ -281,7 +281,7 @@ export function PreviewPanel({ threadId, onClose }: PreviewPanelProps) {
281281
};
282282

283283
const onClosePreview = () => {
284-
setGlobalOpen(false);
284+
setProjectOpen(projectId, false);
285285
void previewBridge?.closeAll();
286286
onClose();
287287
};
@@ -378,8 +378,8 @@ export function PreviewPanel({ threadId, onClose }: PreviewPanelProps) {
378378
<MenuRadioGroup
379379
value={presetId ?? RESPONSIVE_VALUE}
380380
onValueChange={(value) => {
381-
setThreadPreset(
382-
threadId,
381+
setProjectPreset(
382+
projectId,
383383
value === RESPONSIVE_VALUE ? null : (value as BrowserPresetId),
384384
);
385385
}}

apps/web/src/previewStateStore.test.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22

3-
const STORAGE_KEY = "okcode:desktop-preview:v3";
3+
const STORAGE_KEY = "okcode:desktop-preview:v4";
44

55
let usePreviewStateStore: typeof import("./previewStateStore").usePreviewStateStore;
66
let storage: Map<string, string>;
@@ -26,10 +26,10 @@ describe("previewStateStore", () => {
2626

2727
({ usePreviewStateStore } = await import("./previewStateStore"));
2828
usePreviewStateStore.setState({
29-
globalOpen: false,
30-
dockByThreadId: {},
31-
sizeByThreadId: {},
32-
presetByThreadId: {},
29+
openByProjectId: {},
30+
dockByProjectId: {},
31+
sizeByProjectId: {},
32+
presetByProjectId: {},
3333
favoriteUrls: [],
3434
});
3535
storage.clear();
@@ -46,14 +46,29 @@ describe("previewStateStore", () => {
4646
expect(usePreviewStateStore.getState().favoriteUrls).not.toContain("http://localhost:3000/");
4747
});
4848

49-
it("toggles globalOpen", () => {
49+
it("toggles project open state", () => {
5050
const store = usePreviewStateStore.getState();
51+
const projectId = "test-project-id" as any;
5152

52-
store.setGlobalOpen(true);
53-
expect(usePreviewStateStore.getState().globalOpen).toBe(true);
54-
expect(storage.get(STORAGE_KEY)).toContain('"globalOpen":true');
53+
store.setProjectOpen(projectId, true);
54+
expect(usePreviewStateStore.getState().openByProjectId[projectId]).toBe(true);
55+
expect(storage.get(STORAGE_KEY)).toContain('"openByProjectId"');
5556

56-
store.toggleGlobalOpen();
57-
expect(usePreviewStateStore.getState().globalOpen).toBe(false);
57+
store.toggleProjectOpen(projectId);
58+
expect(usePreviewStateStore.getState().openByProjectId[projectId]).toBe(false);
59+
});
60+
61+
it("scopes open state per project", () => {
62+
const store = usePreviewStateStore.getState();
63+
const projectA = "project-a" as any;
64+
const projectB = "project-b" as any;
65+
66+
store.setProjectOpen(projectA, true);
67+
expect(usePreviewStateStore.getState().openByProjectId[projectA]).toBe(true);
68+
expect(usePreviewStateStore.getState().openByProjectId[projectB]).toBeUndefined();
69+
70+
store.setProjectOpen(projectB, false);
71+
expect(usePreviewStateStore.getState().openByProjectId[projectA]).toBe(true);
72+
expect(usePreviewStateStore.getState().openByProjectId[projectB]).toBe(false);
5873
});
5974
});

0 commit comments

Comments
 (0)