import { Schema } from "effect"; import { EnvironmentId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts"; import { PREVIEW_VIEWPORT_MAX_AREA, PreviewRenderedViewportSize, PreviewTabId, PreviewViewportPresetId, PreviewViewportSetting, PreviewViewportSize, } from "./preview.ts"; import { ProviderInstanceId } from "./providerInstance.ts"; import { BrowserProfileId } from "./browserProfile.ts"; const BoundedUrl = Schema.String.check(Schema.isTrimmed()) .check(Schema.isNonEmpty()) .check(Schema.isMaxLength(2048)); const URL_GUIDANCE = "Absolute http(s) URL, or a bare host (https; http for loopback)."; const PROFILE_GUIDANCE = "Profile id or name from preview_status, for a new tab. Default: the user's. A tab keeps its profile."; const OptionalTimeoutMs = Schema.optional( Schema.Int.check(Schema.isGreaterThan(0)).check(Schema.isLessThanOrEqualTo(60_000)), ).annotate({ description: "Default 15000." }); export const PREVIEW_AUTOMATION_V1_OPERATIONS = [ "status", "open", "navigate", "snapshot", "click", "type", "press", "scroll", "evaluate", "waitFor", "recordingStart", "recordingStop", ] as const; const PREVIEW_AUTOMATION_OPERATIONS = [ ...PREVIEW_AUTOMATION_V1_OPERATIONS, "resize", "setColorScheme", ] as const; export const PREVIEW_AUTOMATION_SERVER_OPERATIONS = [ ...PREVIEW_AUTOMATION_OPERATIONS, "dialog", "close", "upload", "hover", "select", "drag", ] as const; export const PreviewAutomationOperation = Schema.Literals(PREVIEW_AUTOMATION_SERVER_OPERATIONS); export type PreviewAutomationOperation = typeof PreviewAutomationOperation.Type; const PreviewAutomationTabTargetFields = { tabId: Schema.optional(PreviewTabId), }; export const PreviewAutomationTabTargetInput = Schema.Struct(PreviewAutomationTabTargetFields); export type PreviewAutomationTabTargetInput = typeof PreviewAutomationTabTargetInput.Type; export const PreviewAutomationTabOwner = Schema.Literals(["agent", "human", "unclaimed"]); export const PreviewAutomationProfile = Schema.Struct({ id: BrowserProfileId, name: Schema.String, incognito: Schema.optional(Schema.Boolean), }); export type PreviewAutomationProfile = typeof PreviewAutomationProfile.Type; export const PreviewAutomationStatus = Schema.Struct({ available: Schema.Boolean, visible: Schema.Boolean, tabId: Schema.NullOr(PreviewTabId), url: Schema.NullOr(Schema.String), title: Schema.NullOr(Schema.String), loading: Schema.Boolean, control: Schema.optional( Schema.Struct({ owner: PreviewAutomationTabOwner, ownedByCaller: Schema.Boolean, generation: Schema.Number, }), ), dialog: Schema.optional( Schema.NullOr( Schema.Struct({ type: Schema.String, message: Schema.String, defaultValue: Schema.String, }), ), ), viewportSetting: Schema.optional(PreviewViewportSetting), viewport: Schema.optional(PreviewRenderedViewportSize), fileChooser: Schema.optional( Schema.NullOr(Schema.Struct({ multiple: Schema.Boolean, accept: Schema.String })), ), downloads: Schema.optional( Schema.Array( Schema.Struct({ fileName: Schema.String, path: Schema.String, sizeBytes: Schema.Number, url: Schema.String, completedAt: Schema.String, }), ), ), /** Server hosts: all thread tabs are readable; actions require ownedByCaller or an unclaimed tab without human control. */ tabs: Schema.optional( Schema.Array( Schema.Struct({ tabId: PreviewTabId, url: Schema.NullOr(Schema.String), openerTabId: Schema.optional(PreviewTabId), owner: Schema.optional(PreviewAutomationTabOwner), ownedByCaller: Schema.optional(Schema.Boolean), visible: Schema.optional(Schema.Boolean), profileId: Schema.optional(BrowserProfileId), }), ), ), profiles: Schema.optional(Schema.Array(PreviewAutomationProfile)), defaultProfileId: Schema.optional(BrowserProfileId), }); export type PreviewAutomationStatus = typeof PreviewAutomationStatus.Type; export const PreviewAutomationDialogInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, accept: Schema.Boolean, promptText: Schema.optional(Schema.String), }); export type PreviewAutomationDialogInput = typeof PreviewAutomationDialogInput.Type; export const PreviewAutomationOpenInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, url: Schema.optional(BoundedUrl).annotate({ description: `First page URL. ${URL_GUIDANCE} Omit to open a blank tab.`, }), open: Schema.optional( Schema.Boolean.annotate({ description: "Show the preview to the user. Default true. False for background only.", }), ), show: Schema.optional( Schema.Boolean.annotate({ description: "Old alias for `open`.", }), ), reuseExistingTab: Schema.optional( Schema.Boolean.annotate({ description: "Reuse `tabId`, or your current tab. Default true. False makes a new tab.", }), ), profileId: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(64))).annotate({ description: PROFILE_GUIDANCE, }), }).check( Schema.makeFilter( (input) => !(input.tabId !== undefined && input.reuseExistingTab === false) || "tabId cannot be combined with reuseExistingTab=false.", ), ); export type PreviewAutomationOpenInput = typeof PreviewAutomationOpenInput.Type; export const BrowserNavigationTarget = Schema.Union([ Schema.Struct({ kind: Schema.Literal("url"), url: BoundedUrl.annotate({ description: `Website URL. ${URL_GUIDANCE}`, }), }), Schema.Struct({ kind: Schema.Literal("environment-port"), port: Schema.Int.check(Schema.isGreaterThan(0)) .check(Schema.isLessThan(65_536)) .annotate({ description: "Dev-server port here." }), protocol: Schema.optional( Schema.Literals(["http", "https"]).annotate({ description: "Default http.", }), ), path: Schema.optional( Schema.String.annotate({ description: "Path, query, and fragment, such as /settings?tab=account.", }), ), }), ]); export type BrowserNavigationTarget = typeof BrowserNavigationTarget.Type; export const PreviewAutomationNavigateInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, url: Schema.optional(BoundedUrl).annotate({ description: `Website URL. ${URL_GUIDANCE} For public pages.`, }), target: Schema.optional(BrowserNavigationTarget), readiness: Schema.optional(Schema.Literals(["load", "domContentLoaded", "none"])).annotate({ description: "Default 'load'. 'none': return at once.", }), timeoutMs: OptionalTimeoutMs, }).check( Schema.makeFilter( (input) => Number(input.url !== undefined) + Number(input.target !== undefined) === 1 || "Provide exactly one of url or target.", ), ); export type PreviewAutomationNavigateInput = typeof PreviewAutomationNavigateInput.Type; export const PreviewAutomationResizeInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, mode: Schema.Literals(["fill", "freeform", "preset"]), preset: Schema.optional(PreviewViewportPresetId), width: Schema.optional(PreviewViewportSize.fields.width), height: Schema.optional(PreviewViewportSize.fields.height), orientation: Schema.optional(Schema.Literals(["portrait", "landscape"])), timeoutMs: OptionalTimeoutMs, }).check( Schema.makeFilter((input) => { const hasPreset = input.preset !== undefined; const hasWidth = input.width !== undefined; const hasHeight = input.height !== undefined; if (hasWidth !== hasHeight) return "Custom dimensions require both width and height."; if (input.mode === "fill") { return !hasPreset && !hasWidth && input.orientation === undefined ? true : "Fill mode does not accept a preset, dimensions, or orientation."; } if (input.mode === "freeform") { if (!hasWidth || !hasHeight || hasPreset || input.orientation !== undefined) { return "Freeform mode requires width and height and does not accept a preset or orientation."; } } else if (!hasPreset || hasWidth || hasHeight) { return "Preset mode requires a preset and does not accept custom dimensions."; } if (hasWidth && hasHeight && input.width! * input.height! > PREVIEW_VIEWPORT_MAX_AREA) { return `Custom viewport area must not exceed ${PREVIEW_VIEWPORT_MAX_AREA} pixels.`; } return true; }), ); export type PreviewAutomationResizeInput = typeof PreviewAutomationResizeInput.Type; export const PreviewAutomationResizeResult = Schema.Struct({ tabId: PreviewTabId, setting: PreviewViewportSetting, viewport: PreviewRenderedViewportSize, }); export type PreviewAutomationResizeResult = typeof PreviewAutomationResizeResult.Type; // Mirrors DesktopPreviewColorScheme without importing ipc.ts. export const PreviewAutomationColorScheme = Schema.Literals(["system", "light", "dark"]); export type PreviewAutomationColorScheme = typeof PreviewAutomationColorScheme.Type; export const PreviewAutomationSetColorSchemeInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, colorScheme: PreviewAutomationColorScheme, }); export type PreviewAutomationSetColorSchemeInput = typeof PreviewAutomationSetColorSchemeInput.Type; export const PreviewAutomationSetColorSchemeResult = Schema.Struct({ tabId: PreviewTabId, colorScheme: PreviewAutomationColorScheme, }); export type PreviewAutomationSetColorSchemeResult = typeof PreviewAutomationSetColorSchemeResult.Type; const Locator = TrimmedNonEmptyString; const LegacySelector = TrimmedNonEmptyString; const PointerTargetFields = { ...PreviewAutomationTabTargetFields, selector: Schema.optional(LegacySelector), locator: Schema.optional(Locator), x: Schema.optional(Schema.Finite), y: Schema.optional(Schema.Finite), timeoutMs: OptionalTimeoutMs, }; const singlePointerTarget = Schema.makeFilter( (input: { readonly selector?: string | undefined; readonly locator?: string | undefined; readonly x?: number | undefined; readonly y?: number | undefined; }) => { const selectorModes = Number(input.selector !== undefined) + Number(input.locator !== undefined); const hasX = input.x !== undefined; const hasY = input.y !== undefined; if (hasX !== hasY) return "Coordinates require both x and y."; const coordinateModes = hasX && hasY ? 1 : 0; return selectorModes + coordinateModes === 1 || "Provide exactly one target."; }, ); export const PreviewAutomationClickInput = Schema.Struct({ ...PointerTargetFields, button: Schema.optional( Schema.Literals(["left", "right", "middle"]).annotate({ description: "Button. Default left. Right opens the menu.", }), ), clickCount: Schema.optional( Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 3 })).annotate({ description: "1 click, 2 double, 3 triple. Default 1.", }), ), }).check(singlePointerTarget); export type PreviewAutomationClickInput = typeof PreviewAutomationClickInput.Type; export const PreviewAutomationHoverInput = Schema.Struct(PointerTargetFields).check(singlePointerTarget); export type PreviewAutomationHoverInput = typeof PreviewAutomationHoverInput.Type; export const PreviewAutomationSelectInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, selector: Schema.optional(LegacySelector), locator: Schema.optional(Locator), values: Schema.Array(Schema.String).check(Schema.isMaxLength(100)).annotate({ description: "Empty clears it.", }), timeoutMs: OptionalTimeoutMs, }).check( Schema.makeFilter( (input) => Number(input.selector !== undefined) + Number(input.locator !== undefined) === 1 || "Provide exactly one of selector or locator.", ), ); export type PreviewAutomationSelectInput = typeof PreviewAutomationSelectInput.Type; export const PreviewAutomationSelectResult = Schema.Struct({ selected: Schema.Array(Schema.String).annotate({ description: "The values of the options now selected.", }), }); export type PreviewAutomationSelectResult = typeof PreviewAutomationSelectResult.Type; export const PreviewAutomationDragInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, source: Locator, target: Locator, timeoutMs: OptionalTimeoutMs, }); export type PreviewAutomationDragInput = typeof PreviewAutomationDragInput.Type; export const PreviewAutomationUploadInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, paths: Schema.Array(TrimmedNonEmptyString).check(Schema.isMaxLength(20)), selector: Schema.optional(LegacySelector), locator: Schema.optional(Locator).annotate({ description: "Set files on this instead of the open picker.", }), timeoutMs: OptionalTimeoutMs, }).check( Schema.makeFilter( (input) => !(input.selector !== undefined && input.locator !== undefined) || "Provide at most one of selector or locator.", ), ); export type PreviewAutomationUploadInput = typeof PreviewAutomationUploadInput.Type; export const PreviewAutomationTypeInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, text: Schema.String, selector: Schema.optional(LegacySelector), locator: Schema.optional(Locator), clear: Schema.optional( Schema.Boolean.annotate({ description: "Clear the input first. Default false.", }), ), timeoutMs: OptionalTimeoutMs, }).check( Schema.makeFilter( (input) => !(input.selector !== undefined && input.locator !== undefined) || "Provide at most one of selector or locator.", ), ); export type PreviewAutomationTypeInput = typeof PreviewAutomationTypeInput.Type; export const PreviewAutomationPressInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, key: Schema.String.check(Schema.isTrimmed()).check(Schema.isNonEmpty()), modifiers: Schema.optional(Schema.Array(Schema.Literals(["Alt", "Control", "Meta", "Shift"]))), }); export type PreviewAutomationPressInput = typeof PreviewAutomationPressInput.Type; export const PreviewAutomationScrollInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, deltaX: Schema.optional(Schema.Finite), deltaY: Schema.optional(Schema.Finite), selector: Schema.optional(LegacySelector), locator: Schema.optional(Locator), }).check( Schema.makeFilter((input) => { if (input.selector !== undefined && input.locator !== undefined) { return "Provide at most one of selector or locator."; } return input.deltaX !== undefined || input.deltaY !== undefined || "Provide deltaX or deltaY."; }), ); export type PreviewAutomationScrollInput = typeof PreviewAutomationScrollInput.Type; /** Strict input for each `preview_act` action. */ export const PreviewAutomationActInputs = { click: PreviewAutomationClickInput, type: PreviewAutomationTypeInput, hover: PreviewAutomationHoverInput, select: PreviewAutomationSelectInput, drag: PreviewAutomationDragInput, upload: PreviewAutomationUploadInput, press: PreviewAutomationPressInput, scroll: PreviewAutomationScrollInput, } as const; export type PreviewAutomationAction = keyof typeof PreviewAutomationActInputs; /** One flat shape for every page action. The server decodes it with the action's strict input. */ export const PreviewAutomationActInput = Schema.Struct({ action: Schema.Literals([ "click", "type", "hover", "select", "drag", "upload", "press", "scroll", ]), ...PreviewAutomationTabTargetFields, locator: Schema.optional(Locator), selector: Schema.optional(LegacySelector), x: Schema.optional(Schema.Finite), y: Schema.optional(Schema.Finite), button: Schema.optional(Schema.Literals(["left", "right", "middle"])), clickCount: Schema.optional(Schema.Int), text: Schema.optional(Schema.String), clear: Schema.optional(Schema.Boolean), values: Schema.optional(Schema.Array(Schema.String)), source: Schema.optional(Locator), target: Schema.optional(Locator), paths: Schema.optional(Schema.Array(Schema.String)), key: Schema.optional(Schema.String), modifiers: PreviewAutomationPressInput.fields.modifiers, deltaX: Schema.optional(Schema.Finite), deltaY: Schema.optional(Schema.Finite), timeoutMs: Schema.optional(Schema.Int), }); export type PreviewAutomationActInput = typeof PreviewAutomationActInput.Type; export const PreviewAutomationEvaluateInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, expression: Schema.String.check(Schema.isTrimmed()) .check(Schema.isNonEmpty()) .check(Schema.isMaxLength(64_000)), awaitPromise: Schema.optional(Schema.Boolean.annotate({ description: "Default true." })), returnByValue: Schema.optional( Schema.Boolean.annotate({ description: "False returns a ref. Default true.", }), ), }); export type PreviewAutomationEvaluateInput = typeof PreviewAutomationEvaluateInput.Type; export const PreviewAutomationWaitForInput = Schema.Struct({ ...PreviewAutomationTabTargetFields, selector: Schema.optional(LegacySelector), locator: Schema.optional(Locator), text: Schema.optional(TrimmedNonEmptyString).annotate({ description: "Case counts.", }), urlIncludes: Schema.optional(TrimmedNonEmptyString), timeoutMs: OptionalTimeoutMs, }).check( Schema.makeFilter((input) => { if (input.selector !== undefined && input.locator !== undefined) { return "Provide at most one of selector or locator."; } return ( input.selector !== undefined || input.locator !== undefined || input.text !== undefined || input.urlIncludes !== undefined || "Provide at least one wait condition." ); }), ); export type PreviewAutomationWaitForInput = typeof PreviewAutomationWaitForInput.Type; export const PreviewAutomationElement = Schema.Struct({ tag: Schema.String, role: Schema.NullOr(Schema.String), name: Schema.String, selector: Schema.String, x: Schema.Number, y: Schema.Number, width: Schema.Number, height: Schema.Number, }); export type PreviewAutomationElement = typeof PreviewAutomationElement.Type; export const PreviewAutomationConsoleEntry = Schema.Struct({ level: Schema.String, text: Schema.String, timestamp: Schema.String, source: Schema.optional(Schema.String), }); export type PreviewAutomationConsoleEntry = typeof PreviewAutomationConsoleEntry.Type; export const PreviewAutomationNetworkEntry = Schema.Struct({ url: Schema.String, method: Schema.String, status: Schema.NullOr(Schema.Number), failed: Schema.Boolean, errorText: Schema.optional(Schema.String), timestamp: Schema.String, }); export type PreviewAutomationNetworkEntry = typeof PreviewAutomationNetworkEntry.Type; export const PreviewAutomationActionEvent = Schema.Struct({ id: Schema.String, action: Schema.String, status: Schema.Literals(["running", "succeeded", "failed", "interrupted"]), startedAt: Schema.String, completedAt: Schema.optional(Schema.String), error: Schema.optional(Schema.String), }); export type PreviewAutomationActionEvent = typeof PreviewAutomationActionEvent.Type; export const PreviewAutomationSnapshot = Schema.Struct({ url: Schema.String, title: Schema.String, loading: Schema.Boolean, visibleText: Schema.String, interactiveElements: Schema.Array(PreviewAutomationElement), accessibilityTree: Schema.Unknown, consoleEntries: Schema.Array(PreviewAutomationConsoleEntry), networkEntries: Schema.Array(PreviewAutomationNetworkEntry), actionTimeline: Schema.Array(PreviewAutomationActionEvent), screenshot: Schema.Struct({ mimeType: Schema.Literal("image/png"), data: Schema.String, width: Schema.Int, height: Schema.Int, }), }); export type PreviewAutomationSnapshot = typeof PreviewAutomationSnapshot.Type; export const PreviewAutomationRecordingStatus = Schema.Struct({ tabId: PreviewTabId, recording: Schema.Boolean, startedAt: Schema.NullOr(Schema.String), }); export type PreviewAutomationRecordingStatus = typeof PreviewAutomationRecordingStatus.Type; export const PREVIEW_RECORDING_STOP_TIMEOUT_MS = 120_000; export const PreviewAutomationRecordingArtifact = Schema.Struct({ id: Schema.String, tabId: PreviewTabId, path: Schema.String, mimeType: Schema.String, sizeBytes: Schema.Int, createdAt: Schema.String, }); export type PreviewAutomationRecordingArtifact = typeof PreviewAutomationRecordingArtifact.Type; export const PreviewAutomationClientId = TrimmedNonEmptyString.check(Schema.isMaxLength(128)); export type PreviewAutomationClientId = typeof PreviewAutomationClientId.Type; export const PreviewAutomationConnectionId = TrimmedNonEmptyString.check(Schema.isMaxLength(64)); export type PreviewAutomationConnectionId = typeof PreviewAutomationConnectionId.Type; export const PreviewAutomationHostIdentity = Schema.Struct({ clientId: PreviewAutomationClientId, environmentId: EnvironmentId, }); export type PreviewAutomationHostIdentity = typeof PreviewAutomationHostIdentity.Type; export const PreviewAutomationHost = Schema.Struct({ ...PreviewAutomationHostIdentity.fields, /** Missing means the V1 operation set from before capability negotiation. */ supportedOperations: Schema.optional(Schema.Array(PreviewAutomationOperation)), }); export type PreviewAutomationHost = typeof PreviewAutomationHost.Type; export const PreviewAutomationHostFocus = Schema.Struct({ ...PreviewAutomationHostIdentity.fields, connectionId: PreviewAutomationConnectionId, focused: Schema.Boolean, liveTabs: Schema.optional( Schema.Array( Schema.Struct({ threadId: ThreadId, tabId: PreviewTabId, visible: Schema.optional(Schema.Boolean), }), ), ), }); export type PreviewAutomationHostFocus = typeof PreviewAutomationHostFocus.Type; export const PreviewAutomationRequest = Schema.Struct({ requestId: TrimmedNonEmptyString, threadId: ThreadId, tabId: Schema.optional(PreviewTabId), tabIdExplicit: Schema.optional(Schema.Boolean), /** Supplied by the broker from authenticated scope, never from tool input. */ agentSessionId: Schema.optional(Schema.String), operation: PreviewAutomationOperation, input: Schema.Unknown, timeoutMs: Schema.Int.check(Schema.isGreaterThan(0)), }); export type PreviewAutomationRequest = typeof PreviewAutomationRequest.Type; export const PreviewAutomationStreamEvent = Schema.Union([ Schema.Struct({ type: Schema.Literal("connected"), connectionId: PreviewAutomationConnectionId, }), Schema.Struct({ type: Schema.Literal("request"), connectionId: PreviewAutomationConnectionId, request: PreviewAutomationRequest, }), ]); export type PreviewAutomationStreamEvent = typeof PreviewAutomationStreamEvent.Type; export const PreviewAutomationResponse = Schema.Struct({ clientId: PreviewAutomationClientId, connectionId: PreviewAutomationConnectionId, requestId: TrimmedNonEmptyString, ok: Schema.Boolean, result: Schema.optional(Schema.Unknown), error: Schema.optional( Schema.Struct({ _tag: TrimmedNonEmptyString, message: Schema.String, detail: Schema.optional(Schema.Unknown), }), ), }); export type PreviewAutomationResponse = typeof PreviewAutomationResponse.Type; const McpCapabilityErrorFields = { environmentId: EnvironmentId, threadId: Schema.optional(ThreadId), providerSessionId: Schema.optional(TrimmedNonEmptyString), providerInstanceId: Schema.optional(ProviderInstanceId), }; export class PreviewAutomationUnavailableError extends Schema.TaggedError()( "PreviewAutomationUnavailableError", { capability: Schema.Literal("preview"), ...McpCapabilityErrorFields, }, ) { override get message(): string { return `MCP credential does not grant the ${this.capability} capability: browser preview tools are off for this thread. Do not retry them. To check a page, use a headless browser from the shell, such as Playwright, or curl. The user can turn on "Agent browser access" in Settings; it applies when the agent session next starts.`; } } export class McpCapabilityUnavailableError extends Schema.TaggedError()( "McpCapabilityUnavailableError", { capability: TrimmedNonEmptyString, ...McpCapabilityErrorFields, }, ) { override get message(): string { return `MCP credential does not grant the ${this.capability} capability.`; } } const PreviewAutomationScopeErrorFields = { operation: PreviewAutomationOperation, environmentId: EnvironmentId, threadId: ThreadId, providerSessionId: TrimmedNonEmptyString, providerInstanceId: ProviderInstanceId, }; export const SERVER_BROWSER_AUTOMATION_CLIENT_ID = "server-browser"; const PreviewAutomationRequestErrorFields = { ...PreviewAutomationScopeErrorFields, clientId: TrimmedNonEmptyString, connectionId: PreviewAutomationConnectionId, requestId: TrimmedNonEmptyString, tabId: Schema.optional(PreviewTabId), timeoutMs: Schema.Int.check(Schema.isGreaterThan(0)), }; const PreviewAutomationRemoteDiagnosticFields = { remoteTag: TrimmedNonEmptyString, remoteMessageLength: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), remoteDetailKind: Schema.optional( Schema.Literals(["null", "array", "object", "string", "number", "boolean"]), ), cause: Schema.Defect(), }; const PreviewAutomationOptionalRemoteDiagnosticFields = { remoteTag: Schema.optional(TrimmedNonEmptyString), remoteMessageLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), remoteDetailKind: Schema.optional( Schema.Literals(["null", "array", "object", "string", "number", "boolean"]), ), cause: Schema.optional(Schema.Defect()), }; export class PreviewAutomationNoAvailableHostError extends Schema.TaggedError()( "PreviewAutomationNoAvailableHostError", { ...PreviewAutomationScopeErrorFields, clientId: Schema.optional(TrimmedNonEmptyString), connectionId: Schema.optional(PreviewAutomationConnectionId), requestId: Schema.optional(TrimmedNonEmptyString), tabId: Schema.optional(PreviewTabId), timeoutMs: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))), ...PreviewAutomationOptionalRemoteDiagnosticFields, }, ) { override get message(): string { return `No preview automation host is available for ${this.operation} in environment ${this.environmentId}. The server-owned browser may be starting or reconnecting. Retry preview_status, then call preview_open if no tab is available. If it remains unavailable, report the browser connection failure.`; } } export class PreviewAutomationUnsupportedClientError extends Schema.TaggedError()( "PreviewAutomationUnsupportedClientError", { ...PreviewAutomationRequestErrorFields, ...PreviewAutomationRemoteDiagnosticFields, }, ) { override get message(): string { return `Preview automation client ${this.clientId} does not support ${this.operation}.`; } } export class PreviewAutomationTabNotFoundError extends Schema.TaggedError()( "PreviewAutomationTabNotFoundError", { ...PreviewAutomationRequestErrorFields, ...PreviewAutomationRemoteDiagnosticFields, }, ) { override get message(): string { return this.tabId ? `Preview tab ${this.tabId} was not found for ${this.operation}. Omit tabId to use the current tab, or call preview_open.` : `No active preview tab was found for ${this.operation}. Call preview_open first.`; } } export class PreviewAutomationTimeoutError extends Schema.TaggedError()( "PreviewAutomationTimeoutError", { ...PreviewAutomationRequestErrorFields, ...PreviewAutomationOptionalRemoteDiagnosticFields, reason: Schema.optional(Schema.String), }, ) { override get message(): string { const summary = `Preview automation ${this.operation} timed out after ${this.timeoutMs}ms.`; return this.reason === undefined ? summary : `${summary} ${this.reason}`; } } export const PreviewAutomationControlReason = Schema.Literals([ "agentMismatch", "humanControl", "tabRequired", "closed", "interrupted", "dialogPending", "tabLimit", ]); export type PreviewAutomationControlReason = typeof PreviewAutomationControlReason.Type; export class PreviewAutomationControlInterruptedError extends Schema.TaggedError()( "PreviewAutomationControlInterruptedError", { ...PreviewAutomationRequestErrorFields, ...PreviewAutomationRemoteDiagnosticFields, reason: Schema.optional(PreviewAutomationControlReason), }, ) { override get message(): string { if (this.reason === "agentMismatch") return "This browser tab belongs to another agent session or a human. Open your own tab."; if (this.reason === "humanControl") return "A human controls this browser tab. Wait until they release control."; if (this.reason === "tabRequired") return "Multiple browser tabs belong to this session. Pass a tabId from preview_open or from the tabs listed by preview_status."; if (this.reason === "dialogPending") return "A browser dialog is pending. Read preview_status and resolve it with preview_dialog."; if (this.reason === "tabLimit") return "Too many server browser tabs are open. Close one with t3_preview_close, or reuse a tabId from preview_status tabs."; if (this.reason === "closed") return "This browser tab is closed. Call preview_open."; if (this.reason === "interrupted") return "Browser control changed. Take a fresh snapshot before trying again."; return `Preview automation ${this.operation} was interrupted on client ${this.clientId}.`; } } export class PreviewAutomationExecutionError extends Schema.TaggedError()( "PreviewAutomationExecutionError", { ...PreviewAutomationRequestErrorFields, ...PreviewAutomationRemoteDiagnosticFields, reason: Schema.optional(Schema.String), }, ) { override get message(): string { return this.reason === undefined ? `Preview automation ${this.operation} failed on client ${this.clientId}.` : `Preview automation ${this.operation} failed: ${this.reason}`; } } export class PreviewAutomationInvalidSelectorError extends Schema.TaggedError()( "PreviewAutomationInvalidSelectorError", { ...PreviewAutomationRequestErrorFields, ...PreviewAutomationRemoteDiagnosticFields, selectorKind: Schema.optional(Schema.Literals(["locator", "selector"])), selectorLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), staleRef: Schema.optional(Schema.Boolean), }, ) { override get message(): string { if (this.staleRef) return "This element ref is stale. Navigation, dialogs, and new snapshots replace refs; take a fresh snapshot and use its refs."; if (this.selectorKind !== undefined && this.selectorLength !== undefined) { return `Preview automation ${this.operation} received an invalid ${this.selectorKind} (${this.selectorLength} characters).`; } return `Preview automation ${this.operation} received an invalid selector.`; } } export class PreviewAutomationTargetNotEditableError extends Schema.TaggedError()( "PreviewAutomationTargetNotEditableError", { ...PreviewAutomationRequestErrorFields, ...PreviewAutomationRemoteDiagnosticFields, selectorKind: Schema.optional(Schema.Literals(["focused-element", "locator", "selector"])), selectorLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))), }, ) { override get message(): string { if (this.selectorKind === "focused-element") { return `Preview automation ${this.operation} requires an editable focused element.`; } if (this.selectorKind !== undefined && this.selectorLength !== undefined) { return `Preview automation ${this.operation} requires an editable ${this.selectorKind} (${this.selectorLength} characters).`; } return `Preview automation ${this.operation} requires an editable target.`; } } export class PreviewAutomationResultTooLargeError extends Schema.TaggedError()( "PreviewAutomationResultTooLargeError", { ...PreviewAutomationRequestErrorFields, ...PreviewAutomationRemoteDiagnosticFields, maximumBytes: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))), }, ) { override get message(): string { const summary = this.maximumBytes === undefined ? `Preview automation ${this.operation} produced a result that is too large.` : `Preview automation ${this.operation} produced a result larger than ${this.maximumBytes} bytes.`; return summary; } } export class PreviewAutomationClientDisconnectedError extends Schema.TaggedError()( "PreviewAutomationClientDisconnectedError", PreviewAutomationRequestErrorFields, ) { override get message(): string { return `Preview automation client ${this.clientId} disconnected during ${this.operation}.`; } } export class PreviewAutomationRequestQueueClosedError extends Schema.TaggedError()( "PreviewAutomationRequestQueueClosedError", PreviewAutomationRequestErrorFields, ) { override get message(): string { return `Preview automation client ${this.clientId} stopped accepting ${this.operation} requests.`; } } export class PreviewAutomationRemoteUnavailableError extends Schema.TaggedError()( "PreviewAutomationRemoteUnavailableError", { ...PreviewAutomationRequestErrorFields, ...PreviewAutomationRemoteDiagnosticFields, }, ) { override get message(): string { return `Preview automation ${this.operation} is unavailable on client ${this.clientId}.`; } } export class PreviewAutomationMalformedResponseError extends Schema.TaggedError()( "PreviewAutomationMalformedResponseError", PreviewAutomationRequestErrorFields, ) { override get message(): string { return `Preview automation client ${this.clientId} returned a malformed response for ${this.operation}.`; } } export class PreviewAutomationRecordingTransferError extends Schema.TaggedError()( "PreviewAutomationRecordingTransferError", { threadId: ThreadId, cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return "Preview recording could not be saved to the agent environment."; } } export class PreviewAutomationRecordingDesktopUpdateRequiredError extends Schema.TaggedError()( "PreviewAutomationRecordingDesktopUpdateRequiredError", { threadId: ThreadId, cause: Schema.optional(Schema.Defect()) }, ) { override get message(): string { return "Update the desktop app to transfer recordings. The recording remains on the desktop."; } } export class PreviewAutomationRecordingTooLargeError extends Schema.TaggedError()( "PreviewAutomationRecordingTooLargeError", { threadId: ThreadId, cause: Schema.optional(Schema.Defect()) }, ) { override get message(): string { return "The recording exceeds 50 MiB. Make a shorter recording."; } } export class PreviewAutomationRecordingDeadlineExpiredError extends Schema.TaggedError()( "PreviewAutomationRecordingDeadlineExpiredError", { threadId: ThreadId, cause: Schema.optional(Schema.Defect()) }, ) { override get message(): string { return "The recording transfer deadline expired."; } } export const PreviewAutomationError = Schema.Union([ PreviewAutomationRecordingTransferError, PreviewAutomationRecordingDesktopUpdateRequiredError, PreviewAutomationRecordingTooLargeError, PreviewAutomationRecordingDeadlineExpiredError, PreviewAutomationUnavailableError, PreviewAutomationNoAvailableHostError, PreviewAutomationUnsupportedClientError, PreviewAutomationTabNotFoundError, PreviewAutomationTimeoutError, PreviewAutomationControlInterruptedError, PreviewAutomationExecutionError, PreviewAutomationInvalidSelectorError, PreviewAutomationTargetNotEditableError, PreviewAutomationResultTooLargeError, PreviewAutomationClientDisconnectedError, PreviewAutomationRequestQueueClosedError, PreviewAutomationRemoteUnavailableError, PreviewAutomationMalformedResponseError, ]); export type PreviewAutomationError = typeof PreviewAutomationError.Type; export const PreviewUrlResolution = Schema.Struct({ requestedUrl: Schema.String, resolvedUrl: Schema.String, resolutionKind: Schema.Literals(["direct", "direct-private-network"]), environmentId: EnvironmentId, }); export type PreviewUrlResolution = typeof PreviewUrlResolution.Type;