appilot 0.0.1 → 0.1.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/README.md +101 -7
- package/dist/dom/surfaces.d.ts +81 -0
- package/dist/dom/surfaces.js +91 -0
- package/dist/dom/uniqueSelector.d.ts +23 -0
- package/dist/dom/uniqueSelector.js +104 -0
- package/dist/domResponder.d.ts +38 -0
- package/dist/domResponder.js +344 -0
- package/dist/focusedSessions/pageSdk.d.ts +107 -0
- package/dist/focusedSessions/pageSdk.js +238 -0
- package/dist/httpFetch.d.ts +52 -0
- package/dist/httpFetch.js +137 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +24 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +18 -0
- package/dist/sessions/types.d.ts +140 -0
- package/dist/sessions/types.js +12 -0
- package/dist/webmcp/registry.d.ts +75 -0
- package/dist/webmcp/registry.js +152 -0
- package/dist/webmcp/sdk.d.ts +38 -0
- package/dist/webmcp/sdk.js +81 -0
- package/dist/widget/boot.d.ts +72 -0
- package/dist/widget/boot.js +264 -0
- package/package.json +51 -16
package/README.md
CHANGED
|
@@ -1,12 +1,106 @@
|
|
|
1
1
|
# appilot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Appilot browser SDK. Boot the assistant, let the agent act through your own
|
|
4
|
+
UI, and run mission-scoped sessions.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
act on their behalf, shipping as a browser extension or an embeddable widget.
|
|
7
|
-
The developer SDK (widget boot, client-registered actions aligned with WebMCP,
|
|
8
|
-
focused sessions) will publish here.
|
|
6
|
+
Browser-only, React-free, zero runtime dependencies.
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
```bash
|
|
9
|
+
npm install appilot
|
|
10
|
+
```
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Pair it with [`appilot-server`](https://www.npmjs.com/package/appilot-server),
|
|
13
|
+
which runs the identity relay in your backend. The assistant serves identified
|
|
14
|
+
users only, and the credential that mints that identity must never reach a
|
|
15
|
+
browser.
|
|
16
|
+
|
|
17
|
+
## Boot the assistant
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { bootAppilotWidget } from 'appilot';
|
|
21
|
+
|
|
22
|
+
bootAppilotWidget({
|
|
23
|
+
widgetScriptUrl: 'https://cdn.appilot.space/widget/v1/appilot.esm.js',
|
|
24
|
+
widgetKey: 'wk_live_…', // publishable; optional on a registered domain
|
|
25
|
+
tokenEndpoint: '/api/widget/token', // your relay
|
|
26
|
+
brandName: 'Acme', // names the assistant, not your app
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
On a registered production domain the tenant is resolved from the page's domain,
|
|
31
|
+
so the key is optional. It is required on localhost, where no domain resolves.
|
|
32
|
+
|
|
33
|
+
## Let the agent act in your app
|
|
34
|
+
|
|
35
|
+
`registerTool` declares a capability your page performs, in the user's own
|
|
36
|
+
session: navigate the SPA, open a modal, or a mutation your page already knows
|
|
37
|
+
how to do safely. It is WebMCP-aligned, so where the browser exposes a native
|
|
38
|
+
`navigator.modelContext` the same call registers there too.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { registerTool } from 'appilot';
|
|
42
|
+
|
|
43
|
+
const handle = registerTool({
|
|
44
|
+
name: 'open_booking',
|
|
45
|
+
description: 'Open a booking by id so the user can see it.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: { booking_id: { type: 'string' } },
|
|
49
|
+
required: ['booking_id'],
|
|
50
|
+
},
|
|
51
|
+
annotations: { readOnlyHint: true }, // absent or false means mutating,
|
|
52
|
+
// and the agent confirms first
|
|
53
|
+
async execute({ booking_id }) {
|
|
54
|
+
navigate(`/bookings/${booking_id}`);
|
|
55
|
+
return { content: [{ type: 'text', text: `Opened booking ${booking_id}.` }] };
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
handle.unregister(); // on route change
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Backend operations belong in an HTTP-proxy tool configured server-side instead,
|
|
63
|
+
where the credential stays out of the page.
|
|
64
|
+
|
|
65
|
+
## Mission-scoped conversations
|
|
66
|
+
|
|
67
|
+
When the conversation *is* the unit of work (a role-play, a tutoring dialogue, an
|
|
68
|
+
assessment), your app opens a Focused Session against a template you authored and
|
|
69
|
+
receives a structured outcome.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { startFocusedSession, resumeFocusedSession } from 'appilot';
|
|
73
|
+
|
|
74
|
+
const handle = await startFocusedSession({
|
|
75
|
+
template_id: 'role-play-activity',
|
|
76
|
+
variables: { role: 'unhappy customer' },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
handle.onOutcome(outcome => saveEvidence(outcome));
|
|
80
|
+
handle.onEnded(({ status }) => showEndedState(status)); // no outcome is coming
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`startFocusedSession` **rejects** when the template's mandatory grounding is
|
|
84
|
+
unavailable, and no session is created. That is the contract, not an edge case:
|
|
85
|
+
your page renders its own error state instead of a model narrating a backend
|
|
86
|
+
problem at your user.
|
|
87
|
+
|
|
88
|
+
After a full page reload, `resumeFocusedSession()` re-attaches to the session
|
|
89
|
+
this tab started, or resolves to `null` when there is nothing running. Ownership
|
|
90
|
+
is checked server-side.
|
|
91
|
+
|
|
92
|
+
Outcomes reach your page through the user's browser, so they are client-trusted.
|
|
93
|
+
Before treating one as evidence, re-read it server to server.
|
|
94
|
+
|
|
95
|
+
## Also exported
|
|
96
|
+
|
|
97
|
+
`isAppilotSurface(element)` lets your own modal or focus trap recognize Appilot's
|
|
98
|
+
chrome and not dismiss itself when the user interacts with the assistant.
|
|
99
|
+
|
|
100
|
+
## Docs
|
|
101
|
+
|
|
102
|
+
<https://docs.appilot.space>
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
ISC
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Appilot surface contract: one attribute that identifies every piece of
|
|
3
|
+
* Appilot chrome living in the host page's top-level DOM.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* `docs/architecture/in-page-overlays.md` settled WHERE our chrome mounts
|
|
8
|
+
* (always under `document.documentElement`). It says nothing about what happens
|
|
9
|
+
* when the host app opens a modal layer, and that is where a co-resident
|
|
10
|
+
* assistant breaks.
|
|
11
|
+
*
|
|
12
|
+
* Every modal library (Radix, Headless UI, MUI, Ant, Vaadin, the native
|
|
13
|
+
* `<dialog>`) implements two behaviours that are hostile to us, because to them
|
|
14
|
+
* we are indistinguishable from the page underneath:
|
|
15
|
+
*
|
|
16
|
+
* 1. Dismiss-on-outside-interaction. A `pointerdown` or `focusin` outside the
|
|
17
|
+
* modal's own subtree closes it. Our chrome is ALWAYS outside, so clicking
|
|
18
|
+
* the Appilot launcher closes the user's half-filled form.
|
|
19
|
+
* 2. Focus trapping. Focus that lands outside the modal is yanked back. Even
|
|
20
|
+
* with (1) fixed, the user cannot type in our composer.
|
|
21
|
+
*
|
|
22
|
+
* A host app can opt out of both, but only if it can answer "is this element
|
|
23
|
+
* Appilot's?" cheaply and stably. Before this contract it had to guess between
|
|
24
|
+
* `#appilot-panel-container`, `#appilot-toggle`, `<appilot-assistant>` and
|
|
25
|
+
* `[data-appilot-widget]` (which is stamped on `<html>` itself, so a naive
|
|
26
|
+
* `closest()` matches the whole document). That is a trap, and hosts fell into
|
|
27
|
+
* it. Now there is one selector: `[data-appilot-surface]`.
|
|
28
|
+
*
|
|
29
|
+
* See `docs/architecture/host-modal-coexistence.md` for the host-side recipes.
|
|
30
|
+
*/
|
|
31
|
+
/** The attribute every Appilot-injected top-level element carries. */
|
|
32
|
+
export declare const APPILOT_SURFACE_ATTR = "data-appilot-surface";
|
|
33
|
+
/**
|
|
34
|
+
* What a given piece of chrome is. Hosts may branch on the value (for example,
|
|
35
|
+
* allowing interaction with the panel but still dismissing on a stray click on
|
|
36
|
+
* a transient gesture shield), but the common case only tests for presence.
|
|
37
|
+
*/
|
|
38
|
+
export type AppilotSurfaceKind =
|
|
39
|
+
/** The floating launcher button (extension and widget bubble layout). */
|
|
40
|
+
'launcher'
|
|
41
|
+
/** The assistant panel itself, including its Shadow DOM host. */
|
|
42
|
+
| 'panel'
|
|
43
|
+
/** Transient drag/resize gesture shields. */
|
|
44
|
+
| 'shield'
|
|
45
|
+
/** The coach-mark bubble and its dim layer. */
|
|
46
|
+
| 'coach'
|
|
47
|
+
/** The cyan control-locate comet and impact ring. */
|
|
48
|
+
| 'locate'
|
|
49
|
+
/** The in-context editor's element-picker capture surface. */
|
|
50
|
+
| 'picker';
|
|
51
|
+
/**
|
|
52
|
+
* Stamp an element as Appilot chrome. Call this on EVERY element mounted into
|
|
53
|
+
* the host page's top-level DOM, at creation time, before it is appended.
|
|
54
|
+
*
|
|
55
|
+
* Internal to Appilot's own runtimes (widget, extension, shared overlays).
|
|
56
|
+
* Host apps consume the contract through {@link isAppilotSurface}.
|
|
57
|
+
*/
|
|
58
|
+
export declare function markAppilotSurface<T extends Element>(element: T, kind: AppilotSurfaceKind): T;
|
|
59
|
+
/**
|
|
60
|
+
* Does this node belong to Appilot's chrome?
|
|
61
|
+
*
|
|
62
|
+
* Pass the target of a pointer or focus event. Returns true for the chrome
|
|
63
|
+
* element itself and for anything nested inside it, including across a Shadow
|
|
64
|
+
* DOM boundary: our panel lives in a shadow root, so a real click inside the
|
|
65
|
+
* composer retargets at the document level to the shadow HOST, which is the
|
|
66
|
+
* element that carries the attribute.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately tolerant about its input (`EventTarget | null`, text nodes,
|
|
69
|
+
* detached nodes) because callers pass `event.target` straight through and a
|
|
70
|
+
* dismissal guard must never throw.
|
|
71
|
+
*
|
|
72
|
+
* @example Radix / shadcn dialog
|
|
73
|
+
* ```tsx
|
|
74
|
+
* <DialogContent
|
|
75
|
+
* onInteractOutside={(event) => {
|
|
76
|
+
* if (isAppilotSurface(event.target)) event.preventDefault();
|
|
77
|
+
* }}
|
|
78
|
+
* >
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function isAppilotSurface(target: EventTarget | Node | null | undefined): boolean;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Appilot surface contract: one attribute that identifies every piece of
|
|
3
|
+
* Appilot chrome living in the host page's top-level DOM.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* `docs/architecture/in-page-overlays.md` settled WHERE our chrome mounts
|
|
8
|
+
* (always under `document.documentElement`). It says nothing about what happens
|
|
9
|
+
* when the host app opens a modal layer, and that is where a co-resident
|
|
10
|
+
* assistant breaks.
|
|
11
|
+
*
|
|
12
|
+
* Every modal library (Radix, Headless UI, MUI, Ant, Vaadin, the native
|
|
13
|
+
* `<dialog>`) implements two behaviours that are hostile to us, because to them
|
|
14
|
+
* we are indistinguishable from the page underneath:
|
|
15
|
+
*
|
|
16
|
+
* 1. Dismiss-on-outside-interaction. A `pointerdown` or `focusin` outside the
|
|
17
|
+
* modal's own subtree closes it. Our chrome is ALWAYS outside, so clicking
|
|
18
|
+
* the Appilot launcher closes the user's half-filled form.
|
|
19
|
+
* 2. Focus trapping. Focus that lands outside the modal is yanked back. Even
|
|
20
|
+
* with (1) fixed, the user cannot type in our composer.
|
|
21
|
+
*
|
|
22
|
+
* A host app can opt out of both, but only if it can answer "is this element
|
|
23
|
+
* Appilot's?" cheaply and stably. Before this contract it had to guess between
|
|
24
|
+
* `#appilot-panel-container`, `#appilot-toggle`, `<appilot-assistant>` and
|
|
25
|
+
* `[data-appilot-widget]` (which is stamped on `<html>` itself, so a naive
|
|
26
|
+
* `closest()` matches the whole document). That is a trap, and hosts fell into
|
|
27
|
+
* it. Now there is one selector: `[data-appilot-surface]`.
|
|
28
|
+
*
|
|
29
|
+
* See `docs/architecture/host-modal-coexistence.md` for the host-side recipes.
|
|
30
|
+
*/
|
|
31
|
+
/** The attribute every Appilot-injected top-level element carries. */
|
|
32
|
+
export const APPILOT_SURFACE_ATTR = 'data-appilot-surface';
|
|
33
|
+
/**
|
|
34
|
+
* Stamp an element as Appilot chrome. Call this on EVERY element mounted into
|
|
35
|
+
* the host page's top-level DOM, at creation time, before it is appended.
|
|
36
|
+
*
|
|
37
|
+
* Internal to Appilot's own runtimes (widget, extension, shared overlays).
|
|
38
|
+
* Host apps consume the contract through {@link isAppilotSurface}.
|
|
39
|
+
*/
|
|
40
|
+
export function markAppilotSurface(element, kind) {
|
|
41
|
+
element.setAttribute(APPILOT_SURFACE_ATTR, kind);
|
|
42
|
+
return element;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Does this node belong to Appilot's chrome?
|
|
46
|
+
*
|
|
47
|
+
* Pass the target of a pointer or focus event. Returns true for the chrome
|
|
48
|
+
* element itself and for anything nested inside it, including across a Shadow
|
|
49
|
+
* DOM boundary: our panel lives in a shadow root, so a real click inside the
|
|
50
|
+
* composer retargets at the document level to the shadow HOST, which is the
|
|
51
|
+
* element that carries the attribute.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately tolerant about its input (`EventTarget | null`, text nodes,
|
|
54
|
+
* detached nodes) because callers pass `event.target` straight through and a
|
|
55
|
+
* dismissal guard must never throw.
|
|
56
|
+
*
|
|
57
|
+
* @example Radix / shadcn dialog
|
|
58
|
+
* ```tsx
|
|
59
|
+
* <DialogContent
|
|
60
|
+
* onInteractOutside={(event) => {
|
|
61
|
+
* if (isAppilotSurface(event.target)) event.preventDefault();
|
|
62
|
+
* }}
|
|
63
|
+
* >
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function isAppilotSurface(target) {
|
|
67
|
+
const element = toElement(target);
|
|
68
|
+
if (!element)
|
|
69
|
+
return false;
|
|
70
|
+
// Walk the composed tree by hand rather than trusting `closest()`: `closest()`
|
|
71
|
+
// stops at a shadow root, and an element inside a nested shadow tree would
|
|
72
|
+
// report no match even though its host is ours.
|
|
73
|
+
let node = element;
|
|
74
|
+
while (node) {
|
|
75
|
+
if (node instanceof Element && node.hasAttribute(APPILOT_SURFACE_ATTR))
|
|
76
|
+
return true;
|
|
77
|
+
const parent = node.parentNode;
|
|
78
|
+
node = parent instanceof ShadowRoot ? parent.host : parent;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
function toElement(target) {
|
|
83
|
+
if (!target)
|
|
84
|
+
return null;
|
|
85
|
+
if (target instanceof Element)
|
|
86
|
+
return target;
|
|
87
|
+
// Text nodes are legitimate event targets in some engines; climb to the element.
|
|
88
|
+
if (target instanceof Node)
|
|
89
|
+
return target.parentElement;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute a unique-enough CSS selector for a live element.
|
|
3
|
+
*
|
|
4
|
+
* Shared by:
|
|
5
|
+
* - the in-page DOM responder (`agents/domResponder.ts`), which stamps a
|
|
6
|
+
* `selector` onto every element hit at observe time so the backend can mint
|
|
7
|
+
* a server-trusted `element_ref` -> `dom_target` (Universal Mode Stage 1);
|
|
8
|
+
* - the action-plan runtime's DOM-target resolver
|
|
9
|
+
* (`components/actionPlans/utils/resolveDomTarget.ts`), which re-derives a
|
|
10
|
+
* fresh selector when a page step's original selector goes stale after a
|
|
11
|
+
* re-render (Stage 2 fallback).
|
|
12
|
+
*
|
|
13
|
+
* Strategy, cheapest-first:
|
|
14
|
+
* 1. A stable `#id` when the id is unique and CSS-safe.
|
|
15
|
+
* 2. Otherwise walk up to the nearest id-anchored ancestor (or the root),
|
|
16
|
+
* appending `tag:nth-of-type(k)` segments, stopping as soon as the partial
|
|
17
|
+
* selector matches exactly one element.
|
|
18
|
+
* Returns '' when no selector can be derived (the element is then
|
|
19
|
+
* unreferenceable; callers fall back to role/name or pause).
|
|
20
|
+
*
|
|
21
|
+
* This NEVER trusts model input: it reads only the live DOM.
|
|
22
|
+
*/
|
|
23
|
+
export declare function computeUniqueSelector(el: Element): string;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute a unique-enough CSS selector for a live element.
|
|
3
|
+
*
|
|
4
|
+
* Shared by:
|
|
5
|
+
* - the in-page DOM responder (`agents/domResponder.ts`), which stamps a
|
|
6
|
+
* `selector` onto every element hit at observe time so the backend can mint
|
|
7
|
+
* a server-trusted `element_ref` -> `dom_target` (Universal Mode Stage 1);
|
|
8
|
+
* - the action-plan runtime's DOM-target resolver
|
|
9
|
+
* (`components/actionPlans/utils/resolveDomTarget.ts`), which re-derives a
|
|
10
|
+
* fresh selector when a page step's original selector goes stale after a
|
|
11
|
+
* re-render (Stage 2 fallback).
|
|
12
|
+
*
|
|
13
|
+
* Strategy, cheapest-first:
|
|
14
|
+
* 1. A stable `#id` when the id is unique and CSS-safe.
|
|
15
|
+
* 2. Otherwise walk up to the nearest id-anchored ancestor (or the root),
|
|
16
|
+
* appending `tag:nth-of-type(k)` segments, stopping as soon as the partial
|
|
17
|
+
* selector matches exactly one element.
|
|
18
|
+
* Returns '' when no selector can be derived (the element is then
|
|
19
|
+
* unreferenceable; callers fall back to role/name or pause).
|
|
20
|
+
*
|
|
21
|
+
* This NEVER trusts model input: it reads only the live DOM.
|
|
22
|
+
*/
|
|
23
|
+
function cssEscape(value) {
|
|
24
|
+
const esc = window.CSS?.escape;
|
|
25
|
+
if (typeof esc === 'function')
|
|
26
|
+
return esc(value);
|
|
27
|
+
// Minimal fallback: escape characters that break attribute/id selectors.
|
|
28
|
+
return value.replace(/([^a-zA-Z0-9_-])/g, '\\$1');
|
|
29
|
+
}
|
|
30
|
+
function isUniqueSelector(selector) {
|
|
31
|
+
try {
|
|
32
|
+
return document.querySelectorAll(selector).length === 1;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function nthOfTypeSegment(el) {
|
|
39
|
+
const tag = el.tagName.toLowerCase();
|
|
40
|
+
const parent = el.parentElement;
|
|
41
|
+
if (!parent)
|
|
42
|
+
return tag;
|
|
43
|
+
const sameTag = Array.from(parent.children).filter(c => c.tagName === el.tagName);
|
|
44
|
+
if (sameTag.length === 1)
|
|
45
|
+
return tag;
|
|
46
|
+
const index = sameTag.indexOf(el) + 1;
|
|
47
|
+
return `${tag}:nth-of-type(${index})`;
|
|
48
|
+
}
|
|
49
|
+
// Per-element selector cache. The walk below issues up to 12
|
|
50
|
+
// `querySelectorAll(candidate)` uniqueness probes per element; the action-plan
|
|
51
|
+
// DOM-target heal re-derives a selector for the same live element on repeated
|
|
52
|
+
// ticks, so caching collapses that to a SINGLE validating `querySelector` on
|
|
53
|
+
// the cache hit. The cache is invalidated structurally: a cached selector is
|
|
54
|
+
// only returned when it still resolves to the SAME element (a re-render that
|
|
55
|
+
// moves the element produces a miss and a fresh compute). A WeakMap keyed by
|
|
56
|
+
// the element never leaks across detached nodes.
|
|
57
|
+
const selectorCache = new WeakMap();
|
|
58
|
+
export function computeUniqueSelector(el) {
|
|
59
|
+
if (!(el instanceof Element))
|
|
60
|
+
return '';
|
|
61
|
+
const cached = selectorCache.get(el);
|
|
62
|
+
if (cached) {
|
|
63
|
+
try {
|
|
64
|
+
if (document.querySelector(cached) === el)
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* cached selector went malformed against a changed DOM: recompute */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const result = computeUniqueSelectorUncached(el);
|
|
72
|
+
if (result)
|
|
73
|
+
selectorCache.set(el, result);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function computeUniqueSelectorUncached(el) {
|
|
77
|
+
const id = el.getAttribute('id');
|
|
78
|
+
if (id && /^[A-Za-z][\w-]*$/.test(id)) {
|
|
79
|
+
const byId = `#${cssEscape(id)}`;
|
|
80
|
+
if (isUniqueSelector(byId))
|
|
81
|
+
return byId;
|
|
82
|
+
}
|
|
83
|
+
const segments = [];
|
|
84
|
+
let node = el;
|
|
85
|
+
let depth = 0;
|
|
86
|
+
while (node && node.nodeType === 1 && depth < 12) {
|
|
87
|
+
const nodeId = node.getAttribute('id');
|
|
88
|
+
if (nodeId && /^[A-Za-z][\w-]*$/.test(nodeId) && isUniqueSelector(`#${cssEscape(nodeId)}`)) {
|
|
89
|
+
segments.unshift(`#${cssEscape(nodeId)}`);
|
|
90
|
+
const candidate = segments.join(' > ');
|
|
91
|
+
if (isUniqueSelector(candidate))
|
|
92
|
+
return candidate;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
segments.unshift(nthOfTypeSegment(node));
|
|
96
|
+
const candidate = segments.join(' > ');
|
|
97
|
+
if (isUniqueSelector(candidate))
|
|
98
|
+
return candidate;
|
|
99
|
+
node = node.parentElement;
|
|
100
|
+
depth++;
|
|
101
|
+
}
|
|
102
|
+
const full = segments.join(' > ');
|
|
103
|
+
return isUniqueSelector(full) ? full : '';
|
|
104
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-page DOM responder for the agent runtime.
|
|
3
|
+
*
|
|
4
|
+
* Implements the six Web-MCP-aligned DOM tools (plus the `http_fetch`
|
|
5
|
+
* HTTP-proxy tool) the agent calls via the agent_dom_request SSE round-trip.
|
|
6
|
+
* See docs/agent/dom-tools.md for the wire format.
|
|
7
|
+
*
|
|
8
|
+
* This module is the in-page side of the contract, shared by both surfaces:
|
|
9
|
+
* - Chrome extension: invoked from the content-script message listener
|
|
10
|
+
* (content.ts) when the background relays an agent_dom_request to the
|
|
11
|
+
* active tab.
|
|
12
|
+
* - Embeddable widget: invoked directly by the widget's AgentDomBridge
|
|
13
|
+
* (services/AgentDomBridge.ts), which intercepts agent_dom_request on the
|
|
14
|
+
* widget SSE stream and POSTs the result to /widget/agent/dom-response.
|
|
15
|
+
*
|
|
16
|
+
* Element-id correlation: each call to get_page_outline / find_elements /
|
|
17
|
+
* inspect_element returns ephemeral ids that subsequent calls (inspect /
|
|
18
|
+
* wait_for) can dereference. Ids are kept in `currentTurnElementMap`; the
|
|
19
|
+
* map is cleared on each NEW turn (`resetTurn` on the request, or
|
|
20
|
+
* `resetDomToolTurn()`).
|
|
21
|
+
*
|
|
22
|
+
* Authoring controls bridge: when find_elements / inspect_element hit an
|
|
23
|
+
* element matched by an authored Control's locator, the `matches_authored_control`
|
|
24
|
+
* field returns the Control's semantic_id. The Controls registry is provided
|
|
25
|
+
* per turn (controlsForTurn).
|
|
26
|
+
*/
|
|
27
|
+
/** Clear the per-turn element-id map. Call at the start of every new turn. */
|
|
28
|
+
export declare function resetDomToolTurn(): void;
|
|
29
|
+
export interface DomToolRequest {
|
|
30
|
+
tool: string;
|
|
31
|
+
args: Record<string, unknown>;
|
|
32
|
+
controlsForTurn?: Array<{
|
|
33
|
+
semantic_id: string;
|
|
34
|
+
locator: string;
|
|
35
|
+
}>;
|
|
36
|
+
resetTurn?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare function runDomTool(req: DomToolRequest): Promise<unknown>;
|