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
57 changes: 32 additions & 25 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,17 +382,24 @@ export default function ChatView({ threadId }: ChatViewProps) {
const setStickyComposerModel = useComposerDraftStore((store) => store.setStickyModel);
const timestampFormat = settings.timestampFormat;
const navigate = useNavigate();
const previewOpen = usePreviewStateStore((state) => state.globalOpen);
const togglePreviewOpen = usePreviewStateStore((state) => state.toggleGlobalOpen);
const setPreviewOpen = usePreviewStateStore((state) => state.setGlobalOpen);
const previewDock = usePreviewStateStore((state) => state.dockByThreadId[threadId] ?? "right");
const previewSize = usePreviewStateStore(
(state) => state.sizeByThreadId[threadId] ?? PREVIEW_SPLIT_DEFAULT_SIZE_PX,
const activeProjectId = threads.find((t) => t.id === threadId)?.projectId ?? null;
const previewOpen = usePreviewStateStore((state) =>
activeProjectId ? (state.openByProjectId[activeProjectId] ?? false) : false,
);
const togglePreviewOpen = usePreviewStateStore((state) => state.toggleProjectOpen);
const setPreviewOpen = usePreviewStateStore((state) => state.setProjectOpen);
const previewDock = usePreviewStateStore((state) =>
activeProjectId ? (state.dockByProjectId[activeProjectId] ?? "right") : "right",
);
const previewSize = usePreviewStateStore((state) =>
activeProjectId
? (state.sizeByProjectId[activeProjectId] ?? PREVIEW_SPLIT_DEFAULT_SIZE_PX)
: PREVIEW_SPLIT_DEFAULT_SIZE_PX,
);
const previewStacked = previewDock === "top" || previewDock === "bottom";
const setPreviewDock = usePreviewStateStore((state) => state.setThreadDock);
const togglePreviewLayout = usePreviewStateStore((state) => state.toggleThreadLayout);
const setPreviewSize = usePreviewStateStore((state) => state.setThreadSize);
const setPreviewDock = usePreviewStateStore((state) => state.setProjectDock);
const togglePreviewLayout = usePreviewStateStore((state) => state.toggleProjectLayout);
const setPreviewSize = usePreviewStateStore((state) => state.setProjectSize);
const previewSplitRef = useRef<HTMLDivElement | null>(null);
const previewResizeStateRef = useRef<{
pointerId: number;
Expand Down Expand Up @@ -671,9 +678,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
);
const latestTurnSettled = isLatestTurnSettled(activeLatestTurn, activeThread?.session ?? null);
const activeProject = projects.find((p) => p.id === activeThread?.projectId);
const previewPanelKey = activeThread
? `${activeThread.id}:${activeProject?.id ?? "no-project"}:${previewDock}`
: null;
const previewPanelKey = activeProject ? `${activeProject.id}:${previewDock}` : null;

const openPullRequestDialog = useCallback(
(reference?: string) => {
Expand Down Expand Up @@ -1593,7 +1598,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
const handlePreviewUrl = useCallback(
(url: string) => {
if (!activeProject || !activeThread) return;
setPreviewOpen(true);
setPreviewOpen(activeProject.id, true);
void previewBridgeRef?.createTab({ url });
},
[activeProject, activeThread, setPreviewOpen, previewBridgeRef],
Expand Down Expand Up @@ -4549,9 +4554,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
PREVIEW_SPLIT_MIN_SIZE_PX,
Math.min(Math.round(nextSizeUnclamped), maxSize),
);
setPreviewSize(threadId, nextSize);
if (activeProjectId) setPreviewSize(activeProjectId, nextSize);
},
[previewDock, previewStacked, setPreviewSize, threadId],
[activeProjectId, previewDock, previewStacked, setPreviewSize],
);

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

event.preventDefault();
if (!activeProjectId) return;

if (previewOpen && previewDock === targetDock) {
setPreviewOpen(false);
setPreviewOpen(activeProjectId, false);
return;
}

setPreviewOpen(true);
setPreviewDock(threadId, targetDock);
setPreviewOpen(activeProjectId, true);
setPreviewDock(activeProjectId, targetDock);
};

window.addEventListener("keydown", onWindowKeyDown);
return () => {
window.removeEventListener("keydown", onWindowKeyDown);
};
}, [activeProject, previewDock, previewOpen, setPreviewDock, setPreviewOpen, threadId]);
}, [activeProject, activeProjectId, previewDock, previewOpen, setPreviewDock, setPreviewOpen]);

// Empty state: no active thread
if (!activeThread) {
Expand Down Expand Up @@ -4670,8 +4677,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
onImportProjectScripts={importProjectScripts}
onToggleTerminal={toggleTerminalVisibility}
onToggleDiff={onToggleDiff}
onTogglePreview={() => togglePreviewOpen()}
onTogglePreviewLayout={() => togglePreviewLayout(activeThread.id)}
onTogglePreview={() => activeProjectId && togglePreviewOpen(activeProjectId)}
onTogglePreviewLayout={() => activeProjectId && togglePreviewLayout(activeProjectId)}
/>
</header>

Expand Down Expand Up @@ -4706,8 +4713,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
>
<PreviewPanel
key={previewPanelKey ?? undefined}
threadId={activeThread.id}
onClose={() => setPreviewOpen(false)}
projectId={activeProject!.id}
onClose={() => setPreviewOpen(activeProject!.id, false)}
/>
</div>
<div
Expand Down Expand Up @@ -5527,8 +5534,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
>
<PreviewPanel
key={previewPanelKey ?? undefined}
threadId={activeThread.id}
onClose={() => setPreviewOpen(false)}
projectId={activeProject!.id}
onClose={() => setPreviewOpen(activeProject!.id, false)}
/>
</div>
</>
Expand Down
20 changes: 10 additions & 10 deletions apps/web/src/components/PreviewPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PreviewTabsState, PreviewTabState, ThreadId } from "@okcode/contracts";
import type { PreviewTabsState, PreviewTabState, ProjectId } from "@okcode/contracts";
import { type FormEvent, useEffect, useLayoutEffect, useRef, useState } from "react";
import {
ChevronLeftIcon,
Expand Down Expand Up @@ -99,17 +99,17 @@ function tabDisplayTitle(tab: PreviewTabState): string {
}

interface PreviewPanelProps {
threadId: ThreadId;
projectId: ProjectId;
onClose: () => void;
}

export function PreviewPanel({ threadId, onClose }: PreviewPanelProps) {
export function PreviewPanel({ projectId, onClose }: PreviewPanelProps) {
const previewBridge = readDesktopPreviewBridge();
const setGlobalOpen = usePreviewStateStore((state) => state.setGlobalOpen);
const setProjectOpen = usePreviewStateStore((state) => state.setProjectOpen);
const favoriteUrls = usePreviewStateStore((state) => state.favoriteUrls);
const toggleFavoriteUrl = usePreviewStateStore((state) => state.toggleFavoriteUrl);
const presetId = usePreviewStateStore((state) => state.presetByThreadId[threadId] ?? null);
const setThreadPreset = usePreviewStateStore((state) => state.setThreadPreset);
const presetId = usePreviewStateStore((state) => state.presetByProjectId[projectId] ?? null);
const setProjectPreset = usePreviewStateStore((state) => state.setProjectPreset);
const activePreset = presetId ? getBrowserPreset(presetId) : null;
const PresetIcon = presetId ? PRESET_ICONS[presetId] : null;

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

// Cleanup on unmount
useEffect(() => {
Expand Down Expand Up @@ -281,7 +281,7 @@ export function PreviewPanel({ threadId, onClose }: PreviewPanelProps) {
};

const onClosePreview = () => {
setGlobalOpen(false);
setProjectOpen(projectId, false);
void previewBridge?.closeAll();
onClose();
};
Expand Down Expand Up @@ -378,8 +378,8 @@ export function PreviewPanel({ threadId, onClose }: PreviewPanelProps) {
<MenuRadioGroup
value={presetId ?? RESPONSIVE_VALUE}
onValueChange={(value) => {
setThreadPreset(
threadId,
setProjectPreset(
projectId,
value === RESPONSIVE_VALUE ? null : (value as BrowserPresetId),
);
}}
Expand Down
37 changes: 26 additions & 11 deletions apps/web/src/previewStateStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

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

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

({ usePreviewStateStore } = await import("./previewStateStore"));
usePreviewStateStore.setState({
globalOpen: false,
dockByThreadId: {},
sizeByThreadId: {},
presetByThreadId: {},
openByProjectId: {},
dockByProjectId: {},
sizeByProjectId: {},
presetByProjectId: {},
favoriteUrls: [],
});
storage.clear();
Expand All @@ -46,14 +46,29 @@ describe("previewStateStore", () => {
expect(usePreviewStateStore.getState().favoriteUrls).not.toContain("http://localhost:3000/");
});

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

store.setGlobalOpen(true);
expect(usePreviewStateStore.getState().globalOpen).toBe(true);
expect(storage.get(STORAGE_KEY)).toContain('"globalOpen":true');
store.setProjectOpen(projectId, true);
expect(usePreviewStateStore.getState().openByProjectId[projectId]).toBe(true);
expect(storage.get(STORAGE_KEY)).toContain('"openByProjectId"');

store.toggleGlobalOpen();
expect(usePreviewStateStore.getState().globalOpen).toBe(false);
store.toggleProjectOpen(projectId);
expect(usePreviewStateStore.getState().openByProjectId[projectId]).toBe(false);
});

it("scopes open state per project", () => {
const store = usePreviewStateStore.getState();
const projectA = "project-a" as any;
const projectB = "project-b" as any;

store.setProjectOpen(projectA, true);
expect(usePreviewStateStore.getState().openByProjectId[projectA]).toBe(true);
expect(usePreviewStateStore.getState().openByProjectId[projectB]).toBeUndefined();

store.setProjectOpen(projectB, false);
expect(usePreviewStateStore.getState().openByProjectId[projectA]).toBe(true);
expect(usePreviewStateStore.getState().openByProjectId[projectB]).toBe(false);
});
});
Loading
Loading