| author | |
| committer | |
| log | 63d42a5e5811e077965a913192bccac1e458774b |
| tree | 284b9c7fce153caf90942b56e1f17cdb40a7042e |
| parent | 9b43123ba2a90ecaaf2eac1f75b6aebcf181f253 |
Co-authored-by: codex <codex@users.noreply.github.com>8 files changed, 116 insertions(+), 6 deletions(-)
apps/desktop/src/ipc/methods/preview.test.ts created+28| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; | |
| 2 | ||
| 3 | const fromPartition = vi.fn(() => { | |
| 4 | throw new Error("Session can only be received when app is ready"); | |
| 5 | }); | |
| 6 | ||
| 7 | vi.mock("electron", () => ({ | |
| 8 | BrowserWindow: { | |
| 9 | getAllWindows: vi.fn(() => []), | |
| 10 | }, | |
| 11 | session: { | |
| 12 | fromPartition, | |
| 13 | }, | |
| 14 | webContents: { | |
| 15 | fromId: vi.fn(() => null), | |
| 16 | }, | |
| 17 | })); | |
| 18 | ||
| 19 | describe("preview IPC methods", () => { | |
| 20 | beforeEach(() => { | |
| 21 | fromPartition.mockClear(); | |
| 22 | }); | |
| 23 | ||
| 24 | it("does not access the Electron session while the module loads", async () => { | |
| 25 | await expect(import("./preview.ts")).resolves.toBeDefined(); | |
| 26 | expect(fromPartition).not.toHaveBeenCalled(); | |
| 27 | }); | |
| 28 | }); |
apps/desktop/src/ipc/methods/preview.ts-1| ... | ... | @@ -9,7 +9,6 @@ import { PREVIEW_WEBVIEW_PREFERENCES } from "../../preview-webview-preferences.t |
| 9 | 9 | import * as IpcChannels from "../channels.ts"; |
| 10 | 10 | import type { DesktopIpcMethod } from "../DesktopIpc.ts"; |
| 11 | 11 | |
| 12 | previewViewManager.getBrowserSession(); | |
| 13 | 12 | previewViewManager.onStateChange((tabId, state) => { |
| 14 | 13 | for (const window of BrowserWindow.getAllWindows()) { |
| 15 | 14 | if (!window.isDestroyed()) { |
apps/desktop/src/window/DesktopWindow.test.ts+11| ... | ... | @@ -8,6 +8,17 @@ import * as Ref from "effect/Ref"; |
| 8 | 8 | import type * as Electron from "electron"; |
| 9 | 9 | import { vi } from "vite-plus/test"; |
| 10 | 10 | |
| 11 | vi.mock("electron", async (importOriginal) => ({ | |
| 12 | ...(await importOriginal<typeof import("electron")>()), | |
| 13 | session: { | |
| 14 | fromPartition: vi.fn(() => ({ | |
| 15 | getUserAgent: vi.fn(() => "Mozilla/5.0 Electron/41.5.0 t3code/1.2.3"), | |
| 16 | setPermissionRequestHandler: vi.fn(), | |
| 17 | setUserAgent: vi.fn(), | |
| 18 | })), | |
| 19 | }, | |
| 20 | })); | |
| 21 | ||
| 11 | 22 | import * as DesktopAssets from "../app/DesktopAssets.ts"; |
| 12 | 23 | import * as DesktopConfig from "../app/DesktopConfig.ts"; |
| 13 | 24 | import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; |
apps/desktop/src/window/DesktopWindow.ts+1| ... | ... | @@ -171,6 +171,7 @@ const make = Effect.gen(function* () { |
| 171 | 171 | const createWindow = Effect.fn("desktop.window.createWindow")(function* ( |
| 172 | 172 | backendHttpUrl: URL, |
| 173 | 173 | ): Effect.fn.Return<Electron.BrowserWindow, DesktopWindowError> { |
| 174 | previewViewManager.getBrowserSession(); | |
| 174 | 175 | const applicationUrl = environment.isDevelopment |
| 175 | 176 | ? yield* resolveDesktopDevServerUrl(environment) |
| 176 | 177 | : backendHttpUrl.href; |
apps/web/src/components/preview/PreviewView.tsx+10-4| ... | ... | @@ -12,6 +12,7 @@ import { selectThreadPreviewState, usePreviewStateStore } from "~/previewStateSt |
| 12 | 12 | |
| 13 | 13 | import { previewBridge } from "./previewBridge"; |
| 14 | 14 | import { subscribePreviewAction } from "./previewActionBus"; |
| 15 | import { openPreviewSession } from "./openPreviewSession"; | |
| 15 | 16 | import { PreviewChromeRow } from "./PreviewChromeRow"; |
| 16 | 17 | import { PreviewEmptyState } from "./PreviewEmptyState"; |
| 17 | 18 | import { PreviewMoreMenu } from "./PreviewMoreMenu"; |
| ... | ... | @@ -41,6 +42,7 @@ export function PreviewView({ threadRef, configuredUrls, visible }: Props) { |
| 41 | 42 | const previewState = usePreviewStateStore((state) => |
| 42 | 43 | selectThreadPreviewState(state.byThreadKey, threadRef), |
| 43 | 44 | ); |
| 45 | const applyServerSnapshot = usePreviewStateStore((state) => state.applyServerSnapshot); | |
| 44 | 46 | const rememberUrl = usePreviewStateStore((state) => state.rememberUrl); |
| 45 | 47 | const addElementContext = useComposerDraftStore((store) => store.addElementContext); |
| 46 | 48 | |
| ... | ... | @@ -74,15 +76,19 @@ export function PreviewView({ threadRef, configuredUrls, visible }: Props) { |
| 74 | 76 | await previewBridge.navigate(tabId, next); |
| 75 | 77 | rememberUrl(threadRef, next); |
| 76 | 78 | } else { |
| 77 | const resolved = await api.preview.open({ threadId: threadRef.threadId, url: next }); | |
| 78 | const resolvedUrl = resolved.navStatus._tag === "Idle" ? next : resolved.navStatus.url; | |
| 79 | rememberUrl(threadRef, resolvedUrl); | |
| 79 | await openPreviewSession({ | |
| 80 | previewApi: api.preview, | |
| 81 | threadRef, | |
| 82 | url: next, | |
| 83 | applyServerSnapshot, | |
| 84 | rememberUrl, | |
| 85 | }); | |
| 80 | 86 | } |
| 81 | 87 | } catch { |
| 82 | 88 | // Server-side `failed` event renders the unreachable view. |
| 83 | 89 | } |
| 84 | 90 | }, |
| 85 | [rememberUrl, tabId, threadRef], | |
| 91 | [applyServerSnapshot, rememberUrl, tabId, threadRef], | |
| 86 | 92 | ); |
| 87 | 93 | |
| 88 | 94 | const handleRefresh = useCallback(() => { |
apps/web/src/components/preview/openPreviewSession.test.ts created+42| ... | ... | @@ -0,0 +1,42 @@ |
| 1 | import type { EnvironmentApi, PreviewSessionSnapshot, ScopedThreadRef } from "@t3tools/contracts"; | |
| 2 | import { describe, expect, it, vi } from "vite-plus/test"; | |
| 3 | ||
| 4 | import { openPreviewSession } from "./openPreviewSession"; | |
| 5 | ||
| 6 | const threadRef = { | |
| 7 | environmentId: "local" as ScopedThreadRef["environmentId"], | |
| 8 | threadId: "thread-1" as ScopedThreadRef["threadId"], | |
| 9 | }; | |
| 10 | ||
| 11 | const snapshot: PreviewSessionSnapshot = { | |
| 12 | threadId: threadRef.threadId, | |
| 13 | tabId: "tab-1", | |
| 14 | navStatus: { | |
| 15 | _tag: "Loading", | |
| 16 | url: "https://t3.chat/", | |
| 17 | title: "", | |
| 18 | }, | |
| 19 | canGoBack: false, | |
| 20 | canGoForward: false, | |
| 21 | updatedAt: "2026-06-11T23:00:00.000Z", | |
| 22 | }; | |
| 23 | ||
| 24 | describe("openPreviewSession", () => { | |
| 25 | it("applies the RPC response without waiting for a preview event", async () => { | |
| 26 | const open = vi.fn(async () => snapshot); | |
| 27 | const applyServerSnapshot = vi.fn(); | |
| 28 | const rememberUrl = vi.fn(); | |
| 29 | ||
| 30 | await openPreviewSession({ | |
| 31 | previewApi: { open } as Pick<EnvironmentApi["preview"], "open">, | |
| 32 | threadRef, | |
| 33 | url: "t3.chat", | |
| 34 | applyServerSnapshot, | |
| 35 | rememberUrl, | |
| 36 | }); | |
| 37 | ||
| 38 | expect(open).toHaveBeenCalledWith({ threadId: "thread-1", url: "t3.chat" }); | |
| 39 | expect(applyServerSnapshot).toHaveBeenCalledWith(threadRef, snapshot); | |
| 40 | expect(rememberUrl).toHaveBeenCalledWith(threadRef, "https://t3.chat/"); | |
| 41 | }); | |
| 42 | }); |
apps/web/src/components/preview/openPreviewSession.ts created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | import type { EnvironmentApi, ScopedThreadRef } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | import type { PreviewStateStoreState } from "~/previewStateStore"; | |
| 4 | ||
| 5 | interface OpenPreviewSessionInput { | |
| 6 | previewApi: Pick<EnvironmentApi["preview"], "open">; | |
| 7 | threadRef: ScopedThreadRef; | |
| 8 | url: string; | |
| 9 | applyServerSnapshot: PreviewStateStoreState["applyServerSnapshot"]; | |
| 10 | rememberUrl: PreviewStateStoreState["rememberUrl"]; | |
| 11 | } | |
| 12 | ||
| 13 | export async function openPreviewSession(input: OpenPreviewSessionInput): Promise<void> { | |
| 14 | const snapshot = await input.previewApi.open({ | |
| 15 | threadId: input.threadRef.threadId, | |
| 16 | url: input.url, | |
| 17 | }); | |
| 18 | input.applyServerSnapshot(input.threadRef, snapshot); | |
| 19 | input.rememberUrl( | |
| 20 | input.threadRef, | |
| 21 | snapshot.navStatus._tag === "Idle" ? input.url : snapshot.navStatus.url, | |
| 22 | ); | |
| 23 | } |
apps/web/src/previewStateStore.ts+1-1| ... | ... | @@ -42,7 +42,7 @@ const EMPTY_THREAD_PREVIEW_STATE: ThreadPreviewState = Object.freeze({ |
| 42 | 42 | recentlySeenUrls: [] as string[], |
| 43 | 43 | }); |
| 44 | 44 | |
| 45 | interface PreviewStateStoreState { | |
| 45 | export interface PreviewStateStoreState { | |
| 46 | 46 | byThreadKey: Record<string, ThreadPreviewState>; |
| 47 | 47 | applyServerEvent: (ref: ScopedThreadRef, event: PreviewEvent) => void; |
| 48 | 48 | applyServerSnapshot: (ref: ScopedThreadRef, snapshot: PreviewSessionSnapshot | null) => void; |