Open History to see the last commit
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: PreviewRenderedViewportSize,
266});
267export type PreviewAutomationResizeResult = typeof PreviewAutomationResizeResult.Type;
268
269// Mirrors DesktopPreviewColorScheme without importing ipc.ts.
270export const PreviewAutomationColorScheme = Schema.Literals(["system", "light", "dark"]);
271export type PreviewAutomationColorScheme = typeof PreviewAutomationColorScheme.Type;
272
273export const PreviewAutomationSetColorSchemeInput = Schema.Struct({
274 ...PreviewAutomationTabTargetFields,
275 colorScheme: PreviewAutomationColorScheme,
276});
277export type PreviewAutomationSetColorSchemeInput = typeof PreviewAutomationSetColorSchemeInput.Type;
278
279export const PreviewAutomationSetColorSchemeResult = Schema.Struct({
280 tabId: PreviewTabId,
281 colorScheme: PreviewAutomationColorScheme,
282});
283export type PreviewAutomationSetColorSchemeResult =
284 typeof PreviewAutomationSetColorSchemeResult.Type;
285
286const Locator = TrimmedNonEmptyString;
287const LegacySelector = TrimmedNonEmptyString;
288
289const PointerTargetFields = {
290 ...PreviewAutomationTabTargetFields,
291 selector: Schema.optional(LegacySelector),
292 locator: Schema.optional(Locator),
293 x: Schema.optional(Schema.Finite),
294 y: Schema.optional(Schema.Finite),
295 timeoutMs: OptionalTimeoutMs,
296};
297
298const singlePointerTarget = Schema.makeFilter(
299 (input: {
300 readonly selector?: string | undefined;
301 readonly locator?: string | undefined;
302 readonly x?: number | undefined;
303 readonly y?: number | undefined;
304 }) => {
305 const selectorModes =
306 Number(input.selector !== undefined) + Number(input.locator !== undefined);
307 const hasX = input.x !== undefined;
308 const hasY = input.y !== undefined;
309 if (hasX !== hasY) return "Coordinates require both x and y.";
310 const coordinateModes = hasX && hasY ? 1 : 0;
311 return selectorModes + coordinateModes === 1 || "Provide exactly one target.";
312 },
313);
314
315export const PreviewAutomationClickInput = Schema.Struct({
316 ...PointerTargetFields,
317 button: Schema.optional(
318 Schema.Literals(["left", "right", "middle"]).annotate({
319 description: "Button. Default left. Right opens the menu.",
320 }),
321 ),
322 clickCount: Schema.optional(
323 Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 3 })).annotate({
324 description: "1 click, 2 double, 3 triple. Default 1.",
325 }),
326 ),
327}).check(singlePointerTarget);
328export type PreviewAutomationClickInput = typeof PreviewAutomationClickInput.Type;
329
330export const PreviewAutomationHoverInput =
331 Schema.Struct(PointerTargetFields).check(singlePointerTarget);
332export type PreviewAutomationHoverInput = typeof PreviewAutomationHoverInput.Type;
333
334export const PreviewAutomationSelectInput = Schema.Struct({
335 ...PreviewAutomationTabTargetFields,
336 selector: Schema.optional(LegacySelector),
337 locator: Schema.optional(Locator),
338 values: Schema.Array(Schema.String).check(Schema.isMaxLength(100)).annotate({
339 description: "Empty clears it.",
340 }),
341 timeoutMs: OptionalTimeoutMs,
342}).check(
343 Schema.makeFilter(
344 (input) =>
345 Number(input.selector !== undefined) + Number(input.locator !== undefined) === 1 ||
346 "Provide exactly one of selector or locator.",
347 ),
348);
349export type PreviewAutomationSelectInput = typeof PreviewAutomationSelectInput.Type;
350
351export const PreviewAutomationSelectResult = Schema.Struct({
352 selected: Schema.Array(Schema.String).annotate({
353 description: "The values of the options now selected.",
354 }),
355});
356export type PreviewAutomationSelectResult = typeof PreviewAutomationSelectResult.Type;
357
358export const PreviewAutomationDragInput = Schema.Struct({
359 ...PreviewAutomationTabTargetFields,
360 source: Locator,
361 target: Locator,
362 timeoutMs: OptionalTimeoutMs,
363});
364export type PreviewAutomationDragInput = typeof PreviewAutomationDragInput.Type;
365
366export const PreviewAutomationUploadInput = Schema.Struct({
367 ...PreviewAutomationTabTargetFields,
368 paths: Schema.Array(TrimmedNonEmptyString).check(Schema.isMaxLength(20)),
369 selector: Schema.optional(LegacySelector),
370 locator: Schema.optional(Locator).annotate({
371 description: "Set files on this <input type=file> instead of the open picker.",
372 }),
373 timeoutMs: OptionalTimeoutMs,
374}).check(
375 Schema.makeFilter(
376 (input) =>
377 !(input.selector !== undefined && input.locator !== undefined) ||
378 "Provide at most one of selector or locator.",
379 ),
380);
381export type PreviewAutomationUploadInput = typeof PreviewAutomationUploadInput.Type;
382
383export const PreviewAutomationTypeInput = Schema.Struct({
384 ...PreviewAutomationTabTargetFields,
385 text: Schema.String,
386 selector: Schema.optional(LegacySelector),
387 locator: Schema.optional(Locator),
388 clear: Schema.optional(
389 Schema.Boolean.annotate({
390 description: "Clear the input first. Default false.",
391 }),
392 ),
393 timeoutMs: OptionalTimeoutMs,
394}).check(
395 Schema.makeFilter(
396 (input) =>
397 !(input.selector !== undefined && input.locator !== undefined) ||
398 "Provide at most one of selector or locator.",
399 ),
400);
401export type PreviewAutomationTypeInput = typeof PreviewAutomationTypeInput.Type;
402
403export const PreviewAutomationPressInput = Schema.Struct({
404 ...PreviewAutomationTabTargetFields,
405 key: Schema.String.check(Schema.isTrimmed()).check(Schema.isNonEmpty()),
406 modifiers: Schema.optional(Schema.Array(Schema.Literals(["Alt", "Control", "Meta", "Shift"]))),
407});
408export type PreviewAutomationPressInput = typeof PreviewAutomationPressInput.Type;
409
410export const PreviewAutomationScrollInput = Schema.Struct({
411 ...PreviewAutomationTabTargetFields,
412 deltaX: Schema.optional(Schema.Finite),
413 deltaY: Schema.optional(Schema.Finite),
414 selector: Schema.optional(LegacySelector),
415 locator: Schema.optional(Locator),
416}).check(
417 Schema.makeFilter((input) => {
418 if (input.selector !== undefined && input.locator !== undefined) {
419 return "Provide at most one of selector or locator.";
420 }
421 return input.deltaX !== undefined || input.deltaY !== undefined || "Provide deltaX or deltaY.";
422 }),
423);
424export type PreviewAutomationScrollInput = typeof PreviewAutomationScrollInput.Type;
425
426/** Strict input for each `preview_act` action. */
427export const PreviewAutomationActInputs = {
428 click: PreviewAutomationClickInput,
429 type: PreviewAutomationTypeInput,
430 hover: PreviewAutomationHoverInput,
431 select: PreviewAutomationSelectInput,
432 drag: PreviewAutomationDragInput,
433 upload: PreviewAutomationUploadInput,
434 press: PreviewAutomationPressInput,
435 scroll: PreviewAutomationScrollInput,
436} as const;
437export type PreviewAutomationAction = keyof typeof PreviewAutomationActInputs;
438
439/** One flat shape for every page action. The server decodes it with the action's strict input. */
440export const PreviewAutomationActInput = Schema.Struct({
441 action: Schema.Literals([
442 "click",
443 "type",
444 "hover",
445 "select",
446 "drag",
447 "upload",
448 "press",
449 "scroll",
450 ]),
451 ...PreviewAutomationTabTargetFields,
452 locator: Schema.optional(Locator),
453 selector: Schema.optional(LegacySelector),
454 x: Schema.optional(Schema.Finite),
455 y: Schema.optional(Schema.Finite),
456 button: Schema.optional(Schema.Literals(["left", "right", "middle"])),
457 clickCount: Schema.optional(Schema.Int),
458 text: Schema.optional(Schema.String),
459 clear: Schema.optional(Schema.Boolean),
460 values: Schema.optional(Schema.Array(Schema.String)),
461 source: Schema.optional(Locator),
462 target: Schema.optional(Locator),
463 paths: Schema.optional(Schema.Array(Schema.String)),
464 key: Schema.optional(Schema.String),
465 modifiers: PreviewAutomationPressInput.fields.modifiers,
466 deltaX: Schema.optional(Schema.Finite),
467 deltaY: Schema.optional(Schema.Finite),
468 timeoutMs: Schema.optional(Schema.Int),
469});
470export type PreviewAutomationActInput = typeof PreviewAutomationActInput.Type;
471
472export const PreviewAutomationEvaluateInput = Schema.Struct({
473 ...PreviewAutomationTabTargetFields,
474 expression: Schema.String.check(Schema.isTrimmed())
475 .check(Schema.isNonEmpty())
476 .check(Schema.isMaxLength(64_000)),
477 awaitPromise: Schema.optional(Schema.Boolean.annotate({ description: "Default true." })),
478});
479export type PreviewAutomationEvaluateInput = typeof PreviewAutomationEvaluateInput.Type;
480
481export const PreviewAutomationWaitForInput = Schema.Struct({
482 ...PreviewAutomationTabTargetFields,
483 selector: Schema.optional(LegacySelector),
484 locator: Schema.optional(Locator),
485 text: Schema.optional(TrimmedNonEmptyString).annotate({
486 description: "Case counts.",
487 }),
488 urlIncludes: Schema.optional(TrimmedNonEmptyString),
489 timeoutMs: OptionalTimeoutMs,
490}).check(
491 Schema.makeFilter((input) => {
492 if (input.selector !== undefined && input.locator !== undefined) {
493 return "Provide at most one of selector or locator.";
494 }
495 return (
496 input.selector !== undefined ||
497 input.locator !== undefined ||
498 input.text !== undefined ||
499 input.urlIncludes !== undefined ||
500 "Provide at least one wait condition."
501 );
502 }),
503);
504export type PreviewAutomationWaitForInput = typeof PreviewAutomationWaitForInput.Type;
505
506export const PreviewAutomationElement = Schema.Struct({
507 tag: Schema.String,
508 role: Schema.NullOr(Schema.String),
509 name: Schema.String,
510 selector: Schema.String,
511 x: Schema.Number,
512 y: Schema.Number,
513 width: Schema.Number,
514 height: Schema.Number,
515});
516export type PreviewAutomationElement = typeof PreviewAutomationElement.Type;
517
518export const PreviewAutomationConsoleEntry = Schema.Struct({
519 level: Schema.String,
520 text: Schema.String,
521 timestamp: Schema.String,
522 source: Schema.optional(Schema.String),
523});
524export type PreviewAutomationConsoleEntry = typeof PreviewAutomationConsoleEntry.Type;
525
526export const PreviewAutomationNetworkEntry = Schema.Struct({
527 url: Schema.String,
528 method: Schema.String,
529 status: Schema.NullOr(Schema.Number),
530 failed: Schema.Boolean,
531 errorText: Schema.optional(Schema.String),
532 timestamp: Schema.String,
533});
534export type PreviewAutomationNetworkEntry = typeof PreviewAutomationNetworkEntry.Type;
535
536export const PreviewAutomationActionEvent = Schema.Struct({
537 id: Schema.String,
538 action: Schema.String,
539 status: Schema.Literals(["running", "succeeded", "failed", "interrupted"]),
540 startedAt: Schema.String,
541 completedAt: Schema.optional(Schema.String),
542 error: Schema.optional(Schema.String),
543});
544export type PreviewAutomationActionEvent = typeof PreviewAutomationActionEvent.Type;
545
546export const PreviewAutomationSnapshot = Schema.Struct({
547 url: Schema.String,
548 title: Schema.String,
549 loading: Schema.Boolean,
550 loadError: Schema.optional(PreviewAutomationLoadError),
551 visibleText: Schema.String,
552 interactiveElements: Schema.Array(PreviewAutomationElement),
553 accessibilityTree: Schema.Unknown,
554 consoleEntries: Schema.Array(PreviewAutomationConsoleEntry),
555 networkEntries: Schema.Array(PreviewAutomationNetworkEntry),
556 actionTimeline: Schema.Array(PreviewAutomationActionEvent),
557 screenshot: Schema.Struct({
558 mimeType: Schema.Literal("image/png"),
559 data: Schema.String,
560 width: Schema.Int,
561 height: Schema.Int,
562 }),
563 /** The capture before `screenshot` was capped for the agent, when requested and larger. */
564 fullScreenshot: Schema.optional(
565 Schema.Struct({ data: Schema.String, width: Schema.Int, height: Schema.Int }),
566 ),
567});
568export type PreviewAutomationSnapshot = typeof PreviewAutomationSnapshot.Type;
569
570export const PreviewAutomationSnapshotInput = Schema.Struct({
571 fullScreenshot: Schema.optional(Schema.Boolean),
572});
573export type PreviewAutomationSnapshotInput = typeof PreviewAutomationSnapshotInput.Type;
574
575export const PreviewAutomationRecordingStatus = Schema.Struct({
576 tabId: PreviewTabId,
577 recording: Schema.Boolean,
578 startedAt: Schema.NullOr(Schema.String),
579});
580export type PreviewAutomationRecordingStatus = typeof PreviewAutomationRecordingStatus.Type;
581
582export const PREVIEW_RECORDING_STOP_TIMEOUT_MS = 120_000;
583
584export const PreviewAutomationRecordingArtifact = Schema.Struct({
585 id: Schema.String,
586 tabId: PreviewTabId,
587 path: Schema.String,
588 mimeType: Schema.String,
589 sizeBytes: Schema.Int,
590 createdAt: Schema.String,
591});
592export type PreviewAutomationRecordingArtifact = typeof PreviewAutomationRecordingArtifact.Type;
593
594export const PreviewAutomationClientId = TrimmedNonEmptyString.check(Schema.isMaxLength(128));
595export type PreviewAutomationClientId = typeof PreviewAutomationClientId.Type;
596export const PreviewAutomationConnectionId = TrimmedNonEmptyString.check(Schema.isMaxLength(64));
597export type PreviewAutomationConnectionId = typeof PreviewAutomationConnectionId.Type;
598
599export const PreviewAutomationHostIdentity = Schema.Struct({
600 clientId: PreviewAutomationClientId,
601 environmentId: EnvironmentId,
602});
603export type PreviewAutomationHostIdentity = typeof PreviewAutomationHostIdentity.Type;
604
605export const PreviewAutomationHost = Schema.Struct({
606 ...PreviewAutomationHostIdentity.fields,
607 /** Missing means the V1 operation set from before capability negotiation. */
608 supportedOperations: Schema.optional(Schema.Array(PreviewAutomationOperation)),
609});
610export type PreviewAutomationHost = typeof PreviewAutomationHost.Type;
611
612export const PreviewAutomationHostFocus = Schema.Struct({
613 ...PreviewAutomationHostIdentity.fields,
614 connectionId: PreviewAutomationConnectionId,
615 focused: Schema.Boolean,
616 liveTabs: Schema.optional(
617 Schema.Array(
618 Schema.Struct({
619 threadId: ThreadId,
620 tabId: PreviewTabId,
621 visible: Schema.optional(Schema.Boolean),
622 }),
623 ),
624 ),
625});
626export type PreviewAutomationHostFocus = typeof PreviewAutomationHostFocus.Type;
627
628export const PreviewAutomationRequest = Schema.Struct({
629 requestId: TrimmedNonEmptyString,
630 threadId: ThreadId,
631 tabId: Schema.optional(PreviewTabId),
632 tabIdExplicit: Schema.optional(Schema.Boolean),
633 /** Supplied by the broker from authenticated scope, never from tool input. */
634 agentSessionId: Schema.optional(Schema.String),
635 operation: PreviewAutomationOperation,
636 input: Schema.Unknown,
637 timeoutMs: Schema.Int.check(Schema.isGreaterThan(0)),
638});
639export type PreviewAutomationRequest = typeof PreviewAutomationRequest.Type;
640
641export const PreviewAutomationStreamEvent = Schema.Union([
642 Schema.Struct({
643 type: Schema.Literal("connected"),
644 connectionId: PreviewAutomationConnectionId,
645 }),
646 Schema.Struct({
647 type: Schema.Literal("request"),
648 connectionId: PreviewAutomationConnectionId,
649 request: PreviewAutomationRequest,
650 }),
651]);
652export type PreviewAutomationStreamEvent = typeof PreviewAutomationStreamEvent.Type;
653
654export const PreviewAutomationResponse = Schema.Struct({
655 clientId: PreviewAutomationClientId,
656 connectionId: PreviewAutomationConnectionId,
657 requestId: TrimmedNonEmptyString,
658 ok: Schema.Boolean,
659 result: Schema.optional(Schema.Unknown),
660 error: Schema.optional(
661 Schema.Struct({
662 _tag: TrimmedNonEmptyString,
663 message: Schema.String,
664 detail: Schema.optional(Schema.Unknown),
665 }),
666 ),
667});
668export type PreviewAutomationResponse = typeof PreviewAutomationResponse.Type;
669
670const McpCapabilityErrorFields = {
671 environmentId: EnvironmentId,
672 threadId: Schema.optional(ThreadId),
673 providerSessionId: Schema.optional(TrimmedNonEmptyString),
674 providerInstanceId: Schema.optional(ProviderInstanceId),
675};
676
677export class PreviewAutomationUnavailableError extends Schema.TaggedError<PreviewAutomationUnavailableError>()(
678 "PreviewAutomationUnavailableError",
679 {
680 capability: Schema.Literal("preview"),
681 ...McpCapabilityErrorFields,
682 },
683) {
684 override get message(): string {
685 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.`;
686 }
687}
688
689export class McpCapabilityUnavailableError extends Schema.TaggedError<McpCapabilityUnavailableError>()(
690 "McpCapabilityUnavailableError",
691 {
692 capability: TrimmedNonEmptyString,
693 ...McpCapabilityErrorFields,
694 },
695) {
696 override get message(): string {
697 return `MCP credential does not grant the ${this.capability} capability.`;
698 }
699}
700
701const PreviewAutomationScopeErrorFields = {
702 operation: PreviewAutomationOperation,
703 environmentId: EnvironmentId,
704 threadId: ThreadId,
705 providerSessionId: TrimmedNonEmptyString,
706 providerInstanceId: ProviderInstanceId,
707};
708
709export const SERVER_BROWSER_AUTOMATION_CLIENT_ID = "server-browser";
710
711const PreviewAutomationRequestErrorFields = {
712 ...PreviewAutomationScopeErrorFields,
713 clientId: TrimmedNonEmptyString,
714 connectionId: PreviewAutomationConnectionId,
715 requestId: TrimmedNonEmptyString,
716 tabId: Schema.optional(PreviewTabId),
717 timeoutMs: Schema.Int.check(Schema.isGreaterThan(0)),
718};
719
720const PreviewAutomationRemoteDiagnosticFields = {
721 remoteTag: TrimmedNonEmptyString,
722 remoteMessageLength: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
723 remoteDetailKind: Schema.optional(
724 Schema.Literals(["null", "array", "object", "string", "number", "boolean"]),
725 ),
726 cause: Schema.Defect(),
727};
728
729const PreviewAutomationOptionalRemoteDiagnosticFields = {
730 remoteTag: Schema.optional(TrimmedNonEmptyString),
731 remoteMessageLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
732 remoteDetailKind: Schema.optional(
733 Schema.Literals(["null", "array", "object", "string", "number", "boolean"]),
734 ),
735 cause: Schema.optional(Schema.Defect()),
736};
737
738export class PreviewAutomationNoAvailableHostError extends Schema.TaggedError<PreviewAutomationNoAvailableHostError>()(
739 "PreviewAutomationNoAvailableHostError",
740 {
741 ...PreviewAutomationScopeErrorFields,
742 clientId: Schema.optional(TrimmedNonEmptyString),
743 connectionId: Schema.optional(PreviewAutomationConnectionId),
744 requestId: Schema.optional(TrimmedNonEmptyString),
745 tabId: Schema.optional(PreviewTabId),
746 timeoutMs: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
747 ...PreviewAutomationOptionalRemoteDiagnosticFields,
748 },
749) {
750 override get message(): string {
751 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.`;
752 }
753}
754
755export class PreviewAutomationUnsupportedClientError extends Schema.TaggedError<PreviewAutomationUnsupportedClientError>()(
756 "PreviewAutomationUnsupportedClientError",
757 {
758 ...PreviewAutomationRequestErrorFields,
759 ...PreviewAutomationRemoteDiagnosticFields,
760 },
761) {
762 override get message(): string {
763 return `Preview automation client ${this.clientId} does not support ${this.operation}.`;
764 }
765}
766
767export class PreviewAutomationTabNotFoundError extends Schema.TaggedError<PreviewAutomationTabNotFoundError>()(
768 "PreviewAutomationTabNotFoundError",
769 {
770 ...PreviewAutomationRequestErrorFields,
771 ...PreviewAutomationRemoteDiagnosticFields,
772 },
773) {
774 override get message(): string {
775 return this.tabId
776 ? `Preview tab ${this.tabId} was not found for ${this.operation}. Omit tabId to use the current tab, or call preview_open.`
777 : `No active preview tab was found for ${this.operation}. Call preview_open first.`;
778 }
779}
780
781export class PreviewAutomationTimeoutError extends Schema.TaggedError<PreviewAutomationTimeoutError>()(
782 "PreviewAutomationTimeoutError",
783 {
784 ...PreviewAutomationRequestErrorFields,
785 ...PreviewAutomationOptionalRemoteDiagnosticFields,
786 reason: Schema.optional(Schema.String),
787 },
788) {
789 override get message(): string {
790 const summary = `Preview automation ${this.operation} timed out after ${this.timeoutMs}ms.`;
791 return this.reason === undefined ? summary : `${summary} ${this.reason}`;
792 }
793}
794
795export const PreviewAutomationControlReason = Schema.Literals([
796 "agentMismatch",
797 "humanControl",
798 "tabRequired",
799 "closed",
800 "interrupted",
801 "dialogPending",
802 "tabLimit",
803]);
804export type PreviewAutomationControlReason = typeof PreviewAutomationControlReason.Type;
805
806export class PreviewAutomationControlInterruptedError extends Schema.TaggedError<PreviewAutomationControlInterruptedError>()(
807 "PreviewAutomationControlInterruptedError",
808 {
809 ...PreviewAutomationRequestErrorFields,
810 ...PreviewAutomationRemoteDiagnosticFields,
811 reason: Schema.optional(PreviewAutomationControlReason),
812 },
813) {
814 override get message(): string {
815 if (this.reason === "agentMismatch")
816 return "This browser tab belongs to another agent session or a human. Open your own tab.";
817 if (this.reason === "humanControl")
818 return "A human controls this browser tab. Wait until they release control.";
819 if (this.reason === "tabRequired")
820 return "Multiple browser tabs belong to this session. Pass a tabId from preview_open or from the tabs listed by preview_status.";
821 if (this.reason === "dialogPending")
822 return "A browser dialog is pending. Read preview_status and resolve it with preview_dialog.";
823 if (this.reason === "tabLimit")
824 return "Too many server browser tabs are open. Close one with t3_preview_close, or reuse a tabId from preview_status tabs.";
825 if (this.reason === "closed") return "This browser tab is closed. Call preview_open.";
826 if (this.reason === "interrupted")
827 return "Browser control changed. Take a fresh snapshot before trying again.";
828 return `Preview automation ${this.operation} was interrupted on client ${this.clientId}.`;
829 }
830}
831
832export class PreviewAutomationExecutionError extends Schema.TaggedError<PreviewAutomationExecutionError>()(
833 "PreviewAutomationExecutionError",
834 {
835 ...PreviewAutomationRequestErrorFields,
836 ...PreviewAutomationRemoteDiagnosticFields,
837 reason: Schema.optional(Schema.String),
838 },
839) {
840 override get message(): string {
841 return this.reason === undefined
842 ? `Preview automation ${this.operation} failed on client ${this.clientId}.`
843 : `Preview automation ${this.operation} failed: ${this.reason}`;
844 }
845}
846
847export class PreviewAutomationInvalidSelectorError extends Schema.TaggedError<PreviewAutomationInvalidSelectorError>()(
848 "PreviewAutomationInvalidSelectorError",
849 {
850 ...PreviewAutomationRequestErrorFields,
851 ...PreviewAutomationRemoteDiagnosticFields,
852 selectorKind: Schema.optional(Schema.Literals(["locator", "selector"])),
853 selectorLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
854 staleRef: Schema.optional(Schema.Boolean),
855 },
856) {
857 override get message(): string {
858 if (this.staleRef)
859 return "This element ref is stale. Navigation, dialogs, and new snapshots replace refs; take a fresh snapshot and use its refs.";
860 if (this.selectorKind !== undefined && this.selectorLength !== undefined) {
861 const guidance =
862 this.selectorKind === "locator"
863 ? "Use aria-ref=<ref> from the latest snapshot or a Playwright role/text selector; put CSS in selector."
864 : "selector takes CSS; use locator for aria-ref=<ref>, role, or text targets.";
865 return `Preview automation ${this.operation} received an invalid ${this.selectorKind} (${this.selectorLength} characters). ${guidance}`;
866 }
867 return `Preview automation ${this.operation} received an invalid selector.`;
868 }
869}
870
871export class PreviewAutomationTargetNotEditableError extends Schema.TaggedError<PreviewAutomationTargetNotEditableError>()(
872 "PreviewAutomationTargetNotEditableError",
873 {
874 ...PreviewAutomationRequestErrorFields,
875 ...PreviewAutomationRemoteDiagnosticFields,
876 selectorKind: Schema.optional(Schema.Literals(["focused-element", "locator", "selector"])),
877 selectorLength: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
878 },
879) {
880 override get message(): string {
881 if (this.selectorKind === "focused-element") {
882 return `Preview automation ${this.operation} requires an editable focused element.`;
883 }
884 if (this.selectorKind !== undefined && this.selectorLength !== undefined) {
885 return `Preview automation ${this.operation} requires an editable ${this.selectorKind} (${this.selectorLength} characters).`;
886 }
887 return `Preview automation ${this.operation} requires an editable target.`;
888 }
889}
890
891export class PreviewAutomationResultTooLargeError extends Schema.TaggedError<PreviewAutomationResultTooLargeError>()(
892 "PreviewAutomationResultTooLargeError",
893 {
894 ...PreviewAutomationRequestErrorFields,
895 ...PreviewAutomationRemoteDiagnosticFields,
896 maximumBytes: Schema.optional(Schema.Int.check(Schema.isGreaterThan(0))),
897 },
898) {
899 override get message(): string {
900 const summary =
901 this.maximumBytes === undefined
902 ? `Preview automation ${this.operation} produced a result that is too large.`
903 : `Preview automation ${this.operation} produced a result larger than ${this.maximumBytes} bytes.`;
904 return summary;
905 }
906}
907
908export class PreviewAutomationClientDisconnectedError extends Schema.TaggedError<PreviewAutomationClientDisconnectedError>()(
909 "PreviewAutomationClientDisconnectedError",
910 PreviewAutomationRequestErrorFields,
911) {
912 override get message(): string {
913 return `Preview automation client ${this.clientId} disconnected during ${this.operation}.`;
914 }
915}
916
917export class PreviewAutomationRequestQueueClosedError extends Schema.TaggedError<PreviewAutomationRequestQueueClosedError>()(
918 "PreviewAutomationRequestQueueClosedError",
919 PreviewAutomationRequestErrorFields,
920) {
921 override get message(): string {
922 return `Preview automation client ${this.clientId} stopped accepting ${this.operation} requests.`;
923 }
924}
925
926export class PreviewAutomationRemoteUnavailableError extends Schema.TaggedError<PreviewAutomationRemoteUnavailableError>()(
927 "PreviewAutomationRemoteUnavailableError",
928 {
929 ...PreviewAutomationRequestErrorFields,
930 ...PreviewAutomationRemoteDiagnosticFields,
931 },
932) {
933 override get message(): string {
934 return `Preview automation ${this.operation} is unavailable on client ${this.clientId}.`;
935 }
936}
937
938export class PreviewAutomationMalformedResponseError extends Schema.TaggedError<PreviewAutomationMalformedResponseError>()(
939 "PreviewAutomationMalformedResponseError",
940 PreviewAutomationRequestErrorFields,
941) {
942 override get message(): string {
943 return `Preview automation client ${this.clientId} returned a malformed response for ${this.operation}.`;
944 }
945}
946
947export class PreviewAutomationRecordingTransferError extends Schema.TaggedError<PreviewAutomationRecordingTransferError>()(
948 "PreviewAutomationRecordingTransferError",
949 {
950 threadId: ThreadId,
951 cause: Schema.optional(Schema.Defect()),
952 },
953) {
954 override get message(): string {
955 return "Preview recording could not be saved to the agent environment.";
956 }
957}
958
959export class PreviewAutomationRecordingDesktopUpdateRequiredError extends Schema.TaggedError<PreviewAutomationRecordingDesktopUpdateRequiredError>()(
960 "PreviewAutomationRecordingDesktopUpdateRequiredError",
961 { threadId: ThreadId, cause: Schema.optional(Schema.Defect()) },
962) {
963 override get message(): string {
964 return "Update the desktop app to transfer recordings. The recording remains on the desktop.";
965 }
966}
967
968export class PreviewAutomationRecordingTooLargeError extends Schema.TaggedError<PreviewAutomationRecordingTooLargeError>()(
969 "PreviewAutomationRecordingTooLargeError",
970 { threadId: ThreadId, cause: Schema.optional(Schema.Defect()) },
971) {
972 override get message(): string {
973 return "The recording exceeds 50 MiB. Make a shorter recording.";
974 }
975}
976
977export class PreviewAutomationRecordingDeadlineExpiredError extends Schema.TaggedError<PreviewAutomationRecordingDeadlineExpiredError>()(
978 "PreviewAutomationRecordingDeadlineExpiredError",
979 { threadId: ThreadId, cause: Schema.optional(Schema.Defect()) },
980) {
981 override get message(): string {
982 return "The recording transfer deadline expired.";
983 }
984}
985
986export const PreviewAutomationError = Schema.Union([
987 PreviewAutomationRecordingTransferError,
988 PreviewAutomationRecordingDesktopUpdateRequiredError,
989 PreviewAutomationRecordingTooLargeError,
990 PreviewAutomationRecordingDeadlineExpiredError,
991 PreviewAutomationUnavailableError,
992 PreviewAutomationNoAvailableHostError,
993 PreviewAutomationUnsupportedClientError,
994 PreviewAutomationTabNotFoundError,
995 PreviewAutomationTimeoutError,
996 PreviewAutomationControlInterruptedError,
997 PreviewAutomationExecutionError,
998 PreviewAutomationInvalidSelectorError,
999 PreviewAutomationTargetNotEditableError,
1000 PreviewAutomationResultTooLargeError,
1001 PreviewAutomationClientDisconnectedError,
1002 PreviewAutomationRequestQueueClosedError,
1003 PreviewAutomationRemoteUnavailableError,
1004 PreviewAutomationMalformedResponseError,
1005]);
1006export type PreviewAutomationError = typeof PreviewAutomationError.Type;
1007
1008export const PreviewUrlResolution = Schema.Struct({
1009 requestedUrl: Schema.String,
1010 resolvedUrl: Schema.String,
1011 resolutionKind: Schema.Literals(["direct", "direct-private-network"]),
1012 environmentId: EnvironmentId,
1013});
1014export type PreviewUrlResolution = typeof PreviewUrlResolution.Type;