authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 21:22:04-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 21:22:04-07:00
log01cd564557c2e96019f7e7060869097bbe4f6805
tree48dffaa8a917b91d61cf4dda8f4c0bb2524a6590
parent4ee719a094d4c4f30fab88c1f6969483890e223e
signature Signed by PGP key B5690EEEBB952194

[codex] Align desktop preview Effect services (#3199)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 143 insertions(+), 138 deletions(-)

apps/desktop/src/ipc/methods/preview.ts+1-1
......@@ -96,7 +96,7 @@ const tabMethod = (
9696 channel: string,
9797 name: string,
9898 invoke: (
99 manager: PreviewManager.PreviewManagerShape,
99 manager: PreviewManager.PreviewManager["Service"],
100100 tabId: string,
101101 ) => Effect.Effect<void, PreviewManager.PreviewManagerError>,
102102) =>
apps/desktop/src/main.ts+2-2
......@@ -46,7 +46,7 @@ import * as DesktopSshEnvironment from "./ssh/DesktopSshEnvironment.ts";
4646import * as DesktopSshPasswordPrompts from "./ssh/DesktopSshPasswordPrompts.ts";
4747import * as DesktopState from "./app/DesktopState.ts";
4848import * as DesktopUpdates from "./updates/DesktopUpdates.ts";
49import * as PreviewBrowserSession from "./preview/BrowserSession.ts";
49import * as BrowserSession from "./preview/BrowserSession.ts";
5050import * as PreviewManager from "./preview/Manager.ts";
5151import * as DesktopWindow from "./window/DesktopWindow.ts";
5252
......@@ -133,7 +133,7 @@ const desktopServerExposureLayer = DesktopServerExposure.layer.pipe(
133133);
134134
135135const desktopPreviewLayer = PreviewManager.layer.pipe(
136 Layer.provideMerge(PreviewBrowserSession.layer),
136 Layer.provideMerge(BrowserSession.layer),
137137 Layer.provideMerge(desktopFoundationLayer),
138138);
139139
apps/desktop/src/preview/BrowserSession.ts+20-18
......@@ -2,36 +2,38 @@ import type { Session } from "electron";
22import { session } from "electron";
33import * as Context from "effect/Context";
44import * as Crypto from "effect/Crypto";
5import * as Data from "effect/Data";
65import * as Effect from "effect/Effect";
76import * as Encoding from "effect/Encoding";
87import * as Layer from "effect/Layer";
8import * as Schema from "effect/Schema";
99import * as SynchronizedRef from "effect/SynchronizedRef";
1010
1111const PREVIEW_PARTITION_PREFIX = "persist:t3code-preview-";
1212
13export class BrowserSessionError extends Data.TaggedError("BrowserSessionError")<{
14 readonly operation: string;
15 readonly cause: unknown;
16}> {
17 override get message() {
13export class BrowserSessionError extends Schema.TaggedErrorClass<BrowserSessionError>()(
14 "BrowserSessionError",
15 {
16 operation: Schema.Literals(["getPartition", "getSession", "clearCookies", "clearCache"]),
17 cause: Schema.Defect(),
18 },
19) {
20 override get message(): string {
1821 return `Desktop preview browser session operation failed: ${this.operation}`;
1922 }
2023}
2124
22export interface BrowserSessionShape {
23 readonly getPartition: (scope?: string) => Effect.Effect<string, BrowserSessionError>;
24 readonly isPartition: (partition: string) => boolean;
25 readonly getSession: (scope?: string) => Effect.Effect<Session, BrowserSessionError>;
26 readonly clearCookies: () => Effect.Effect<void, BrowserSessionError>;
27 readonly clearCache: () => Effect.Effect<void, BrowserSessionError>;
28}
29
30export class BrowserSession extends Context.Service<BrowserSession, BrowserSessionShape>()(
31 "@t3tools/desktop/preview/BrowserSession",
32) {}
25export class BrowserSession extends Context.Service<
26 BrowserSession,
27 {
28 readonly getPartition: (scope?: string) => Effect.Effect<string, BrowserSessionError>;
29 readonly isPartition: (partition: string) => boolean;
30 readonly getSession: (scope?: string) => Effect.Effect<Session, BrowserSessionError>;
31 readonly clearCookies: () => Effect.Effect<void, BrowserSessionError>;
32 readonly clearCache: () => Effect.Effect<void, BrowserSessionError>;
33 }
34>()("@t3tools/desktop/preview/BrowserSession") {}
3335
34const make = Effect.gen(function* BrowserSessionMake() {
36export const make = Effect.gen(function* BrowserSessionMake() {
3537 const crypto = yield* Crypto.Crypto;
3638 const sessionsRef = yield* SynchronizedRef.make<ReadonlyMap<string, Session>>(new Map());
3739
apps/desktop/src/preview/Manager.ts+120-117
......@@ -37,7 +37,6 @@ import {
3737import * as Cause from "effect/Cause";
3838import * as Clock from "effect/Clock";
3939import * as Context from "effect/Context";
40import * as Data from "effect/Data";
4140import * as DateTime from "effect/DateTime";
4241import * as Effect from "effect/Effect";
4342import * as Exit from "effect/Exit";
......@@ -432,15 +431,17 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
432431 ) {
433432 const tabs = yield* SynchronizedRef.get(tabsRef);
434433 const tab = tabs.get(tabId);
435 if (!tab) return yield* fail("requireWebContents", new PreviewTabNotFoundError(tabId));
434 if (!tab) {
435 return yield* fail("requireWebContents", new PreviewTabNotFoundError({ tabId }));
436 }
436437 if (tab.webContentsId == null) {
437 return yield* fail("requireWebContents", new PreviewWebviewNotInitializedError(tabId));
438 return yield* fail("requireWebContents", new PreviewWebviewNotInitializedError({ tabId }));
438439 }
439440 const wc = webContents.fromId(tab.webContentsId);
440441 if (!wc) {
441442 return yield* fail(
442443 "requireWebContents",
443 new PreviewWebContentsNotFoundError(tabId, tab.webContentsId),
444 new PreviewWebContentsNotFoundError({ tabId, webContentsId: tab.webContentsId }),
444445 );
445446 }
446447 return wc;
......@@ -845,7 +846,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
845846 });
846847 } else {
847848 const error = Option.getOrNull(Cause.findErrorOption(exit.cause));
848 const underlying = error instanceof PreviewManagerError ? error.cause : error;
849 const underlying = isPreviewManagerError(error) ? error.cause : error;
849850 const interrupted =
850851 underlying instanceof Error &&
851852 underlying.name === "PreviewAutomationControlInterruptedError";
......@@ -1161,7 +1162,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
11611162 ) {
11621163 const tab = (yield* SynchronizedRef.get(tabsRef)).get(tabId);
11631164 if (!tab) {
1164 return yield* fail("registerWebview", new PreviewTabNotFoundError(tabId));
1165 return yield* fail("registerWebview", new PreviewTabNotFoundError({ tabId }));
11651166 }
11661167 const wc = webContents.fromId(webContentsId);
11671168 const mainWindow = yield* Ref.get(mainWindowRef);
......@@ -1172,7 +1173,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
11721173 ) {
11731174 return yield* fail(
11741175 "registerWebview",
1175 new PreviewWebContentsNotFoundError(tabId, webContentsId),
1176 new PreviewWebContentsNotFoundError({ tabId, webContentsId }),
11761177 );
11771178 }
11781179 const attached = yield* Ref.get(attachedRef);
......@@ -1224,7 +1225,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
12241225 ] as const;
12251226 });
12261227 if (Option.isNone(registration)) {
1227 return yield* fail("registerWebview", new PreviewTabNotFoundError(tabId));
1228 return yield* fail("registerWebview", new PreviewTabNotFoundError({ tabId }));
12281229 }
12291230 const { state: registered, pendingUrl } = registration.value;
12301231 yield* emit(tabId, registered);
......@@ -2200,129 +2201,131 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
22002201 };
22012202});
22022203
2203export class PreviewTabNotFoundError extends Error {
2204 readonly tabId: string;
2205 constructor(tabId: string) {
2206 super(`Preview tab not found: ${tabId}`);
2207 this.name = "PreviewTabNotFoundError";
2208 this.tabId = tabId;
2204export class PreviewTabNotFoundError extends Schema.TaggedErrorClass<PreviewTabNotFoundError>()(
2205 "PreviewTabNotFoundError",
2206 { tabId: Schema.String },
2207) {
2208 override get message(): string {
2209 return `Preview tab not found: ${this.tabId}`;
22092210 }
22102211}
22112212
2212export class PreviewWebContentsNotFoundError extends Error {
2213 readonly tabId: string;
2214 readonly webContentsId: number;
2215 constructor(tabId: string, webContentsId: number) {
2216 super(`WebContents ${webContentsId} not found for preview tab ${tabId}`);
2217 this.name = "PreviewWebContentsNotFoundError";
2218 this.tabId = tabId;
2219 this.webContentsId = webContentsId;
2213export class PreviewWebContentsNotFoundError extends Schema.TaggedErrorClass<PreviewWebContentsNotFoundError>()(
2214 "PreviewWebContentsNotFoundError",
2215 { tabId: Schema.String, webContentsId: Schema.Number },
2216) {
2217 override get message(): string {
2218 return `WebContents ${this.webContentsId} not found for preview tab ${this.tabId}`;
22202219 }
22212220}
22222221
2223export class PreviewWebviewNotInitializedError extends Error {
2224 readonly tabId: string;
2225 constructor(tabId: string) {
2226 super(`Preview tab "${tabId}" has no webview registered`);
2227 this.name = "PreviewWebviewNotInitializedError";
2228 this.tabId = tabId;
2222export class PreviewWebviewNotInitializedError extends Schema.TaggedErrorClass<PreviewWebviewNotInitializedError>()(
2223 "PreviewWebviewNotInitializedError",
2224 { tabId: Schema.String },
2225) {
2226 override get message(): string {
2227 return `Preview tab "${this.tabId}" has no webview registered`;
22292228 }
22302229}
22312230
2232export class PreviewManagerError extends Data.TaggedError("PreviewManagerError")<{
2233 readonly operation: string;
2234 readonly cause: unknown;
2235}> {
2236 override get message() {
2231export class PreviewManagerError extends Schema.TaggedErrorClass<PreviewManagerError>()(
2232 "PreviewManagerError",
2233 {
2234 operation: Schema.String,
2235 cause: Schema.Defect(),
2236 },
2237) {
2238 override get message(): string {
22372239 return `Desktop preview operation failed: ${this.operation}`;
22382240 }
22392241}
22402242
2241export interface PreviewManagerShape {
2242 readonly setMainWindow: (window: BrowserWindow) => Effect.Effect<void, PreviewManagerError>;
2243 readonly getBrowserSession: (scope?: string) => Effect.Effect<Session, PreviewManagerError>;
2244 readonly isBrowserPartition: (partition: string) => boolean;
2245 readonly createTab: (tabId: string) => Effect.Effect<PreviewTabState, PreviewManagerError>;
2246 readonly closeTab: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2247 readonly registerWebview: (
2248 tabId: string,
2249 webContentsId: number,
2250 ) => Effect.Effect<void, PreviewManagerError>;
2251 readonly navigate: (tabId: string, url: string) => Effect.Effect<void, PreviewManagerError>;
2252 readonly goBack: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2253 readonly goForward: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2254 readonly refresh: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2255 readonly zoomIn: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2256 readonly zoomOut: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2257 readonly resetZoom: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2258 readonly hardReload: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2259 readonly openDevTools: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2260 readonly clearCookies: () => Effect.Effect<void, PreviewManagerError>;
2261 readonly clearCache: () => Effect.Effect<void, PreviewManagerError>;
2262 readonly getBrowserPartition: (scope?: string) => Effect.Effect<string, PreviewManagerError>;
2263 readonly setAnnotationTheme: (
2264 theme: DesktopPreviewAnnotationTheme,
2265 ) => Effect.Effect<void, PreviewManagerError>;
2266 readonly pickElement: (
2267 tabId: string,
2268 ) => Effect.Effect<PreviewAnnotationPayload | null, PreviewManagerError>;
2269 readonly cancelPickElement: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2270 readonly captureScreenshot: (
2271 tabId: string,
2272 ) => Effect.Effect<DesktopPreviewScreenshotArtifact, PreviewManagerError>;
2273 readonly revealArtifact: (path: string) => Effect.Effect<void, PreviewManagerError>;
2274 readonly copyArtifactToClipboard: (path: string) => Effect.Effect<void, PreviewManagerError>;
2275 readonly startRecording: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2276 readonly stopRecording: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2277 readonly saveRecording: (
2278 tabId: string,
2279 mimeType: string,
2280 data: Uint8Array,
2281 ) => Effect.Effect<DesktopPreviewRecordingArtifact, PreviewManagerError>;
2282 readonly automationStatus: (
2283 tabId: string,
2284 ) => Effect.Effect<PreviewAutomationStatus, PreviewManagerError>;
2285 readonly automationSnapshot: (
2286 tabId: string,
2287 ) => Effect.Effect<PreviewAutomationSnapshot, PreviewManagerError>;
2288 readonly automationClick: (
2289 tabId: string,
2290 input: PreviewAutomationClickInput,
2291 ) => Effect.Effect<void, PreviewManagerError>;
2292 readonly automationType: (
2293 tabId: string,
2294 input: PreviewAutomationTypeInput,
2295 ) => Effect.Effect<void, PreviewManagerError>;
2296 readonly automationPress: (
2297 tabId: string,
2298 input: PreviewAutomationPressInput,
2299 ) => Effect.Effect<void, PreviewManagerError>;
2300 readonly automationScroll: (
2301 tabId: string,
2302 input: PreviewAutomationScrollInput,
2303 ) => Effect.Effect<void, PreviewManagerError>;
2304 readonly automationEvaluate: (
2305 tabId: string,
2306 input: PreviewAutomationEvaluateInput,
2307 ) => Effect.Effect<unknown, PreviewManagerError>;
2308 readonly automationWaitFor: (
2309 tabId: string,
2310 input: PreviewAutomationWaitForInput,
2311 ) => Effect.Effect<void, PreviewManagerError>;
2312 readonly subscribeStateChanges: (listener: Listener) => Effect.Effect<void, never, Scope.Scope>;
2313 readonly subscribePointerEvents: (
2314 listener: PointerEventListener,
2315 ) => Effect.Effect<void, never, Scope.Scope>;
2316 readonly subscribeRecordingFrames: (
2317 listener: RecordingFrameListener,
2318 ) => Effect.Effect<void, never, Scope.Scope>;
2319}
2320
2321export class PreviewManager extends Context.Service<PreviewManager, PreviewManagerShape>()(
2322 "@t3tools/desktop/preview/Manager/PreviewManager",
2323) {}
2243const isPreviewManagerError = Schema.is(PreviewManagerError);
2244
2245export class PreviewManager extends Context.Service<
2246 PreviewManager,
2247 {
2248 readonly setMainWindow: (window: BrowserWindow) => Effect.Effect<void, PreviewManagerError>;
2249 readonly getBrowserSession: (scope?: string) => Effect.Effect<Session, PreviewManagerError>;
2250 readonly isBrowserPartition: (partition: string) => boolean;
2251 readonly createTab: (tabId: string) => Effect.Effect<PreviewTabState, PreviewManagerError>;
2252 readonly closeTab: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2253 readonly registerWebview: (
2254 tabId: string,
2255 webContentsId: number,
2256 ) => Effect.Effect<void, PreviewManagerError>;
2257 readonly navigate: (tabId: string, url: string) => Effect.Effect<void, PreviewManagerError>;
2258 readonly goBack: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2259 readonly goForward: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2260 readonly refresh: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2261 readonly zoomIn: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2262 readonly zoomOut: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2263 readonly resetZoom: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2264 readonly hardReload: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2265 readonly openDevTools: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2266 readonly clearCookies: () => Effect.Effect<void, PreviewManagerError>;
2267 readonly clearCache: () => Effect.Effect<void, PreviewManagerError>;
2268 readonly getBrowserPartition: (scope?: string) => Effect.Effect<string, PreviewManagerError>;
2269 readonly setAnnotationTheme: (
2270 theme: DesktopPreviewAnnotationTheme,
2271 ) => Effect.Effect<void, PreviewManagerError>;
2272 readonly pickElement: (
2273 tabId: string,
2274 ) => Effect.Effect<PreviewAnnotationPayload | null, PreviewManagerError>;
2275 readonly cancelPickElement: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2276 readonly captureScreenshot: (
2277 tabId: string,
2278 ) => Effect.Effect<DesktopPreviewScreenshotArtifact, PreviewManagerError>;
2279 readonly revealArtifact: (path: string) => Effect.Effect<void, PreviewManagerError>;
2280 readonly copyArtifactToClipboard: (path: string) => Effect.Effect<void, PreviewManagerError>;
2281 readonly startRecording: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2282 readonly stopRecording: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2283 readonly saveRecording: (
2284 tabId: string,
2285 mimeType: string,
2286 data: Uint8Array,
2287 ) => Effect.Effect<DesktopPreviewRecordingArtifact, PreviewManagerError>;
2288 readonly automationStatus: (
2289 tabId: string,
2290 ) => Effect.Effect<PreviewAutomationStatus, PreviewManagerError>;
2291 readonly automationSnapshot: (
2292 tabId: string,
2293 ) => Effect.Effect<PreviewAutomationSnapshot, PreviewManagerError>;
2294 readonly automationClick: (
2295 tabId: string,
2296 input: PreviewAutomationClickInput,
2297 ) => Effect.Effect<void, PreviewManagerError>;
2298 readonly automationType: (
2299 tabId: string,
2300 input: PreviewAutomationTypeInput,
2301 ) => Effect.Effect<void, PreviewManagerError>;
2302 readonly automationPress: (
2303 tabId: string,
2304 input: PreviewAutomationPressInput,
2305 ) => Effect.Effect<void, PreviewManagerError>;
2306 readonly automationScroll: (
2307 tabId: string,
2308 input: PreviewAutomationScrollInput,
2309 ) => Effect.Effect<void, PreviewManagerError>;
2310 readonly automationEvaluate: (
2311 tabId: string,
2312 input: PreviewAutomationEvaluateInput,
2313 ) => Effect.Effect<unknown, PreviewManagerError>;
2314 readonly automationWaitFor: (
2315 tabId: string,
2316 input: PreviewAutomationWaitForInput,
2317 ) => Effect.Effect<void, PreviewManagerError>;
2318 readonly subscribeStateChanges: (listener: Listener) => Effect.Effect<void, never, Scope.Scope>;
2319 readonly subscribePointerEvents: (
2320 listener: PointerEventListener,
2321 ) => Effect.Effect<void, never, Scope.Scope>;
2322 readonly subscribeRecordingFrames: (
2323 listener: RecordingFrameListener,
2324 ) => Effect.Effect<void, never, Scope.Scope>;
2325 }
2326>()("@t3tools/desktop/preview/Manager/PreviewManager") {}
23242327
2325const make = Effect.gen(function* PreviewManagerMake() {
2328export const make = Effect.gen(function* PreviewManagerMake() {
23262329 const environment = yield* DesktopEnvironment.DesktopEnvironment;
23272330 const browserSession = yield* BrowserSession.BrowserSession;
23282331 const operations = yield* makeNativeOperations(environment.browserArtifactsDir);