@jini-ai/agentic 0.4.3 → 0.4.4

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 (81) hide show
  1. package/dist/core/handle.d.ts +13 -0
  2. package/dist/core/handle.js +19 -0
  3. package/dist/core/index.d.ts +2 -1
  4. package/dist/core/index.js +2 -1
  5. package/dist/core/site-guide/index.d.ts +4 -0
  6. package/dist/core/site-guide/index.js +4 -0
  7. package/dist/core/site-guide/site-guide.d.ts +18 -0
  8. package/dist/core/site-guide/site-guide.js +144 -0
  9. package/dist/core/site-guide/topic-schema.d.ts +16 -0
  10. package/dist/core/site-guide/topic-schema.js +68 -0
  11. package/dist/core/site-guide/types.d.ts +83 -0
  12. package/dist/core/site-guide/types.js +2 -0
  13. package/dist/skills/install/archive.js +1 -0
  14. package/dist/skills/install/index.d.ts +1 -0
  15. package/dist/skills/install/index.js +1 -0
  16. package/dist/skills/install/install-service.d.ts +20 -1
  17. package/dist/skills/install/install-service.js +23 -11
  18. package/dist/skills/install/layout.d.ts +8 -0
  19. package/dist/skills/install/layout.js +26 -1
  20. package/dist/skills/install/live-registration.js +1 -0
  21. package/dist/skills/install/validation.d.ts +5 -1
  22. package/dist/skills/install/validation.js +2 -2
  23. package/dist/skills/install/yauzl-archive-reader.d.ts +29 -0
  24. package/dist/skills/install/yauzl-archive-reader.js +31 -0
  25. package/package.json +4 -4
  26. package/dist/a2ui/agent-to-renderer.d.ts +0 -613
  27. package/dist/a2ui/agent-to-renderer.js +0 -191
  28. package/dist/a2ui/catalog.d.ts +0 -73
  29. package/dist/a2ui/catalog.js +0 -304
  30. package/dist/a2ui/common-types.d.ts +0 -401
  31. package/dist/a2ui/common-types.js +0 -130
  32. package/dist/a2ui/index.d.ts +0 -16
  33. package/dist/a2ui/index.js +0 -16
  34. package/dist/a2ui/interpreter.d.ts +0 -71
  35. package/dist/a2ui/interpreter.js +0 -337
  36. package/dist/a2ui/json-pointer.d.ts +0 -48
  37. package/dist/a2ui/json-pointer.js +0 -174
  38. package/dist/a2ui/renderer-to-agent.d.ts +0 -342
  39. package/dist/a2ui/renderer-to-agent.js +0 -111
  40. package/dist/a2ui/resolve.d.ts +0 -28
  41. package/dist/a2ui/resolve.js +0 -144
  42. package/dist/a2ui/tree.d.ts +0 -66
  43. package/dist/a2ui/tree.js +0 -74
  44. package/dist/ag-ui.d.ts +0 -82
  45. package/dist/ag-ui.js +0 -85
  46. package/dist/agui/encoder.d.ts +0 -50
  47. package/dist/agui/encoder.js +0 -97
  48. package/dist/agui/events.d.ts +0 -59
  49. package/dist/agui/events.js +0 -15
  50. package/dist/capability.d.ts +0 -81
  51. package/dist/capability.js +0 -79
  52. package/dist/dom/dom-page-driver.d.ts +0 -36
  53. package/dist/dom/dom-page-driver.js +0 -601
  54. package/dist/dom/index.d.ts +0 -17
  55. package/dist/dom/index.js +0 -17
  56. package/dist/dom/model-context.d.ts +0 -69
  57. package/dist/dom/model-context.js +0 -44
  58. package/dist/element-handles.d.ts +0 -120
  59. package/dist/element-handles.js +0 -39
  60. package/dist/gen-ui/encoder.d.ts +0 -50
  61. package/dist/gen-ui/encoder.js +0 -97
  62. package/dist/gen-ui/events.d.ts +0 -59
  63. package/dist/gen-ui/events.js +0 -15
  64. package/dist/gen-ui/index.d.ts +0 -18
  65. package/dist/gen-ui/index.js +0 -17
  66. package/dist/guards.d.ts +0 -96
  67. package/dist/guards.js +0 -186
  68. package/dist/handle.d.ts +0 -41
  69. package/dist/handle.js +0 -41
  70. package/dist/mcp-ui-apps.d.ts +0 -223
  71. package/dist/mcp-ui-apps.js +0 -252
  72. package/dist/mcp-ui.d.ts +0 -133
  73. package/dist/mcp-ui.js +0 -134
  74. package/dist/page-capabilities.d.ts +0 -14
  75. package/dist/page-capabilities.js +0 -129
  76. package/dist/page-driver.d.ts +0 -120
  77. package/dist/page-driver.js +0 -2
  78. package/dist/page-executor.d.ts +0 -94
  79. package/dist/page-executor.js +0 -320
  80. package/dist/webmcp.d.ts +0 -202
  81. package/dist/webmcp.js +0 -173
@@ -1,601 +0,0 @@
1
- import { AGENT_ELEMENT_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_PAGE_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE, AGENT_ELEMENT_ROLES, resolveHandleSelector, } from '../element-handles.js';
2
- import { normalizeAgentLabel } from '../guards.js';
3
- /**
4
- * A {@link PageDriver} over a real DOM subtree.
5
- *
6
- * Deliberately mechanical: every refusal — handle validation, the credential-field guard, the
7
- * page allowlist, highlight clamping — already ran in `executePageCapability` before anything
8
- * here is called. Re-checking would duplicate policy in a place that will drift from it.
9
- *
10
- * Scoped to a `root` the host names, never `document`. Scanning the whole document would make
11
- * any markup anywhere — including rendered user content — an authorization decision, which
12
- * inverts the point of an explicit allowlist.
13
- */
14
- /** How the marker is drawn. Inline styles, so no stylesheet or CSP allowance is needed. */
15
- const HIGHLIGHT_STYLE = {
16
- outline: '3px solid #e11d48',
17
- outlineOffset: '2px',
18
- borderRadius: '4px',
19
- };
20
- /**
21
- * Upper bound on {@link createDomPageDriver}'s `settle`.
22
- *
23
- * A hidden tab stops delivering animation frames, so waiting for one never returns — and a
24
- * background tab is exactly where an agent-driven page ends up while the user works elsewhere.
25
- * This is the ceiling that keeps every write bounded regardless.
26
- */
27
- const SETTLE_TIMEOUT_MS = 120;
28
- function isAgentElementRole(value) {
29
- return value !== null && AGENT_ELEMENT_ROLES.includes(value);
30
- }
31
- /**
32
- * Whether the element is a rich-text surface — a `contenteditable` region rather than a form
33
- * control. Rich-text editors are built this way, so treating one as unfillable makes every
34
- * comment box, description field and document editor unreachable.
35
- */
36
- function isEditableRegion(element) {
37
- if (!(element instanceof HTMLElement))
38
- return false;
39
- // A control embedded in a rich-text document is still that control. Without this, a real
40
- // browser — which, unlike jsdom, propagates `isContentEditable` to every descendant — reports
41
- // the Save button inside an editor as a fillable text region, and `fill` overwrites its label
42
- // instead of refusing. The type-based refusals in `findFieldFillRefusal` cannot catch that
43
- // either, because the descriptor would say `contenteditable` rather than `button`.
44
- if (element.matches('input, textarea, select, button, a[href], summary, option'))
45
- return false;
46
- // The attribute is checked before `isContentEditable` because a headless DOM does not compute
47
- // the latter — relying on it alone makes every one of these tests pass vacuously while the real
48
- // browser behaves differently. `contenteditable=""` and `"plaintext-only"` are both editable;
49
- // only an explicit `"false"` is not.
50
- const attribute = element.getAttribute('contenteditable');
51
- if (attribute !== null)
52
- return attribute !== 'false';
53
- // Editability inherited from an ancestor, which only a real layout engine can tell us about.
54
- return element.isContentEditable === true;
55
- }
56
- /** The text of every `<label>` associated with `control`, by any of the relationships that link one. */
57
- function labelTextsOf(control) {
58
- const texts = [];
59
- const add = (text) => {
60
- const trimmed = text?.trim();
61
- if (trimmed)
62
- texts.push(trimmed);
63
- };
64
- // Real form controls carry `.labels` — every associated <label>, both for-linked and wrapping.
65
- const associated = 'labels' in control
66
- ? Array.from(control.labels ?? [])
67
- : [];
68
- for (const label of associated)
69
- add(label.textContent);
70
- // A contenteditable region is not a form control and has no `.labels`; resolve the same two
71
- // relationships by hand — an ancestor <label> (implicit wrapping) and a for-linked one (explicit).
72
- const wrapping = control.closest('label');
73
- if (wrapping)
74
- add(wrapping.textContent);
75
- if (control.id) {
76
- for (const label of Array.from(control.ownerDocument.querySelectorAll('label'))) {
77
- if (label.htmlFor === control.id)
78
- add(label.textContent);
79
- }
80
- }
81
- return texts;
82
- }
83
- /**
84
- * **Every** human-visible naming of the field, for the guard's benefit: `name`/`id` are the machine
85
- * names, and a CMS emitting `name="field_47"` next to a visibly-labelled "Card number" leaves every
86
- * guard here nothing to judge unless this is populated. Sources, in the order a human would read
87
- * them: `aria-label`, `placeholder`, then the text of each associated `<label>` (for-linked or
88
- * wrapping).
89
- *
90
- * All of them, not the first that resolves. Stopping at the first made the *page* the one choosing
91
- * which of its own labels the guard is allowed to see, which is the opposite of what a guard is
92
- * for: `<label for=f>Card number</label><input id=f name=field_47 placeholder="Enter value">` was
93
- * described to the guard as "Enter value" alone, and its value read back in full.
94
- *
95
- * De-duplicated because one label is often reachable twice (a wrapping `<label>` is also in
96
- * `.labels`), and a repeated entry is noise in every refusal built from these.
97
- */
98
- function accessibleLabelsOf(control) {
99
- const candidates = [
100
- control.getAttribute('aria-label')?.trim(),
101
- control.getAttribute('placeholder')?.trim(),
102
- ...labelTextsOf(control),
103
- ];
104
- return [...new Set(candidates.filter((text) => !!text))];
105
- }
106
- /** The field attributes the guards need, or `null` when the control is not a text-bearing input. */
107
- function fieldDescriptorOf(control) {
108
- if (isEditableRegion(control)) {
109
- // Reported as a field so the same guards run: a contenteditable div named `card-number` is
110
- // no more fillable than an input would be. `type` is not a real HTML input type, and is not
111
- // in any denied set, which is correct — the guard should judge it by name and attributes.
112
- return {
113
- type: 'contenteditable',
114
- // Read for the same reason `name` is: neither attribute is standard on a div, but a page
115
- // that bothers to set one is telling the guard what the field holds, and honouring `name`
116
- // while ignoring `autocomplete` would refuse `name="card_number"` and accept the
117
- // `autocomplete="cc-number"` spelling of the same field.
118
- autocomplete: control.getAttribute('autocomplete')?.toLowerCase() || undefined,
119
- name: control.getAttribute('name') || undefined,
120
- id: control.id || undefined,
121
- accessibleLabels: accessibleLabelsOf(control),
122
- readOnly: control.getAttribute('aria-readonly') === 'true',
123
- disabled: control.getAttribute('aria-disabled') === 'true',
124
- };
125
- }
126
- if (!(control instanceof HTMLInputElement) && !(control instanceof HTMLTextAreaElement)) {
127
- return null;
128
- }
129
- return {
130
- type: control instanceof HTMLInputElement ? control.type.toLowerCase() : 'textarea',
131
- autocomplete: control.getAttribute('autocomplete')?.toLowerCase() ?? undefined,
132
- name: control.name || undefined,
133
- id: control.id || undefined,
134
- accessibleLabels: accessibleLabelsOf(control),
135
- readOnly: control.readOnly,
136
- // `.disabled` reflects only this element's own attribute — not the "actually disabled" state
137
- // a control gets from an ancestor `<fieldset disabled>`. `:disabled` is the platform's own
138
- // answer for both, and is what decides whether the value even reaches form submission.
139
- disabled: control.matches(':disabled'),
140
- };
141
- }
142
- /**
143
- * Whether the element is actually rendered, or `undefined` when the platform cannot say.
144
- *
145
- * `checkVisibility` is the only answer that accounts for `display:none` on an ancestor, `hidden`,
146
- * `content-visibility` and an empty box at once. Where it is missing, this reports nothing rather
147
- * than falling back to a layout measurement — a headless DOM has no layout, so that fallback would
148
- * confidently declare every element invisible.
149
- *
150
- * The options are passed explicitly rather than relying on the method's defaults: per spec every
151
- * option defaults to `false`, so a bare `checkVisibility()` misses `visibility:hidden` and
152
- * `opacity:0` ancestors — two of the most common real ways to hide interactive UI (fade
153
- * transitions, visually-hidden-but-focusable patterns) — while still correctly reporting
154
- * `display:none`. Confirmed in real Chromium: `checkOpacity`/`checkVisibilityCSS` (this build's
155
- * spelling) turn a false `visible: true` on a `visibility:hidden`/`opacity:0` ancestor into the
156
- * correct `false`; jsdom does not implement the method at all, so no unit test can show this.
157
- */
158
- function visibilityOf(element) {
159
- const check = element.checkVisibility;
160
- return typeof check === 'function'
161
- ? check.call(element, { checkOpacity: true, checkVisibilityCSS: true })
162
- : undefined;
163
- }
164
- /**
165
- * `disabled` for the controls that have one; `undefined` for everything else, which is not the
166
- * same as `false`.
167
- *
168
- * `:disabled` rather than the `.disabled` IDL property: the property only reflects the element's
169
- * own attribute, so a control disabled solely by an ancestor `<fieldset disabled>` reads as
170
- * enabled even though a real user cannot type into or click it, and its value is excluded from
171
- * form submission entirely. `:disabled` is the platform's own answer and covers both cases.
172
- */
173
- function disabledOf(control) {
174
- return control instanceof HTMLInputElement
175
- || control instanceof HTMLTextAreaElement
176
- || control instanceof HTMLButtonElement
177
- || control instanceof HTMLSelectElement
178
- ? control.matches(':disabled')
179
- : undefined;
180
- }
181
- function describe(element, page) {
182
- const role = element.getAttribute(AGENT_ROLE_ATTRIBUTE);
183
- /* c8 ignore next -- `Node.textContent` is typed nullable for the abstract node; on an Element it is always a string, so the `?? ''` satisfies the type and is not a reachable path. */
184
- const text = element.textContent ?? '';
185
- // Live text is a fine fallback label for an ordinary element — it is the page's own ontology,
186
- // rendered openly. For an editable region it is not: a contenteditable's content is the *user's
187
- // data*, so falling back to it published the contents of a rich-text field to every
188
- // `find_elements` caller, with no `withState` asked for and no read guard anywhere in the way
189
- // (`<div contenteditable name="password">hunter2</div>` reported `label: "hunter2"`). Such a
190
- // region is named by what labels it instead, exactly as `fieldDescriptorOf` describes it.
191
- const fallback = isEditableRegion(element) ? (accessibleLabelsOf(element)[0] ?? '') : text;
192
- const label = element.getAttribute(AGENT_LABEL_ATTRIBUTE) ?? fallback;
193
- /* c8 ignore next -- only ever called on a `[data-agent-element]` match, so the attribute is present by construction. */
194
- const handle = element.getAttribute(AGENT_ELEMENT_ATTRIBUTE) ?? '';
195
- return {
196
- handle,
197
- role: isAgentElementRole(role) ? role : undefined,
198
- // The executor normalizes and bounds this; raw here on purpose so the guard is applied in
199
- // exactly one place rather than half-applied in two.
200
- label,
201
- labelTruncated: false,
202
- page,
203
- };
204
- }
205
- /**
206
- * Builds a driver over `root`.
207
- *
208
- * @param options - The subtree, the navigable pages, and the current page id.
209
- * @returns A driver ready to hand to `executePageCapability`.
210
- */
211
- export function createDomPageDriver(options) {
212
- const { root, pages } = options;
213
- /**
214
- * The page a specific element belongs to — its own nearest `[data-agent-page]` ancestor, not a
215
- * single document-wide guess.
216
- *
217
- * A host that keeps more than one `[data-agent-page]` section mounted at once (tabs, wizard
218
- * steps toggled with CSS rather than unmounted — the kind of SPA this driver's own options doc
219
- * anticipates) previously had every element attributed to whichever section happened to be
220
- * first in the DOM, regardless of which one it actually lived in. `closest` answers per element
221
- * instead of once for the whole call.
222
- *
223
- * Still overridden by a pinned `currentPage`, unchanged: that value is the host's own claim
224
- * about what is showing, taking precedence over whatever the DOM happens to say.
225
- */
226
- const pageOf = (element) => options.currentPage ?? element.closest(`[${AGENT_PAGE_ATTRIBUTE}]`)?.getAttribute(AGENT_PAGE_ATTRIBUTE) ?? undefined;
227
- /**
228
- * Active markers keyed by handle, so re-highlighting the same element restarts rather than
229
- * stacks — and carrying the styling to put back, which must be captured once, on the first
230
- * highlight. Re-reading the element on a restart would snapshot the marker itself, and the
231
- * element would keep it forever: a capability classified `read` precisely because it is
232
- * transient would permanently change how someone's page looks.
233
- */
234
- const highlights = new Map();
235
- /**
236
- * Every element published under `handle`. A page that (accidentally, or via a templating bug)
237
- * publishes the same handle twice makes `find`'s single-match resolution ambiguous — surfaced
238
- * here so both callers below can refuse rather than silently pick one.
239
- */
240
- const findAll = (handle) => Array.from(root.querySelectorAll(resolveHandleSelector(handle)));
241
- /**
242
- * More than one element answers to the same handle — a page-authoring bug (duplicate
243
- * `data-agent-element`), not something a caller passing that handle can resolve on its own.
244
- *
245
- * `findElements` still lists every one of them, so the collision is discoverable; resolving a
246
- * single handle to act on refuses instead of silently taking the first DOM match and leaving the
247
- * others permanently unreachable with no signal that they were ever unreachable at all.
248
- */
249
- const ambiguousHandleError = (handle, count) => new Error(`"${handle}" is published on ${count} elements on this page, so which one it addresses is `
250
- + 'ambiguous — this is a page-authoring bug (duplicate data-agent-element), not something a '
251
- + 'caller can resolve; publish a unique handle per element');
252
- const find = (handle) => {
253
- const matches = findAll(handle);
254
- if (matches.length === 0)
255
- throw new Error(`no element published as "${handle}" on this page`);
256
- if (matches.length > 1)
257
- throw ambiguousHandleError(handle, matches.length);
258
- return matches[0];
259
- };
260
- /**
261
- * The control a handle addresses — for a wrapper like `<li><label><input>`, the input.
262
- *
263
- * `<details>` is called out because its interactive part is the `<summary>`: clicking the
264
- * `<details>` element itself dispatches an event the platform ignores, so a disclosure widget
265
- * would never open and the caller would be told the click landed.
266
- */
267
- const controlOf = (element) => {
268
- // An editable region IS the control. Descending would find a link inside a rich-text document
269
- // and address that instead of the text the caller meant to write.
270
- if (isEditableRegion(element))
271
- return element;
272
- // `:scope >` because only a *direct child* `<summary>` is the disclosure control. An
273
- // unscoped query returns the first summary anywhere in the subtree, so a `<details>`
274
- // containing another one would toggle the inner disclosure while reporting that the outer
275
- // handle was clicked — the false success this whole suite exists to catch.
276
- if (element instanceof HTMLDetailsElement) {
277
- return element.querySelector(':scope > summary') ?? element;
278
- }
279
- if (element.matches('input, textarea, select, button, a'))
280
- return element;
281
- // A descendant that is itself independently published (its own `[data-agent-element]`) must
282
- // never be silently adopted as this handle's control — it is a distinct, separately
283
- // addressable target with its own label, not unhandled wrapper markup like the plain
284
- // `<li><label><input>` this descent exists for. Filtering to descendants whose nearest
285
- // published ancestor is still `element` itself excludes exactly that case while leaving
286
- // ordinary wrapper descent untouched.
287
- const owned = (node) => node.closest(`[${AGENT_ELEMENT_ATTRIBUTE}]`) === element;
288
- const candidate = Array.from(element.querySelectorAll('input, textarea, select, button, a, summary'))
289
- .find(owned);
290
- return candidate ?? element;
291
- };
292
- return {
293
- async findElements(filter) {
294
- const found = Array.from(root.querySelectorAll(`[${AGENT_ELEMENT_ATTRIBUTE}]`))
295
- .map((element) => describe(element, pageOf(element)))
296
- .filter((element) => element.handle.length > 0);
297
- const query = filter.query?.toLowerCase();
298
- return found.filter((element) => {
299
- if (filter.role !== undefined && element.role !== filter.role)
300
- return false;
301
- if (query === undefined)
302
- return true;
303
- return element.handle.toLowerCase().includes(query)
304
- || element.label.toLowerCase().includes(query);
305
- });
306
- },
307
- async listPages() {
308
- // `Object.entries`, same own-enumerable-keys-only guarantee `Object.keys` gives `navigate`'s
309
- // own `hasOwnProperty` check above — no risk of a `pages.label`/`pages.navigate`-shaped
310
- // prototype entry leaking in as a fake page id.
311
- return Object.entries(pages).map(([id, page]) => ({ id, label: page.label }));
312
- },
313
- async describeField(handle) {
314
- return fieldDescriptorOf(controlOf(find(handle)));
315
- },
316
- async describeState(handle) {
317
- // Unlike every other method here, a handle that no longer resolves is an answer rather than
318
- // an error: an element that a click removed is exactly what the caller is asking about. A
319
- // handle resolving to *more than one* element is a different situation entirely — not an
320
- // absence but an ambiguity — so that case still refuses, same as `find`.
321
- const matches = findAll(handle);
322
- if (matches.length === 0)
323
- return null;
324
- if (matches.length > 1)
325
- throw ambiguousHandleError(handle, matches.length);
326
- const element = matches[0];
327
- const control = controlOf(element);
328
- const field = fieldDescriptorOf(control);
329
- const checkable = control instanceof HTMLInputElement
330
- && (control.type === 'checkbox' || control.type === 'radio');
331
- // A dropdown reports its option texts alongside its value. Without them a caller can only
332
- // guess what to pass to `page.select_option`, and a wrong guess is a refusal it could have
333
- // avoided by looking. It carries a descriptor too, so the read guard still runs — a
334
- // `<select name="secret_token">` is no more readable than an input of the same name.
335
- const dropdown = control instanceof HTMLSelectElement ? control : undefined;
336
- const readable = field ?? (dropdown === undefined ? null : {
337
- type: 'select',
338
- name: dropdown.name || undefined,
339
- id: dropdown.id || undefined,
340
- // Same reason `fieldDescriptorOf` carries these: a `<select aria-label="Card number"
341
- // name="f9">` states its sensitivity nowhere else, and this descriptor is built by hand
342
- // rather than going through that function, so it needs the labels wired up separately.
343
- accessibleLabels: accessibleLabelsOf(dropdown),
344
- // Same `:disabled` reasoning as `disabledOf` — a `<select>` inside a disabled `<fieldset>`
345
- // is not itself flagged by the `.disabled` IDL property.
346
- disabled: dropdown.matches(':disabled'),
347
- });
348
- const value = field !== null
349
- ? control.value
350
- : dropdown?.value;
351
- /* c8 ignore next -- as in `describe`: an Element's textContent is always a string. */
352
- const text = element.textContent ?? '';
353
- return {
354
- text,
355
- // A contenteditable region has no `.value`; its content IS its value, so `text` is the
356
- // one channel carrying it and the executor has to gate it exactly like a value. Reported
357
- // as a mechanical fact about the element, not as a judgment about the content — the
358
- // decision of whether it may be shown stays in `projectElementState`, with every other
359
- // secrecy rule.
360
- ...(isEditableRegion(control) ? { textIsValue: true } : {}),
361
- // Reported raw and unconditionally; `projectElementState` applies the read guard. A
362
- // driver deciding here which values are secret is a driver holding policy.
363
- ...(value !== undefined ? { value } : {}),
364
- ...(checkable ? { checked: control.checked } : {}),
365
- ...(disabledOf(control) !== undefined ? { disabled: disabledOf(control) } : {}),
366
- ...(visibilityOf(element) !== undefined ? { visible: visibilityOf(element) } : {}),
367
- ...(readable !== null ? { field: readable } : {}),
368
- ...(dropdown !== undefined
369
- ? {
370
- options: Array.from(dropdown.options)
371
- .map((entry) => entry.text.trim())
372
- .filter((entry) => entry.length > 0),
373
- }
374
- : {}),
375
- };
376
- },
377
- /**
378
- * Two animation frames — long enough for a framework to have re-rendered and the browser to
379
- * have painted — or {@link SETTLE_TIMEOUT_MS}, whichever lands first. Without the timeout this
380
- * would hang for the whole life of a backgrounded tab; without the frames every write would
381
- * report its own target unchanged, because the read would beat the render.
382
- */
383
- async settle() {
384
- await new Promise((resolve) => {
385
- let timer;
386
- // No re-entry guard: whichever side loses the race calls this again, and both of its
387
- // effects are already idempotent — clearing an elapsed timer is a no-op, and a second
388
- // `resolve` on a settled promise is ignored. A flag here would only be a branch nothing
389
- // can distinguish.
390
- const finish = () => {
391
- if (timer !== undefined)
392
- clearTimeout(timer);
393
- resolve();
394
- };
395
- const raf = globalThis.requestAnimationFrame;
396
- // With no animation frames to wait for, one macrotask is the whole mechanism, and waiting
397
- // the full ceiling on every write would be pure latency.
398
- timer = setTimeout(finish, typeof raf === 'function' ? SETTLE_TIMEOUT_MS : 0);
399
- if (typeof raf === 'function')
400
- raf(() => raf(finish));
401
- });
402
- },
403
- async highlight(handle, durationMs) {
404
- const element = find(handle);
405
- // Same refusal as `click()`, for the same reason: an SVG (or other non-HTMLElement) target
406
- // has no `.style` to draw an outline on, so silently returning here drew nothing while
407
- // `page-executor.ts` unconditionally reports the highlight as successful — a guaranteed
408
- // false positive with no way for the caller to tell. Refusing makes that visible instead.
409
- if (!(element instanceof HTMLElement))
410
- throw new Error(`"${handle}" is not highlightable`);
411
- const active = highlights.get(handle);
412
- if (active !== undefined)
413
- clearTimeout(active.timer);
414
- // Reuse the first capture on a restart; see the map's own doc for what re-reading would cost.
415
- const restore = active?.restore ?? {
416
- outline: element.style.outline,
417
- outlineOffset: element.style.outlineOffset,
418
- borderRadius: element.style.borderRadius,
419
- };
420
- Object.assign(element.style, HIGHLIGHT_STYLE);
421
- element.scrollIntoView({ behavior: 'smooth', block: 'center' });
422
- const timer = setTimeout(() => {
423
- // Restore what was there rather than clearing: the element may have had its own outline.
424
- Object.assign(element.style, restore);
425
- highlights.delete(handle);
426
- }, durationMs);
427
- highlights.set(handle, { timer, restore });
428
- },
429
- async scrollTo(handle) {
430
- find(handle).scrollIntoView({ behavior: 'smooth', block: 'center' });
431
- },
432
- async click(handle) {
433
- const control = controlOf(find(handle));
434
- if (!(control instanceof HTMLElement))
435
- throw new Error(`"${handle}" is not clickable`);
436
- control.click();
437
- },
438
- async fill(handle, text) {
439
- const control = controlOf(find(handle));
440
- if (isEditableRegion(control)) {
441
- // A contenteditable region has no `value`; its content *is* its children, and a real
442
- // editor owns that subtree. See `fillEditableRegion` for why this drives it through
443
- // selection + `execCommand` rather than replacing `textContent` directly.
444
- fillEditableRegion(control, text);
445
- return;
446
- }
447
- if (!(control instanceof HTMLInputElement) && !(control instanceof HTMLTextAreaElement)) {
448
- throw new Error(`"${handle}" is not a fillable field`);
449
- }
450
- // Assign through the prototype setter, then dispatch: React tracks the previous value on
451
- // the node and ignores an input event whose value it believes it already has, so a plain
452
- // `control.value = text` updates the DOM but leaves React state stale.
453
- const prototype = control instanceof HTMLInputElement
454
- ? HTMLInputElement.prototype
455
- : HTMLTextAreaElement.prototype;
456
- const setter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
457
- if (setter)
458
- setter.call(control, text);
459
- else
460
- control.value = text;
461
- control.dispatchEvent(new Event('input', { bubbles: true }));
462
- control.dispatchEvent(new Event('change', { bubbles: true }));
463
- },
464
- async selectOption(handle, option, selected = true) {
465
- const control = controlOf(find(handle));
466
- if (!(control instanceof HTMLSelectElement)) {
467
- throw new Error(`"${handle}" is not a dropdown`);
468
- }
469
- // A disabled control is not one a user could touch, so writing to it is a way around the
470
- // page's own rule and the resulting selection cannot be submitted. The per-`<option>` filter
471
- // below reads as if it enforced that, but only ever looked at each option — a
472
- // `<select disabled>` (and a `<select>` inside a `<fieldset disabled>`, which `:disabled`
473
- // also covers and `.disabled` does not) was written to happily, and the executor then
474
- // reported `targetChanged: true` because the value really had changed.
475
- //
476
- // Checked before the option lookup on purpose: the refusal is about the control, so it must
477
- // not depend on the caller having named a real option, and the "Available: ..." message must
478
- // not enumerate a disabled control's options.
479
- if (control.matches(':disabled')) {
480
- throw new Error(`"${handle}" is disabled, so its selection cannot be changed — `
481
- + 'a user cannot change it either, and its value would not be submitted');
482
- }
483
- // Visible text first, then value: the text is what a caller saw in `find_elements`, and
484
- // matching value first would make an option whose value collides with another's label
485
- // resolve to the wrong entry.
486
- // A disabled option is not selectable by the user either, for the same reason.
487
- const options = Array.from(control.options).filter((entry) => !entry.disabled);
488
- const match = options.find((entry) => entry.text.trim() === option)
489
- ?? options.find((entry) => entry.value === option);
490
- if (match === undefined) {
491
- // Every other piece of page-authored text this system hands to a model goes through
492
- // normalizeAgentLabel first — bounded, control- and bidi-stripped. `available` is exactly
493
- // that: the page's own <option> texts, read verbatim with no length bound of their own.
494
- // The caller's `option` is sanitized too, for the same reason page.navigate's caller-
495
- // supplied `page` now is — an unbounded value reaching a thrown message a model reads is
496
- // the same shape of problem regardless of which side authored it. `handle` needs no
497
- // treatment: it is already validated ASCII, length-capped by isValidElementHandle before
498
- // this driver is ever called.
499
- const safeOption = normalizeAgentLabel(option).text;
500
- const available = options.map((entry) => normalizeAgentLabel(entry.text.trim()).text)
501
- .filter((text) => text.length > 0);
502
- throw new Error(`"${safeOption}" is not an option of "${handle}". Available: ${available.length > 0 ? available.join(', ') : '(none)'}`);
503
- }
504
- if (control.multiple) {
505
- // A multi-select accumulates by default. Assigning `value` would silently clear every
506
- // prior choice, so a caller building up a selection would end with only its last call's
507
- // option — and setting `.selected` directly is also how a caller narrows one back off
508
- // without disturbing the rest.
509
- match.selected = selected;
510
- }
511
- else if (!selected) {
512
- // A single-select's one option IS its value; there is no "off" state to put it in. Silently
513
- // ignoring this would tell the caller a deselect happened when nothing changed, and clearing
514
- // the control would invent a third state (no selection) that a native <select> cannot hold.
515
- throw new Error(`"${handle}" is a single-select; its choice cannot be removed, only replaced — `
516
- + `select a different option instead of deselecting "${normalizeAgentLabel(option).text}"`);
517
- }
518
- else {
519
- // Same prototype-setter dance as `fill`, for the same reason: React tracks the previous
520
- // value on the node and ignores a change event whose value it believes it already has.
521
- const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
522
- if (setter)
523
- setter.call(control, match.value);
524
- else
525
- control.value = match.value;
526
- }
527
- control.dispatchEvent(new Event('input', { bubbles: true }));
528
- control.dispatchEvent(new Event('change', { bubbles: true }));
529
- },
530
- async navigate(page) {
531
- // `hasOwnProperty`, not `pages[page]`: a plain object literal inherits everything
532
- // `Object.prototype` contributes, so a bare lookup answered `navigate("constructor")` with
533
- // the `Object` constructor — a function the host never published, which this then called,
534
- // reporting a navigation that never happened. `navigate("__proto__")` was worse in a
535
- // different way: it resolved a non-callable object and failed with "go is not a function"
536
- // instead of the refusal this check exists to produce. `listPages` already reports only own
537
- // keys (`Object.keys`), so this is what makes the two agree.
538
- const published = Object.prototype.hasOwnProperty.call(pages, page);
539
- // The executor already checked the allowlist and sanitizes this same message before this
540
- // driver-level check would ever be reached through it; this is the belt on the braces. But
541
- // `PageDriver` is a public interface — a caller that reaches this method directly, bypassing
542
- // the executor entirely, hands `page` in unsanitized, so the same bound-and-strip treatment
543
- // applies here too rather than assuming the only caller is the one that already checked.
544
- if (!published)
545
- throw new Error(`"${normalizeAgentLabel(page).text}" is not a published page`);
546
- pages[page].navigate();
547
- },
548
- };
549
- }
550
- /**
551
- * Writes into a contenteditable region the way a real user typing would, rather than by replacing
552
- * its DOM directly.
553
- *
554
- * A framework rich-text editor (Lexical, ProseMirror, Quill, CodeMirror) owns its subtree: it
555
- * keeps its own model and reconciles the DOM to match it, so a direct `textContent =` write is
556
- * either invisible to that model or gets reverted on the editor's next render — and it always
557
- * destroys whatever node structure the editor had built (a mention's `<span>`, a paragraph's `<p>`)
558
- * in favour of one bare text node. Selecting the existing contents and issuing a real editing
559
- * command instead is what Playwright's own `fill` does against a contenteditable, for the same
560
- * reason: `execCommand('insertText'|'delete')` is the platform's own command, so the browser fires
561
- * `beforeinput`/`input` itself with the correct `inputType` — the exact signal these editors
562
- * listen for — rather than this driver guessing at a contract it does not own by hand-rolling one.
563
- *
564
- * jsdom implements neither a real editing host nor `execCommand`, so every unit test here exercises
565
- * the fallback: the previous `textContent` + synthetic `input` behaviour, kept for exactly that
566
- * case and for a real browser that reports the command did not run.
567
- *
568
- * Verified 2026-07-26 in Chromium against real Lexical 0.36.2 — the editor
569
- * `packages/ui/src/features/rich-text-input/` is built on — driving both paths at one editor each
570
- * and reading back Lexical's own `EditorState`, not the DOM. The `textContent` write left the
571
- * model at its seed value and was reverted in the DOM too; the `execCommand` path landed in the
572
- * model. That is the whole reason this function exists, and no unit test in this repo can show it.
573
- */
574
- function fillEditableRegion(control, text) {
575
- control.focus();
576
- const selection = globalThis.getSelection?.();
577
- if (selection) {
578
- const range = control.ownerDocument.createRange();
579
- range.selectNodeContents(control);
580
- selection.removeAllRanges();
581
- selection.addRange(range);
582
- }
583
- const execCommand = control.ownerDocument.execCommand?.bind(control.ownerDocument);
584
- if (typeof execCommand === 'function') {
585
- // An empty replacement has no text to insert; `delete` removes the just-selected contents,
586
- // which `insertText` with an empty string is not guaranteed to do in every engine.
587
- const ran = text.length > 0 ? execCommand('insertText', false, text) : execCommand('delete');
588
- if (ran)
589
- return;
590
- }
591
- control.textContent = text;
592
- // Editors listen for `input`, not `change`; `change` is a form-control event and firing it here
593
- // would be inventing an event the platform never sends for this element.
594
- control.dispatchEvent(new Event('input', { bubbles: true }));
595
- }
596
- /** Reads the current page's `data-agent-page`, for hosts that tag the body. */
597
- export function currentAgentPage(root) {
598
- const tagged = root.querySelector(`[${AGENT_PAGE_ATTRIBUTE}]`);
599
- return tagged?.getAttribute(AGENT_PAGE_ATTRIBUTE) ?? undefined;
600
- }
601
- //# sourceMappingURL=dom-page-driver.js.map
@@ -1,17 +0,0 @@
1
- /**
2
- * @jini-ai/agentic/dom — the browser half of agent control.
3
- *
4
- * The one `PageDriver` (see `@jini-ai/agentic`'s `page-driver.ts`) that reads and writes a real DOM
5
- * subtree. Split into its own entry point, compiled under its own `DOM`-lib `tsconfig.dom.json`,
6
- * so the rest of this package can stay provably DOM-free — see source-map.md's "The DOM split".
7
- *
8
- * `@jini-ai/chat-react` is this package's one in-repo consumer today.
9
- */
10
- export { createDomPageDriver, currentAgentPage, type DomPageDriverOptions, type DomPageDriverPage, } from './dom-page-driver.js';
11
- /**
12
- * WebMCP feature detection — moved 2026-07-26 from `@jini-ai/ui`'s `agent-tools/model-context.ts`
13
- * (plan §8 step 6). See `model-context.ts`'s module doc for why it lives here rather than
14
- * alongside `../webmcp.ts`'s DOM-free projection.
15
- */
16
- export { getAgentModelContext, type AgentModelContextLike, type AgentModelContextToolRegistration, } from './model-context.js';
17
- //# sourceMappingURL=index.d.ts.map
package/dist/dom/index.js DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * @jini-ai/agentic/dom — the browser half of agent control.
3
- *
4
- * The one `PageDriver` (see `@jini-ai/agentic`'s `page-driver.ts`) that reads and writes a real DOM
5
- * subtree. Split into its own entry point, compiled under its own `DOM`-lib `tsconfig.dom.json`,
6
- * so the rest of this package can stay provably DOM-free — see source-map.md's "The DOM split".
7
- *
8
- * `@jini-ai/chat-react` is this package's one in-repo consumer today.
9
- */
10
- export { createDomPageDriver, currentAgentPage, } from './dom-page-driver.js';
11
- /**
12
- * WebMCP feature detection — moved 2026-07-26 from `@jini-ai/ui`'s `agent-tools/model-context.ts`
13
- * (plan §8 step 6). See `model-context.ts`'s module doc for why it lives here rather than
14
- * alongside `../webmcp.ts`'s DOM-free projection.
15
- */
16
- export { getAgentModelContext, } from './model-context.js';
17
- //# sourceMappingURL=index.js.map