@vymalo/opencode-browser 0.11.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/agent-client.d.ts +36 -36
  2. package/dist/agent-client.js +201 -189
  3. package/dist/agent-client.js.map +1 -1
  4. package/dist/broker.d.ts +85 -87
  5. package/dist/broker.js +614 -599
  6. package/dist/broker.js.map +1 -1
  7. package/dist/catalog.d.ts +76 -76
  8. package/dist/catalog.js +826 -596
  9. package/dist/catalog.js.map +1 -1
  10. package/dist/endpoint.d.ts +42 -42
  11. package/dist/endpoint.js +132 -131
  12. package/dist/endpoint.js.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/lib.js +1 -0
  16. package/dist/lib.js.map +1 -1
  17. package/dist/logging.d.ts +6 -6
  18. package/dist/logging.js +60 -63
  19. package/dist/logging.js.map +1 -1
  20. package/dist/node-transport.d.ts +6 -6
  21. package/dist/node-transport.js +48 -44
  22. package/dist/node-transport.js.map +1 -1
  23. package/dist/opencode.d.ts +10 -10
  24. package/dist/opencode.js +143 -146
  25. package/dist/opencode.js.map +1 -1
  26. package/dist/protocol.d.ts +127 -127
  27. package/dist/protocol.js +148 -128
  28. package/dist/protocol.js.map +1 -1
  29. package/dist/schema.d.ts +32 -32
  30. package/dist/schema.js +39 -44
  31. package/dist/schema.js.map +1 -1
  32. package/dist/token-file.d.ts +4 -4
  33. package/dist/token-file.js +68 -60
  34. package/dist/token-file.js.map +1 -1
  35. package/dist/tools.d.ts +11 -11
  36. package/dist/tools.js +128 -123
  37. package/dist/tools.js.map +1 -1
  38. package/dist/transport.d.ts +17 -17
  39. package/dist/transport.js +3 -9
  40. package/dist/transport.js.map +1 -1
  41. package/dist/types.d.ts +61 -61
  42. package/dist/types.js +1 -0
  43. package/dist/types.js.map +1 -1
  44. package/package.json +3 -3
package/dist/catalog.js CHANGED
@@ -1,619 +1,849 @@
1
1
  export const TOOL_GROUPS = [
2
- "page",
3
- "control",
4
- "debug",
5
- "interactive"
2
+ "page",
3
+ "control",
4
+ "debug",
5
+ "interactive"
6
6
  ];
7
7
  /** Groups registered when the operator doesn't specify — `debug` and `interactive` are opt-in. */
8
8
  export const DEFAULT_GROUPS = ["page", "control"];
9
9
  // ─── reusable fields ─────────────────────────────────────────────────────────
10
10
  const group = {
11
- type: "string",
12
- description: "Named tab group the action targets. Created on first browser_open."
11
+ type: "string",
12
+ description: "Named tab group the action targets. Created on first browser_open."
13
13
  };
14
14
  const tabId = {
15
- type: "number",
16
- optional: true,
17
- description: "Specific tab id; defaults to the group's active tab."
15
+ type: "number",
16
+ optional: true,
17
+ description: "Specific tab id; defaults to the group's active tab."
18
18
  };
19
19
  const refField = {
20
- type: "string",
21
- optional: true,
22
- description: "Element ref from a prior browser_snapshot."
20
+ type: "string",
21
+ optional: true,
22
+ description: "Element ref from a prior browser_snapshot."
23
+ };
24
+ const selectorField = {
25
+ type: "string",
26
+ optional: true,
27
+ description: "CSS selector."
23
28
  };
24
- const selectorField = { type: "string", optional: true, description: "CSS selector." };
25
29
  // ─── helpers ─────────────────────────────────────────────────────────────────
26
30
  function rec(value) {
27
- return typeof value === "object" && value !== null ? value : {};
31
+ return typeof value === "object" && value !== null ? value : {};
28
32
  }
29
33
  function describeTarget(a) {
30
- if (typeof a.ref === "string") {
31
- return `ref ${a.ref}`;
32
- }
33
- if (typeof a.selector === "string") {
34
- return `selector ${a.selector}`;
35
- }
36
- if (typeof a.x === "number" && typeof a.y === "number") {
37
- return `(${a.x}, ${a.y})`;
38
- }
39
- return "element";
34
+ if (typeof a.ref === "string") {
35
+ return `ref ${a.ref}`;
36
+ }
37
+ if (typeof a.selector === "string") {
38
+ return `selector ${a.selector}`;
39
+ }
40
+ if (typeof a.x === "number" && typeof a.y === "number") {
41
+ return `(${a.x}, ${a.y})`;
42
+ }
43
+ return "element";
40
44
  }
41
- const text = (t) => ({ kind: "text", text: t });
42
- const json = (data, t) => ({ kind: "json", data, text: t });
45
+ const text = (t) => ({
46
+ kind: "text",
47
+ text: t
48
+ });
49
+ const json = (data, t) => ({
50
+ kind: "json",
51
+ data,
52
+ text: t
53
+ });
43
54
  /**
44
- * The single source of truth for the browser tool surface, shared by the
45
- * OpenCode plugin and the MCP server. Filter by `group` to gate what an agent
46
- * sees.
47
- */
55
+ * The single source of truth for the browser tool surface, shared by the
56
+ * OpenCode plugin and the MCP server. Filter by `group` to gate what an agent
57
+ * sees.
58
+ */
48
59
  export const BROWSER_TOOLS = [
49
- {
50
- name: "browser_open",
51
- group: "control",
52
- action: "open",
53
- description: "Open a new browser tab inside a named tab group (creates the group if it doesn't exist yet). Returns the tab id, final URL and page title.",
54
- input: {
55
- group,
56
- url: { type: "string", optional: true, description: "URL to load. Omit for a blank tab." },
57
- focus: {
58
- type: "boolean",
59
- optional: true,
60
- description: "Bring the tab to the foreground (default true)."
61
- },
62
- target: {
63
- type: "string",
64
- optional: true,
65
- description: "Which browser to open in (label or id from browser_targets). Omit unless several browsers are connected."
66
- }
67
- },
68
- result: (data, args) => {
69
- const d = rec(data);
70
- return {
71
- kind: "json",
72
- data: d,
73
- text: `Opened tab in group "${args.group}": ${d.title ?? "(untitled)"} — ${d.url ?? args.url ?? "about:blank"}`
74
- };
75
- }
76
- },
77
- {
78
- name: "browser_navigate",
79
- group: "control",
80
- action: "navigate",
81
- description: "Navigate an existing tab in the group to a new URL.",
82
- input: { group, url: { type: "string", description: "URL to navigate to." }, tabId },
83
- result: (data, args) => {
84
- const d = rec(data);
85
- return {
86
- kind: "json",
87
- data: d,
88
- text: `Navigated to ${d.url ?? args.url} (${d.title ?? ""})`.trim()
89
- };
90
- }
91
- },
92
- {
93
- name: "browser_click",
94
- group: "control",
95
- action: "click",
96
- description: "Click an element. Target it by `ref` (from browser_snapshot, most reliable), CSS `selector`, or absolute `x`/`y` coordinates.",
97
- input: {
98
- group,
99
- ref: refField,
100
- selector: selectorField,
101
- x: { type: "number", optional: true, description: "Absolute x coordinate (with y)." },
102
- y: { type: "number", optional: true, description: "Absolute y coordinate (with x)." },
103
- button: {
104
- type: "string",
105
- optional: true,
106
- enum: ["left", "middle", "right"],
107
- description: "Mouse button (default left)."
108
- },
109
- tabId
110
- },
111
- result: (_data, args) => text(`Clicked ${describeTarget(args)} in group "${args.group}".`)
112
- },
113
- {
114
- name: "browser_double_click",
115
- group: "control",
116
- action: "double_click",
117
- description: "Double-click an element, targeted by `ref`, CSS `selector`, or `x`/`y` coordinates.",
118
- input: {
119
- group,
120
- ref: refField,
121
- selector: selectorField,
122
- x: { type: "number", optional: true },
123
- y: { type: "number", optional: true },
124
- tabId
125
- },
126
- result: (_data, args) => text(`Double-clicked ${describeTarget(args)} in group "${args.group}".`)
127
- },
128
- {
129
- name: "browser_type",
130
- group: "control",
131
- action: "type",
132
- description: "Type text into the focused element (or first focus `ref`/`selector`). Optionally press Enter after.",
133
- input: {
134
- group,
135
- text: { type: "string", description: "Text to type." },
136
- ref: refField,
137
- selector: selectorField,
138
- submit: { type: "boolean", optional: true, description: "Press Enter after typing." },
139
- tabId
140
- },
141
- result: (_data, args) => text(`Typed ${String(args.text ?? "").length} character(s) into ${describeTarget(args)}${args.submit ? " and submitted" : ""}.`)
142
- },
143
- {
144
- name: "browser_fill",
145
- group: "control",
146
- action: "fill",
147
- description: "Fill several form fields in one call. Each field targets a `ref` or CSS `selector` and sets its `value`.",
148
- input: {
149
- group,
150
- fields: {
151
- type: "array",
152
- description: "Fields to fill, in order.",
153
- items: {
154
- type: "object",
155
- properties: { ref: refField, selector: selectorField, value: { type: "string" } }
156
- }
157
- },
158
- tabId
159
- },
160
- result: (data, args) => {
161
- const requested = Array.isArray(args.fields) ? args.fields.length : 0;
162
- const filled = typeof rec(data).filled === "number" ? rec(data).filled : requested;
163
- const missed = requested - filled;
164
- return text(`Filled ${filled} of ${requested} field(s) in group "${args.group}".${missed > 0 ? ` (${missed} not found)` : ""}`);
165
- }
166
- },
167
- {
168
- name: "browser_select",
169
- group: "control",
170
- action: "select",
171
- description: "Choose option(s) in a <select> element, or set the selection of a control.",
172
- input: {
173
- group,
174
- ref: refField,
175
- selector: selectorField,
176
- value: { type: "string", optional: true, description: "Single value to select." },
177
- values: {
178
- type: "array",
179
- optional: true,
180
- description: "Multiple values (multi-select).",
181
- items: { type: "string" }
182
- },
183
- tabId
184
- },
185
- result: (_data, args) => {
186
- const chosen = Array.isArray(args.values) ? args.values.join(", ") : (args.value ?? "");
187
- return text(`Selected ${chosen} in ${describeTarget(args)}.`);
188
- }
189
- },
190
- {
191
- name: "browser_scroll",
192
- group: "control",
193
- action: "scroll",
194
- description: "Scroll the page (or an element). Provide `deltaX`/`deltaY`, or `to: 'top' | 'bottom'`.",
195
- input: {
196
- group,
197
- deltaX: { type: "number", optional: true },
198
- deltaY: { type: "number", optional: true },
199
- ref: {
200
- type: "string",
201
- optional: true,
202
- description: "Scroll within this element instead of the page."
203
- },
204
- to: { type: "string", optional: true, enum: ["top", "bottom"] },
205
- tabId
206
- },
207
- result: (_data, args) => text(`Scrolled in group "${args.group}".`)
208
- },
209
- {
210
- name: "browser_press_key",
211
- group: "control",
212
- action: "press_key",
213
- description: 'Press a single key or chord (e.g. "Enter", "Escape", "Control+a").',
214
- input: { group, key: { type: "string", description: 'Key name, e.g. "Enter".' }, tabId },
215
- result: (_data, args) => text(`Pressed ${args.key} in group "${args.group}".`)
216
- },
217
- {
218
- name: "browser_wait",
219
- group: "control",
220
- action: "wait",
221
- description: "Wait for a fixed delay (`ms`) or until a `selector` reaches `state` (visible/hidden/attached).",
222
- input: {
223
- group,
224
- ms: { type: "number", optional: true, description: "Fixed delay in milliseconds." },
225
- selector: { type: "string", optional: true, description: "CSS selector to wait for." },
226
- state: { type: "string", optional: true, enum: ["visible", "hidden", "attached"] },
227
- tabId
228
- },
229
- result: (_data, args) => text(args.selector
230
- ? `Waited for ${args.selector} (${args.state ?? "visible"}).`
231
- : `Waited ${args.ms ?? 0}ms.`)
232
- },
233
- {
234
- name: "browser_close",
235
- group: "control",
236
- action: "close",
237
- description: "Close a tab (pass `tabId`) or the whole group (omit `tabId`).",
238
- input: { group, tabId },
239
- result: (_data, args) => text(args.tabId
240
- ? `Closed tab ${args.tabId} in group "${args.group}".`
241
- : `Closed group "${args.group}".`)
242
- },
243
- {
244
- name: "browser_release",
245
- group: "control",
246
- action: "release",
247
- description: "Release control of the browser: stop driving and detach the debugger (clears the 'being debugged' banner) without closing tabs. The next browser_* call re-attaches. Use this when you're done so the user gets their browser back.",
248
- input: {},
249
- result: () => text("Released browser control. Tabs are left open; the next browser_* action re-attaches.")
250
- },
251
- {
252
- name: "browser_snapshot",
253
- group: "page",
254
- action: "snapshot",
255
- description: "Capture an accessibility/DOM snapshot of the page with stable element refs. Prefer this over guessing selectors — pass the returned refs to browser_click/type/etc.",
256
- input: { group, tabId },
257
- result: (data) => {
258
- const d = rec(data);
259
- return text(typeof d.snapshot === "string" ? d.snapshot : JSON.stringify(d, null, 2));
260
- }
261
- },
262
- {
263
- name: "browser_get_text",
264
- group: "page",
265
- action: "get_text",
266
- description: "Extract the visible text / readable content of the group's active tab.",
267
- input: { group, tabId },
268
- result: (data) => {
269
- const d = rec(data);
270
- return text(typeof d.text === "string" ? d.text : JSON.stringify(d));
271
- }
272
- },
273
- {
274
- name: "browser_tabs",
275
- group: "page",
276
- action: "tabs",
277
- description: "List open tab groups and their tabs. Omit `group` to list everything.",
278
- input: { group: { type: "string", optional: true, description: "Restrict to one group." } },
279
- params: (args) => ({ group: args.group }),
280
- result: (data) => ({ kind: "json", data, text: JSON.stringify(data, null, 2) })
281
- },
282
- {
283
- name: "browser_targets",
284
- group: "page",
285
- action: "targets",
286
- description: "List the browsers (extensions) currently connected to the bridge, with their id, label, browser and owned groups. Use a returned id/label as `target` in browser_open when several are connected.",
287
- input: {},
288
- result: (data) => ({ kind: "json", data, text: JSON.stringify(data, null, 2) })
289
- },
290
- {
291
- name: "browser_screenshot",
292
- group: "page",
293
- action: "screenshot",
294
- description: "Capture a screenshot of the group's active tab. Returns the image (the OpenCode plugin saves it to disk and returns the path — use the read tool to view it).",
295
- input: {
296
- group,
297
- fullPage: {
298
- type: "boolean",
299
- optional: true,
300
- description: "Capture the full scrollable page, not just the viewport."
301
- },
302
- tabId
303
- },
304
- result: (data) => {
305
- const d = rec(data);
306
- const width = Number(d.width ?? 0);
307
- const height = Number(d.height ?? 0);
308
- return {
309
- kind: "image",
310
- base64: String(d.base64 ?? ""),
311
- mimeType: "image/png",
312
- width,
313
- height,
314
- partial: Boolean(d.partial),
315
- text: `screenshot ${width}×${height}`
316
- };
317
- }
318
- },
319
- // ─── navigation (control) ──────────────────────────────────────────────────
320
- {
321
- name: "browser_back",
322
- group: "control",
323
- action: "back",
324
- description: "Go back in the tab's history.",
325
- input: { group, tabId },
326
- result: (data) => json(rec(data), `Went back to ${rec(data).url ?? ""}`.trim())
327
- },
328
- {
329
- name: "browser_forward",
330
- group: "control",
331
- action: "forward",
332
- description: "Go forward in the tab's history.",
333
- input: { group, tabId },
334
- result: (data) => json(rec(data), `Went forward to ${rec(data).url ?? ""}`.trim())
335
- },
336
- {
337
- name: "browser_reload",
338
- group: "control",
339
- action: "reload",
340
- description: "Reload the group's active tab.",
341
- input: { group, tabId },
342
- result: (data) => json(rec(data), `Reloaded ${rec(data).url ?? ""}`.trim())
343
- },
344
- {
345
- name: "browser_activate",
346
- group: "control",
347
- action: "activate",
348
- description: "Bring a group's tab to the foreground (focus its window and tab).",
349
- input: { group, tabId },
350
- result: (data) => json(rec(data), `Activated tab ${rec(data).tabId ?? ""}`.trim())
351
- },
352
- {
353
- name: "browser_hover",
354
- group: "control",
355
- action: "hover",
356
- description: "Move the pointer over an element (reveals hover menus/tooltips). Target by `ref`, `selector`, or `x`/`y`.",
357
- input: {
358
- group,
359
- ref: refField,
360
- selector: selectorField,
361
- x: { type: "number", optional: true },
362
- y: { type: "number", optional: true },
363
- tabId
364
- },
365
- result: (_d, args) => text(`Hovered ${describeTarget(args)} in group "${args.group}".`)
366
- },
367
- {
368
- name: "browser_drag",
369
- group: "control",
370
- action: "drag",
371
- description: "Drag from a source element to a target element (each by `ref` or `selector`).",
372
- input: {
373
- group,
374
- fromRef: { type: "string", optional: true, description: "Source element ref." },
375
- fromSelector: { type: "string", optional: true, description: "Source CSS selector." },
376
- ref: { type: "string", optional: true, description: "Target element ref." },
377
- selector: { type: "string", optional: true, description: "Target CSS selector." },
378
- tabId
379
- },
380
- result: (_d, args) => text(`Dragged to ${describeTarget(args)} in group "${args.group}".`)
381
- },
382
- {
383
- name: "browser_upload",
384
- group: "control",
385
- action: "upload",
386
- description: "Set the files on a file <input>, by absolute path(s) on the machine running the bridge. CDP executor only (Chromium).",
387
- input: {
388
- group,
389
- ref: refField,
390
- selector: selectorField,
391
- paths: {
392
- type: "array",
393
- description: "Absolute file path(s) to attach.",
394
- items: { type: "string" }
395
- },
396
- tabId
397
- },
398
- result: (_d, args) => text(`Uploaded ${Array.isArray(args.paths) ? args.paths.length : 0} file(s) to ${describeTarget(args)}.`)
399
- },
400
- // ─── reading (page) ────────────────────────────────────────────────────────
401
- {
402
- name: "browser_get_html",
403
- group: "page",
404
- action: "get_html",
405
- description: "Get the HTML of the page (or of a `ref`/`selector` element). `outer: false` for innerHTML.",
406
- input: {
407
- group,
408
- ref: refField,
409
- selector: selectorField,
410
- outer: { type: "boolean", optional: true, description: "Outer HTML (default true)." },
411
- tabId
412
- },
413
- result: (data) => text(String(rec(data).html ?? ""))
414
- },
415
- {
416
- name: "browser_get_attribute",
417
- group: "page",
418
- action: "get_attribute",
419
- description: "Read an element's tag, text, value, checked state, bounding box, and attributes (or one named attribute).",
420
- input: {
421
- group,
422
- ref: refField,
423
- selector: selectorField,
424
- name: { type: "string", optional: true, description: "A single attribute name to read." },
425
- tabId
426
- },
427
- result: (data) => json(rec(data), JSON.stringify(rec(data), null, 2))
428
- },
429
- {
430
- name: "browser_query",
431
- group: "page",
432
- action: "query",
433
- description: "Find visible elements matching a CSS selector. Returns each with a stable ref you can pass to other tools.",
434
- input: {
435
- group,
436
- selector: { type: "string", description: "CSS selector to match." },
437
- limit: { type: "number", optional: true, description: "Max matches (default 50)." },
438
- tabId
439
- },
440
- result: (data) => json(rec(data), JSON.stringify(rec(data), null, 2))
441
- },
442
- // ─── debug (off by default) ────────────────────────────────────────────────
443
- {
444
- name: "browser_eval",
445
- group: "debug",
446
- action: "eval",
447
- description: "Evaluate JavaScript in the page's DOM context and return the JSON-serializable result. Powerful — enabled only when the `debug` group is on.",
448
- input: { group, code: { type: "string", description: "JavaScript to evaluate." }, tabId },
449
- result: (data) => {
450
- const r = rec(data).result;
451
- return json(rec(data), typeof r === "string" ? r : JSON.stringify(r, null, 2));
452
- }
453
- },
454
- {
455
- name: "browser_console",
456
- group: "debug",
457
- action: "console",
458
- description: "Return recent console output (logs, warnings, errors). CDP executor only (Chromium).",
459
- input: { group, tabId },
460
- result: (data) => {
461
- const entries = rec(data).entries ?? [];
462
- return json(rec(data), JSON.stringify(entries, null, 2));
463
- }
464
- },
465
- {
466
- name: "browser_network",
467
- group: "debug",
468
- action: "network",
469
- description: "Return recent network requests (method, URL, status). CDP executor only (Chromium).",
470
- input: { group, tabId },
471
- result: (data) => {
472
- const entries = rec(data).entries ?? [];
473
- return json(rec(data), JSON.stringify(entries, null, 2));
474
- }
475
- },
476
- {
477
- name: "browser_handle_dialog",
478
- group: "debug",
479
- action: "handle_dialog",
480
- description: "Accept or dismiss a pending JavaScript dialog (alert/confirm/prompt). CDP executor only (Chromium).",
481
- input: {
482
- group,
483
- accept: { type: "boolean", optional: true, description: "Accept (default true) or dismiss." },
484
- promptText: { type: "string", optional: true, description: "Text for a prompt() dialog." },
485
- tabId
486
- },
487
- result: (_d, args) => text(`${args.accept === false ? "Dismissed" : "Accepted"} the dialog.`)
488
- },
489
- {
490
- name: "browser_set_viewport",
491
- group: "debug",
492
- action: "set_viewport",
493
- description: "Emulate a viewport size / device metrics. CDP executor only (Chromium).",
494
- input: {
495
- group,
496
- width: { type: "number", description: "Viewport width in CSS px." },
497
- height: { type: "number", description: "Viewport height in CSS px." },
498
- mobile: { type: "boolean", optional: true, description: "Emulate a mobile device." },
499
- deviceScaleFactor: {
500
- type: "number",
501
- optional: true,
502
- description: "Device pixel ratio (0 = default)."
503
- },
504
- tabId
505
- },
506
- result: (_d, args) => text(`Set viewport to ${args.width}×${args.height}.`)
507
- },
508
- {
509
- name: "browser_cookies",
510
- group: "debug",
511
- action: "cookies",
512
- description: "Read or modify cookies. `op`: get (by url+name), list (by url), set (url+name+value), clear (url+name).",
513
- input: {
514
- group: {
515
- type: "string",
516
- optional: true,
517
- description: "Group (unused; cookies are profile-wide)."
518
- },
519
- op: { type: "string", enum: ["get", "list", "set", "clear"], description: "Operation." },
520
- url: { type: "string", optional: true, description: "URL the cookie applies to." },
521
- name: { type: "string", optional: true },
522
- value: { type: "string", optional: true },
523
- domain: { type: "string", optional: true },
524
- path: { type: "string", optional: true }
525
- },
526
- params: (args) => ({
527
- op: args.op,
528
- url: args.url,
529
- name: args.name,
530
- value: args.value,
531
- domain: args.domain,
532
- path: args.path
533
- }),
534
- result: (data) => json(rec(data), JSON.stringify(rec(data), null, 2))
535
- },
536
- {
537
- name: "browser_request_feedback",
538
- group: "interactive",
539
- action: "request_feedback",
540
- description: "Ask the human at the browser to respond on the page, and block until they do (or it times out). Use when you're unsure what the user means and a screenshot or snapshot isn't enough — e.g. 'which of these did you mean?'. `mode`: confirm (yes/no bar), choose (pick one of `options`), point (click one spot), element (hover-highlight then click one element), region (drag a box over an area), comment (point + a free-text note). point/element/region resolve to element ref(s) you can then click/snapshot. Returns the user's response; on timeout returns `{ timedOut: true }` so you can fall back. interactive group (opt-in).",
541
- timeoutMs: 300_000,
542
- input: {
543
- group,
544
- mode: {
545
- type: "string",
546
- enum: ["confirm", "choose", "point", "element", "region", "comment"],
547
- description: "Kind of prompt: confirm | choose | point | element | region | comment."
548
- },
549
- prompt: {
550
- type: "string",
551
- optional: true,
552
- description: "Question/instruction shown to the user above the controls."
553
- },
554
- options: {
555
- type: "array",
556
- optional: true,
557
- items: { type: "string" },
558
- description: "Choices for `choose` mode (ignored otherwise)."
559
- },
560
- timeoutMs: {
561
- type: "number",
562
- optional: true,
563
- description: "How long to wait for the user, in ms (default 120000, max 290000)."
564
- },
565
- tabId
566
- },
567
- params: (args) => {
568
- const requested = typeof args.timeoutMs === "number" ? args.timeoutMs : 120_000;
569
- return {
570
- mode: args.mode,
571
- prompt: args.prompt,
572
- options: Array.isArray(args.options) ? args.options : undefined,
573
- // Keep the overlay's own countdown below the broker deadline (300s) so it
574
- // self-resolves into a clean `timedOut` result rather than being cancelled.
575
- timeoutMs: Math.min(Math.max(requested, 1_000), 290_000),
576
- tabId: args.tabId
577
- };
578
- },
579
- result: (data) => {
580
- const d = data;
581
- if (d?.error) {
582
- return json({ responded: false, error: d.error, annotations: [] }, `Couldn't ask the user on this page: ${d.error}. Fall back to a screenshot/snapshot.`);
583
- }
584
- if (!d || d.responded === false || d.timedOut) {
585
- return json({ responded: false, timedOut: true, annotations: [] }, "No response from the user within the time limit.");
586
- }
587
- const annotations = Array.isArray(d.annotations) ? d.annotations : [];
588
- return json({ responded: true, annotations }, summarizeAnnotations(annotations));
589
- }
590
- }
60
+ {
61
+ name: "browser_open",
62
+ group: "control",
63
+ action: "open",
64
+ description: "Open a new browser tab inside a named tab group (creates the group if it doesn't exist yet). Returns the tab id, final URL and page title.",
65
+ input: {
66
+ group,
67
+ url: {
68
+ type: "string",
69
+ optional: true,
70
+ description: "URL to load. Omit for a blank tab."
71
+ },
72
+ focus: {
73
+ type: "boolean",
74
+ optional: true,
75
+ description: "Bring the tab to the foreground (default true)."
76
+ },
77
+ target: {
78
+ type: "string",
79
+ optional: true,
80
+ description: "Which browser to open in (label or id from browser_targets). Omit unless several browsers are connected."
81
+ }
82
+ },
83
+ result: (data, args) => {
84
+ const d = rec(data);
85
+ return {
86
+ kind: "json",
87
+ data: d,
88
+ text: `Opened tab in group "${args.group}": ${d.title ?? "(untitled)"} — ${d.url ?? args.url ?? "about:blank"}`
89
+ };
90
+ }
91
+ },
92
+ {
93
+ name: "browser_navigate",
94
+ group: "control",
95
+ action: "navigate",
96
+ description: "Navigate an existing tab in the group to a new URL.",
97
+ input: {
98
+ group,
99
+ url: {
100
+ type: "string",
101
+ description: "URL to navigate to."
102
+ },
103
+ tabId
104
+ },
105
+ result: (data, args) => {
106
+ const d = rec(data);
107
+ return {
108
+ kind: "json",
109
+ data: d,
110
+ text: `Navigated to ${d.url ?? args.url} (${d.title ?? ""})`.trim()
111
+ };
112
+ }
113
+ },
114
+ {
115
+ name: "browser_click",
116
+ group: "control",
117
+ action: "click",
118
+ description: "Click an element. Target it by `ref` (from browser_snapshot, most reliable), CSS `selector`, or absolute `x`/`y` coordinates.",
119
+ input: {
120
+ group,
121
+ ref: refField,
122
+ selector: selectorField,
123
+ x: {
124
+ type: "number",
125
+ optional: true,
126
+ description: "Absolute x coordinate (with y)."
127
+ },
128
+ y: {
129
+ type: "number",
130
+ optional: true,
131
+ description: "Absolute y coordinate (with x)."
132
+ },
133
+ button: {
134
+ type: "string",
135
+ optional: true,
136
+ enum: [
137
+ "left",
138
+ "middle",
139
+ "right"
140
+ ],
141
+ description: "Mouse button (default left)."
142
+ },
143
+ tabId
144
+ },
145
+ result: (_data, args) => text(`Clicked ${describeTarget(args)} in group "${args.group}".`)
146
+ },
147
+ {
148
+ name: "browser_double_click",
149
+ group: "control",
150
+ action: "double_click",
151
+ description: "Double-click an element, targeted by `ref`, CSS `selector`, or `x`/`y` coordinates.",
152
+ input: {
153
+ group,
154
+ ref: refField,
155
+ selector: selectorField,
156
+ x: {
157
+ type: "number",
158
+ optional: true
159
+ },
160
+ y: {
161
+ type: "number",
162
+ optional: true
163
+ },
164
+ tabId
165
+ },
166
+ result: (_data, args) => text(`Double-clicked ${describeTarget(args)} in group "${args.group}".`)
167
+ },
168
+ {
169
+ name: "browser_type",
170
+ group: "control",
171
+ action: "type",
172
+ description: "Type text into the focused element (or first focus `ref`/`selector`). Optionally press Enter after.",
173
+ input: {
174
+ group,
175
+ text: {
176
+ type: "string",
177
+ description: "Text to type."
178
+ },
179
+ ref: refField,
180
+ selector: selectorField,
181
+ submit: {
182
+ type: "boolean",
183
+ optional: true,
184
+ description: "Press Enter after typing."
185
+ },
186
+ tabId
187
+ },
188
+ result: (_data, args) => text(`Typed ${String(args.text ?? "").length} character(s) into ${describeTarget(args)}${args.submit ? " and submitted" : ""}.`)
189
+ },
190
+ {
191
+ name: "browser_fill",
192
+ group: "control",
193
+ action: "fill",
194
+ description: "Fill several form fields in one call. Each field targets a `ref` or CSS `selector` and sets its `value`.",
195
+ input: {
196
+ group,
197
+ fields: {
198
+ type: "array",
199
+ description: "Fields to fill, in order.",
200
+ items: {
201
+ type: "object",
202
+ properties: {
203
+ ref: refField,
204
+ selector: selectorField,
205
+ value: { type: "string" }
206
+ }
207
+ }
208
+ },
209
+ tabId
210
+ },
211
+ result: (data, args) => {
212
+ const requested = Array.isArray(args.fields) ? args.fields.length : 0;
213
+ const filled = typeof rec(data).filled === "number" ? rec(data).filled : requested;
214
+ const missed = requested - filled;
215
+ return text(`Filled ${filled} of ${requested} field(s) in group "${args.group}".${missed > 0 ? ` (${missed} not found)` : ""}`);
216
+ }
217
+ },
218
+ {
219
+ name: "browser_select",
220
+ group: "control",
221
+ action: "select",
222
+ description: "Choose option(s) in a <select> element, or set the selection of a control.",
223
+ input: {
224
+ group,
225
+ ref: refField,
226
+ selector: selectorField,
227
+ value: {
228
+ type: "string",
229
+ optional: true,
230
+ description: "Single value to select."
231
+ },
232
+ values: {
233
+ type: "array",
234
+ optional: true,
235
+ description: "Multiple values (multi-select).",
236
+ items: { type: "string" }
237
+ },
238
+ tabId
239
+ },
240
+ result: (_data, args) => {
241
+ const chosen = Array.isArray(args.values) ? args.values.join(", ") : args.value ?? "";
242
+ return text(`Selected ${chosen} in ${describeTarget(args)}.`);
243
+ }
244
+ },
245
+ {
246
+ name: "browser_scroll",
247
+ group: "control",
248
+ action: "scroll",
249
+ description: "Scroll the page (or an element). Provide `deltaX`/`deltaY`, or `to: 'top' | 'bottom'`.",
250
+ input: {
251
+ group,
252
+ deltaX: {
253
+ type: "number",
254
+ optional: true
255
+ },
256
+ deltaY: {
257
+ type: "number",
258
+ optional: true
259
+ },
260
+ ref: {
261
+ type: "string",
262
+ optional: true,
263
+ description: "Scroll within this element instead of the page."
264
+ },
265
+ to: {
266
+ type: "string",
267
+ optional: true,
268
+ enum: ["top", "bottom"]
269
+ },
270
+ tabId
271
+ },
272
+ result: (_data, args) => text(`Scrolled in group "${args.group}".`)
273
+ },
274
+ {
275
+ name: "browser_press_key",
276
+ group: "control",
277
+ action: "press_key",
278
+ description: "Press a single key or chord (e.g. \"Enter\", \"Escape\", \"Control+a\").",
279
+ input: {
280
+ group,
281
+ key: {
282
+ type: "string",
283
+ description: "Key name, e.g. \"Enter\"."
284
+ },
285
+ tabId
286
+ },
287
+ result: (_data, args) => text(`Pressed ${args.key} in group "${args.group}".`)
288
+ },
289
+ {
290
+ name: "browser_wait",
291
+ group: "control",
292
+ action: "wait",
293
+ description: "Wait for a fixed delay (`ms`) or until a `selector` reaches `state` (visible/hidden/attached).",
294
+ input: {
295
+ group,
296
+ ms: {
297
+ type: "number",
298
+ optional: true,
299
+ description: "Fixed delay in milliseconds."
300
+ },
301
+ selector: {
302
+ type: "string",
303
+ optional: true,
304
+ description: "CSS selector to wait for."
305
+ },
306
+ state: {
307
+ type: "string",
308
+ optional: true,
309
+ enum: [
310
+ "visible",
311
+ "hidden",
312
+ "attached"
313
+ ]
314
+ },
315
+ tabId
316
+ },
317
+ result: (_data, args) => text(args.selector ? `Waited for ${args.selector} (${args.state ?? "visible"}).` : `Waited ${args.ms ?? 0}ms.`)
318
+ },
319
+ {
320
+ name: "browser_close",
321
+ group: "control",
322
+ action: "close",
323
+ description: "Close a tab (pass `tabId`) or the whole group (omit `tabId`).",
324
+ input: {
325
+ group,
326
+ tabId
327
+ },
328
+ result: (_data, args) => text(args.tabId ? `Closed tab ${args.tabId} in group "${args.group}".` : `Closed group "${args.group}".`)
329
+ },
330
+ {
331
+ name: "browser_release",
332
+ group: "control",
333
+ action: "release",
334
+ description: "Release control of the browser: stop driving and detach the debugger (clears the 'being debugged' banner) without closing tabs. The next browser_* call re-attaches. Use this when you're done so the user gets their browser back.",
335
+ input: {},
336
+ result: () => text("Released browser control. Tabs are left open; the next browser_* action re-attaches.")
337
+ },
338
+ {
339
+ name: "browser_snapshot",
340
+ group: "page",
341
+ action: "snapshot",
342
+ description: "Capture an accessibility/DOM snapshot of the page with stable element refs. Prefer this over guessing selectors — pass the returned refs to browser_click/type/etc.",
343
+ input: {
344
+ group,
345
+ tabId
346
+ },
347
+ result: (data) => {
348
+ const d = rec(data);
349
+ return text(typeof d.snapshot === "string" ? d.snapshot : JSON.stringify(d, null, 2));
350
+ }
351
+ },
352
+ {
353
+ name: "browser_get_text",
354
+ group: "page",
355
+ action: "get_text",
356
+ description: "Extract the visible text / readable content of the group's active tab.",
357
+ input: {
358
+ group,
359
+ tabId
360
+ },
361
+ result: (data) => {
362
+ const d = rec(data);
363
+ return text(typeof d.text === "string" ? d.text : JSON.stringify(d));
364
+ }
365
+ },
366
+ {
367
+ name: "browser_tabs",
368
+ group: "page",
369
+ action: "tabs",
370
+ description: "List open tab groups and their tabs. Omit `group` to list everything.",
371
+ input: { group: {
372
+ type: "string",
373
+ optional: true,
374
+ description: "Restrict to one group."
375
+ } },
376
+ params: (args) => ({ group: args.group }),
377
+ result: (data) => ({
378
+ kind: "json",
379
+ data,
380
+ text: JSON.stringify(data, null, 2)
381
+ })
382
+ },
383
+ {
384
+ name: "browser_targets",
385
+ group: "page",
386
+ action: "targets",
387
+ description: "List the browsers (extensions) currently connected to the bridge, with their id, label, browser and owned groups. Use a returned id/label as `target` in browser_open when several are connected.",
388
+ input: {},
389
+ result: (data) => ({
390
+ kind: "json",
391
+ data,
392
+ text: JSON.stringify(data, null, 2)
393
+ })
394
+ },
395
+ {
396
+ name: "browser_screenshot",
397
+ group: "page",
398
+ action: "screenshot",
399
+ description: "Capture a screenshot of the group's active tab. Returns the image (the OpenCode plugin saves it to disk and returns the path — use the read tool to view it).",
400
+ input: {
401
+ group,
402
+ fullPage: {
403
+ type: "boolean",
404
+ optional: true,
405
+ description: "Capture the full scrollable page, not just the viewport."
406
+ },
407
+ tabId
408
+ },
409
+ result: (data) => {
410
+ const d = rec(data);
411
+ const width = Number(d.width ?? 0);
412
+ const height = Number(d.height ?? 0);
413
+ return {
414
+ kind: "image",
415
+ base64: String(d.base64 ?? ""),
416
+ mimeType: "image/png",
417
+ width,
418
+ height,
419
+ partial: Boolean(d.partial),
420
+ text: `screenshot ${width}×${height}`
421
+ };
422
+ }
423
+ },
424
+ (
425
+ // ─── navigation (control) ──────────────────────────────────────────────────
426
+ {
427
+ name: "browser_back",
428
+ group: "control",
429
+ action: "back",
430
+ description: "Go back in the tab's history.",
431
+ input: {
432
+ group,
433
+ tabId
434
+ },
435
+ result: (data) => json(rec(data), `Went back to ${rec(data).url ?? ""}`.trim())
436
+ }),
437
+ {
438
+ name: "browser_forward",
439
+ group: "control",
440
+ action: "forward",
441
+ description: "Go forward in the tab's history.",
442
+ input: {
443
+ group,
444
+ tabId
445
+ },
446
+ result: (data) => json(rec(data), `Went forward to ${rec(data).url ?? ""}`.trim())
447
+ },
448
+ {
449
+ name: "browser_reload",
450
+ group: "control",
451
+ action: "reload",
452
+ description: "Reload the group's active tab.",
453
+ input: {
454
+ group,
455
+ tabId
456
+ },
457
+ result: (data) => json(rec(data), `Reloaded ${rec(data).url ?? ""}`.trim())
458
+ },
459
+ {
460
+ name: "browser_activate",
461
+ group: "control",
462
+ action: "activate",
463
+ description: "Bring a group's tab to the foreground (focus its window and tab).",
464
+ input: {
465
+ group,
466
+ tabId
467
+ },
468
+ result: (data) => json(rec(data), `Activated tab ${rec(data).tabId ?? ""}`.trim())
469
+ },
470
+ {
471
+ name: "browser_hover",
472
+ group: "control",
473
+ action: "hover",
474
+ description: "Move the pointer over an element (reveals hover menus/tooltips). Target by `ref`, `selector`, or `x`/`y`.",
475
+ input: {
476
+ group,
477
+ ref: refField,
478
+ selector: selectorField,
479
+ x: {
480
+ type: "number",
481
+ optional: true
482
+ },
483
+ y: {
484
+ type: "number",
485
+ optional: true
486
+ },
487
+ tabId
488
+ },
489
+ result: (_d, args) => text(`Hovered ${describeTarget(args)} in group "${args.group}".`)
490
+ },
491
+ {
492
+ name: "browser_drag",
493
+ group: "control",
494
+ action: "drag",
495
+ description: "Drag from a source element to a target element (each by `ref` or `selector`).",
496
+ input: {
497
+ group,
498
+ fromRef: {
499
+ type: "string",
500
+ optional: true,
501
+ description: "Source element ref."
502
+ },
503
+ fromSelector: {
504
+ type: "string",
505
+ optional: true,
506
+ description: "Source CSS selector."
507
+ },
508
+ ref: {
509
+ type: "string",
510
+ optional: true,
511
+ description: "Target element ref."
512
+ },
513
+ selector: {
514
+ type: "string",
515
+ optional: true,
516
+ description: "Target CSS selector."
517
+ },
518
+ tabId
519
+ },
520
+ result: (_d, args) => text(`Dragged to ${describeTarget(args)} in group "${args.group}".`)
521
+ },
522
+ {
523
+ name: "browser_upload",
524
+ group: "control",
525
+ action: "upload",
526
+ description: "Set the files on a file <input>, by absolute path(s) on the machine running the bridge. CDP executor only (Chromium).",
527
+ input: {
528
+ group,
529
+ ref: refField,
530
+ selector: selectorField,
531
+ paths: {
532
+ type: "array",
533
+ description: "Absolute file path(s) to attach.",
534
+ items: { type: "string" }
535
+ },
536
+ tabId
537
+ },
538
+ result: (_d, args) => text(`Uploaded ${Array.isArray(args.paths) ? args.paths.length : 0} file(s) to ${describeTarget(args)}.`)
539
+ },
540
+ (
541
+ // ─── reading (page) ────────────────────────────────────────────────────────
542
+ {
543
+ name: "browser_get_html",
544
+ group: "page",
545
+ action: "get_html",
546
+ description: "Get the HTML of the page (or of a `ref`/`selector` element). `outer: false` for innerHTML.",
547
+ input: {
548
+ group,
549
+ ref: refField,
550
+ selector: selectorField,
551
+ outer: {
552
+ type: "boolean",
553
+ optional: true,
554
+ description: "Outer HTML (default true)."
555
+ },
556
+ tabId
557
+ },
558
+ result: (data) => text(String(rec(data).html ?? ""))
559
+ }),
560
+ {
561
+ name: "browser_get_attribute",
562
+ group: "page",
563
+ action: "get_attribute",
564
+ description: "Read an element's tag, text, value, checked state, bounding box, and attributes (or one named attribute).",
565
+ input: {
566
+ group,
567
+ ref: refField,
568
+ selector: selectorField,
569
+ name: {
570
+ type: "string",
571
+ optional: true,
572
+ description: "A single attribute name to read."
573
+ },
574
+ tabId
575
+ },
576
+ result: (data) => json(rec(data), JSON.stringify(rec(data), null, 2))
577
+ },
578
+ {
579
+ name: "browser_query",
580
+ group: "page",
581
+ action: "query",
582
+ description: "Find visible elements matching a CSS selector. Returns each with a stable ref you can pass to other tools.",
583
+ input: {
584
+ group,
585
+ selector: {
586
+ type: "string",
587
+ description: "CSS selector to match."
588
+ },
589
+ limit: {
590
+ type: "number",
591
+ optional: true,
592
+ description: "Max matches (default 50)."
593
+ },
594
+ tabId
595
+ },
596
+ result: (data) => json(rec(data), JSON.stringify(rec(data), null, 2))
597
+ },
598
+ (
599
+ // ─── debug (off by default) ────────────────────────────────────────────────
600
+ {
601
+ name: "browser_eval",
602
+ group: "debug",
603
+ action: "eval",
604
+ description: "Evaluate JavaScript in the page's DOM context and return the JSON-serializable result. Powerful — enabled only when the `debug` group is on.",
605
+ input: {
606
+ group,
607
+ code: {
608
+ type: "string",
609
+ description: "JavaScript to evaluate."
610
+ },
611
+ tabId
612
+ },
613
+ result: (data) => {
614
+ const r = rec(data).result;
615
+ return json(rec(data), typeof r === "string" ? r : JSON.stringify(r, null, 2));
616
+ }
617
+ }),
618
+ {
619
+ name: "browser_console",
620
+ group: "debug",
621
+ action: "console",
622
+ description: "Return recent console output (logs, warnings, errors). CDP executor only (Chromium).",
623
+ input: {
624
+ group,
625
+ tabId
626
+ },
627
+ result: (data) => {
628
+ const entries = rec(data).entries ?? [];
629
+ return json(rec(data), JSON.stringify(entries, null, 2));
630
+ }
631
+ },
632
+ {
633
+ name: "browser_network",
634
+ group: "debug",
635
+ action: "network",
636
+ description: "Return recent network requests (method, URL, status). CDP executor only (Chromium).",
637
+ input: {
638
+ group,
639
+ tabId
640
+ },
641
+ result: (data) => {
642
+ const entries = rec(data).entries ?? [];
643
+ return json(rec(data), JSON.stringify(entries, null, 2));
644
+ }
645
+ },
646
+ {
647
+ name: "browser_handle_dialog",
648
+ group: "debug",
649
+ action: "handle_dialog",
650
+ description: "Accept or dismiss a pending JavaScript dialog (alert/confirm/prompt). CDP executor only (Chromium).",
651
+ input: {
652
+ group,
653
+ accept: {
654
+ type: "boolean",
655
+ optional: true,
656
+ description: "Accept (default true) or dismiss."
657
+ },
658
+ promptText: {
659
+ type: "string",
660
+ optional: true,
661
+ description: "Text for a prompt() dialog."
662
+ },
663
+ tabId
664
+ },
665
+ result: (_d, args) => text(`${args.accept === false ? "Dismissed" : "Accepted"} the dialog.`)
666
+ },
667
+ {
668
+ name: "browser_set_viewport",
669
+ group: "debug",
670
+ action: "set_viewport",
671
+ description: "Emulate a viewport size / device metrics. CDP executor only (Chromium).",
672
+ input: {
673
+ group,
674
+ width: {
675
+ type: "number",
676
+ description: "Viewport width in CSS px."
677
+ },
678
+ height: {
679
+ type: "number",
680
+ description: "Viewport height in CSS px."
681
+ },
682
+ mobile: {
683
+ type: "boolean",
684
+ optional: true,
685
+ description: "Emulate a mobile device."
686
+ },
687
+ deviceScaleFactor: {
688
+ type: "number",
689
+ optional: true,
690
+ description: "Device pixel ratio (0 = default)."
691
+ },
692
+ tabId
693
+ },
694
+ result: (_d, args) => text(`Set viewport to ${args.width}×${args.height}.`)
695
+ },
696
+ {
697
+ name: "browser_cookies",
698
+ group: "debug",
699
+ action: "cookies",
700
+ description: "Read or modify cookies. `op`: get (by url+name), list (by url), set (url+name+value), clear (url+name).",
701
+ input: {
702
+ group: {
703
+ type: "string",
704
+ optional: true,
705
+ description: "Group (unused; cookies are profile-wide)."
706
+ },
707
+ op: {
708
+ type: "string",
709
+ enum: [
710
+ "get",
711
+ "list",
712
+ "set",
713
+ "clear"
714
+ ],
715
+ description: "Operation."
716
+ },
717
+ url: {
718
+ type: "string",
719
+ optional: true,
720
+ description: "URL the cookie applies to."
721
+ },
722
+ name: {
723
+ type: "string",
724
+ optional: true
725
+ },
726
+ value: {
727
+ type: "string",
728
+ optional: true
729
+ },
730
+ domain: {
731
+ type: "string",
732
+ optional: true
733
+ },
734
+ path: {
735
+ type: "string",
736
+ optional: true
737
+ }
738
+ },
739
+ params: (args) => ({
740
+ op: args.op,
741
+ url: args.url,
742
+ name: args.name,
743
+ value: args.value,
744
+ domain: args.domain,
745
+ path: args.path
746
+ }),
747
+ result: (data) => json(rec(data), JSON.stringify(rec(data), null, 2))
748
+ },
749
+ {
750
+ name: "browser_request_feedback",
751
+ group: "interactive",
752
+ action: "request_feedback",
753
+ description: "Ask the human at the browser to respond on the page, and block until they do (or it times out). Use when you're unsure what the user means and a screenshot or snapshot isn't enough — e.g. 'which of these did you mean?'. `mode`: confirm (yes/no bar), choose (pick one of `options`), point (click one spot), element (hover-highlight then click one element), region (drag a box over an area), comment (point + a free-text note). point/element/region resolve to element ref(s) you can then click/snapshot. Returns the user's response; on timeout returns `{ timedOut: true }` so you can fall back. interactive group (opt-in).",
754
+ timeoutMs: 3e5,
755
+ input: {
756
+ group,
757
+ mode: {
758
+ type: "string",
759
+ enum: [
760
+ "confirm",
761
+ "choose",
762
+ "point",
763
+ "element",
764
+ "region",
765
+ "comment"
766
+ ],
767
+ description: "Kind of prompt: confirm | choose | point | element | region | comment."
768
+ },
769
+ prompt: {
770
+ type: "string",
771
+ optional: true,
772
+ description: "Question/instruction shown to the user above the controls."
773
+ },
774
+ options: {
775
+ type: "array",
776
+ optional: true,
777
+ items: { type: "string" },
778
+ description: "Choices for `choose` mode (ignored otherwise)."
779
+ },
780
+ timeoutMs: {
781
+ type: "number",
782
+ optional: true,
783
+ description: "How long to wait for the user, in ms (default 120000, max 290000)."
784
+ },
785
+ tabId
786
+ },
787
+ params: (args) => {
788
+ const requested = typeof args.timeoutMs === "number" ? args.timeoutMs : 12e4;
789
+ return {
790
+ mode: args.mode,
791
+ prompt: args.prompt,
792
+ options: Array.isArray(args.options) ? args.options : undefined,
793
+ // Keep the overlay's own countdown below the broker deadline (300s) so it
794
+ // self-resolves into a clean `timedOut` result rather than being cancelled.
795
+ timeoutMs: Math.min(Math.max(requested, 1e3), 29e4),
796
+ tabId: args.tabId
797
+ };
798
+ },
799
+ result: (data) => {
800
+ const d = data;
801
+ if (d?.error) {
802
+ return json({
803
+ responded: false,
804
+ error: d.error,
805
+ annotations: []
806
+ }, `Couldn't ask the user on this page: ${d.error}. Fall back to a screenshot/snapshot.`);
807
+ }
808
+ if (!d || d.responded === false || d.timedOut) {
809
+ return json({
810
+ responded: false,
811
+ timedOut: true,
812
+ annotations: []
813
+ }, "No response from the user within the time limit.");
814
+ }
815
+ const annotations = Array.isArray(d.annotations) ? d.annotations : [];
816
+ return json({
817
+ responded: true,
818
+ annotations
819
+ }, summarizeAnnotations(annotations));
820
+ }
821
+ }
591
822
  ];
592
823
  /** A short human-readable line describing what the user marked. */
593
824
  function summarizeAnnotations(annotations) {
594
- if (annotations.length === 0) {
595
- return "User responded with no marks.";
596
- }
597
- return annotations
598
- .map((a) => {
599
- const note = "text" in a && a.text ? ` — "${a.text}"` : "";
600
- if (a.kind === "confirm") {
601
- return `User ${a.value ? "confirmed" : "declined"}.`;
602
- }
603
- if (a.kind === "choice") {
604
- return `User chose "${a.value}".`;
605
- }
606
- if (a.kind === "element") {
607
- const what = a.ref ? `ref ${a.ref}` : (a.selector ?? "an element");
608
- return `User selected ${what}${note}.`;
609
- }
610
- if (a.kind === "region") {
611
- const refs = a.refs.length ? ` covering ${a.refs.join(", ")}` : "";
612
- return `User boxed a ${Math.round(a.rect.width)}×${Math.round(a.rect.height)} region${refs}${note}.`;
613
- }
614
- const where = a.ref ? `ref ${a.ref}` : `(${Math.round(a.x)}, ${Math.round(a.y)})`;
615
- return `User pointed at ${where}${note}.`;
616
- })
617
- .join(" ");
825
+ if (annotations.length === 0) {
826
+ return "User responded with no marks.";
827
+ }
828
+ return annotations.map((a) => {
829
+ const note = "text" in a && a.text ? ` — "${a.text}"` : "";
830
+ if (a.kind === "confirm") {
831
+ return `User ${a.value ? "confirmed" : "declined"}.`;
832
+ }
833
+ if (a.kind === "choice") {
834
+ return `User chose "${a.value}".`;
835
+ }
836
+ if (a.kind === "element") {
837
+ const what = a.ref ? `ref ${a.ref}` : a.selector ?? "an element";
838
+ return `User selected ${what}${note}.`;
839
+ }
840
+ if (a.kind === "region") {
841
+ const refs = a.refs.length ? ` covering ${a.refs.join(", ")}` : "";
842
+ return `User boxed a ${Math.round(a.rect.width)}×${Math.round(a.rect.height)} region${refs}${note}.`;
843
+ }
844
+ const where = a.ref ? `ref ${a.ref}` : `(${Math.round(a.x)}, ${Math.round(a.y)})`;
845
+ return `User pointed at ${where}${note}.`;
846
+ }).join(" ");
618
847
  }
848
+
619
849
  //# sourceMappingURL=catalog.js.map