From 63d42a5e5811e077965a913192bccac1e458774b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 11 Jun 2026 16:05:28 -0700 Subject: [PATCH] fix(preview): initialize and open browser reliably Co-authored-by: codex --- apps/desktop/src/ipc/methods/preview.test.ts | 28 +++++++++++++ apps/desktop/src/ipc/methods/preview.ts | 1 - apps/desktop/src/window/DesktopWindow.test.ts | 11 +++++ apps/desktop/src/window/DesktopWindow.ts | 1 + .../src/components/preview/PreviewView.tsx | 14 +++++-- .../preview/openPreviewSession.test.ts | 42 +++++++++++++++++++ .../components/preview/openPreviewSession.ts | 23 ++++++++++ apps/web/src/previewStateStore.ts | 2 +- 8 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 apps/desktop/src/ipc/methods/preview.test.ts create mode 100644 apps/web/src/components/preview/openPreviewSession.test.ts create mode 100644 apps/web/src/components/preview/openPreviewSession.ts diff --git a/apps/desktop/src/ipc/methods/preview.test.ts b/apps/desktop/src/ipc/methods/preview.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8332d5b80ca557c467c92ac083aa6f5b57b2f81d --- /dev/null +++ b/apps/desktop/src/ipc/methods/preview.test.ts @@ -0,0 +1,28 @@ +import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; + +const fromPartition = vi.fn(() => { + throw new Error("Session can only be received when app is ready"); +}); + +vi.mock("electron", () => ({ + BrowserWindow: { + getAllWindows: vi.fn(() => []), + }, + session: { + fromPartition, + }, + webContents: { + fromId: vi.fn(() => null), + }, +})); + +describe("preview IPC methods", () => { + beforeEach(() => { + fromPartition.mockClear(); + }); + + it("does not access the Electron session while the module loads", async () => { + await expect(import("./preview.ts")).resolves.toBeDefined(); + expect(fromPartition).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/desktop/src/ipc/methods/preview.ts b/apps/desktop/src/ipc/methods/preview.ts index b4476fe6fc1b58227dbc29f2ea10f1a5632b1e2f..dca8d64110884a9882fa8b2f13d86013a545e71a 100644 --- a/apps/desktop/src/ipc/methods/preview.ts +++ b/apps/desktop/src/ipc/methods/preview.ts @@ -9,7 +9,6 @@ import { PREVIEW_WEBVIEW_PREFERENCES } from "../../preview-webview-preferences.t import * as IpcChannels from "../channels.ts"; import type { DesktopIpcMethod } from "../DesktopIpc.ts"; -previewViewManager.getBrowserSession(); previewViewManager.onStateChange((tabId, state) => { for (const window of BrowserWindow.getAllWindows()) { if (!window.isDestroyed()) { diff --git a/apps/desktop/src/window/DesktopWindow.test.ts b/apps/desktop/src/window/DesktopWindow.test.ts index 5e977de2dea923486494566fc8e4382f8bf3e09d..01dbf3af1e851f97761b15aaf5cef3d2a0ff9992 100644 --- a/apps/desktop/src/window/DesktopWindow.test.ts +++ b/apps/desktop/src/window/DesktopWindow.test.ts @@ -8,6 +8,17 @@ import * as Ref from "effect/Ref"; import type * as Electron from "electron"; import { vi } from "vite-plus/test"; +vi.mock("electron", async (importOriginal) => ({ + ...(await importOriginal()), + session: { + fromPartition: vi.fn(() => ({ + getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/1.2.3"), + setPermissionRequestHandler: vi.fn(), + setUserAgent: vi.fn(), + })), + }, +})); + import * as DesktopAssets from "../app/DesktopAssets.ts"; import * as DesktopConfig from "../app/DesktopConfig.ts"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; diff --git a/apps/desktop/src/window/DesktopWindow.ts b/apps/desktop/src/window/DesktopWindow.ts index a19eb0ce2639c08430c44c7e41423166ecef520b..0450b413c131fde2153313d30e5ff1dd5c2f3060 100644 --- a/apps/desktop/src/window/DesktopWindow.ts +++ b/apps/desktop/src/window/DesktopWindow.ts @@ -171,6 +171,7 @@ const make = Effect.gen(function* () { const createWindow = Effect.fn("desktop.window.createWindow")(function* ( backendHttpUrl: URL, ): Effect.fn.Return { + previewViewManager.getBrowserSession(); const applicationUrl = environment.isDevelopment ? yield* resolveDesktopDevServerUrl(environment) : backendHttpUrl.href; diff --git a/apps/web/src/components/preview/PreviewView.tsx b/apps/web/src/components/preview/PreviewView.tsx index ba456f4aba131445261e27b6f3283c1ca457de3c..fb61fcb354cba8e68e6225489f35b0451e8e833b 100644 --- a/apps/web/src/components/preview/PreviewView.tsx +++ b/apps/web/src/components/preview/PreviewView.tsx @@ -12,6 +12,7 @@ import { selectThreadPreviewState, usePreviewStateStore } from "~/previewStateSt import { previewBridge } from "./previewBridge"; import { subscribePreviewAction } from "./previewActionBus"; +import { openPreviewSession } from "./openPreviewSession"; import { PreviewChromeRow } from "./PreviewChromeRow"; import { PreviewEmptyState } from "./PreviewEmptyState"; import { PreviewMoreMenu } from "./PreviewMoreMenu"; @@ -41,6 +42,7 @@ export function PreviewView({ threadRef, configuredUrls, visible }: Props) { const previewState = usePreviewStateStore((state) => selectThreadPreviewState(state.byThreadKey, threadRef), ); + const applyServerSnapshot = usePreviewStateStore((state) => state.applyServerSnapshot); const rememberUrl = usePreviewStateStore((state) => state.rememberUrl); const addElementContext = useComposerDraftStore((store) => store.addElementContext); @@ -74,15 +76,19 @@ export function PreviewView({ threadRef, configuredUrls, visible }: Props) { await previewBridge.navigate(tabId, next); rememberUrl(threadRef, next); } else { - const resolved = await api.preview.open({ threadId: threadRef.threadId, url: next }); - const resolvedUrl = resolved.navStatus._tag === "Idle" ? next : resolved.navStatus.url; - rememberUrl(threadRef, resolvedUrl); + await openPreviewSession({ + previewApi: api.preview, + threadRef, + url: next, + applyServerSnapshot, + rememberUrl, + }); } } catch { // Server-side `failed` event renders the unreachable view. } }, - [rememberUrl, tabId, threadRef], + [applyServerSnapshot, rememberUrl, tabId, threadRef], ); const handleRefresh = useCallback(() => { diff --git a/apps/web/src/components/preview/openPreviewSession.test.ts b/apps/web/src/components/preview/openPreviewSession.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..98ad7be9a86e5d466c861255ad1510fa8e52bdeb --- /dev/null +++ b/apps/web/src/components/preview/openPreviewSession.test.ts @@ -0,0 +1,42 @@ +import type { EnvironmentApi, PreviewSessionSnapshot, ScopedThreadRef } from "@t3tools/contracts"; +import { describe, expect, it, vi } from "vite-plus/test"; + +import { openPreviewSession } from "./openPreviewSession"; + +const threadRef = { + environmentId: "local" as ScopedThreadRef["environmentId"], + threadId: "thread-1" as ScopedThreadRef["threadId"], +}; + +const snapshot: PreviewSessionSnapshot = { + threadId: threadRef.threadId, + tabId: "tab-1", + navStatus: { + _tag: "Loading", + url: "https://t3.chat/", + title: "", + }, + canGoBack: false, + canGoForward: false, + updatedAt: "2026-06-11T23:00:00.000Z", +}; + +describe("openPreviewSession", () => { + it("applies the RPC response without waiting for a preview event", async () => { + const open = vi.fn(async () => snapshot); + const applyServerSnapshot = vi.fn(); + const rememberUrl = vi.fn(); + + await openPreviewSession({ + previewApi: { open } as Pick, + threadRef, + url: "t3.chat", + applyServerSnapshot, + rememberUrl, + }); + + expect(open).toHaveBeenCalledWith({ threadId: "thread-1", url: "t3.chat" }); + expect(applyServerSnapshot).toHaveBeenCalledWith(threadRef, snapshot); + expect(rememberUrl).toHaveBeenCalledWith(threadRef, "https://t3.chat/"); + }); +}); diff --git a/apps/web/src/components/preview/openPreviewSession.ts b/apps/web/src/components/preview/openPreviewSession.ts new file mode 100644 index 0000000000000000000000000000000000000000..37ef33069ce8fcfd1ba89d63ba3e40253ddecb49 --- /dev/null +++ b/apps/web/src/components/preview/openPreviewSession.ts @@ -0,0 +1,23 @@ +import type { EnvironmentApi, ScopedThreadRef } from "@t3tools/contracts"; + +import type { PreviewStateStoreState } from "~/previewStateStore"; + +interface OpenPreviewSessionInput { + previewApi: Pick; + threadRef: ScopedThreadRef; + url: string; + applyServerSnapshot: PreviewStateStoreState["applyServerSnapshot"]; + rememberUrl: PreviewStateStoreState["rememberUrl"]; +} + +export async function openPreviewSession(input: OpenPreviewSessionInput): Promise { + const snapshot = await input.previewApi.open({ + threadId: input.threadRef.threadId, + url: input.url, + }); + input.applyServerSnapshot(input.threadRef, snapshot); + input.rememberUrl( + input.threadRef, + snapshot.navStatus._tag === "Idle" ? input.url : snapshot.navStatus.url, + ); +} diff --git a/apps/web/src/previewStateStore.ts b/apps/web/src/previewStateStore.ts index 90719efd4c363458623aaf6d10390008ed22fb23..10f5cf585042eb826761e4b96aef114b90a8e575 100644 --- a/apps/web/src/previewStateStore.ts +++ b/apps/web/src/previewStateStore.ts @@ -42,7 +42,7 @@ const EMPTY_THREAD_PREVIEW_STATE: ThreadPreviewState = Object.freeze({ recentlySeenUrls: [] as string[], }); -interface PreviewStateStoreState { +export interface PreviewStateStoreState { byThreadKey: Record; applyServerEvent: (ref: ScopedThreadRef, event: PreviewEvent) => void; applyServerSnapshot: (ref: ScopedThreadRef, snapshot: PreviewSessionSnapshot | null) => void; -- 2.54.0