/** * Desktop side of the in-app browser preview. * * Hosts per-tab Chromium WebContents references (the actual <webview> * elements live in the renderer; we only attach listeners and forward state * here). Single layer-scoped browser session partition. */import type { DesktopPreviewAnnotationTheme, DesktopPreviewColorScheme, DesktopPreviewPointerEvent, PreviewAnnotationPayload, PreviewAnnotationRect, DesktopPreviewRecordingArtifact, DesktopPreviewRecordingFrame, DesktopPreviewScreenshotArtifact, PreviewAutomationClickInput, PreviewAutomationActionEvent, PreviewAutomationConsoleEntry, PreviewAutomationEvaluateInput, PreviewAutomationPressInput, PreviewAutomationNetworkEntry, PreviewAutomationScrollInput, PreviewAutomationSnapshot, PreviewAutomationStatus, PreviewAutomationTypeInput, PreviewAutomationWaitForInput,} from "@t3tools/contracts";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import { normalizePreviewUrl } from "@t3tools/shared/preview";import { type BrowserWindow, type Session, clipboard, nativeImage, shell, webContents,} from "electron";import * as Cause from "effect/Cause";import * as Clock from "effect/Clock";import * as Context from "effect/Context";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Semaphore from "effect/Semaphore";import * as Scope from "effect/Scope";import * as SynchronizedRef from "effect/SynchronizedRef"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import * as BrowserSession from "./BrowserSession.ts";import { ANNOTATION_CAPTURED_CHANNEL, ANNOTATION_THEME_CHANNEL, CANCEL_PICK_CHANNEL, ELEMENT_PICKED_CHANNEL, HUMAN_INPUT_CHANNEL, START_PICK_CHANNEL,} from "./GuestProtocol.ts";import { isPreviewAnnotationPayload } from "./PickedElementPayload.ts";import { playwrightInjectedRuntimeInstallExpression } from "./PlaywrightInjectedRuntime.ts";import { makePreviewAutomationKeySequence } from "./PreviewKeyboard.ts"; export type PreviewNavStatus = | { kind: "Idle" } | { kind: "Loading"; url: string; title: string } | { kind: "Success"; url: string; title: string } | { kind: "LoadFailed"; url: string; title: string; code: number; description: string; }; export interface PreviewTabState { tabId: string; webContentsId: number | null; navStatus: PreviewNavStatus; canGoBack: boolean; canGoForward: boolean; zoomFactor: number; colorScheme: DesktopPreviewColorScheme; controller: "human" | "agent" | "none"; updatedAt: string;} /** Discrete zoom levels mirroring Chrome's preset list. */const ZOOM_LEVELS: ReadonlyArray<number> = [ 0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 4.0, 5.0,]; const DEFAULT_ZOOM_FACTOR = 1.0;const ZOOM_EPSILON = 0.001;const MAX_EVALUATION_BYTES = 64_000;const MAX_VISIBLE_TEXT_LENGTH = 20_000;const MAX_INTERACTIVE_ELEMENTS = 200;const MAX_SCREENSHOT_WIDTH = 1280;const DIAGNOSTIC_BUFFER_LIMIT = 200;const MAX_ARTIFACT_SITE_SLUG_LENGTH = 80;const AGENT_CURSOR_MOVE_MS = 160;const AGENT_CURSOR_CLICK_LEAD_MS = 40;const encodeUnknownJson = Schema.encodeUnknownEffect(Schema.UnknownFromJsonString);const DEFAULT_ANNOTATION_THEME: DesktopPreviewAnnotationTheme = { colorScheme: "light", radius: "0.625rem", background: "white", foreground: "oklch(0.269 0 0)", popover: "white", popoverForeground: "oklch(0.269 0 0)", primary: "oklch(0.488 0.217 264)", primaryForeground: "white", muted: "rgb(0 0 0 / 4%)", mutedForeground: "oklch(0.556 0 0)", accent: "rgb(0 0 0 / 4%)", accentForeground: "oklch(0.269 0 0)", border: "rgb(0 0 0 / 8%)", input: "rgb(0 0 0 / 10%)", ring: "oklch(0.488 0.217 264)", fontSans: "system-ui, sans-serif", fontMono: "ui-monospace, monospace",}; const artifactSiteSlug = (rawUrl: string): string => { try { const url = new URL(rawUrl); const slug = url.hostname .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, MAX_ARTIFACT_SITE_SLUG_LENGTH) .replace(/-+$/g, ""); return slug || "site"; } catch { return "site"; }}; interface CdpEvaluationResult { readonly result?: { readonly value?: unknown; readonly description?: string; }; readonly exceptionDetails?: { readonly text?: string; readonly exception?: { readonly description?: string }; };} export const PreviewAutomationSelectorKind = Schema.Literals([ "focused-element", "selector", "locator",]);export type PreviewAutomationSelectorKind = typeof PreviewAutomationSelectorKind.Type; export const PreviewAutomationEvaluationDetailKind = Schema.Literals([ "exception-description", "exception-text", "unknown",]);export type PreviewAutomationEvaluationDetailKind = typeof PreviewAutomationEvaluationDetailKind.Type; const previewAutomationEvaluationDetail = (exceptionDetails: unknown) => { if (typeof exceptionDetails !== "object" || exceptionDetails === null) { return { detailKind: "unknown" as const }; } const details = exceptionDetails as Record<string, unknown>; const exception = details["exception"]; const description = typeof exception === "object" && exception !== null && typeof (exception as Record<string, unknown>)["description"] === "string" ? (exception as Record<string, unknown>)["description"] : undefined; if (typeof description === "string" && description.length > 0) { return { detailKind: "exception-description" as const, detail: description }; } const text = details["text"]; if (typeof text === "string" && text.length > 0) { return { detailKind: "exception-text" as const, detail: text }; } return { detailKind: "unknown" as const };}; const previewAutomationTargetLabel = ( selectorKind: PreviewAutomationSelectorKind, selectorLength?: number,) => selectorKind === "focused-element" ? "the focused element" : `${selectorKind} (${selectorLength ?? 0} characters)`; interface PreviewOperationContext { readonly operation: string; readonly tabId?: string; readonly webContentsId?: number; readonly artifactPath?: string;} const normalizeCaptureRect = (value: unknown): PreviewAnnotationRect | null => { if (typeof value !== "object" || value === null) return null; const rect = value as Record<string, unknown>; const x = rect["x"]; const y = rect["y"]; const width = rect["width"]; const height = rect["height"]; if ( typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y) || typeof width !== "number" || !Number.isFinite(width) || typeof height !== "number" || !Number.isFinite(height) || width <= 0 || height <= 0 ) { return null; } return { x: Math.max(0, Math.floor(x)), y: Math.max(0, Math.floor(y)), width: Math.max(1, Math.ceil(width)), height: Math.max(1, Math.ceil(height)), };}; const captureAnnotationScreenshot = ( tabId: string, wc: Electron.WebContents, cropRect: PreviewAnnotationRect | null,): Effect.Effect<PreviewAnnotationPayload["screenshot"], PreviewManagerError> => Effect.tryPromise({ try: () => wc.capturePage( cropRect ? { x: cropRect.x, y: cropRect.y, width: cropRect.width, height: cropRect.height, } : undefined, ), catch: (cause) => new PreviewOperationError({ operation: "captureAnnotationScreenshot", tabId, webContentsId: wc.id, cause, }), }).pipe( Effect.map((image) => { const size = image.getSize(); return { dataUrl: image.toDataURL(), width: size.width, height: size.height, cropRect: cropRect ?? { x: 0, y: 0, width: size.width, height: size.height }, }; }), ); const findZoomStep = (current: number): number => { const index = ZOOM_LEVELS.findIndex( (level) => Math.abs(level - current) < ZOOM_EPSILON || level > current, ); if (index < 0) return ZOOM_LEVELS.length - 1; return Math.abs(ZOOM_LEVELS[index]! - current) < ZOOM_EPSILON ? index : index - 1;}; const nextZoomLevel = (current: number, direction: "in" | "out"): number => { const step = findZoomStep(current); if (direction === "in") { return ZOOM_LEVELS[Math.min(step + 1, ZOOM_LEVELS.length - 1)] ?? current; } return ZOOM_LEVELS[Math.max(step - 1, 0)] ?? current;}; type Listener = (tabId: string, state: PreviewTabState) => Effect.Effect<void>;type RecordingFrameListener = (frame: DesktopPreviewRecordingFrame) => Effect.Effect<void>; type PreviewInputSignal = | { readonly kind: "pointer"; readonly x: number; readonly y: number; readonly button: number } | { readonly kind: "key"; readonly key: string; readonly code: string }; interface ManagedListeners { readonly scope: Scope.Closeable;} interface PickSession { readonly cancel: Effect.Effect<void>;} interface BrowserControlSession { readonly webContentsId: number; readonly semaphore: Semaphore.Semaphore; readonly scope: Scope.Closeable; readonly onMessage: ( event: Electron.Event, method: string, params: Record<string, unknown>, ) => void;} interface BrowserDiagnostics { readonly consoleEntries: ReadonlyArray<PreviewAutomationConsoleEntry>; readonly networkEntries: ReadonlyArray<PreviewAutomationNetworkEntry>; readonly requests: ReadonlyMap<string, { url: string; method: string }>;} type PointerEventListener = (event: DesktopPreviewPointerEvent) => Effect.Effect<void>; interface ExpectedAgentInput { readonly signal: PreviewInputSignal; readonly expiresAt: number;} const APP_FORWARDED_SHORTCUTS: ReadonlyArray<{ key: string; meta: boolean; shift: boolean; control: boolean;}> = Object.freeze([ // mod+shift+J → preview.toggle { key: "j", meta: true, shift: true, control: false }, // mod+K → command palette { key: "k", meta: true, shift: false, control: false }, // mod+, → settings (macOS convention) { key: ",", meta: true, shift: false, control: false }, // mod+W → close tab/panel { key: "w", meta: true, shift: false, control: false },]); const isPreviewInputSignal = (value: unknown): value is PreviewInputSignal => { if (typeof value !== "object" || value === null || !("kind" in value)) return false; if (value.kind === "pointer") { return ( "x" in value && typeof value.x === "number" && "y" in value && typeof value.y === "number" && "button" in value && typeof value.button === "number" ); } return ( value.kind === "key" && "key" in value && typeof value.key === "string" && "code" in value && typeof value.code === "string" );}; const inputSignalsMatch = (left: PreviewInputSignal, right: PreviewInputSignal): boolean => { if (left.kind !== right.kind) return false; if (left.kind === "pointer" && right.kind === "pointer") { return ( Math.abs(left.x - right.x) <= 1 && Math.abs(left.y - right.y) <= 1 && left.button === right.button ); } return ( left.kind === "key" && right.kind === "key" && left.key === right.key && left.code === right.code );}; const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function* ( artifactDirectory: string,) { const fileSystem = yield* FileSystem.FileSystem; const hostPlatform = yield* HostProcessPlatform; const path = yield* Path.Path; const parentScope = yield* Scope.Scope; const context = yield* Effect.context<never>(); const runFork = Effect.runForkWith(context); const resolvedArtifactDirectory = path.resolve(artifactDirectory); const playwrightInstallExpression = yield* Effect.cached( playwrightInjectedRuntimeInstallExpression(), ); const annotationThemeRef = yield* Ref.make(DEFAULT_ANNOTATION_THEME); const mainWindowRef = yield* Ref.make<Option.Option<BrowserWindow>>(Option.none()); const tabsRef = yield* SynchronizedRef.make<ReadonlyMap<string, PreviewTabState>>(new Map()); const attachedRef = yield* Ref.make<ReadonlyMap<number, ManagedListeners>>(new Map()); const listenersRef = yield* Ref.make<ReadonlySet<Listener>>(new Set()); const pointerEventListenersRef = yield* Ref.make<ReadonlySet<PointerEventListener>>(new Set()); const recordingFrameListenersRef = yield* Ref.make<ReadonlySet<RecordingFrameListener>>( new Set(), ); const pickSessionsRef = yield* Ref.make<ReadonlyMap<string, PickSession>>(new Map()); const controlSessionsRef = yield* SynchronizedRef.make< ReadonlyMap<number, BrowserControlSession> >(new Map()); const diagnosticsRef = yield* Ref.make<ReadonlyMap<number, BrowserDiagnostics>>(new Map()); const expectedAgentInputsRef = yield* Ref.make< ReadonlyMap<string, ReadonlyArray<ExpectedAgentInput>> >(new Map()); const controlEpochRef = yield* Ref.make<ReadonlyMap<string, number>>(new Map()); const actionTimelineRef = yield* Ref.make< ReadonlyMap<string, ReadonlyArray<PreviewAutomationActionEvent>> >(new Map()); const actionSequenceRef = yield* Ref.make(0); const pointerSequenceRef = yield* Ref.make(0); const recordingTabIdRef = yield* Ref.make<Option.Option<string>>(Option.none()); const attempt = <A>(errorContext: PreviewOperationContext, evaluate: () => A) => Effect.try({ try: evaluate, catch: (cause) => new PreviewOperationError({ ...errorContext, cause }), }); const attemptPromise = <A>( errorContext: PreviewOperationContext, evaluate: () => PromiseLike<A>, ) => Effect.tryPromise({ try: evaluate, catch: (cause) => new PreviewOperationError({ ...errorContext, cause }), }); const currentIso = DateTime.now.pipe(Effect.map(DateTime.formatIso)); const currentMillis = Clock.currentTimeMillis; const encodeJson = (errorContext: PreviewOperationContext, value: unknown) => encodeUnknownJson(value).pipe( Effect.mapError((cause) => new PreviewOperationError({ ...errorContext, cause })), ); const nextCounter = (ref: Ref.Ref<number>) => Ref.modify(ref, (value) => [value, value + 1] as const); const replaceMap = <K, V>( source: ReadonlyMap<K, V>, update: (copy: Map<K, V>) => void, ): ReadonlyMap<K, V> => { const copy = new Map(source); update(copy); return copy; }; const deliverEvent = ( eventKind: "state-change" | "recording-frame" | "pointer-event", tabId: string, delivery: () => Effect.Effect<void>, ) => Effect.suspend(delivery).pipe( Effect.catchCause((cause) => Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.logWarning("Desktop preview event listener failed.", { eventKind, tabId, cause, }), ), ); const emit = Effect.fn("PreviewManager.emit")(function* (tabId: string, state: PreviewTabState) { const listeners = yield* Ref.get(listenersRef); yield* Effect.forEach( listeners, (listener) => deliverEvent("state-change", tabId, () => listener(tabId, state)), { discard: true }, ); }); const update = Effect.fn("PreviewManager.update")(function* ( tabId: string, patch: Partial<PreviewTabState>, ) { const updatedAt = yield* currentIso; const next = yield* SynchronizedRef.modify(tabsRef, (tabs) => { const current = tabs.get(tabId); if (!current) return [Option.none<PreviewTabState>(), tabs] as const; const state: PreviewTabState = { ...current, ...patch, updatedAt }; return [ Option.some(state), replaceMap(tabs, (copy) => { copy.set(tabId, state); }), ] as const; }); if (Option.isSome(next)) yield* emit(tabId, next.value); }); const requireWebContents = Effect.fn("PreviewManager.requireWebContents")(function* ( tabId: string, ) { const tabs = yield* SynchronizedRef.get(tabsRef); const tab = tabs.get(tabId); if (!tab) {Lines 1–500 of 3043. Load more
/** * Desktop side of the in-app browser preview. * * Hosts per-tab Chromium WebContents references (the actual <webview> * elements live in the renderer; we only attach listeners and forward state * here). Single layer-scoped browser session partition. */ import type { DesktopPreviewAnnotationTheme, DesktopPreviewColorScheme, DesktopPreviewPointerEvent, PreviewAnnotationPayload, PreviewAnnotationRect, DesktopPreviewRecordingArtifact, DesktopPreviewRecordingFrame, DesktopPreviewScreenshotArtifact, PreviewAutomationClickInput, PreviewAutomationActionEvent, PreviewAutomationConsoleEntry, PreviewAutomationEvaluateInput, PreviewAutomationPressInput, PreviewAutomationNetworkEntry, PreviewAutomationScrollInput, PreviewAutomationSnapshot, PreviewAutomationStatus, PreviewAutomationTypeInput, PreviewAutomationWaitForInput, } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { normalizePreviewUrl } from "@t3tools/shared/preview"; import { type BrowserWindow, type Session, clipboard, nativeImage, shell, webContents, } from "electron"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; import * as Scope from "effect/Scope"; import * as SynchronizedRef from "effect/SynchronizedRef"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; import * as BrowserSession from "./BrowserSession.ts"; import { ANNOTATION_CAPTURED_CHANNEL, ANNOTATION_THEME_CHANNEL, CANCEL_PICK_CHANNEL, ELEMENT_PICKED_CHANNEL, HUMAN_INPUT_CHANNEL, START_PICK_CHANNEL, } from "./GuestProtocol.ts"; import { isPreviewAnnotationPayload } from "./PickedElementPayload.ts"; import { playwrightInjectedRuntimeInstallExpression } from "./PlaywrightInjectedRuntime.ts"; import { makePreviewAutomationKeySequence } from "./PreviewKeyboard.ts"; export type PreviewNavStatus = | { kind: "Idle" } | { kind: "Loading"; url: string; title: string } | { kind: "Success"; url: string; title: string } | { kind: "LoadFailed"; url: string; title: string; code: number; description: string; }; export interface PreviewTabState { tabId: string; webContentsId: number | null; navStatus: PreviewNavStatus; canGoBack: boolean; canGoForward: boolean; zoomFactor: number; colorScheme: DesktopPreviewColorScheme; controller: "human" | "agent" | "none"; updatedAt: string; } /** Discrete zoom levels mirroring Chrome's preset list. */ const ZOOM_LEVELS: ReadonlyArray<number> = [ 0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 4.0, 5.0, ]; const DEFAULT_ZOOM_FACTOR = 1.0; const ZOOM_EPSILON = 0.001; const MAX_EVALUATION_BYTES = 64_000; const MAX_VISIBLE_TEXT_LENGTH = 20_000; const MAX_INTERACTIVE_ELEMENTS = 200; const MAX_SCREENSHOT_WIDTH = 1280; const DIAGNOSTIC_BUFFER_LIMIT = 200; const MAX_ARTIFACT_SITE_SLUG_LENGTH = 80; const AGENT_CURSOR_MOVE_MS = 160; const AGENT_CURSOR_CLICK_LEAD_MS = 40; const encodeUnknownJson = Schema.encodeUnknownEffect(Schema.UnknownFromJsonString); const DEFAULT_ANNOTATION_THEME: DesktopPreviewAnnotationTheme = { colorScheme: "light", radius: "0.625rem", background: "white", foreground: "oklch(0.269 0 0)", popover: "white", popoverForeground: "oklch(0.269 0 0)", primary: "oklch(0.488 0.217 264)", primaryForeground: "white", muted: "rgb(0 0 0 / 4%)", mutedForeground: "oklch(0.556 0 0)", accent: "rgb(0 0 0 / 4%)", accentForeground: "oklch(0.269 0 0)", border: "rgb(0 0 0 / 8%)", input: "rgb(0 0 0 / 10%)", ring: "oklch(0.488 0.217 264)", fontSans: "system-ui, sans-serif", fontMono: "ui-monospace, monospace", }; const artifactSiteSlug = (rawUrl: string): string => { try { const url = new URL(rawUrl); const slug = url.hostname .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, MAX_ARTIFACT_SITE_SLUG_LENGTH) .replace(/-+$/g, ""); return slug || "site"; } catch { return "site"; } }; interface CdpEvaluationResult { readonly result?: { readonly value?: unknown; readonly description?: string; }; readonly exceptionDetails?: { readonly text?: string; readonly exception?: { readonly description?: string }; }; } export const PreviewAutomationSelectorKind = Schema.Literals([ "focused-element", "selector", "locator", ]); export type PreviewAutomationSelectorKind = typeof PreviewAutomationSelectorKind.Type; export const PreviewAutomationEvaluationDetailKind = Schema.Literals([ "exception-description", "exception-text", "unknown", ]); export type PreviewAutomationEvaluationDetailKind = typeof PreviewAutomationEvaluationDetailKind.Type; const previewAutomationEvaluationDetail = (exceptionDetails: unknown) => { if (typeof exceptionDetails !== "object" || exceptionDetails === null) { return { detailKind: "unknown" as const }; } const details = exceptionDetails as Record<string, unknown>; const exception = details["exception"]; const description = typeof exception === "object" && exception !== null && typeof (exception as Record<string, unknown>)["description"] === "string" ? (exception as Record<string, unknown>)["description"] : undefined; if (typeof description === "string" && description.length > 0) { return { detailKind: "exception-description" as const, detail: description }; } const text = details["text"]; if (typeof text === "string" && text.length > 0) { return { detailKind: "exception-text" as const, detail: text }; } return { detailKind: "unknown" as const }; }; const previewAutomationTargetLabel = ( selectorKind: PreviewAutomationSelectorKind, selectorLength?: number, ) => selectorKind === "focused-element" ? "the focused element" : `${selectorKind} (${selectorLength ?? 0} characters)`; interface PreviewOperationContext { readonly operation: string; readonly tabId?: string; readonly webContentsId?: number; readonly artifactPath?: string; } const normalizeCaptureRect = (value: unknown): PreviewAnnotationRect | null => { if (typeof value !== "object" || value === null) return null; const rect = value as Record<string, unknown>; const x = rect["x"]; const y = rect["y"]; const width = rect["width"]; const height = rect["height"]; if ( typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y) || typeof width !== "number" || !Number.isFinite(width) || typeof height !== "number" || !Number.isFinite(height) || width <= 0 || height <= 0 ) { return null; } return { x: Math.max(0, Math.floor(x)), y: Math.max(0, Math.floor(y)), width: Math.max(1, Math.ceil(width)), height: Math.max(1, Math.ceil(height)), }; }; const captureAnnotationScreenshot = ( tabId: string, wc: Electron.WebContents, cropRect: PreviewAnnotationRect | null, ): Effect.Effect<PreviewAnnotationPayload["screenshot"], PreviewManagerError> => Effect.tryPromise({ try: () => wc.capturePage( cropRect ? { x: cropRect.x, y: cropRect.y, width: cropRect.width, height: cropRect.height, } : undefined, ), catch: (cause) => new PreviewOperationError({ operation: "captureAnnotationScreenshot", tabId, webContentsId: wc.id, cause, }), }).pipe( Effect.map((image) => { const size = image.getSize(); return { dataUrl: image.toDataURL(), width: size.width, height: size.height, cropRect: cropRect ?? { x: 0, y: 0, width: size.width, height: size.height }, }; }), ); const findZoomStep = (current: number): number => { const index = ZOOM_LEVELS.findIndex( (level) => Math.abs(level - current) < ZOOM_EPSILON || level > current, ); if (index < 0) return ZOOM_LEVELS.length - 1; return Math.abs(ZOOM_LEVELS[index]! - current) < ZOOM_EPSILON ? index : index - 1; }; const nextZoomLevel = (current: number, direction: "in" | "out"): number => { const step = findZoomStep(current); if (direction === "in") { return ZOOM_LEVELS[Math.min(step + 1, ZOOM_LEVELS.length - 1)] ?? current; } return ZOOM_LEVELS[Math.max(step - 1, 0)] ?? current; }; type Listener = (tabId: string, state: PreviewTabState) => Effect.Effect<void>; type RecordingFrameListener = (frame: DesktopPreviewRecordingFrame) => Effect.Effect<void>; type PreviewInputSignal = | { readonly kind: "pointer"; readonly x: number; readonly y: number; readonly button: number } | { readonly kind: "key"; readonly key: string; readonly code: string }; interface ManagedListeners { readonly scope: Scope.Closeable; } interface PickSession { readonly cancel: Effect.Effect<void>; } interface BrowserControlSession { readonly webContentsId: number; readonly semaphore: Semaphore.Semaphore; readonly scope: Scope.Closeable; readonly onMessage: ( event: Electron.Event, method: string, params: Record<string, unknown>, ) => void; } interface BrowserDiagnostics { readonly consoleEntries: ReadonlyArray<PreviewAutomationConsoleEntry>; readonly networkEntries: ReadonlyArray<PreviewAutomationNetworkEntry>; readonly requests: ReadonlyMap<string, { url: string; method: string }>; } type PointerEventListener = (event: DesktopPreviewPointerEvent) => Effect.Effect<void>; interface ExpectedAgentInput { readonly signal: PreviewInputSignal; readonly expiresAt: number; } const APP_FORWARDED_SHORTCUTS: ReadonlyArray<{ key: string; meta: boolean; shift: boolean; control: boolean; }> = Object.freeze([ // mod+shift+J → preview.toggle { key: "j", meta: true, shift: true, control: false }, // mod+K → command palette { key: "k", meta: true, shift: false, control: false }, // mod+, → settings (macOS convention) { key: ",", meta: true, shift: false, control: false }, // mod+W → close tab/panel { key: "w", meta: true, shift: false, control: false }, ]); const isPreviewInputSignal = (value: unknown): value is PreviewInputSignal => { if (typeof value !== "object" || value === null || !("kind" in value)) return false; if (value.kind === "pointer") { return ( "x" in value && typeof value.x === "number" && "y" in value && typeof value.y === "number" && "button" in value && typeof value.button === "number" ); } return ( value.kind === "key" && "key" in value && typeof value.key === "string" && "code" in value && typeof value.code === "string" ); }; const inputSignalsMatch = (left: PreviewInputSignal, right: PreviewInputSignal): boolean => { if (left.kind !== right.kind) return false; if (left.kind === "pointer" && right.kind === "pointer") { return ( Math.abs(left.x - right.x) <= 1 && Math.abs(left.y - right.y) <= 1 && left.button === right.button ); } return ( left.kind === "key" && right.kind === "key" && left.key === right.key && left.code === right.code ); }; const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function* ( artifactDirectory: string, ) { const fileSystem = yield* FileSystem.FileSystem; const hostPlatform = yield* HostProcessPlatform; const path = yield* Path.Path; const parentScope = yield* Scope.Scope; const context = yield* Effect.context<never>(); const runFork = Effect.runForkWith(context); const resolvedArtifactDirectory = path.resolve(artifactDirectory); const playwrightInstallExpression = yield* Effect.cached( playwrightInjectedRuntimeInstallExpression(), ); const annotationThemeRef = yield* Ref.make(DEFAULT_ANNOTATION_THEME); const mainWindowRef = yield* Ref.make<Option.Option<BrowserWindow>>(Option.none()); const tabsRef = yield* SynchronizedRef.make<ReadonlyMap<string, PreviewTabState>>(new Map()); const attachedRef = yield* Ref.make<ReadonlyMap<number, ManagedListeners>>(new Map()); const listenersRef = yield* Ref.make<ReadonlySet<Listener>>(new Set()); const pointerEventListenersRef = yield* Ref.make<ReadonlySet<PointerEventListener>>(new Set()); const recordingFrameListenersRef = yield* Ref.make<ReadonlySet<RecordingFrameListener>>( new Set(), ); const pickSessionsRef = yield* Ref.make<ReadonlyMap<string, PickSession>>(new Map()); const controlSessionsRef = yield* SynchronizedRef.make< ReadonlyMap<number, BrowserControlSession> >(new Map()); const diagnosticsRef = yield* Ref.make<ReadonlyMap<number, BrowserDiagnostics>>(new Map()); const expectedAgentInputsRef = yield* Ref.make< ReadonlyMap<string, ReadonlyArray<ExpectedAgentInput>> >(new Map()); const controlEpochRef = yield* Ref.make<ReadonlyMap<string, number>>(new Map()); const actionTimelineRef = yield* Ref.make< ReadonlyMap<string, ReadonlyArray<PreviewAutomationActionEvent>> >(new Map()); const actionSequenceRef = yield* Ref.make(0); const pointerSequenceRef = yield* Ref.make(0); const recordingTabIdRef = yield* Ref.make<Option.Option<string>>(Option.none()); const attempt = <A>(errorContext: PreviewOperationContext, evaluate: () => A) => Effect.try({ try: evaluate, catch: (cause) => new PreviewOperationError({ ...errorContext, cause }), }); const attemptPromise = <A>( errorContext: PreviewOperationContext, evaluate: () => PromiseLike<A>, ) => Effect.tryPromise({ try: evaluate, catch: (cause) => new PreviewOperationError({ ...errorContext, cause }), }); const currentIso = DateTime.now.pipe(Effect.map(DateTime.formatIso)); const currentMillis = Clock.currentTimeMillis; const encodeJson = (errorContext: PreviewOperationContext, value: unknown) => encodeUnknownJson(value).pipe( Effect.mapError((cause) => new PreviewOperationError({ ...errorContext, cause })), ); const nextCounter = (ref: Ref.Ref<number>) => Ref.modify(ref, (value) => [value, value + 1] as const); const replaceMap = <K, V>( source: ReadonlyMap<K, V>, update: (copy: Map<K, V>) => void, ): ReadonlyMap<K, V> => { const copy = new Map(source); update(copy); return copy; }; const deliverEvent = ( eventKind: "state-change" | "recording-frame" | "pointer-event", tabId: string, delivery: () => Effect.Effect<void>, ) => Effect.suspend(delivery).pipe( Effect.catchCause((cause) => Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.logWarning("Desktop preview event listener failed.", { eventKind, tabId, cause, }), ), ); const emit = Effect.fn("PreviewManager.emit")(function* (tabId: string, state: PreviewTabState) { const listeners = yield* Ref.get(listenersRef); yield* Effect.forEach( listeners, (listener) => deliverEvent("state-change", tabId, () => listener(tabId, state)), { discard: true }, ); }); const update = Effect.fn("PreviewManager.update")(function* ( tabId: string, patch: Partial<PreviewTabState>, ) { const updatedAt = yield* currentIso; const next = yield* SynchronizedRef.modify(tabsRef, (tabs) => { const current = tabs.get(tabId); if (!current) return [Option.none<PreviewTabState>(), tabs] as const; const state: PreviewTabState = { ...current, ...patch, updatedAt }; return [ Option.some(state), replaceMap(tabs, (copy) => { copy.set(tabId, state); }), ] as const; }); if (Option.isSome(next)) yield* emit(tabId, next.value); }); const requireWebContents = Effect.fn("PreviewManager.requireWebContents")(function* ( tabId: string, ) { const tabs = yield* SynchronizedRef.get(tabsRef); const tab = tabs.get(tabId); if (!tab) {