Skip to content
Source
Sign inSign up

previewAutomation.ts

1,019 lines37.7 KB

1import { Schema } from "effect";
2​
3import { EnvironmentId, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts";
4import {
5 PREVIEW_VIEWPORT_MAX_AREA,
6 PreviewRenderedViewportSize,
7 PreviewTabId,
8 PreviewViewportPresetId,
9 PreviewViewportSetting,
10 PreviewViewportSize,
11} from "./preview.ts";
12import { ProviderInstanceId } from "./providerInstance.ts";
13import { BrowserProfileId } from "./browserProfile.ts";
14​
15const BoundedUrl = Schema.String.check(Schema.isTrimmed())
16 .check(Schema.isNonEmpty())
17 .check(Schema.isMaxLength(2048));
18const URL_GUIDANCE = "Absolute http(s) URL, or a bare host (https; http for loopback).";
19const PROFILE_GUIDANCE =
20 "Profile id or name from preview_status, for a new tab. Default: the user's. A tab keeps its profile.";
21const OptionalTimeoutMs = Schema.optional(
22 Schema.Int.check(Schema.isGreaterThan(0)).check(Schema.isLessThanOrEqualTo(60_000)),
23).annotate({ description: "Default 15000." });
24​
25export const PREVIEW_AUTOMATION_V1_OPERATIONS = [
26 "status",
27 "open",
28 "navigate",
29 "snapshot",
30 "click",
31 "type",
32 "press",
33 "scroll",
34 "evaluate",
35 "waitFor",
36 "recordingStart",
37 "recordingStop",
38] as const;
39​
40const PREVIEW_AUTOMATION_OPERATIONS = [
41 ...PREVIEW_AUTOMATION_V1_OPERATIONS,
42 "resize",
43 "setColorScheme",
44] as const;
45​
46export const PREVIEW_AUTOMATION_SERVER_OPERATIONS = [
47 ...PREVIEW_AUTOMATION_OPERATIONS,
48 "dialog",
49 "close",
50 "upload",
51 "hover",
52 "select",
53 "drag",
54] as const;
55export const PreviewAutomationOperation = Schema.Literals(PREVIEW_AUTOMATION_SERVER_OPERATIONS);
56export type PreviewAutomationOperation = typeof PreviewAutomationOperation.Type;
57​
58const PreviewAutomationTabTargetFields = {
59 tabId: Schema.optional(PreviewTabId),
60};
61​
62export const PreviewAutomationTabTargetInput = Schema.Struct(PreviewAutomationTabTargetFields);
63export type PreviewAutomationTabTargetInput = typeof PreviewAutomationTabTargetInput.Type;
64​
65export const PreviewAutomationTabOwner = Schema.Literals(["agent", "human", "unclaimed"]);
66​
67export const PreviewAutomationProfile = Schema.Struct({
68 id: BrowserProfileId,
69 name: Schema.String,
70 incognito: Schema.optional(Schema.Boolean),
71});
72export type PreviewAutomationProfile = typeof PreviewAutomationProfile.Type;
73​
74const VISIBLE_DESCRIPTION =
75 "True while a client shows this tab: a streamed live view, or the desktop app's own browser panel.";
76​
77/** Why the tab shows the browser's error page instead of the page it tried to load. */
78export const PreviewAutomationLoadError = Schema.Struct({
79 url: Schema.String,
80 code: Schema.Int,
81 description: Schema.String,
82});
83export type PreviewAutomationLoadError = typeof PreviewAutomationLoadError.Type;
84​
85export const PreviewAutomationStatus = Schema.Struct({
86 available: Schema.Boolean,
87 visible: Schema.Boolean.annotate({ description: VISIBLE_DESCRIPTION }),
88 tabId: Schema.NullOr(PreviewTabId),
89 url: Schema.NullOr(Schema.String),
90 title: Schema.NullOr(Schema.String),
91 loading: Schema.Boolean,
92 loadError: Schema.optional(PreviewAutomationLoadError),
93 control: Schema.optional(
94 Schema.Struct({
95 owner: PreviewAutomationTabOwner,
96 ownedByCaller: Schema.Boolean,
97 generation: Schema.Number,
98 }),
99 ),
100 dialog: Schema.optional(
101 Schema.NullOr(
102 Schema.Struct({
103 type: Schema.String,
104 message: Schema.String,
105 defaultValue: Schema.String,
106 }),
107 ),
108 ),
109 viewportSetting: Schema.optional(PreviewViewportSetting),
110 viewport: Schema.optional(PreviewRenderedViewportSize),
111 fileChooser: Schema.optional(
112 Schema.NullOr(Schema.Struct({ multiple: Schema.Boolean, accept: Schema.String })),
113 ),
114 downloads: Schema.optional(
115 Schema.Array(
116 Schema.Struct({
117 fileName: Schema.String,
118 path: Schema.String,
119 sizeBytes: Schema.Number,
120 url: Schema.String,
121 completedAt: Schema.String,
122 }),
123 ),
124 ),
125 /** Server hosts: all thread tabs are readable; actions require ownedByCaller or an unclaimed tab without human control. */
126 tabs: Schema.optional(
127 Schema.Array(
128 Schema.Struct({
129 tabId: PreviewTabId,
130 url: Schema.NullOr(Schema.String),
131 openerTabId: Schema.optional(PreviewTabId),
132 owner: Schema.optional(PreviewAutomationTabOwner),
133 ownedByCaller: Schema.optional(Schema.Boolean),
134 visible: Schema.optional(Schema.Boolean.annotate({ description: VISIBLE_DESCRIPTION })),
135 profileId: Schema.optional(BrowserProfileId),
136 }),
137 ),
138 ),
139 profiles: Schema.optional(Schema.Array(PreviewAutomationProfile)),
140 defaultProfileId: Schema.optional(BrowserProfileId),
141});
142export type PreviewAutomationStatus = typeof PreviewAutomationStatus.Type;
143​
144export const PreviewAutomationDialogInput = Schema.Struct({
145 ...PreviewAutomationTabTargetFields,
146 accept: Schema.Boolean,
147 promptText: Schema.optional(Schema.String),
148});
149export type PreviewAutomationDialogInput = typeof PreviewAutomationDialogInput.Type;
150​
151export const PreviewAutomationOpenInput = Schema.Struct({
152 ...PreviewAutomationTabTargetFields,
153 url: Schema.optional(BoundedUrl).annotate({
154 description: `First page URL. ${URL_GUIDANCE} Omit to open a blank tab.`,
155 }),
156 open: Schema.optional(
157 Schema.Boolean.annotate({
158 description: "Show the preview to the user. Default true. False for background only.",
159 }),
160 ),
161 show: Schema.optional(
162 Schema.Boolean.annotate({
163 description: "Old alias for `open`.",
164 }),
165 ),
166 reuseExistingTab: Schema.optional(
167 Schema.Boolean.annotate({
168 description: "Reuse `tabId`, or your current tab. Default true. False makes a new tab.",
169 }),
170 ),
171 profileId: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(64))).annotate({
172 description: PROFILE_GUIDANCE,
173 }),
174}).check(
175 Schema.makeFilter(
176 (input) =>
177 !(input.tabId !== undefined && input.reuseExistingTab === false) ||
178 "tabId cannot be combined with reuseExistingTab=false.",
179 ),
180);
181export type PreviewAutomationOpenInput = typeof PreviewAutomationOpenInput.Type;
182​
183export const BrowserNavigationTarget = Schema.Union([
184 Schema.Struct({
185 kind: Schema.Literal("url"),
186 url: BoundedUrl.annotate({
187 description: `Website URL. ${URL_GUIDANCE}`,
188 }),
189 }),
190 Schema.Struct({
191 kind: Schema.Literal("environment-port"),
192 port: Schema.Int.check(Schema.isGreaterThan(0))
193 .check(Schema.isLessThan(65_536))
194 .annotate({ description: "Dev-server port here." }),
195 protocol: Schema.optional(
196 Schema.Literals(["http", "https"]).annotate({
197 description: "Default http.",
198 }),
199 ),
200 path: Schema.optional(
201 Schema.String.annotate({
202 description: "Path, query, and fragment, such as /settings?tab=account.",
203 }),
204 ),
205 }),
206]);
207export type BrowserNavigationTarget = typeof BrowserNavigationTarget.Type;
208​
209export const PreviewAutomationNavigateInput = Schema.Struct({
210 ...PreviewAutomationTabTargetFields,
211 url: Schema.optional(BoundedUrl).annotate({
212 description: `Website URL. ${URL_GUIDANCE} For public pages.`,
213 }),
214 target: Schema.optional(BrowserNavigationTarget),
215 readiness: Schema.optional(Schema.Literals(["load", "domContentLoaded", "none"])).annotate({
216 description: "Default 'load'. 'none': return at once.",
217 }),
218 timeoutMs: OptionalTimeoutMs,
219}).check(
220 Schema.makeFilter(
221 (input) =>
222 Number(input.url !== undefined) + Number(input.target !== undefined) === 1 ||
223 "Provide exactly one of url or target.",
224 ),
225);
226export type PreviewAutomationNavigateInput = typeof PreviewAutomationNavigateInput.Type;
227​
228export const PreviewAutomationResizeInput = Schema.Struct({
229 ...PreviewAutomationTabTargetFields,
230 mode: Schema.Literals(["fill", "freeform", "preset"]),
231 preset: Schema.optional(PreviewViewportPresetId),
232 width: Schema.optional(PreviewViewportSize.fields.width),
233 height: Schema.optional(PreviewViewportSize.fields.height),
234 orientation: Schema.optional(Schema.Literals(["portrait", "landscape"])),
235 timeoutMs: OptionalTimeoutMs,
236}).check(
237 Schema.makeFilter((input) => {
238 const hasPreset = input.preset !== undefined;
239 const hasWidth = input.width !== undefined;
240 const hasHeight = input.height !== undefined;
241 if (hasWidth !== hasHeight) return "Custom dimensions require both width and height.";
242 if (input.mode === "fill") {
243 return !hasPreset && !hasWidth && input.orientation === undefined
244 ? true
245 : "Fill mode does not accept a preset, dimensions, or orientation.";
246 }
247 if (input.mode === "freeform") {
248 if (!hasWidth || !hasHeight || hasPreset || input.orientation !== undefined) {
249 return "Freeform mode requires width and height and does not accept a preset or orientation.";
250 }
251 } else if (!hasPreset || hasWidth || hasHeight) {
252 return "Preset mode requires a preset and does not accept custom dimensions.";
253 }
254 if (hasWidth && hasHeight && input.width! * input.height! > PREVIEW_VIEWPORT_MAX_AREA) {
255 return `Custom viewport area must not exceed ${PREVIEW_VIEWPORT_MAX_AREA} pixels.`;
256 }
257 return true;
258 }),
259);
260export type PreviewAutomationResizeInput = typeof PreviewAutomationResizeInput.Type;
261​
262export const PreviewAutomationResizeResult = Schema.Struct({
263 tabId: PreviewTabId,
264 setting: PreviewViewportSetting,
265 viewport: Schema.optional(PreviewRenderedViewportSize).annotate({
266 description: "Viewport size after the resize. Omitted when it could not be measured.",
267 }),
268});
269export type PreviewAutomationResizeResult = typeof PreviewAutomationResizeResult.Type;
270​
271// Mirrors DesktopPreviewColorScheme without importing ipc.ts.
272export const PreviewAutomationColorScheme = Schema.Literals(["system", "light", "dark"]);
273export type PreviewAutomationColorScheme = typeof PreviewAutomationColorScheme.Type;
274​
275export const PreviewAutomationSetColorSchemeInput = Schema.Struct({
276 ...PreviewAutomationTabTargetFields,
277 colorScheme: PreviewAutomationColorScheme,
278});
279export type PreviewAutomationSetColorSchemeInput = typeof PreviewAutomationSetColorSchemeInput.Type;
280​
281export const PreviewAutomationSetColorSchemeResult = Schema.Struct({
282 tabId: PreviewTabId,
283 colorScheme: PreviewAutomationColorScheme,
284});
285export type PreviewAutomationSetColorSchemeResult =
286 typeof PreviewAutomationSetColorSchemeResult.Type;
287​
288const Locator = TrimmedNonEmptyString;
289const LegacySelector = TrimmedNonEmptyString;
290​
291const PointerTargetFields = {
292 ...PreviewAutomationTabTargetFields,
293 selector: Schema.optional(LegacySelector),
294 locator: Schema.optional(Locator),
295 x: Schema.optional(Schema.Finite),
296 y: Schema.optional(Schema.Finite),
297 timeoutMs: OptionalTimeoutMs,
298};
299​
300const singlePointerTarget = Schema.makeFilter(
301 (input: {
302 readonly selector?: string | undefined;
303 readonly locator?: string | undefined;
304 readonly x?: number | undefined;
305 readonly y?: number | undefined;
306 }) => {
307 const selectorModes =
308 Number(input.selector !== undefined) + Number(input.locator !== undefined);
309 const hasX = input.x !== undefined;
310 const hasY = input.y !== undefined;
311 if (hasX !== hasY) return "Coordinates require both x and y.";
312 const coordinateModes = hasX && hasY ? 1 : 0;
313 return selectorModes + coordinateModes === 1 || "Provide exactly one target.";
314 },
315);
316​
317export const PreviewAutomationClickInput = Schema.Struct({
318 ...PointerTargetFields,
319 button: Schema.optional(
320 Schema.Literals(["left", "right", "middle"]).annotate({
321 description: "Button. Default left. Right opens the menu.",
322 }),
323 ),
324 clickCount: Schema.optional(
325 Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 3 })).annotate({
326 description: "1 click, 2 double, 3 triple. Default 1.",
327 }),
328 ),
329}).check(singlePointerTarget);
330export type PreviewAutomationClickInput = typeof PreviewAutomationClickInput.Type;
331​
332export const PreviewAutomationHoverInput =
333 Schema.Struct(PointerTargetFields).check(singlePointerTarget);
334export type PreviewAutomationHoverInput = typeof PreviewAutomationHoverInput.Type;
335​
336export const PreviewAutomationSelectInput = Schema.Struct({
337 ...PreviewAutomationTabTargetFields,
338 selector: Schema.optional(LegacySelector),
339 locator: Schema.optional(Locator),
340 values: Schema.Array(Schema.String).check(Schema.isMaxLength(100)).annotate({
341 description: "Empty clears it.",
342 }),
343 timeoutMs: OptionalTimeoutMs,
344}).check(
345 Schema.makeFilter(
346 (input) =>
347 Number(input.selector !== undefined) + Number(input.locator !== undefined) === 1 ||
348 "Provide exactly one of selector or locator.",
349 ),
350);
351export type PreviewAutomationSelectInput = typeof PreviewAutomationSelectInput.Type;
352​
353export const PreviewAutomationSelectResult = Schema.Struct({
354 selected: Schema.Array(Schema.String).annotate({
355 description: "The values of the options now selected.",
356 }),
357});
358export type PreviewAutomationSelectResult = typeof PreviewAutomationSelectResult.Type;
359​
360export const PreviewAutomationDragInput = Schema.Struct({
361 ...PreviewAutomationTabTargetFields,
362 source: Locator,
363 target: Locator,
364 timeoutMs: OptionalTimeoutMs,
365});
366export type PreviewAutomationDragInput = typeof PreviewAutomationDragInput.Type;
367​
368export const PreviewAutomationUploadInput = Schema.Struct({
369 ...PreviewAutomationTabTargetFields,
370 paths: Schema.Array(TrimmedNonEmptyString).check(Schema.isMaxLength(20)),
371 selector: Schema.optional(LegacySelector),
372 locator: Schema.optional(Locator).annotate({
373 description: "Set files on this <input type=file> instead of the open picker.",
374 }),
375 timeoutMs: OptionalTimeoutMs,
376}).check(
377 Schema.makeFilter(
378 (input) =>
379 !(input.selector !== undefined && input.locator !== undefined) ||
380 "Provide at most one of selector or locator.",
381 ),
382);
383export type PreviewAutomationUploadInput = typeof PreviewAutomationUploadInput.Type;
384​
385export const PreviewAutomationTypeInput = Schema.Struct({
386 ...PreviewAutomationTabTargetFields,
387 text: Schema.String,
388 selector: Schema.optional(LegacySelector),
389 locator: Schema.optional(Locator),
390 clear: Schema.optional(
391 Schema.Boolean.annotate({
392 description: "Clear the input first. Default false.",
393 }),
394 ),
395 timeoutMs: OptionalTimeoutMs,
396}).check(
397 Schema.makeFilter(
398 (input) =>
399 !(input.selector !== undefined && input.locator !== undefined) ||
400 "Provide at most one of selector or locator.",
401 ),
402);
403export type PreviewAutomationTypeInput = typeof PreviewAutomationTypeInput.Type;
404​
405export const PreviewAutomationPressInput = Schema.Struct({
406 ...PreviewAutomationTabTargetFields,
407 key: Schema.String.check(Schema.isTrimmed()).check(Schema.isNonEmpty()),
408 modifiers: Schema.optional(Schema.Array(Schema.Literals(["Alt", "Control", "Meta", "Shift"]))),
409});
410export type PreviewAutomationPressInput = typeof PreviewAutomationPressInput.Type;
411​
412export const PreviewAutomationScrollInput = Schema.Struct({
413 ...PreviewAutomationTabTargetFields,
414 deltaX: Schema.optional(Schema.Finite),
415 deltaY: Schema.optional(Schema.Finite),
416 selector: Schema.optional(LegacySelector),
417 locator: Schema.optional(Locator),
418}).check(
419 Schema.makeFilter((input) => {
420 if (input.selector !== undefined && input.locator !== undefined) {
421 return "Provide at most one of selector or locator.";
422 }
423 return input.deltaX !== undefined || input.deltaY !== undefined || "Provide deltaX or deltaY.";
424 }),
425);
426export type PreviewAutomationScrollInput = typeof PreviewAutomationScrollInput.Type;
427​
428/** Strict input for each `preview_act` action. */
429export const PreviewAutomationActInputs = {
430 click: PreviewAutomationClickInput,
431 type: PreviewAutomationTypeInput,
432 hover: PreviewAutomationHoverInput,
433 select: PreviewAutomationSelectInput,
434 drag: PreviewAutomationDragInput,
435 upload: PreviewAutomationUploadInput,
436 press: PreviewAutomationPressInput,
437 scroll: PreviewAutomationScrollInput,
438} as const;
439export type PreviewAutomationAction = keyof typeof PreviewAutomationActInputs;
440​
441/** One flat shape for every page action. The server decodes it with the action's strict input. */
442export const PreviewAutomationActInput = Schema.Struct({
443 action: Schema.Literals([
444 "click",
445 "type",
446 "hover",
447 "select",
448 "drag",
449 "upload",
450 "press",
451 "scroll",
452 ]),
453 ...PreviewAutomationTabTargetFields,
454 locator: Schema.optional(Locator),
455 selector: Schema.optional(LegacySelector),
456 x: Schema.optional(Schema.Finite),
457 y: Schema.optional(Schema.Finite),
458 button: Schema.optional(Schema.Literals(["left", "right", "middle"])),
459 clickCount: Schema.optional(Schema.Int),
460 text: Schema.optional(Schema.String),
461 clear: Schema.optional(Schema.Boolean),
462 values: Schema.optional(Schema.Array(Schema.String)),
463 source: Schema.optional(Locator),
464 target: Schema.optional(Locator),
465 paths: Schema.optional(Schema.Array(Schema.String)),
466 key: Schema.optional(Schema.String),
467 modifiers: PreviewAutomationPressInput.fields.modifiers,
468 deltaX: Schema.optional(Schema.Finite),
469 deltaY: Schema.optional(Schema.Finite),
470 timeoutMs: Schema.optional(Schema.Int),
471});
472export type PreviewAutomationActInput = typeof PreviewAutomationActInput.Type;
473​
474export const PreviewAutomationEvaluateInput = Schema.Struct({
475 ...PreviewAutomationTabTargetFields,
476 expression: Schema.String.check(Schema.isTrimmed())
477 .check(Schema.isNonEmpty())
478 .check(Schema.isMaxLength(64_000)),
479 awaitPromise: Schema.optional(Schema.Boolean.annotate({ description: "Default true." })),
480});
481export type PreviewAutomationEvaluateInput = typeof PreviewAutomationEvaluateInput.Type;
482​
483export const PreviewAutomationWaitForInput = Schema.Struct({
484 ...PreviewAutomationTabTargetFields,
485 selector: Schema.optional(LegacySelector),
486 locator: Schema.optional(Locator),
487 text: Schema.optional(TrimmedNonEmptyString).annotate({
488 description: "Case counts.",
489 }),
490 urlIncludes: Schema.optional(TrimmedNonEmptyString),
491 timeoutMs: OptionalTimeoutMs,
492}).check(
493 Schema.makeFilter((input) => {
494 if (input.selector !== undefined && input.locator !== undefined) {
495 return "Provide at most one of selector or locator.";
496 }
497 return (
498 input.selector !== undefined ||
499 input.locator !== undefined ||
500 input.text !== undefined ||
501 input.urlIncludes !== undefined ||
502 "Provide at least one wait condition."
503 );
504 }),
505);
506export type PreviewAutomationWaitForInput = typeof PreviewAutomationWaitForInput.Type;
507​
508export const PreviewAutomationElement = Schema.Struct({
509 tag: Schema.String,
510 role: Schema.NullOr(Schema.String),
511 name: Schema.String,
512 selector: Schema.String,
513 x: Schema.Number,
514 y: Schema.Number,
515 width: Schema.Number,
516 height: Schema.Number,
517});
518export type PreviewAutomationElement = typeof PreviewAutomationElement.Type;
519​
520export const PreviewAutomationConsoleEntry = Schema.Struct({
521 level: Schema.String,
522 text: Schema.String,
523 timestamp: Schema.String,
524 source: Schema.optional(Schema.String),
525});
526export type PreviewAutomationConsoleEntry = typeof PreviewAutomationConsoleEntry.Type;
527​
528export const PreviewAutomationNetworkEntry = Schema.Struct({
529 url: Schema.String,
530 method: Schema.String,
531 status: Schema.NullOr(Schema.Number),
532 failed: Schema.Boolean,
533 errorText: Schema.optional(Schema.String),
534 timestamp: Schema.String,
535});
536export type PreviewAutomationNetworkEntry = typeof PreviewAutomationNetworkEntry.Type;
537​
538export const PreviewAutomationActionEvent = Schema.Struct({
539 id: Schema.String,
540 action: Schema.String,
541 status: Schema.Literals(["running", "succeeded", "failed", "interrupted"]),
542 startedAt: Schema.String,
543 completedAt: Schema.optional(Schema.String),
544 error: Schema.optional(Schema.String),
545});
546export type PreviewAutomationActionEvent = typeof PreviewAutomationActionEvent.Type;
547​
548export const PreviewAutomationSnapshot = Schema.Struct({
549 url: Schema.String,
550 title: Schema.String,
551 loading: Schema.Boolean,
552 loadError: Schema.optional(PreviewAutomationLoadError),
553 visibleText: Schema.String,
554 interactiveElements: Schema.Array(PreviewAutomationElement),
555 accessibilityTree: Schema.Unknown,
556 consoleEntries: Schema.Array(PreviewAutomationConsoleEntry),
557 networkEntries: Schema.Array(PreviewAutomationNetworkEntry),
558 actionTimeline: Schema.Array(PreviewAutomationActionEvent),
559 screenshot: Schema.Struct({
560 mimeType: Schema.Literal("image/png"),
561 data: Schema.String,
562 width: Schema.Int,
563 height: Schema.Int,
564 }),
565 /** The capture before `screenshot` was capped for the agent, when requested and larger. */
566 fullScreenshot: Schema.optional(
567 Schema.Struct({ data: Schema.String, width: Schema.Int, height: Schema.Int }),
568 ),
569});
570export type PreviewAutomationSnapshot = typeof PreviewAutomationSnapshot.Type;
571​
572export const PreviewAutomationSnapshotInput = Schema.Struct({
573 fullScreenshot: Schema.optional(Schema.Boolean),
574});
575export type PreviewAutomationSnapshotInput = typeof PreviewAutomationSnapshotInput.Type;
576​
577export const PreviewAutomationRecordingStatus = Schema.Struct({
578 tabId: PreviewTabId,
579 recording: Schema.Boolean,
580 startedAt: Schema.NullOr(Schema.String),
581});
582export type PreviewAutomationRecordingStatus = typeof PreviewAutomationRecordingStatus.Type;
583​
584export const PREVIEW_RECORDING_STOP_TIMEOUT_MS = 120_000;
585​
586export const PreviewAutomationRecordingArtifact = Schema.Struct({
587 id: Schema.String,
588 tabId: PreviewTabId,
589 path: Schema.String,
590 mimeType: Schema.String,
591 sizeBytes: Schema.Int,
592 createdAt: Schema.String,
593});
594export type PreviewAutomationRecordingArtifact = typeof PreviewAutomationRecordingArtifact.Type;
595​
596export const PreviewAutomationClientId = TrimmedNonEmptyString.check(Schema.isMaxLength(128));
597export type PreviewAutomationClientId = typeof PreviewAutomationClientId.Type;
598export const PreviewAutomationConnectionId = TrimmedNonEmptyString.check(Schema.isMaxLength(64));
599export type PreviewAutomationConnectionId = typeof PreviewAutomationConnectionId.Type;
600​
601export const PreviewAutomationHostIdentity = Schema.Struct({
602 clientId: PreviewAutomationClientId,
603 environmentId: EnvironmentId,
604});
605export type PreviewAutomationHostIdentity = typeof PreviewAutomationHostIdentity.Type;
606​
607export const PreviewAutomationHost = Schema.Struct({
608 ...PreviewAutomationHostIdentity.fields,
609 /** Missing means the V1 operation set from before capability negotiation. */
610 supportedOperations: Schema.optional(Schema.Array(PreviewAutomationOperation)),
611});
612export type PreviewAutomationHost = typeof PreviewAutomationHost.Type;
613​
614export const PreviewAutomationHostFocus = Schema.Struct({
615 ...PreviewAutomationHostIdentity.fields,
616 connectionId: PreviewAutomationConnectionId,
617 focused: Schema.Boolean,
618 liveTabs: Schema.optional(
619 Schema.Array(
620 Schema.Struct({
621 threadId: ThreadId,
622 tabId: PreviewTabId,
623 visible: Schema.optional(Schema.Boolean),
624 }),
625 ),
626 ),
627});
628export type PreviewAutomationHostFocus = typeof PreviewAutomationHostFocus.Type;
629​
630export const PreviewAutomationRequest = Schema.Struct({
631 requestId: TrimmedNonEmptyString,
632 threadId: ThreadId,
633 tabId: Schema.optional(PreviewTabId),
634 tabIdExplicit: Schema.optional(Schema.Boolean),
635 /** Supplied by the broker from authenticated scope, never from tool input. */
636 agentSessionId: Schema.optional(Schema.String),
637 operation: PreviewAutomationOperation,
638 input: Schema.Unknown,
639 timeoutMs: Schema.Int.check(Schema.isGreaterThan(0)),
640});
641export type PreviewAutomationRequest = typeof PreviewAutomationRequest.Type;
642​
643export const PreviewAutomationStreamEvent = Schema.Union([
644 Schema.Struct({
645 type: Schema.Literal("connected"),
646 connectionId: PreviewAutomationConnectionId,
647 }),
648 Schema.Struct({
649 type: Schema.Literal("request"),
650 connectionId: PreviewAutomationConnectionId,
651 request: PreviewAutomationRequest,
652 }),
653]);
654export type PreviewAutomationStreamEvent = typeof PreviewAutomationStreamEvent.Type;
655​
656export const PreviewAutomationResponse = Schema.Struct({
657 clientId: PreviewAutomationClientId,
658 connectionId: PreviewAutomationConnectionId,
659 requestId: TrimmedNonEmptyString,
660 ok: Schema.Boolean,
661 result: Schema.optional(Schema.Unknown),
662 error: Schema.optional(
663 Schema.Struct({
664 _tag: TrimmedNonEmptyString,
665 message: Schema.String,
666 detail: Schema.optional(Schema.Unknown),
667 }),
668 ),
669});
670export type PreviewAutomationResponse = typeof PreviewAutomationResponse.Type;
671​
672const McpCapabilityErrorFields = {
673 environmentId: EnvironmentId,
674 threadId: Schema.optional(ThreadId),
675 providerSessionId: Schema.optional(TrimmedNonEmptyString),
676 providerInstanceId: Schema.optional(ProviderInstanceId),
677};
678​
679export class PreviewAutomationUnavailableError extends Schema.TaggedError<PreviewAutomationUnavailableError>()(
680 "PreviewAutomationUnavailableError",
681 {
682 capability: Schema.Literal("preview"),
683 ...McpCapabilityErrorFields,
684 },
685) {
686 override get message(): string {
687 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.`;
688 }
690​
691export class McpCapabilityUnavailableError extends Schema.TaggedError<McpCapabilityUnavailableError>()(
692 "McpCapabilityUnavailableError",
693 {
694 capability: TrimmedNonEmptyString,
695 ...McpCapabilityErrorFields,
696 },
697) {
698 override get message(): string {
699 return `MCP credential does not grant the ${this.capability} capability.`;
700 }
702​
703const PreviewAutomationScopeErrorFields = {
704 operation: PreviewAutomationOperation,
705 environmentId: EnvironmentId,
706 threadId: ThreadId,
707 providerSessionId: TrimmedNonEmptyString,
708 providerInstanceId: ProviderInstanceId,
709};
710​
711export const SERVER_BROWSER_AUTOMATION_CLIENT_ID = "server-browser";
712​
713const PreviewAutomationRequestErrorFields = {
714 ...PreviewAutomationScopeErrorFields,
715 clientId: TrimmedNonEmptyString,
716 connectionId: PreviewAutomationConnectionId,
717 requestId: TrimmedNonEmptyString,
718 tabId: Schema.optional(PreviewTabId),
719 timeoutMs: Schema.Int.check(Schema.isGreaterThan(0)),
720};
721​
722const PreviewAutomationRemoteDiagnosticFields = {
723 remoteTag: TrimmedNonEmptyString,
724 remoteMessageLength: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
725 remoteDetailKind: Schema.optional(
726 Schema.Literals(["null", "array", "object", "string", "number", "boolean"]),
727 ),
728 cause: Schema.Defect(),
729};
730​
731const PreviewAutomationOptionalRemoteDiagnosticFields = {
732 remoteTag: Schema.optional(TrimmedNonEmptyString),
733 remoteMessageLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
734 remoteDetailKind: Schema.optional(
735 Schema.Literals(["null", "array", "object", "string", "number", "boolean"]),
736 ),
737 cause: Schema.optional(Schema.Defect()),
738};
739​
740export class PreviewAutomationNoAvailableHostError extends Schema.TaggedError<PreviewAutomationNoAvailableHostError>()(
741 "PreviewAutomationNoAvailableHostError",
742 {
743 ...PreviewAutomationScopeErrorFields,
744 clientId: Schema.optional(TrimmedNonEmptyString),
745 connectionId: Schema.optional(PreviewAutomationConnectionId),
746 requestId: Schema.optional(TrimmedNonEmptyString),
747 tabId: Schema.optional(PreviewTabId),
748 timeoutMs: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
749 ...PreviewAutomationOptionalRemoteDiagnosticFields,
750 },
751) {
752 override get message(): string {
753 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.`;
754 }
756​
757export class PreviewAutomationUnsupportedClientError extends Schema.TaggedError<PreviewAutomationUnsupportedClientError>()(
758 "PreviewAutomationUnsupportedClientError",
759 {
760 ...PreviewAutomationRequestErrorFields,
761 ...PreviewAutomationRemoteDiagnosticFields,
762 },
763) {
764 override get message(): string {
765 return `Preview automation client ${this.clientId} does not support ${this.operation}.`;
766 }
768​
769export class PreviewAutomationTabNotFoundError extends Schema.TaggedError<PreviewAutomationTabNotFoundError>()(
770 "PreviewAutomationTabNotFoundError",
771 {
772 ...PreviewAutomationRequestErrorFields,
773 ...PreviewAutomationRemoteDiagnosticFields,
774 },
775) {
776 override get message(): string {
777 return this.tabId
778 ? `Preview tab ${this.tabId} was not found for ${this.operation}. Omit tabId to use the current tab, or call preview_open.`
779 : `No active preview tab was found for ${this.operation}. Call preview_open first.`;
780 }
782​
783export class PreviewAutomationTimeoutError extends Schema.TaggedError<PreviewAutomationTimeoutError>()(
784 "PreviewAutomationTimeoutError",
785 {
786 ...PreviewAutomationRequestErrorFields,
787 ...PreviewAutomationOptionalRemoteDiagnosticFields,
788 reason: Schema.optional(Schema.String),
789 },
790) {
791 override get message(): string {
792 const summary = `Preview automation ${this.operation} timed out after ${this.timeoutMs}ms.`;
793 return this.reason === undefined ? summary : `${summary} ${this.reason}`;
794 }
796​
797export const PreviewAutomationControlReason = Schema.Literals([
798 "agentMismatch",
799 "unclaimedTab",
800 "humanControl",
801 "tabRequired",
802 "closed",
803 "interrupted",
804 "dialogPending",
805 "tabLimit",
806]);
807export type PreviewAutomationControlReason = typeof PreviewAutomationControlReason.Type;
808​
809export class PreviewAutomationControlInterruptedError extends Schema.TaggedError<PreviewAutomationControlInterruptedError>()(
810 "PreviewAutomationControlInterruptedError",
811 {
812 ...PreviewAutomationRequestErrorFields,
813 ...PreviewAutomationRemoteDiagnosticFields,
814 reason: Schema.optional(PreviewAutomationControlReason),
815 },
816) {
817 override get message(): string {
818 if (this.reason === "agentMismatch")
819 return "This browser tab belongs to another agent session or a human. Open your own tab.";
820 if (this.reason === "unclaimedTab")
821 return "This tab has no agent owner. Ask the user to close it in the browser panel.";
822 if (this.reason === "humanControl")
823 return "A human controls this browser tab. Wait until they release control.";
824 if (this.reason === "tabRequired")
825 return "Multiple browser tabs belong to this session. Pass a tabId from preview_open or from the tabs listed by preview_status.";
826 if (this.reason === "dialogPending")
827 return "A browser dialog is pending. Read preview_status and resolve it with preview_dialog.";
828 if (this.reason === "tabLimit")
829 return "Too many server browser tabs are open. Close one with t3_preview_close, or reuse a tabId from preview_status tabs.";
830 if (this.reason === "closed") return "This browser tab is closed. Call preview_open.";
831 if (this.reason === "interrupted")
832 return "Browser control changed. Take a fresh snapshot before trying again.";
833 return `Preview automation ${this.operation} was interrupted on client ${this.clientId}.`;
834 }
836​
837export class PreviewAutomationExecutionError extends Schema.TaggedError<PreviewAutomationExecutionError>()(
838 "PreviewAutomationExecutionError",
839 {
840 ...PreviewAutomationRequestErrorFields,
841 ...PreviewAutomationRemoteDiagnosticFields,
842 reason: Schema.optional(Schema.String),
843 },
844) {
845 override get message(): string {
846 return this.reason === undefined
847 ? `Preview automation ${this.operation} failed on client ${this.clientId}.`
848 : `Preview automation ${this.operation} failed: ${this.reason}`;
849 }
851​
852export class PreviewAutomationInvalidSelectorError extends Schema.TaggedError<PreviewAutomationInvalidSelectorError>()(
853 "PreviewAutomationInvalidSelectorError",
854 {
855 ...PreviewAutomationRequestErrorFields,
856 ...PreviewAutomationRemoteDiagnosticFields,
857 selectorKind: Schema.optional(Schema.Literals(["locator", "selector"])),
858 selectorLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
859 staleRef: Schema.optional(Schema.Boolean),
860 },
861) {
862 override get message(): string {
863 if (this.staleRef)
864 return "This element ref is stale. Navigation, dialogs, and new snapshots replace refs; take a fresh snapshot and use its refs.";
865 if (this.selectorKind !== undefined && this.selectorLength !== undefined) {
866 const guidance =
867 this.selectorKind === "locator"
868 ? "Use aria-ref=<ref> from the latest snapshot or a Playwright role/text selector; put CSS in selector."
869 : "selector takes CSS; use locator for aria-ref=<ref>, role, or text targets.";
870 return `Preview automation ${this.operation} received an invalid ${this.selectorKind} (${this.selectorLength} characters). ${guidance}`;
871 }
872 return `Preview automation ${this.operation} received an invalid selector.`;
873 }
875​
876export class PreviewAutomationTargetNotEditableError extends Schema.TaggedError<PreviewAutomationTargetNotEditableError>()(
877 "PreviewAutomationTargetNotEditableError",
878 {
879 ...PreviewAutomationRequestErrorFields,
880 ...PreviewAutomationRemoteDiagnosticFields,
881 selectorKind: Schema.optional(Schema.Literals(["focused-element", "locator", "selector"])),
882 selectorLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
883 },
884) {
885 override get message(): string {
886 if (this.selectorKind === "focused-element") {
887 return `Preview automation ${this.operation} requires an editable focused element.`;
888 }
889 if (this.selectorKind !== undefined && this.selectorLength !== undefined) {
890 return `Preview automation ${this.operation} requires an editable ${this.selectorKind} (${this.selectorLength} characters).`;
891 }
892 return `Preview automation ${this.operation} requires an editable target.`;
893 }
895​
896export class PreviewAutomationResultTooLargeError extends Schema.TaggedError<PreviewAutomationResultTooLargeError>()(
897 "PreviewAutomationResultTooLargeError",
898 {
899 ...PreviewAutomationRequestErrorFields,
900 ...PreviewAutomationRemoteDiagnosticFields,
901 maximumBytes: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
902 },
903) {
904 override get message(): string {
905 const summary =
906 this.maximumBytes === undefined
907 ? `Preview automation ${this.operation} produced a result that is too large.`
908 : `Preview automation ${this.operation} produced a result larger than ${this.maximumBytes} bytes.`;
909 return summary;
910 }
912​
913export class PreviewAutomationClientDisconnectedError extends Schema.TaggedError<PreviewAutomationClientDisconnectedError>()(
914 "PreviewAutomationClientDisconnectedError",
915 PreviewAutomationRequestErrorFields,
916) {
917 override get message(): string {
918 return `Preview automation client ${this.clientId} disconnected during ${this.operation}.`;
919 }
921​
922export class PreviewAutomationRequestQueueClosedError extends Schema.TaggedError<PreviewAutomationRequestQueueClosedError>()(
923 "PreviewAutomationRequestQueueClosedError",
924 PreviewAutomationRequestErrorFields,
925) {
926 override get message(): string {
927 return `Preview automation client ${this.clientId} stopped accepting ${this.operation} requests.`;
928 }
930​
931export class PreviewAutomationRemoteUnavailableError extends Schema.TaggedError<PreviewAutomationRemoteUnavailableError>()(
932 "PreviewAutomationRemoteUnavailableError",
933 {
934 ...PreviewAutomationRequestErrorFields,
935 ...PreviewAutomationRemoteDiagnosticFields,
936 },
937) {
938 override get message(): string {
939 return `Preview automation ${this.operation} is unavailable on client ${this.clientId}.`;
940 }
942​
943export class PreviewAutomationMalformedResponseError extends Schema.TaggedError<PreviewAutomationMalformedResponseError>()(
944 "PreviewAutomationMalformedResponseError",
945 PreviewAutomationRequestErrorFields,
946) {
947 override get message(): string {
948 return `Preview automation client ${this.clientId} returned a malformed response for ${this.operation}.`;
949 }
951​
952export class PreviewAutomationRecordingTransferError extends Schema.TaggedError<PreviewAutomationRecordingTransferError>()(
953 "PreviewAutomationRecordingTransferError",
954 {
955 threadId: ThreadId,
956 cause: Schema.optional(Schema.Defect()),
957 },
958) {
959 override get message(): string {
960 return "Preview recording could not be saved to the agent environment.";
961 }
963​
964export class PreviewAutomationRecordingDesktopUpdateRequiredError extends Schema.TaggedError<PreviewAutomationRecordingDesktopUpdateRequiredError>()(
965 "PreviewAutomationRecordingDesktopUpdateRequiredError",
966 { threadId: ThreadId, cause: Schema.optional(Schema.Defect()) },
967) {
968 override get message(): string {
969 return "Update the desktop app to transfer recordings. The recording remains on the desktop.";
970 }
972​
973export class PreviewAutomationRecordingTooLargeError extends Schema.TaggedError<PreviewAutomationRecordingTooLargeError>()(
974 "PreviewAutomationRecordingTooLargeError",
975 { threadId: ThreadId, cause: Schema.optional(Schema.Defect()) },
976) {
977 override get message(): string {
978 return "The recording exceeds 50 MiB. Make a shorter recording.";
979 }
981​
982export class PreviewAutomationRecordingDeadlineExpiredError extends Schema.TaggedError<PreviewAutomationRecordingDeadlineExpiredError>()(
983 "PreviewAutomationRecordingDeadlineExpiredError",
984 { threadId: ThreadId, cause: Schema.optional(Schema.Defect()) },
985) {
986 override get message(): string {
987 return "The recording transfer deadline expired.";
988 }
990​
991export const PreviewAutomationError = Schema.Union([
992 PreviewAutomationRecordingTransferError,
993 PreviewAutomationRecordingDesktopUpdateRequiredError,
994 PreviewAutomationRecordingTooLargeError,
995 PreviewAutomationRecordingDeadlineExpiredError,
996 PreviewAutomationUnavailableError,
997 PreviewAutomationNoAvailableHostError,
998 PreviewAutomationUnsupportedClientError,
999 PreviewAutomationTabNotFoundError,
1000 PreviewAutomationTimeoutError,
1001 PreviewAutomationControlInterruptedError,
1002 PreviewAutomationExecutionError,
1003 PreviewAutomationInvalidSelectorError,
1004 PreviewAutomationTargetNotEditableError,
1005 PreviewAutomationResultTooLargeError,
1006 PreviewAutomationClientDisconnectedError,
1007 PreviewAutomationRequestQueueClosedError,
1008 PreviewAutomationRemoteUnavailableError,
1009 PreviewAutomationMalformedResponseError,
1010]);
1011export type PreviewAutomationError = typeof PreviewAutomationError.Type;
1012​
1013export const PreviewUrlResolution = Schema.Struct({
1014 requestedUrl: Schema.String,
1015 resolvedUrl: Schema.String,
1016 resolutionKind: Schema.Literals(["direct", "direct-private-network"]),
1017 environmentId: EnvironmentId,
1018});
1019export type PreviewUrlResolution = typeof PreviewUrlResolution.Type;