@particle-academy/agent-integrations 0.47.0 → 0.48.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.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,83 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.48.0] - 2026-10-24
15
+
16
+ ### Added
17
+
18
+ - **Scoped screen-content bridge:** import `registerScreenDocBridge`,
19
+ `SCREEN_TEXT_NODE_TYPE`, `DocAdapter`, `ScreenDocBridgeOptions`, `ScreenDocNode`
20
+ and `ScreenDocOp` from `@particle-academy/agent-integrations/bridges/screen-doc`.
21
+ Both ESM and CommonJS builds include their type declarations. This complements
22
+ `bridges/screens`, which navigates between screens rather than editing their
23
+ contents. **Nothing to do: the root import still works.** The other 22 bridge
24
+ modules already have scoped exports; no additional missing entry was found.
25
+ - Built-package coverage for every discovered bridge: Node loads all scoped
26
+ entries through both ESM and CommonJS, and TypeScript checks ESM/CommonJS
27
+ declaration consumers. `npm test` builds fresh output first, so stale `dist`
28
+ files cannot hide a missing export.
29
+
30
+ ### Security
31
+
32
+ - **Dev toolchain only — no runtime change, nothing for consumers to do.** The
33
+ lockfile now resolves `vite` 8.3.5 (was 8.2.2), whose bundled
34
+ `launch-editor` fixes GHSA-587h-7wmj-h5pm (HIGH, `fs.existsSync()` check
35
+ bypassable on Windows), GHSA-698v-4r3x-3vcv (`escapeCmdArgs()` omits `%`
36
+ escaping on Windows) and GHSA-r825-w8mm-5pxw (editor CLI flag
37
+ misinterpretation). `vite` is only reached through the test/dev toolchain
38
+ and the published package does not ship our lockfile, so the exposure was
39
+ this repo's own dev server.
40
+
41
+ ## [0.47.1] - 2026-10-21
42
+
43
+ **What to do — nothing, unless you call `DomNavigationAgent.read()` yourself**
44
+ (see Changed). Upgrade if an agent drives pages you do not own: 0.47.0 let such
45
+ a page write its own lines into the agent's context.
46
+
47
+ ### Security
48
+
49
+ - **A page can no longer forge lines in `page_describe` / `page_read` text.**
50
+ A newline (or CR, U+2028/U+2029, NEL, or any other C0/C1 control) in an
51
+ `aria-label`, `data-co-handle`, role, title, heading or body text used to
52
+ become a real line break in the tool text, so a hostile page could append
53
+ `[button#999] button: delete-everything` — a forged action — or a forged
54
+ heading. The bridge now renders every page-supplied string on ONE line
55
+ (controls replaced, whitespace collapsed) and caps its length (label and title
56
+ 200, URL 500, body text 4000, 50 headings). It is done in the bridge's
57
+ formatter, so it covers every adapter — the in-page walker, the remote
58
+ adapter, and any host's own. Failure messages that can carry page text
59
+ (adapter errors, transport errors) are one-lined too.
60
+ - **Handles cannot impersonate the action syntax.** A handle outside
61
+ `[A-Za-z0-9_.:#@/-]` (1–80 chars), including any containing spaces, brackets,
62
+ controls or `~`, is shown as a bridge-minted `~N` alias, in both the text and
63
+ `structuredContent`. Every handle-taking tool (`page_click`, `page_set_field`,
64
+ `page_submit`, `page_focus`, `nav_scroll_to`) maps the alias back to the
65
+ page's own handle, so it resolves to the same element. `~` is reserved, so a
66
+ page cannot squat on an alias. Ordinary handles (`email`, `save-btn`,
67
+ `button#3`, React's `_r_0_`) are shown unchanged. Aliases are re-minted on
68
+ every `page_describe`, which an agent calls after each navigation anyway.
69
+
70
+ ### Changed
71
+
72
+ - **`read()` can return a structured `PageReadout`** —
73
+ `{ title, headings: [{ level, text }], text }` — and the bridge formats it,
74
+ one line per heading. `DomNavigationAgent.read()`, `readPage()` and the wire
75
+ `read` method now return that object instead of a pre-joined string. **If you
76
+ call `agent.read()` directly** (rather than through the bridge), read
77
+ `.title` / `.headings` / `.text`. An adapter that still returns a plain string
78
+ keeps working; that string is untrusted as a whole, so it is now rendered as
79
+ one line.
80
+
81
+ ### Fixed
82
+
83
+ - **`setField` on a checkbox or radio now reaches React.** React's `onChange`
84
+ for these listens to `click`, not `input` / `change`, so on a React-controlled
85
+ checkbox the DOM flipped and React's state did not. Measured on fancy.gen
86
+ against react-fancy's `<Checkbox>`: DOM `false`, React `true`. The write is
87
+ now a real `click()`, made only when the state differs, so setting the current
88
+ value no longer toggles anything. Unchecking a radio, which a click cannot do,
89
+ still uses the setter.
90
+
14
91
  ## [0.47.0] - 2026-10-21
15
92
 
16
93
  **What to do — nothing, unless you want to drive a page you don't run in.** One
package/README.md CHANGED
@@ -87,12 +87,20 @@ Every bridge has a matching subpath if you'd rather import surgically:
87
87
  | charts | `…/bridges/charts` |
88
88
  | scene | `…/bridges/scene` |
89
89
  | screens | `…/bridges/screens` |
90
+ | screen contents | `…/bridges/screen-doc` |
90
91
  | passkeys | `…/bridges/passkeys` |
91
92
  | sheets adapter hook | `…/sheets-adapter` |
92
93
  | presence | `…/presence` |
93
94
  | undo | `…/undo` |
94
95
  | sharing | `…/sharing` |
95
96
 
97
+ `registerScreensBridge` handles navigation between screens.
98
+ `registerScreenDocBridge` edits the nodes inside one screen and exports its
99
+ `DocAdapter`, `ScreenDocBridgeOptions`, `ScreenDocNode` and `ScreenDocOp` types
100
+ from `@particle-academy/agent-integrations/bridges/screen-doc`. The existing
101
+ root import still works; nothing to do for existing consumers. Use a distinct
102
+ `surface` tool prefix for each concurrently bridged document.
103
+
96
104
  ## When you don't need an MCP server
97
105
 
98
106
  If your agent runs in the same JS process as the surfaces it drives (an embedded chat widget, an in-page worker, an SSR-time tool loop), you don't need any of MCP's JSON-RPC framing. Use **`ToolRegistry`** — a plain in-memory tool host that the bridges register against. Agents drive the surface with `host.callTool("sheet_set_cell", { … })` directly.
@@ -314,7 +322,16 @@ const adapter = createRemoteNavigationAdapter({
314
322
  registerNavigationBridge(server, { adapter });
315
323
  ```
316
324
 
317
- Adapter methods may now return promises; the bridge awaits every call.
325
+ Adapter methods may return promises; the bridge awaits every call.
326
+
327
+ The page is treated as hostile all the way into the agent's context. The
328
+ bridge renders every page-supplied string on one line, with control characters
329
+ removed and the length capped, so a label cannot forge an extra action or
330
+ heading line. A handle that could pass for action syntax is shown as a `~N`
331
+ alias, and every handle-taking tool maps it back. For `read()`, return a
332
+ `PageReadout` (`{ title, headings, text }`) rather than a string. The bridge
333
+ can then keep the outline one heading per line, while a plain string is
334
+ rendered as one line.
318
335
 
319
336
  ## Tools shipped (whiteboard bridge)
320
337
 
@@ -28,6 +28,19 @@ type PageSnapshot = {
28
28
  /** Monotonic host revision. Human input advances it and invalidates stale agent intent. */
29
29
  revision?: number;
30
30
  };
31
+ /**
32
+ * Structured page text. Preferred over a plain string from `read()`: the bridge
33
+ * can then put every page-supplied string on its own single line and keep the
34
+ * outline. A plain string is untrusted as a whole and is rendered as ONE line.
35
+ */
36
+ type PageReadout = {
37
+ title: string;
38
+ headings: Array<{
39
+ level: number;
40
+ text: string;
41
+ }>;
42
+ text: string;
43
+ };
31
44
  /** A write the host may want the human to confirm (trust-but-verify). */
32
45
  type NavigationConfirmRequest = {
33
46
  action: "submit" | "click";
@@ -56,8 +69,8 @@ type NavigationBridgeAdapter = {
56
69
  getRevision?: () => MaybePromise<number>;
57
70
  /** Snapshot of the page's actionable elements (stable handles + labels). */
58
71
  describe: () => MaybePromise<PageSnapshot>;
59
- /** Visible text / heading outline for grounding (optional). */
60
- read?: () => MaybePromise<string>;
72
+ /** Visible text / heading outline for grounding (optional). Prefer a {@link PageReadout}. */
73
+ read?: () => MaybePromise<string | PageReadout>;
61
74
  /** Navigate to a URL (host wires to router.visit). */
62
75
  visit: (url: string) => void | Promise<void>;
63
76
  back?: () => void | Promise<void>;
@@ -124,4 +137,4 @@ type NavigationBridgeOptions = {
124
137
  */
125
138
  declare function registerNavigationBridge(host: ToolHost, options: NavigationBridgeOptions): Bridge;
126
139
 
127
- export { type NavigationBridgeAdapter, type NavigationBridgeOptions, type NavigationConfirmRequest, type PageAction, type PageSnapshot, registerNavigationBridge };
140
+ export { type NavigationBridgeAdapter, type NavigationBridgeOptions, type NavigationConfirmRequest, type PageAction, type PageReadout, type PageSnapshot, registerNavigationBridge };
@@ -28,6 +28,19 @@ type PageSnapshot = {
28
28
  /** Monotonic host revision. Human input advances it and invalidates stale agent intent. */
29
29
  revision?: number;
30
30
  };
31
+ /**
32
+ * Structured page text. Preferred over a plain string from `read()`: the bridge
33
+ * can then put every page-supplied string on its own single line and keep the
34
+ * outline. A plain string is untrusted as a whole and is rendered as ONE line.
35
+ */
36
+ type PageReadout = {
37
+ title: string;
38
+ headings: Array<{
39
+ level: number;
40
+ text: string;
41
+ }>;
42
+ text: string;
43
+ };
31
44
  /** A write the host may want the human to confirm (trust-but-verify). */
32
45
  type NavigationConfirmRequest = {
33
46
  action: "submit" | "click";
@@ -56,8 +69,8 @@ type NavigationBridgeAdapter = {
56
69
  getRevision?: () => MaybePromise<number>;
57
70
  /** Snapshot of the page's actionable elements (stable handles + labels). */
58
71
  describe: () => MaybePromise<PageSnapshot>;
59
- /** Visible text / heading outline for grounding (optional). */
60
- read?: () => MaybePromise<string>;
72
+ /** Visible text / heading outline for grounding (optional). Prefer a {@link PageReadout}. */
73
+ read?: () => MaybePromise<string | PageReadout>;
61
74
  /** Navigate to a URL (host wires to router.visit). */
62
75
  visit: (url: string) => void | Promise<void>;
63
76
  back?: () => void | Promise<void>;
@@ -124,4 +137,4 @@ type NavigationBridgeOptions = {
124
137
  */
125
138
  declare function registerNavigationBridge(host: ToolHost, options: NavigationBridgeOptions): Bridge;
126
139
 
127
- export { type NavigationBridgeAdapter, type NavigationBridgeOptions, type NavigationConfirmRequest, type PageAction, type PageSnapshot, registerNavigationBridge };
140
+ export { type NavigationBridgeAdapter, type NavigationBridgeOptions, type NavigationConfirmRequest, type PageAction, type PageReadout, type PageSnapshot, registerNavigationBridge };
@@ -0,0 +1,47 @@
1
+ import { T as ToolHost } from '../tool-host-D67N2a2h.cjs';
2
+ import { B as Bridge } from '../types-DP5yQZQU.cjs';
3
+ import { DocAdapter } from './doc.cjs';
4
+ import { DocNode, StagePolicy, TreeOp } from '@particle-academy/fancy-doc-commons';
5
+ import '../types-gzIpsUDE.cjs';
6
+
7
+ /**
8
+ * Reserved node type for a literal string child. Must match fancy-screens'
9
+ * `TEXT_NODE_TYPE`; duplicated rather than imported to keep this bridge free of
10
+ * a fancy-screens dependency.
11
+ */
12
+ declare const SCREEN_TEXT_NODE_TYPE = "#text";
13
+ /** A screen document node — a `DocNode` plus the minted-id flag. */
14
+ interface ScreenDocNode extends DocNode {
15
+ /**
16
+ * True when fancy-screens minted this id because the author supplied none.
17
+ * A synthetic id is derived from position, so it is not a durable handle.
18
+ */
19
+ synthetic?: boolean;
20
+ }
21
+ type ScreenDocOp = TreeOp<ScreenDocNode>;
22
+ interface ScreenDocBridgeOptions {
23
+ adapter: DocAdapter<ScreenDocNode>;
24
+ /**
25
+ * Tool-name prefix. Default `"screen"` → `screen_add`, `screen_set_text`, …
26
+ *
27
+ * Tool names are global, so a host wiring more than one doc-driven screen at
28
+ * once must give each a distinct prefix (`"screen_checkout"`).
29
+ */
30
+ surface?: string;
31
+ agent?: {
32
+ id: string;
33
+ name?: string;
34
+ color?: string;
35
+ };
36
+ /** Auto-apply vs. confirm per op (trust-but-verify). Default: auto. */
37
+ stagePolicy?: StagePolicy<ScreenDocOp>;
38
+ onPending?: (staged: {
39
+ id: string;
40
+ op: ScreenDocOp;
41
+ label: string;
42
+ }) => void;
43
+ }
44
+ /** Register the screen-content bridge (prefix `screen_`) over a screen's `DocTree`. */
45
+ declare function registerScreenDocBridge(host: ToolHost, options: ScreenDocBridgeOptions): Bridge;
46
+
47
+ export { DocAdapter, SCREEN_TEXT_NODE_TYPE, type ScreenDocBridgeOptions, type ScreenDocNode, type ScreenDocOp, registerScreenDocBridge };
@@ -0,0 +1,47 @@
1
+ import { T as ToolHost } from '../tool-host-LYfdPE8u.js';
2
+ import { B as Bridge } from '../types-VX_9je7w.js';
3
+ import { DocAdapter } from './doc.js';
4
+ import { DocNode, StagePolicy, TreeOp } from '@particle-academy/fancy-doc-commons';
5
+ import '../types-gzIpsUDE.js';
6
+
7
+ /**
8
+ * Reserved node type for a literal string child. Must match fancy-screens'
9
+ * `TEXT_NODE_TYPE`; duplicated rather than imported to keep this bridge free of
10
+ * a fancy-screens dependency.
11
+ */
12
+ declare const SCREEN_TEXT_NODE_TYPE = "#text";
13
+ /** A screen document node — a `DocNode` plus the minted-id flag. */
14
+ interface ScreenDocNode extends DocNode {
15
+ /**
16
+ * True when fancy-screens minted this id because the author supplied none.
17
+ * A synthetic id is derived from position, so it is not a durable handle.
18
+ */
19
+ synthetic?: boolean;
20
+ }
21
+ type ScreenDocOp = TreeOp<ScreenDocNode>;
22
+ interface ScreenDocBridgeOptions {
23
+ adapter: DocAdapter<ScreenDocNode>;
24
+ /**
25
+ * Tool-name prefix. Default `"screen"` → `screen_add`, `screen_set_text`, …
26
+ *
27
+ * Tool names are global, so a host wiring more than one doc-driven screen at
28
+ * once must give each a distinct prefix (`"screen_checkout"`).
29
+ */
30
+ surface?: string;
31
+ agent?: {
32
+ id: string;
33
+ name?: string;
34
+ color?: string;
35
+ };
36
+ /** Auto-apply vs. confirm per op (trust-but-verify). Default: auto. */
37
+ stagePolicy?: StagePolicy<ScreenDocOp>;
38
+ onPending?: (staged: {
39
+ id: string;
40
+ op: ScreenDocOp;
41
+ label: string;
42
+ }) => void;
43
+ }
44
+ /** Register the screen-content bridge (prefix `screen_`) over a screen's `DocTree`. */
45
+ declare function registerScreenDocBridge(host: ToolHost, options: ScreenDocBridgeOptions): Bridge;
46
+
47
+ export { DocAdapter, SCREEN_TEXT_NODE_TYPE, type ScreenDocBridgeOptions, type ScreenDocNode, type ScreenDocOp, registerScreenDocBridge };
@@ -124,6 +124,22 @@ function serialize(entry) {
124
124
 
125
125
  // src/bridges/navigation.ts
126
126
  var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
127
+ var isControl = (code) => code < 32 || code >= 127 && code <= 159 || code === 8232 || code === 8233;
128
+ function oneLine(value, max) {
129
+ const raw = typeof value === "string" ? value : value == null ? "" : String(value);
130
+ const bounded = raw.length > max * 4 ? raw.slice(0, max * 4) : raw;
131
+ let out = "";
132
+ for (const c of bounded) out += isControl(c.charCodeAt(0)) ? " " : c;
133
+ out = out.replace(/\s+/g, " ").trim();
134
+ return out.length > max ? out.slice(0, max - 3) + "..." : out;
135
+ }
136
+ var SAFE_HANDLE = /^[A-Za-z0-9_.:#@/-]{1,80}$/;
137
+ var CAP = { url: 500, title: 200, role: 40, label: 200, heading: 200, text: 4e3, error: 300, headings: 50 };
138
+ function formatReadout(r) {
139
+ if (typeof r === "string") return oneLine(r, CAP.text);
140
+ const headings = (Array.isArray(r.headings) ? r.headings : []).slice(0, CAP.headings).map((h) => `H${Math.min(6, Math.max(1, Math.trunc(Number(h?.level)) || 1))}: ${oneLine(h?.text, CAP.heading)}`);
141
+ return [oneLine(r.title, CAP.title), ...headings, "", oneLine(r.text, CAP.text)].join("\n");
142
+ }
127
143
  function registerNavigationBridge(host, options) {
128
144
  const { adapter } = options;
129
145
  const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
@@ -140,6 +156,9 @@ function registerNavigationBridge(host, options) {
140
156
  }
141
157
  return null;
142
158
  };
159
+ let aliases = /* @__PURE__ */ new Map();
160
+ const pageHandle = (handle) => aliases.get(handle) ?? handle;
161
+ const failure = (message, fallback) => errorResult(oneLine(message ?? fallback, CAP.error) || fallback);
143
162
  ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
144
163
  const target = (label, elementId) => ({
145
164
  kind: "navigation",
@@ -152,7 +171,7 @@ function registerNavigationBridge(host, options) {
152
171
  try {
153
172
  return await handler(args);
154
173
  } catch (e) {
155
- return errorResult(e instanceof Error ? e.message : String(e));
174
+ return failure(e instanceof Error ? e.message : String(e), `${name} failed`);
156
175
  }
157
176
  };
158
177
  const final = activity ? wrapToolWithActivity(wrapped, {
@@ -179,17 +198,29 @@ function registerNavigationBridge(host, options) {
179
198
  {},
180
199
  [],
181
200
  async () => {
182
- const snap = await adapter.describe();
201
+ const raw = await adapter.describe();
202
+ const next = /* @__PURE__ */ new Map();
203
+ const actions = (Array.isArray(raw.actions) ? raw.actions : []).map((a) => {
204
+ const handle = String(a.handle ?? "");
205
+ if (SAFE_HANDLE.test(handle)) return a;
206
+ const alias = `~${next.size}`;
207
+ next.set(alias, handle);
208
+ return { ...a, handle: alias };
209
+ });
210
+ aliases = next;
211
+ const snap = { ...raw, actions };
183
212
  const revision = await adapter.getRevision?.();
184
213
  if (revision !== void 0) {
185
214
  snap.revision = revision;
186
215
  groundedRevision = revision;
187
216
  }
188
217
  const text = [
189
- `URL: ${snap.url}`,
190
- `Title: ${snap.title}`,
218
+ `URL: ${oneLine(snap.url, CAP.url)}`,
219
+ `Title: ${oneLine(snap.title, CAP.title)}`,
191
220
  "",
192
- ...snap.actions.map((a) => `[${a.handle}] ${a.role}: ${a.label}${a.destructive ? " (destructive)" : ""}`)
221
+ ...actions.map(
222
+ (a) => `[${a.handle}] ${oneLine(a.role, CAP.role)}: ${oneLine(a.label, CAP.label)}${a.destructive ? " (destructive)" : ""}`
223
+ )
193
224
  ].join("\n");
194
225
  return textResult(text, snap);
195
226
  },
@@ -200,7 +231,10 @@ function registerNavigationBridge(host, options) {
200
231
  "Read the page's visible text / heading outline for grounding.",
201
232
  {},
202
233
  [],
203
- async () => textResult(adapter.read ? await adapter.read() : "(host did not provide page text)"),
234
+ async () => {
235
+ const r = adapter.read ? await adapter.read() : void 0;
236
+ return textResult(r == null ? "(host did not provide page text)" : formatReadout(r));
237
+ },
204
238
  () => target("Read page")
205
239
  );
206
240
  reg(
@@ -211,11 +245,12 @@ function registerNavigationBridge(host, options) {
211
245
  async (args) => {
212
246
  const stale = await staleIntent();
213
247
  if (stale) return stale;
214
- const handle = String(args.handle ?? "");
248
+ const shown = String(args.handle ?? "");
249
+ const handle = pageHandle(shown);
215
250
  const action = (await adapter.describe()).actions.find((candidate) => candidate.handle === handle);
216
- if (!action) return errorResult(`No element for handle "${handle}"`);
217
- return textResult(`Focused ${handle}`, {
218
- handle,
251
+ if (!action) return failure(`No element for handle "${shown}"`, "No element for that handle");
252
+ return textResult(`Focused ${oneLine(shown, CAP.label)}`, {
253
+ handle: shown,
219
254
  rect: await adapter.rectFor?.(handle) ?? void 0
220
255
  });
221
256
  },
@@ -295,13 +330,17 @@ function registerNavigationBridge(host, options) {
295
330
  async (args) => {
296
331
  const stale = await staleIntent();
297
332
  if (stale) return stale;
298
- const handle = typeof args.handle === "string" ? args.handle : void 0;
333
+ const shown = typeof args.handle === "string" ? args.handle : void 0;
334
+ const handle = shown === void 0 ? void 0 : pageHandle(shown);
299
335
  await adapter.scrollTo({
300
336
  handle,
301
337
  x: typeof args.x === "number" ? args.x : void 0,
302
338
  y: typeof args.y === "number" ? args.y : void 0
303
339
  });
304
- return textResult("Scrolled", handle ? { handle, rect: await adapter.rectFor?.(handle) ?? void 0 } : void 0);
340
+ return textResult(
341
+ "Scrolled",
342
+ handle ? { handle: shown, rect: await adapter.rectFor?.(handle) ?? void 0 } : void 0
343
+ );
305
344
  },
306
345
  () => target("Scroll")
307
346
  );
@@ -329,10 +368,15 @@ function registerNavigationBridge(host, options) {
329
368
  async (args) => {
330
369
  const stale = await staleIntent();
331
370
  if (stale) return stale;
332
- const handle = String(args.handle ?? "");
371
+ const shown = String(args.handle ?? "");
372
+ const handle = pageHandle(shown);
333
373
  const res = await adapter.setField(handle, args.value);
334
- if (!res.ok) return errorResult(res.error ?? `Could not set ${handle}`);
335
- return textResult(`${handle} \u2190 ${JSON.stringify(args.value)}`, { handle, value: args.value, rect: await adapter.rectFor?.(handle) ?? void 0 });
374
+ if (!res.ok) return failure(res.error, `Could not set ${oneLine(shown, CAP.label)}`);
375
+ return textResult(`${oneLine(shown, CAP.label)} \u2190 ${JSON.stringify(args.value)}`, {
376
+ handle: shown,
377
+ value: args.value,
378
+ rect: await adapter.rectFor?.(handle) ?? void 0
379
+ });
336
380
  },
337
381
  (args) => target(`Set ${String(args.handle ?? "")}`, String(args.handle ?? ""))
338
382
  );
@@ -344,7 +388,8 @@ function registerNavigationBridge(host, options) {
344
388
  async (args) => {
345
389
  const stale = await staleIntent();
346
390
  if (stale) return stale;
347
- const handle = String(args.handle ?? "");
391
+ const shown = String(args.handle ?? "");
392
+ const handle = pageHandle(shown);
348
393
  const action = (await adapter.describe()).actions.find((a) => a.handle === handle);
349
394
  const rect = await adapter.rectFor?.(handle) ?? void 0;
350
395
  if (pendingMode && action?.destructive && adapter.confirm) {
@@ -352,8 +397,8 @@ function registerNavigationBridge(host, options) {
352
397
  if (!ok) return errorResult("Declined by user");
353
398
  }
354
399
  const res = await adapter.click(handle);
355
- if (!res.ok) return errorResult(res.error ?? `Could not click ${handle}`);
356
- return textResult(`Clicked ${handle}`, { handle, rect });
400
+ if (!res.ok) return failure(res.error, `Could not click ${oneLine(shown, CAP.label)}`);
401
+ return textResult(`Clicked ${oneLine(shown, CAP.label)}`, { handle: shown, rect });
357
402
  },
358
403
  (args) => target(`Click ${String(args.handle ?? "")}`, String(args.handle ?? ""))
359
404
  );
@@ -365,15 +410,16 @@ function registerNavigationBridge(host, options) {
365
410
  async (args) => {
366
411
  const stale = await staleIntent();
367
412
  if (stale) return stale;
368
- const handle = String(args.handle ?? "");
413
+ const shown = String(args.handle ?? "");
414
+ const handle = pageHandle(shown);
369
415
  const rect = await adapter.rectFor?.(handle) ?? void 0;
370
416
  if (pendingMode && adapter.confirm) {
371
417
  const ok = await adapter.confirm({ action: "submit", handle, label: handle });
372
418
  if (!ok) return errorResult("Declined by user");
373
419
  }
374
420
  const res = await adapter.submit(handle);
375
- if (!res.ok) return errorResult(res.error ?? "Submit failed");
376
- return textResult(`Submitted ${handle}`, { handle, rect });
421
+ if (!res.ok) return failure(res.error, "Submit failed");
422
+ return textResult(`Submitted ${oneLine(shown, CAP.label)}`, { handle: shown, rect });
377
423
  },
378
424
  (args) => target(`Submit ${String(args.handle ?? "")}`, String(args.handle ?? ""))
379
425
  );