@classic-homes/page-agent-core 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # @classic-homes/page-agent-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 `rectOf(effect)` for drawing. Effects expire
41
+ (default 8 s, max 60 s); at most 8 at once, one pointer and one spotlight.
42
+ `attachPageAgent()` wires Escape and `popstate` to clear; call
43
+ `notifyNavigated()` after SPA navigation.
44
+ - **Executor**: `configurePageAgent({ navigate, aliasAttributes,
45
+ ignoreSelector })` and `executePageCall(call)`, the single entry point for
46
+ the model's tool calls. `PAGE_TOOL_NAMES` and `PAGE_TOOLS` (plain-JSON
47
+ input shapes) let the agent server mirror the contract exactly.
48
+ - **Geometry**: `unionRects`, `inflate`, `clampToViewport`, `placeLabel`,
49
+ `pointerTarget` for the UI layer.
50
+
51
+ ```ts
52
+ import {
53
+ attachPageAgent,
54
+ configurePageAgent,
55
+ executePageCall,
56
+ } from '@classic-homes/page-agent-core';
57
+ import { goto } from '$app/navigation';
58
+
59
+ configurePageAgent({ navigate: (path) => goto(path), aliasAttributes: ['data-tour'] });
60
+ const detach = attachPageAgent();
61
+
62
+ // From the chat's client-call hook:
63
+ const result = await executePageCall({
64
+ tool: 'highlight',
65
+ input: { target: { id: 'closing-date' } },
66
+ });
67
+ // → { status: 'ok', output: { effectId, target: { id, label }, style, scrolled, inViewport } }
68
+ ```
69
+
70
+ ```html
71
+ <section data-agent-id="closing-date" data-agent-label="Closing date">…</section>
72
+ <div data-agent-ignore><!-- never read, never pointed at --></div>
73
+ ```
74
+
75
+ ## Tools
76
+
77
+ | Tool | Input | Does |
78
+ | ------------------ | ----------------------------------------- | --------------------------------------------------------- |
79
+ | `list_targets` | `{ kind?, limit? }` | Named targets and tabs on the page |
80
+ | `highlight` | `{ target, style?, label?, durationMs? }` | Outline or spotlight, scrolling into view first if needed |
81
+ | `point` | `{ target, label?, durationMs? }` | Moves the ghost cursor to the target |
82
+ | `scroll_to` | `{ target }` | Scrolls into view (no-op if already fully visible) |
83
+ | `clear_highlights` | `{}` | Removes every effect |
84
+ | `select_tab` | `{ target }` | Clicks a `role="tab"`, `ok` once `aria-selected="true"` |
85
+ | `navigate` | `{ path }` | Calls the host's `navigate` with a same-origin path |
86
+
87
+ `target` is `{ id }` or `{ text, within? }`. Results are `{ status, message?,
88
+ output? }` with `status` one of `ok`, `not_found`, `ambiguous`, `hidden`,
89
+ `refused`, `error`.
90
+
91
+ ## Security
92
+
93
+ `executePageCall` treats its input as untrusted:
94
+
95
+ - Every field is checked by hand (type, length, enum, range); unknown tools,
96
+ unknown keys and malformed input return `refused`. It never throws.
97
+ - Targets are ids (compared verbatim with attribute values) or visible
98
+ text. No CSS selectors, HTML or code are accepted.
99
+ - Nothing is clicked except a non-disabled `role="tab"` element.
100
+ - `navigate` accepts only paths starting with a single `/` (no `//`,
101
+ backslashes, schemes, whitespace or control characters, also after
102
+ decoding) and goes through the host's handler; without one it is refused.
103
+ - `ok` is reported only once the effect is in place (the effect is in the
104
+ store, the tab is selected, the host's navigation resolved).
105
+ - `[data-agent-ignore]`, the host `ignoreSelector` and the layer's own UI
106
+ (`.chpa`) are invisible to resolution, snapshots and `readPageText`, which
107
+ also never reads password, hidden, card or one-time-code field values.
108
+ Treat page text as untrusted data on the server.
109
+
110
+ ## Styles
111
+
112
+ `styles.css` holds the `.chpa-*` classes the bindings render: the
113
+ click-through layer (`.chpa-layer`), outlines (`.chpa-outline` +
114
+ `.chpa-tag`), the spotlight (`.chpa-spotlight`, dims with a spread shadow and
115
+ keeps the page clickable), the ghost cursor (`.chpa-pointer`,
116
+ `.chpa-pointer-icon`, `.chpa-pointer-label`), the "Agent is pointing ·
117
+ Dismiss" chip (`.chpa-chip`, `.chpa-chip-text`, `.chpa-chip-btn`, 44 px
118
+ target) and a live region (`.chpa-sr`). Override `--chpa-accent` (default
119
+ `#6d28d9`) and `--chpa-z` (default `2147482000`, below the Access
120
+ Inspector). Dark mode follows `.dark` / `[data-theme='dark']`; reduced
121
+ motion drops the glide and pulse.
122
+
123
+ ## Testing
124
+
125
+ jsdom has no layout, so pass `configurePageAgent({ measure })` to supply
126
+ element boxes. The style rules (`hidden`, `display: none`, `visibility`,
127
+ `opacity: 0`) still run for real.
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Public types for `@classic-homes/page-agent-core`.
3
+ *
4
+ * Everything the model can send arrives as a {@link PageCall} and is
5
+ * validated by hand in `executePageCall` before any of it touches the DOM.
6
+ * Nothing in a call is ever used as a CSS selector, HTML or code.
7
+ */
8
+ /** What a target is, so the model can tell a section from a button. */
9
+ type AgentTargetKind = 'region' | 'tab' | 'field' | 'action';
10
+ /** A named thing on the page the agent may point at. */
11
+ interface AgentTarget {
12
+ el: Element;
13
+ /** Stable id the model uses, e.g. `closing-date`. */
14
+ id: string;
15
+ /** Human label, e.g. "Closing date". */
16
+ label: string;
17
+ kind: AgentTargetKind;
18
+ /** Optional extra hint for the model. */
19
+ description?: string;
20
+ }
21
+ /**
22
+ * How a call names its target: a registered/attribute id, or visible text
23
+ * (optionally scoped to inside another target, by id). Never a selector.
24
+ */
25
+ type TargetRef = {
26
+ id: string;
27
+ } | {
28
+ text: string;
29
+ within?: string;
30
+ };
31
+ type HighlightStyle = 'outline' | 'spotlight';
32
+ type EffectKind = 'outline' | 'spotlight' | 'pointer';
33
+ /** One visual effect on the page (an outline, the spotlight or the ghost cursor). */
34
+ interface PageEffect {
35
+ /** Unique per effect, e.g. `chpa-3`. */
36
+ id: string;
37
+ kind: EffectKind;
38
+ el: Element;
39
+ /** Plain-text caption from the call (capped, control characters removed). */
40
+ label?: string;
41
+ /** Label of the target itself, for announcements ("Closing date"). */
42
+ targetLabel: string;
43
+ /** Target id when the target has one. */
44
+ targetId?: string;
45
+ createdAt: number;
46
+ expiresAt: number;
47
+ }
48
+ interface HighlightOptions {
49
+ style?: HighlightStyle;
50
+ label?: string;
51
+ /** Default 8000, clamped to 1000…60000. */
52
+ durationMs?: number;
53
+ }
54
+ interface PointOptions {
55
+ label?: string;
56
+ /** Default 8000, clamped to 1000…60000. */
57
+ durationMs?: number;
58
+ }
59
+ interface ListTargetsInput {
60
+ kind?: AgentTargetKind;
61
+ /** Default 200 (the maximum). */
62
+ limit?: number;
63
+ }
64
+ interface HighlightInput {
65
+ target: TargetRef;
66
+ style?: HighlightStyle;
67
+ label?: string;
68
+ durationMs?: number;
69
+ }
70
+ interface PointInput {
71
+ target: TargetRef;
72
+ label?: string;
73
+ durationMs?: number;
74
+ }
75
+ interface ScrollToInput {
76
+ target: TargetRef;
77
+ }
78
+ interface SelectTabInput {
79
+ target: TargetRef;
80
+ }
81
+ interface NavigateInput {
82
+ /** Same-origin app path: starts with a single `/`, e.g. `/homes/123?tab=docs`. */
83
+ path: string;
84
+ }
85
+ /** A tool call from the model, discriminated on `tool`. */
86
+ type PageCall = {
87
+ tool: 'list_targets';
88
+ input?: ListTargetsInput;
89
+ } | {
90
+ tool: 'highlight';
91
+ input: HighlightInput;
92
+ } | {
93
+ tool: 'point';
94
+ input: PointInput;
95
+ } | {
96
+ tool: 'scroll_to';
97
+ input: ScrollToInput;
98
+ } | {
99
+ tool: 'clear_highlights';
100
+ input?: Record<string, never>;
101
+ } | {
102
+ tool: 'select_tab';
103
+ input: SelectTabInput;
104
+ } | {
105
+ tool: 'navigate';
106
+ input: NavigateInput;
107
+ };
108
+ type PageToolName = PageCall['tool'];
109
+ type PageCallStatus = 'ok' | 'not_found' | 'ambiguous' | 'hidden' | 'refused' | 'error';
110
+ /** What goes back to the model. `ok` only once the effect is confirmed. */
111
+ interface PageCallResult {
112
+ status: PageCallStatus;
113
+ /** Short plain-text explanation, safe to show the model. */
114
+ message?: string;
115
+ /** Tool-specific data, JSON-serialisable. */
116
+ output?: unknown;
117
+ }
118
+ interface TargetSummary {
119
+ id: string;
120
+ label: string;
121
+ kind: AgentTargetKind;
122
+ visible: boolean;
123
+ inViewport: boolean;
124
+ }
125
+ interface TabSummary {
126
+ /** Present when the tab has a registered or attribute id. */
127
+ id?: string;
128
+ label: string;
129
+ selected: boolean;
130
+ disabled: boolean;
131
+ visible: boolean;
132
+ }
133
+ interface Candidate {
134
+ id?: string;
135
+ label: string;
136
+ }
137
+ interface ListTargetsOutput {
138
+ targets: TargetSummary[];
139
+ tabs: TabSummary[];
140
+ truncated: boolean;
141
+ }
142
+ /** Host router hook. Resolve (or return true / nothing) once navigation finished. */
143
+ type NavigateHandler = (path: string) => void | boolean | Promise<void | boolean>;
144
+ /**
145
+ * Reads an element's layout box. Return null when it has none. The default
146
+ * uses `getClientRects()` / `getBoundingClientRect()`; tests (jsdom has no
147
+ * layout) and unusual hosts can swap it. Style checks (`display`,
148
+ * `visibility`, `hidden`, `inert`) always run regardless.
149
+ */
150
+ type MeasureElement = (el: Element) => DOMRect | null;
151
+ interface PageAgentConfig {
152
+ /** Called by the `navigate` tool. Without it, `navigate` is refused. */
153
+ navigate?: NavigateHandler;
154
+ /** Extra id attributes scanned as targets, e.g. `['data-tour']`. Must match `data-[a-z0-9-]+`. */
155
+ aliasAttributes?: string[];
156
+ /** Host CSS selector for regions the agent must never see or point at (in addition to `[data-agent-ignore]`). */
157
+ ignoreSelector?: string;
158
+ /** Layout reader; see {@link MeasureElement}. */
159
+ measure?: MeasureElement;
160
+ /** How long `select_tab` waits for `aria-selected="true"`. Default 600 ms, max 5000. */
161
+ tabSettleMs?: number;
162
+ }
163
+ interface ResolvedPageAgentConfig {
164
+ navigate: NavigateHandler | null;
165
+ aliasAttributes: string[];
166
+ ignoreSelector: string | null;
167
+ measure: MeasureElement | null;
168
+ tabSettleMs: number;
169
+ }
170
+ type ResolveResult = {
171
+ status: 'ok';
172
+ el: Element;
173
+ id?: string;
174
+ label: string;
175
+ } | {
176
+ status: 'not_found';
177
+ message: string;
178
+ } | {
179
+ status: 'hidden';
180
+ message: string;
181
+ } | {
182
+ status: 'ambiguous';
183
+ message: string;
184
+ candidates: Candidate[];
185
+ };
186
+ type ResolveFailure = Exclude<ResolveResult, {
187
+ status: 'ok';
188
+ }>;
189
+
190
+ /**
191
+ * Configure the page agent once, at app start. Later calls merge into the
192
+ * current config (pass `navigate: undefined` to keep the handler). Alias
193
+ * attributes that are not plain `data-*` names are dropped.
194
+ */
195
+ declare function configurePageAgent(next: PageAgentConfig): ResolvedPageAgentConfig;
196
+ declare function getPageAgentConfig(): ResolvedPageAgentConfig;
197
+
198
+ declare const EFFECT_DEFAULT_MS = 8000;
199
+ declare const EFFECT_MIN_MS = 1000;
200
+ declare const EFFECT_MAX_MS = 60000;
201
+ /** Concurrent effects; the oldest is dropped beyond this. */
202
+ declare const EFFECTS_MAX = 8;
203
+ /** Longest caption on an effect. */
204
+ declare const EFFECT_LABEL_MAX = 80;
205
+ type EffectOutcome = {
206
+ status: 'ok';
207
+ effect: PageEffect;
208
+ } | ResolveFailure;
209
+ type ScrollOutcome = {
210
+ status: 'ok';
211
+ el: Element;
212
+ scrolled: boolean;
213
+ } | ResolveFailure;
214
+ /** Default 8 s; anything else clamped to 1…60 s. Non-numbers get the default. */
215
+ declare function clampDuration(ms: number | undefined): number;
216
+ /** Outline (default) or spotlight a target. Does not scroll; see {@link scrollToTarget}. */
217
+ declare function highlight(ref: TargetRef | Element, options?: HighlightOptions): EffectOutcome;
218
+ /** Move the ghost cursor to a target (replaces the previous pointer). */
219
+ declare function point(ref: TargetRef | Element, options?: PointOptions): EffectOutcome;
220
+ /**
221
+ * Bring a target into view, scrolling each scrollable ancestor and then the
222
+ * window only as far as needed. No-op when it is already fully visible.
223
+ * Smooth unless the user prefers reduced motion.
224
+ */
225
+ declare function scrollToTarget(ref: TargetRef | Element): ScrollOutcome;
226
+ /** Drop every effect now. */
227
+ declare function clearEffects(): void;
228
+ /**
229
+ * Current effects (stable array between changes, safe for
230
+ * `useSyncExternalStore`). Effects whose element left the document are
231
+ * dropped here, with a change notification.
232
+ */
233
+ declare function getEffects(): readonly PageEffect[];
234
+ /** Called (batched, once per microtask) whenever effects change. */
235
+ declare function onEffectsChange(listener: () => void): () => void;
236
+ /** Viewport rect of an effect's element, or null when it has no layout box (skip drawing). */
237
+ declare function rectOf(effect: PageEffect): DOMRect | null;
238
+ /** Tell the agent layer the route changed (SPA navigation): clears every effect. */
239
+ declare function notifyNavigated(): void;
240
+ /**
241
+ * Wire Escape-to-clear and clear-on-history-navigation (`popstate`). Call
242
+ * once from the app shell; returns the detach function. Browser-only, no-op
243
+ * on the server. Escape only acts while something is shown, and never stops
244
+ * the event, so dialogs still get it.
245
+ */
246
+ declare function attachPageAgent(): () => void;
247
+ /**
248
+ * Scroll `el` into view through its scroll containers. Unlike
249
+ * `scrollIntoView`, never scrolls `overflow: hidden` clippers (carousels,
250
+ * clamp boxes) and does nothing when the element is already fully in view.
251
+ * Returns whether anything scrolled.
252
+ */
253
+ declare function scrollIntoContainers(el: Element): boolean;
254
+
255
+ /**
256
+ * A same-origin app path, normalised, or null. Accepts only paths starting
257
+ * with exactly one `/`; refuses `//host`, backslashes (raw or `%5C`),
258
+ * encoded leading slashes, schemes (`javascript:`, `https:`), whitespace,
259
+ * control characters and malformed escapes.
260
+ */
261
+ declare function parseAppPath(path: unknown): string | null;
262
+ /**
263
+ * Run one tool call from the model. Accepts `unknown` on purpose: anything
264
+ * that is not `{ tool, input }` with a known tool and a valid input comes
265
+ * back `refused`. Never throws.
266
+ */
267
+ declare function executePageCall(call: unknown): Promise<PageCallResult>;
268
+
269
+ /** Longest label taken from markup or registration. */
270
+ declare const TARGET_LABEL_MAX = 80;
271
+ interface AgentTargetHandle {
272
+ update(next: Partial<Omit<AgentTarget, 'el'>>): void;
273
+ destroy(): void;
274
+ }
275
+ declare function registerAgentTarget(entry: AgentTarget): AgentTargetHandle;
276
+ /** Registered targets whose element is still in the document. */
277
+ declare function getAgentTargets(): AgentTarget[];
278
+ /** Called (batched, once per microtask) whenever a registration changes. */
279
+ declare function onAgentTargetsChange(listener: () => void): () => void;
280
+ /**
281
+ * Targets marked in plain markup, for pages that cannot use the framework
282
+ * helpers, plus the host's alias attributes (`configurePageAgent({
283
+ * aliasAttributes: ['data-tour'] })`):
284
+ *
285
+ * ```html
286
+ * <section data-agent-id="closing" data-agent-label="Closing date" data-agent-kind="region">…</section>
287
+ * <button data-tour="export-button">Export</button>
288
+ * ```
289
+ *
290
+ * Ignored subtrees are not filtered here; resolution and snapshots do that.
291
+ */
292
+ declare function scanAgentAttributes(root?: ParentNode): AgentTarget[];
293
+
294
+ /** Longest id a call may use. */
295
+ declare const TARGET_ID_MAX = 128;
296
+ /** Longest text a call may search for. */
297
+ declare const TARGET_TEXT_MAX = 200;
298
+ /** Most candidates returned with `ambiguous`. */
299
+ declare const CANDIDATES_MAX = 8;
300
+ interface ResolveOptions {
301
+ /**
302
+ * Only `role="tab"` elements count (for `select_tab`): a text match is
303
+ * promoted to its enclosing tab, and matches outside any tab are dropped.
304
+ */
305
+ tabsOnly?: boolean;
306
+ }
307
+ /**
308
+ * Find the element a call means. Never guesses: several equally good text
309
+ * matches return `ambiguous` with a capped candidate list (labels and ids
310
+ * only, never markup). Ignored regions and the layer's own UI are invisible
311
+ * to it; elements without a layout box resolve as `hidden`.
312
+ */
313
+ declare function resolveTarget(ref: TargetRef, options?: ResolveOptions): ResolveResult;
314
+ /** Id of an element: its registration, else its markup attribute. */
315
+ declare function targetIdOf(el: Element): string | undefined;
316
+
317
+ /** Most targets / tabs `listTargets()` / `listTabs()` return. */
318
+ declare const LIST_MAX = 200;
319
+ /** Default and maximum `readPageText` budget. */
320
+ declare const PAGE_TEXT_DEFAULT = 20000;
321
+ declare const PAGE_TEXT_MAX = 100000;
322
+ declare const TRUNCATION_MARKER = "\n[\u2026truncated]";
323
+ /**
324
+ * Named targets on the page (registered and markup), ignored regions
325
+ * removed, at most `limit` (default and max 200). Visible ones first.
326
+ */
327
+ declare function listTargets(options?: {
328
+ kind?: AgentTargetKind;
329
+ limit?: number;
330
+ }): TargetSummary[];
331
+ /** Every `[role="tab"]` outside ignored regions, with label and state. */
332
+ declare function listTabs(options?: {
333
+ limit?: number;
334
+ }): TabSummary[];
335
+ interface ReadPageTextOptions {
336
+ /** Default 20000, max 100000. */
337
+ maxChars?: number;
338
+ /** Extra host selector to skip, on top of `[data-agent-ignore]`, `.chpa` and the configured `ignoreSelector`. */
339
+ ignoreSelector?: string;
340
+ /** Default `document.body`. */
341
+ root?: Element;
342
+ }
343
+ /**
344
+ * Visible page text for the model (treat it as untrusted data on the
345
+ * server). Skips ignored regions, the layer's own UI, script/style/template/
346
+ * noscript, hidden elements, and secret inputs (passwords, card fields,
347
+ * one-time codes, hidden inputs). Form values of other text inputs are
348
+ * included in brackets. Truncated with {@link TRUNCATION_MARKER}.
349
+ */
350
+ declare function readPageText(options?: ReadPageTextOptions): string;
351
+
352
+ declare const PAGE_TOOL_NAMES: readonly ["list_targets", "highlight", "point", "scroll_to", "clear_highlights", "select_tab", "navigate"];
353
+ /** Longest path `navigate` accepts. */
354
+ declare const NAVIGATE_PATH_MAX = 2048;
355
+ /** Longest raw `label` a call may send (it is then cut to {@link EFFECT_LABEL_MAX}). */
356
+ declare const CALL_LABEL_MAX = 200;
357
+ /** JSON-schema-like shape, enough to generate a zod schema or a tool definition. */
358
+ interface JsonShape {
359
+ type?: 'object' | 'string' | 'number' | 'integer' | 'boolean';
360
+ description?: string;
361
+ properties?: Record<string, JsonShape>;
362
+ required?: readonly string[];
363
+ additionalProperties?: boolean;
364
+ enum?: readonly string[];
365
+ oneOf?: readonly JsonShape[];
366
+ minLength?: number;
367
+ maxLength?: number;
368
+ minimum?: number;
369
+ maximum?: number;
370
+ default?: unknown;
371
+ pattern?: string;
372
+ }
373
+ interface PageToolDescription {
374
+ name: PageToolName;
375
+ description: string;
376
+ input: JsonShape;
377
+ }
378
+ declare const PAGE_TOOLS: readonly PageToolDescription[];
379
+
380
+ /** The layer's own UI. Nothing inside it is ever a target or page text. */
381
+ declare const LAYER_CLASS = "chpa";
382
+ /**
383
+ * Per-call memo of "this element and its ancestors are not hidden by
384
+ * `hidden`, `display: none` or `opacity: 0`". Snapshots check up to 200
385
+ * targets that share most of their ancestors; without it every target
386
+ * re-reads the computed style of the whole chain.
387
+ */
388
+ type RenderCache = Map<Element, boolean>;
389
+ /**
390
+ * Rendered and perceivable: connected, no `hidden` / `display: none` /
391
+ * `opacity: 0` on it or an ancestor, not `visibility: hidden`, and with a
392
+ * layout box. The style walk runs even when `measure` is swapped, so jsdom
393
+ * tests exercise the same rules as browsers. Pass one `cache` for a batch of
394
+ * checks made against an unchanging DOM.
395
+ */
396
+ declare function isRendered(el: Element, cache?: RenderCache): boolean;
397
+ interface ViewportSize {
398
+ width: number;
399
+ height: number;
400
+ }
401
+ declare function viewportSize(): ViewportSize;
402
+ declare function prefersReducedMotion(): boolean;
403
+
404
+ /**
405
+ * Pure rect maths for drawing effects. No DOM access: callers pass the
406
+ * viewport size (see `viewportSize()` on the UI side).
407
+ */
408
+ interface Box {
409
+ top: number;
410
+ left: number;
411
+ width: number;
412
+ height: number;
413
+ }
414
+ interface Size {
415
+ width: number;
416
+ height: number;
417
+ }
418
+ interface Point {
419
+ top: number;
420
+ left: number;
421
+ }
422
+ type RectLike = Pick<DOMRect, 'top' | 'left' | 'width' | 'height'>;
423
+ /** Smallest box covering every rect; null for none. */
424
+ declare function unionRects(rects: readonly RectLike[]): Box | null;
425
+ /** Grow a box by `pad` on every side (outline breathing room). */
426
+ declare function inflate(rect: RectLike, pad: number): Box;
427
+ /** The part of a box inside the viewport (minus `margin`); null when none is. */
428
+ declare function clampToViewport(rect: RectLike, viewport: Size, margin?: number): Box | null;
429
+ /** Wholly inside the viewport. */
430
+ declare function isFullyInViewport(rect: RectLike, viewport: Size): boolean;
431
+ /** At least partly inside the viewport. */
432
+ declare function intersectsViewport(rect: RectLike, viewport: Size): boolean;
433
+ interface LabelPlacement extends Point {
434
+ placement: 'above' | 'below';
435
+ }
436
+ /**
437
+ * Where to put a caption of `size` next to a target rect: above its
438
+ * top-left corner, or below when there is no room above (or `preferBelow`),
439
+ * then clamped into the viewport. Same rules as feedback-core's selection
440
+ * button, re-implemented to keep this package dependency-free.
441
+ */
442
+ declare function placeLabel(rect: RectLike, size: Size, viewport: Size, opts?: {
443
+ gap?: number;
444
+ preferBelow?: boolean;
445
+ }): LabelPlacement;
446
+ /**
447
+ * Where the ghost cursor's tip should rest: just inside the target's
448
+ * bottom-right quarter, clamped into the viewport so it never points off
449
+ * screen.
450
+ */
451
+ declare function pointerTarget(rect: RectLike, viewport: Size, margin?: number): Point;
452
+
453
+ /** Back to a clean slate: default config, no effects. Registrations are kept (their owners destroy them). */
454
+ declare function resetPageAgent(): void;
455
+
456
+ export { type AgentTarget, type AgentTargetHandle, type AgentTargetKind, type Box, CALL_LABEL_MAX, CANDIDATES_MAX, type Candidate, EFFECTS_MAX, EFFECT_DEFAULT_MS, EFFECT_LABEL_MAX, EFFECT_MAX_MS, EFFECT_MIN_MS, type EffectKind, type EffectOutcome, type HighlightInput, type HighlightOptions, type HighlightStyle, type JsonShape, LAYER_CLASS, LIST_MAX, type LabelPlacement, type ListTargetsInput, type ListTargetsOutput, type MeasureElement, NAVIGATE_PATH_MAX, type NavigateHandler, type NavigateInput, PAGE_TEXT_DEFAULT, PAGE_TEXT_MAX, PAGE_TOOLS, PAGE_TOOL_NAMES, type PageAgentConfig, type PageCall, type PageCallResult, type PageCallStatus, type PageEffect, type PageToolDescription, type PageToolName, type Point, type PointInput, type PointOptions, type ReadPageTextOptions, type RenderCache, type ResolveFailure, type ResolveOptions, type ResolveResult, type ResolvedPageAgentConfig, type ScrollOutcome, type ScrollToInput, type SelectTabInput, type Size, TARGET_ID_MAX, TARGET_LABEL_MAX, TARGET_TEXT_MAX, TRUNCATION_MARKER, type TabSummary, type TargetRef, type TargetSummary, type ViewportSize, attachPageAgent, clampDuration, clampToViewport, clearEffects, configurePageAgent, executePageCall, getAgentTargets, getEffects, getPageAgentConfig, highlight, inflate, intersectsViewport, isFullyInViewport, isRendered, listTabs, listTargets, notifyNavigated, onAgentTargetsChange, onEffectsChange, parseAppPath, placeLabel, point, pointerTarget, prefersReducedMotion, readPageText, rectOf, registerAgentTarget, resetPageAgent, resolveTarget, scanAgentAttributes, scrollIntoContainers, scrollToTarget, targetIdOf, unionRects, viewportSize };