@classic-homes/page-agent-core 0.0.0-stage → 0.2.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 +169 -2
- package/dist/index.d.ts +599 -0
- package/dist/index.js +1547 -0
- package/dist/styles.css +335 -0
- package/package.json +50 -4
package/README.md
CHANGED
|
@@ -1,3 +1,170 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @classic-homes/page-agent-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework-agnostic page layer for an in-app AI agent: it lets the agent
|
|
4
|
+
**point at things on the host page** (outline, spotlight, a labelled ghost
|
|
5
|
+
cursor, scroll into view, switch a tab, go to another page) without ever
|
|
6
|
+
running model-generated selectors, HTML or code.
|
|
7
|
+
|
|
8
|
+
Most apps install the framework binding instead (it draws the effects):
|
|
9
|
+
`@classic-homes/page-agent-svelte`.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @classic-homes/page-agent-core
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
@import '@classic-homes/page-agent-core/styles.css';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
No runtime dependencies. ESM only (the registry, config and effect store are
|
|
20
|
+
module singletons). Safe to import during SSR.
|
|
21
|
+
|
|
22
|
+
## What is in here
|
|
23
|
+
|
|
24
|
+
- **Registry**: `registerAgentTarget({ el, id, label, kind })` →
|
|
25
|
+
`{ update, destroy }`, `getAgentTargets`, batched `onAgentTargetsChange`,
|
|
26
|
+
and `scanAgentAttributes` for markup targets (`data-agent-id`,
|
|
27
|
+
`data-agent-label`, `data-agent-kind`, `data-agent-description`, plus the
|
|
28
|
+
host's alias attributes such as `data-tour`).
|
|
29
|
+
- **Resolution**: `resolveTarget({ id } | { text, within? })`. By id
|
|
30
|
+
(registry, then markup), or by visible text (exact, then case-insensitive;
|
|
31
|
+
the smallest match, promoted to the button/tab/link carrying the same
|
|
32
|
+
text). Several matches return `ambiguous` with up to 8 candidates rather
|
|
33
|
+
than guessing.
|
|
34
|
+
- **Snapshot**: `listTargets()` (max 200, `{ id, label, kind, visible,
|
|
35
|
+
inViewport }`), `listTabs()` and `readPageText({ maxChars,
|
|
36
|
+
ignoreSelector })`.
|
|
37
|
+
- **Effects**: `highlight(ref, { style: 'outline' | 'spotlight', label,
|
|
38
|
+
durationMs })`, `point(ref, { label, durationMs })`, `scrollToTarget(ref)`,
|
|
39
|
+
`clearEffects()`, `getEffects()` / `onEffectsChange()` (safe for
|
|
40
|
+
`useSyncExternalStore`), and `visibilityOf(effect)` / `rectOf(effect)`
|
|
41
|
+
for drawing. Effects expire
|
|
42
|
+
(default 8 s, max 60 s); at most 8 at once, one pointer and one spotlight.
|
|
43
|
+
`attachPageAgent()` wires Escape and `popstate` to clear; call
|
|
44
|
+
`notifyNavigated()` after SPA navigation.
|
|
45
|
+
- **Executor**: `configurePageAgent({ navigate, aliasAttributes,
|
|
46
|
+
ignoreSelector })` and `executePageCall(call)`, the single entry point for
|
|
47
|
+
the model's tool calls. `PAGE_TOOL_NAMES` and `PAGE_TOOLS` (plain-JSON
|
|
48
|
+
input shapes) let the agent server mirror the contract exactly.
|
|
49
|
+
- **Visibility**: `measureVisibility(el)` says what of a target the user can
|
|
50
|
+
see: clipped to the viewport and its scroll boxes, cut where a sticky
|
|
51
|
+
header / footer / dock covers it (hit-tested with
|
|
52
|
+
`document.elementsFromPoint`, the layer skipped), and `covered` behind a
|
|
53
|
+
modal dialog or `inert`. Bindings draw only `visible` and hide (not
|
|
54
|
+
remove) effects while it is null. The maths is the pure
|
|
55
|
+
`computeVisibility({ rect, viewport, clips, behindModal, coverAt })`.
|
|
56
|
+
- **Collapsed tab rows**: `collapsedTabMenu(tab)` and the
|
|
57
|
+
`data-tablist-menu` DOM contract (below).
|
|
58
|
+
- **Geometry**: `unionRects`, `inflate`, `clampToViewport`, `intersectBoxes`,
|
|
59
|
+
`placeLabel` (with optional `bounds`), `pointerTarget` for the UI layer.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import {
|
|
63
|
+
attachPageAgent,
|
|
64
|
+
configurePageAgent,
|
|
65
|
+
executePageCall,
|
|
66
|
+
} from '@classic-homes/page-agent-core';
|
|
67
|
+
import { goto } from '$app/navigation';
|
|
68
|
+
|
|
69
|
+
configurePageAgent({ navigate: (path) => goto(path), aliasAttributes: ['data-tour'] });
|
|
70
|
+
const detach = attachPageAgent();
|
|
71
|
+
|
|
72
|
+
// From the chat's client-call hook:
|
|
73
|
+
const result = await executePageCall({
|
|
74
|
+
tool: 'highlight',
|
|
75
|
+
input: { target: { id: 'closing-date' } },
|
|
76
|
+
});
|
|
77
|
+
// → { status: 'ok', output: { effectId, target: { id, label }, style, scrolled, inViewport } }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<section data-agent-id="closing-date" data-agent-label="Closing date">…</section>
|
|
82
|
+
<div data-agent-ignore><!-- never read, never pointed at --></div>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tools
|
|
86
|
+
|
|
87
|
+
| Tool | Input | Does |
|
|
88
|
+
| ------------------ | ----------------------------------------- | --------------------------------------------------------- |
|
|
89
|
+
| `list_targets` | `{ kind?, limit? }` | Named targets and tabs on the page |
|
|
90
|
+
| `highlight` | `{ target, style?, label?, durationMs? }` | Outline or spotlight, scrolling into view first if needed |
|
|
91
|
+
| `point` | `{ target, label?, durationMs? }` | Moves the ghost cursor, scrolling into view if needed |
|
|
92
|
+
| `scroll_to` | `{ target }` | Scrolls into view (no-op if already fully visible) |
|
|
93
|
+
| `clear_highlights` | `{}` | Removes every effect |
|
|
94
|
+
| `select_tab` | `{ target }` | Clicks a `role="tab"`, `ok` once `aria-selected="true"` |
|
|
95
|
+
| `navigate` | `{ path }` | Calls the host's `navigate` with a same-origin path |
|
|
96
|
+
|
|
97
|
+
`target` is `{ id }` or `{ text, within? }`. Results are `{ status, message?,
|
|
98
|
+
output? }` with `status` one of `ok`, `not_found`, `ambiguous`, `hidden`,
|
|
99
|
+
`refused`, `error`.
|
|
100
|
+
|
|
101
|
+
`highlight`, `point` and `scroll_to` wait for their scroll to settle, scroll
|
|
102
|
+
once more if a sticky header or footer still covers the target, and measure
|
|
103
|
+
`inViewport` after that. `highlight` / `point` on a target the user cannot
|
|
104
|
+
see because something covers it return `hidden` with `output.reason`
|
|
105
|
+
`'behind_dialog'` or `'covered'`, and show nothing.
|
|
106
|
+
|
|
107
|
+
## Collapsed tab rows
|
|
108
|
+
|
|
109
|
+
A tab widget that swaps its row for a drop-down when it does not fit (theme
|
|
110
|
+
`Tabs`, `collapse="auto"`) keeps the `role="tab"` buttons in the DOM,
|
|
111
|
+
invisible and `inert`, and marks the row while collapsed:
|
|
112
|
+
|
|
113
|
+
```html
|
|
114
|
+
<div role="tablist" data-tablist-menu="rec-menu">
|
|
115
|
+
<button role="tab" aria-selected="false">Documents</button>
|
|
116
|
+
</div>
|
|
117
|
+
<button id="rec-menu">Overview ▾</button>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`select_tab` then still clicks the hidden tab (browsers run a script
|
|
121
|
+
`click()` on inert elements), confirms `aria-selected="true"` and answers
|
|
122
|
+
with `output.shownAs: 'menu'` and a `message` that the tabs are a drop-down.
|
|
123
|
+
`highlight` / `point` on such a tab go to the visible stand-in (the
|
|
124
|
+
attribute's id). `listTabs()` reports them `visible` (while the stand-in is)
|
|
125
|
+
with `shownAs: 'menu'`. A tab is never treated as collapsed without the
|
|
126
|
+
attribute, and nothing but `role="tab"` is ever clicked.
|
|
127
|
+
|
|
128
|
+
## Security
|
|
129
|
+
|
|
130
|
+
`executePageCall` treats its input as untrusted:
|
|
131
|
+
|
|
132
|
+
- Every field is checked by hand (type, length, enum, range); unknown tools,
|
|
133
|
+
unknown keys and malformed input return `refused`. It never throws.
|
|
134
|
+
- Targets are ids (compared verbatim with attribute values) or visible
|
|
135
|
+
text. No CSS selectors, HTML or code are accepted.
|
|
136
|
+
- Nothing is clicked except a non-disabled `role="tab"` element (also in a
|
|
137
|
+
collapsed row; never its drop-down or any `<select>`).
|
|
138
|
+
- `navigate` accepts only paths starting with a single `/` (no `//`,
|
|
139
|
+
backslashes, schemes, whitespace or control characters, also after
|
|
140
|
+
decoding) and goes through the host's handler; without one it is refused.
|
|
141
|
+
- `ok` is reported only once the effect is in place (the effect is in the
|
|
142
|
+
store and its target is not covered, the tab is selected, the host's
|
|
143
|
+
navigation resolved).
|
|
144
|
+
- `[data-agent-ignore]`, the host `ignoreSelector` and the layer's own UI
|
|
145
|
+
(`.chpa`) are invisible to resolution, snapshots and `readPageText`, which
|
|
146
|
+
also never reads password, hidden, card or one-time-code field values.
|
|
147
|
+
Treat page text as untrusted data on the server.
|
|
148
|
+
|
|
149
|
+
## Styles
|
|
150
|
+
|
|
151
|
+
`styles.css` holds the `.chpa-*` classes the bindings render: the
|
|
152
|
+
click-through layer (`.chpa-layer`), outlines (`.chpa-outline` +
|
|
153
|
+
`.chpa-tag`), the spotlight (`.chpa-spotlight`, dims with a spread shadow and
|
|
154
|
+
keeps the page clickable), the ghost cursor (`.chpa-pointer`,
|
|
155
|
+
`.chpa-pointer-icon`, `.chpa-pointer-label`), the "Agent is pointing ·
|
|
156
|
+
Dismiss" chip (`.chpa-chip`, `.chpa-chip-text`, `.chpa-chip-btn`, 44 px
|
|
157
|
+
target) and a live region (`.chpa-sr`). Override `--chpa-accent` (default
|
|
158
|
+
`#6d28d9`) and `--chpa-z` (default `2147482000`, below the Access
|
|
159
|
+
Inspector). The layer stays on top so targets inside dialogs can be pointed
|
|
160
|
+
at; covers are handled by drawing only the visible part (an outline cut by
|
|
161
|
+
a header carries `data-cut="top"` and is left open on that side). Dark mode follows `.dark` / `[data-theme='dark']`; reduced
|
|
162
|
+
motion drops the glide and pulse.
|
|
163
|
+
|
|
164
|
+
## Testing
|
|
165
|
+
|
|
166
|
+
jsdom has no layout, so pass `configurePageAgent({ measure })` to supply
|
|
167
|
+
element boxes. The style rules (`hidden`, `display: none`, `visibility`,
|
|
168
|
+
`opacity: 0`) still run for real. jsdom has no hit testing either: nothing
|
|
169
|
+
counts as covered unless you pass `configurePageAgent({ hitTest: (x, y) =>
|
|
170
|
+
element | null })`.
|