@reticlehq/browser 2.3.0 → 2.5.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/dist/actions/actions-dom.js +10 -10
- package/dist/actions/actions.d.ts +13 -0
- package/dist/actions/actions.js +45 -15
- package/dist/actions/scroll.js +1 -1
- package/dist/commands/commands.js +39 -22
- package/dist/connect-options.d.ts +103 -0
- package/dist/connect-options.js +1 -0
- package/dist/dom/a11y.js +20 -20
- package/dist/dom/auto-anchor.js +1 -1
- package/dist/dom/blind-spots.js +1 -1
- package/dist/dom/chart.js +5 -5
- package/dist/dom/desktop-capture.d.ts +2 -1
- package/dist/dom/desktop-capture.js +4 -4
- package/dist/dom/occlusion.js +2 -2
- package/dist/dom/query.js +28 -18
- package/dist/dom/realm.js +13 -13
- package/dist/dom/refs.d.ts +14 -0
- package/dist/dom/refs.js +20 -1
- package/dist/dom/shadow-registry.js +2 -2
- package/dist/dom/snapshot.js +7 -7
- package/dist/dom/source.js +3 -3
- package/dist/dom/theme.js +6 -6
- package/dist/dom/virtualized.js +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/observers/blind-spots.js +2 -2
- package/dist/observers/console.js +3 -3
- package/dist/observers/dom.js +5 -5
- package/dist/observers/download.d.ts +2 -1
- package/dist/observers/download.js +2 -2
- package/dist/observers/focus.js +1 -1
- package/dist/observers/frames.js +1 -1
- package/dist/observers/health.js +70 -2
- package/dist/observers/install-all.d.ts +2 -1
- package/dist/observers/ipc.d.ts +2 -7
- package/dist/observers/ipc.js +7 -7
- package/dist/observers/network-body.js +2 -2
- package/dist/observers/network-redact.js +5 -5
- package/dist/observers/network-stream.js +2 -2
- package/dist/observers/network.d.ts +3 -3
- package/dist/observers/network.js +9 -9
- package/dist/observers/perf.js +1 -1
- package/dist/observers/scroll.js +1 -1
- package/dist/observers/state.js +1 -1
- package/dist/observers/storage.js +7 -7
- package/dist/presenter/presenter-brand.d.ts +19 -0
- package/dist/presenter/presenter-brand.js +21 -0
- package/dist/presenter/presenter-config.d.ts +17 -0
- package/dist/presenter/presenter-config.js +21 -0
- package/dist/presenter/presenter-controls.d.ts +1 -11
- package/dist/presenter/presenter-controls.js +6 -7
- package/dist/presenter/presenter-log.js +2 -2
- package/dist/presenter/presenter-run-state.js +1 -1
- package/dist/presenter/presenter-styles.d.ts +1 -1
- package/dist/presenter/presenter-styles.js +20 -9
- package/dist/presenter/presenter-tally.js +3 -3
- package/dist/presenter/presenter-test-helpers.d.ts +2 -1
- package/dist/presenter/presenter-test-helpers.js +2 -2
- package/dist/presenter/presenter-verbs.js +16 -11
- package/dist/presenter/presenter.js +13 -10
- package/dist/recorder/recorder.js +10 -9
- package/dist/registry/capabilities.d.ts +2 -0
- package/dist/registry/capabilities.js +16 -0
- package/dist/registry/store-adapters.d.ts +127 -1
- package/dist/registry/store-adapters.js +178 -0
- package/dist/registry/stores.js +3 -3
- package/dist/reticle-presenter-helpers.js +3 -3
- package/dist/reticle.d.ts +2 -57
- package/dist/reticle.js +57 -14
- package/dist/review/annotator.js +7 -7
- package/dist/security/serialization.js +9 -9
- package/dist/session-continuity.d.ts +17 -0
- package/dist/session-continuity.js +42 -0
- package/dist/timers/clock.js +4 -4
- package/dist/timers/native-console.js +1 -1
- package/dist/timers/native-timers.js +4 -4
- package/dist/transport/transport.d.ts +10 -0
- package/dist/transport/transport.js +19 -5
- package/package.json +11 -4
|
@@ -17,7 +17,7 @@ export function fireClickSequence(el) {
|
|
|
17
17
|
const from = doc.activeElement ?? doc.body;
|
|
18
18
|
firePointer(el, 'pointerdown', from);
|
|
19
19
|
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
20
|
-
if (el.tabIndex >= 0 && typeof el.focus
|
|
20
|
+
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
21
21
|
el.focus();
|
|
22
22
|
firePointer(el, 'pointerup', from);
|
|
23
23
|
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
|
|
@@ -31,7 +31,7 @@ function isMeasurable(rect) {
|
|
|
31
31
|
/** The center of `rect` falls outside the visible viewport. */
|
|
32
32
|
function isOffViewport(el, rect) {
|
|
33
33
|
const win = el.ownerDocument.defaultView;
|
|
34
|
-
if (
|
|
34
|
+
if (null === win)
|
|
35
35
|
return false;
|
|
36
36
|
const cx = rect.left + rect.width / 2;
|
|
37
37
|
const cy = rect.top + rect.height / 2;
|
|
@@ -40,7 +40,7 @@ function isOffViewport(el, rect) {
|
|
|
40
40
|
/** Hit-test the center: occluded iff the top NON-Reticle element is a foreign subtree (not target/ancestor/descendant). */
|
|
41
41
|
function hitTest(el, rect) {
|
|
42
42
|
const top = hitTestOccluder(el, rect);
|
|
43
|
-
return
|
|
43
|
+
return null === top
|
|
44
44
|
? { occluded: false, occludedBy: null }
|
|
45
45
|
: { occluded: true, occludedBy: refs.refFor(top) };
|
|
46
46
|
}
|
|
@@ -56,7 +56,7 @@ export function clickGeometry(el) {
|
|
|
56
56
|
if (!isMeasurable(rect))
|
|
57
57
|
return NO_GEOMETRY;
|
|
58
58
|
let scrolledIntoView = false;
|
|
59
|
-
if (isOffViewport(el, rect) && typeof el.scrollIntoView
|
|
59
|
+
if (isOffViewport(el, rect) && 'function' === typeof el.scrollIntoView) {
|
|
60
60
|
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
61
61
|
scrolledIntoView = true;
|
|
62
62
|
rect = el.getBoundingClientRect();
|
|
@@ -64,7 +64,7 @@ export function clickGeometry(el) {
|
|
|
64
64
|
return { ...hitTest(el, rect), scrolledIntoView };
|
|
65
65
|
}
|
|
66
66
|
export function firePointer(el, type, relatedTarget = null) {
|
|
67
|
-
if (typeof PointerEvent
|
|
67
|
+
if ('function' === typeof PointerEvent) {
|
|
68
68
|
el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
|
|
69
69
|
}
|
|
70
70
|
else {
|
|
@@ -73,7 +73,7 @@ export function firePointer(el, type, relatedTarget = null) {
|
|
|
73
73
|
}
|
|
74
74
|
/** Enter/leave pointer events are non-bubbling per spec; keep them so to avoid double-firing. */
|
|
75
75
|
export function firePointerNonBubbling(el, type, relatedTarget = null) {
|
|
76
|
-
if (typeof PointerEvent
|
|
76
|
+
if ('function' === typeof PointerEvent) {
|
|
77
77
|
el.dispatchEvent(new PointerEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
|
|
78
78
|
}
|
|
79
79
|
else {
|
|
@@ -87,9 +87,9 @@ function makeDataTransfer(data) {
|
|
|
87
87
|
// data: { mime, value } or [{ mime, value }, …]
|
|
88
88
|
const entries = Array.isArray(data) ? data : data !== undefined ? [data] : [];
|
|
89
89
|
for (const entry of entries) {
|
|
90
|
-
if (typeof entry
|
|
90
|
+
if ('object' === typeof entry && entry !== null) {
|
|
91
91
|
const e = entry;
|
|
92
|
-
if (typeof e.mime
|
|
92
|
+
if ('string' === typeof e.mime && 'string' === typeof e.value)
|
|
93
93
|
dt.setData(e.mime, e.value);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -102,7 +102,7 @@ function makeDataTransfer(data) {
|
|
|
102
102
|
export async function dragElement(source, target, data) {
|
|
103
103
|
const dest = target ?? source;
|
|
104
104
|
const fire = (el, type) => {
|
|
105
|
-
if (typeof PointerEvent
|
|
105
|
+
if ('function' === typeof PointerEvent && type.startsWith('pointer')) {
|
|
106
106
|
el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true }));
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
@@ -118,7 +118,7 @@ export async function dragElement(source, target, data) {
|
|
|
118
118
|
fire(dest, 'pointerup');
|
|
119
119
|
fire(dest, 'mouseup');
|
|
120
120
|
let dropPrevented = false;
|
|
121
|
-
if (typeof DragEvent
|
|
121
|
+
if ('function' === typeof DragEvent) {
|
|
122
122
|
const dataTransfer = makeDataTransfer(data);
|
|
123
123
|
const init = { bubbles: true, cancelable: true };
|
|
124
124
|
if (dataTransfer !== null)
|
|
@@ -81,7 +81,20 @@ interface SequenceStepResult {
|
|
|
81
81
|
dispatched: boolean;
|
|
82
82
|
settled: boolean;
|
|
83
83
|
settleReason: SettleReason | null;
|
|
84
|
+
/**
|
|
85
|
+
* The SAME anchors a single act reports. Reporting only the testid meant a testid-less app
|
|
86
|
+
* compiled every sub-step to a volatile ref, so the saved flow carried the degraded sentinel and
|
|
87
|
+
* every replay drifted.
|
|
88
|
+
*/
|
|
84
89
|
testid?: string;
|
|
90
|
+
component?: string;
|
|
91
|
+
role?: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
source?: {
|
|
94
|
+
file: string;
|
|
95
|
+
line: number;
|
|
96
|
+
column?: number;
|
|
97
|
+
};
|
|
85
98
|
/** best-effort caveat for this step (e.g. synthetic hover may not fire enter/leave). */
|
|
86
99
|
warning?: string;
|
|
87
100
|
}
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, isDangerousActionText, SettleReason, } from '@reticlehq/core';
|
|
2
|
-
import { refs } from '../dom/refs.js';
|
|
2
|
+
import { echoRef, refs } from '../dom/refs.js';
|
|
3
3
|
import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
|
|
4
4
|
import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
|
|
5
5
|
import { elementHasHoverHandlers, identifyComponent } from '../registry/adapters.js';
|
|
6
6
|
import { isForm, isHtmlElement, isInput, isSelect, isTextArea } from '../dom/realm.js';
|
|
7
7
|
import { nativeSetTimeout, settle } from '../timers/native-timers.js';
|
|
8
8
|
function asString(value, fallback = '') {
|
|
9
|
-
return typeof value
|
|
9
|
+
return 'string' === typeof value ? value : fallback;
|
|
10
10
|
}
|
|
11
11
|
function requireElement(ref) {
|
|
12
12
|
const el = refs.resolve(ref);
|
|
13
|
-
if (
|
|
14
|
-
throw new Error(`ref '${ref}' no longer resolves to an element`);
|
|
13
|
+
if (null === el)
|
|
14
|
+
throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
|
|
15
15
|
if (!isHtmlElement(el))
|
|
16
|
-
throw new Error(`ref '${ref}' is not an HTMLElement`);
|
|
16
|
+
throw new Error(`ref '${echoRef(ref)}' is not an HTMLElement`);
|
|
17
17
|
return el;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
@@ -39,12 +39,20 @@ function anchorOf(el) {
|
|
|
39
39
|
out.component = component;
|
|
40
40
|
if (info?.source !== undefined)
|
|
41
41
|
out.source = info.source;
|
|
42
|
+
// Reported INDEPENDENTLY. They used to be all-or-nothing, so an element with a role and no
|
|
43
|
+
// accessible name — an icon button, a clickable div, a control labelled by an SVG — came back with
|
|
44
|
+
// no identity at all once identifyComponent (React-specific) also found nothing. Two features read
|
|
45
|
+
// this and both silently degrade without it: the flow recorder anchors a step by it, and coverage
|
|
46
|
+
// recognises a control it already drove across a re-render by it.
|
|
47
|
+
//
|
|
48
|
+
// A role alone is not a unique anchor, and the flow compiler still requires role AND name before it
|
|
49
|
+
// will anchor a step that way. But it is real information, and reporting it beats reporting nothing.
|
|
42
50
|
const role = getRole(el);
|
|
43
51
|
const name = getAccessibleName(el);
|
|
44
|
-
if (role.length > 0
|
|
52
|
+
if (role.length > 0)
|
|
45
53
|
out.role = role;
|
|
54
|
+
if (name.length > 0)
|
|
46
55
|
out.name = name;
|
|
47
|
-
}
|
|
48
56
|
return out;
|
|
49
57
|
}
|
|
50
58
|
const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
|
|
@@ -108,7 +116,7 @@ function assertActionAllowed(el, action, args) {
|
|
|
108
116
|
action === ActionType.DBLCLICK ||
|
|
109
117
|
action === ActionType.DRAG ||
|
|
110
118
|
action === ActionType.SUBMIT ||
|
|
111
|
-
(action === ActionType.PRESS && asString(args['key'], 'Enter')
|
|
119
|
+
(action === ActionType.PRESS && 'Enter' === asString(args['key'], 'Enter'));
|
|
112
120
|
const dragTarget = action === ActionType.DRAG ? refs.resolve(asString(args['toRef'])) : null;
|
|
113
121
|
const context = isHtmlElement(dragTarget)
|
|
114
122
|
? `${dangerousActionContext(el)} ${dangerousActionContext(dragTarget)}`
|
|
@@ -135,7 +143,7 @@ function valueOf(el) {
|
|
|
135
143
|
/** Ref of the currently-focused element, treating body/null as "no focus". */
|
|
136
144
|
function activeRef(el) {
|
|
137
145
|
const active = el.ownerDocument.activeElement;
|
|
138
|
-
if (
|
|
146
|
+
if (null === active || active === el.ownerDocument.body)
|
|
139
147
|
return null;
|
|
140
148
|
return refs.refFor(active);
|
|
141
149
|
}
|
|
@@ -165,7 +173,7 @@ async function dispatchFor(el, action, args) {
|
|
|
165
173
|
firePointer(el, 'pointermove', from);
|
|
166
174
|
const moved = el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true }));
|
|
167
175
|
// hover-dwell: keep "hovering" for holdMs so timer-gated reveals can mount.
|
|
168
|
-
const holdMs = typeof args['holdMs']
|
|
176
|
+
const holdMs = 'number' === typeof args['holdMs'] ? args['holdMs'] : 0;
|
|
169
177
|
if (holdMs > 0)
|
|
170
178
|
await sleep(holdMs);
|
|
171
179
|
return !moved;
|
|
@@ -224,13 +232,27 @@ async function dispatchFor(el, action, args) {
|
|
|
224
232
|
if (isSelect(el)) {
|
|
225
233
|
// A MISSING value assigned '' to the <select>. No option carries that, so the browser sets
|
|
226
234
|
// selectedIndex to -1 — deselecting everything — from a call that was simply malformed.
|
|
227
|
-
// (Selecting a value that no option carries is a different case and is deliberately NOT
|
|
228
|
-
// refused: `actions.effect.test.ts` pins it as a detectable no-op, where the reported
|
|
229
|
-
// valueChanged delta is what tells the agent the option never took.)
|
|
230
235
|
if (typeof args['value'] !== 'string') {
|
|
231
236
|
throw new Error("select requires a string `value` — pass it nested, as args: { value: '…' }.");
|
|
232
237
|
}
|
|
233
|
-
|
|
238
|
+
const wanted = args['value'];
|
|
239
|
+
const options = Array.from(el.options);
|
|
240
|
+
// REFUSE an option that does not exist, before touching the element.
|
|
241
|
+
//
|
|
242
|
+
// This used to assign anyway and report the resulting `valueChanged` delta as proof the
|
|
243
|
+
// option never took. That only works if nobody is listening. An unmatched value drives
|
|
244
|
+
// selectedIndex to -1, so `el.value` becomes '' — and we then dispatched `change`. Reported
|
|
245
|
+
// from a real session: the app read that empty value in its change handler and persisted it,
|
|
246
|
+
// corrupting a stored language setting. The "detectable no-op" both mutated the app into a
|
|
247
|
+
// state no user could reach AND did it through the app's own handler.
|
|
248
|
+
//
|
|
249
|
+
// Same rule as the readonly/disabled refusal: if a real user could not do it, forcing it is
|
|
250
|
+
// not a test, it is damage.
|
|
251
|
+
if (!options.some((option) => option.value === wanted)) {
|
|
252
|
+
const available = options.map((option) => `${option.value} (${option.label})`).join(', ');
|
|
253
|
+
throw new Error(`no <option> with value '${echoRef(wanted)}' — available: ${0 === options.length ? '(none)' : available}`);
|
|
254
|
+
}
|
|
255
|
+
el.value = wanted;
|
|
234
256
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
235
257
|
return false; // change is not cancelable.
|
|
236
258
|
}
|
|
@@ -245,7 +267,7 @@ async function dispatchFor(el, action, args) {
|
|
|
245
267
|
throw new Error(`cannot (un)check a <${el.tagName.toLowerCase()}>`);
|
|
246
268
|
case ActionType.SUBMIT: {
|
|
247
269
|
const form = isForm(el) ? el : el.closest('form');
|
|
248
|
-
if (
|
|
270
|
+
if (null === form)
|
|
249
271
|
throw new Error('no form to submit');
|
|
250
272
|
form.requestSubmit();
|
|
251
273
|
return false; // requestSubmit returns void; the internal submit event is unobservable.
|
|
@@ -377,6 +399,14 @@ export async function executeSequence(steps) {
|
|
|
377
399
|
};
|
|
378
400
|
if (res.testid !== undefined)
|
|
379
401
|
stepBase.testid = res.testid;
|
|
402
|
+
if (res.component !== undefined)
|
|
403
|
+
stepBase.component = res.component;
|
|
404
|
+
if (res.role !== undefined)
|
|
405
|
+
stepBase.role = res.role;
|
|
406
|
+
if (res.name !== undefined)
|
|
407
|
+
stepBase.name = res.name;
|
|
408
|
+
if (res.source !== undefined)
|
|
409
|
+
stepBase.source = res.source;
|
|
380
410
|
if (res.warning !== undefined)
|
|
381
411
|
stepBase.warning = res.warning;
|
|
382
412
|
stepResults.push(stepBase);
|
package/dist/actions/scroll.js
CHANGED
|
@@ -6,7 +6,7 @@ function nearestScrollable(el) {
|
|
|
6
6
|
let cur = el;
|
|
7
7
|
while (cur !== null && cur !== document.body) {
|
|
8
8
|
const oy = getComputedStyle(cur).overflowY;
|
|
9
|
-
if ((
|
|
9
|
+
if (('auto' === oy || 'scroll' === oy) && cur.scrollHeight > cur.clientHeight)
|
|
10
10
|
return cur;
|
|
11
11
|
cur = cur.parentElement;
|
|
12
12
|
}
|
|
@@ -3,8 +3,9 @@ import { buildSnapshot } from '../dom/snapshot.js';
|
|
|
3
3
|
import { matchQuery, runQuery } from '../dom/query.js';
|
|
4
4
|
import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/actions.js';
|
|
5
5
|
import { describe } from '../dom/a11y.js';
|
|
6
|
+
import { sourceFor, formatSource } from '../dom/source.js';
|
|
6
7
|
import { themeReport } from '../dom/theme.js';
|
|
7
|
-
import { refs } from '../dom/refs.js';
|
|
8
|
+
import { echoRef, refs } from '../dom/refs.js';
|
|
8
9
|
import { isButton, isInput } from '../dom/realm.js';
|
|
9
10
|
import { hitTestOccluder } from '../dom/occlusion.js';
|
|
10
11
|
import { readStorage } from '../observers/storage.js';
|
|
@@ -18,16 +19,16 @@ import { scrollContainer } from '../actions/scroll.js';
|
|
|
18
19
|
/** Query param appended on a hard reload to bypass the browser cache. */
|
|
19
20
|
export const RELOAD_CACHE_BUST_PARAM = '_reticle_reload';
|
|
20
21
|
function str(value) {
|
|
21
|
-
return typeof value
|
|
22
|
+
return 'string' === typeof value ? value : undefined;
|
|
22
23
|
}
|
|
23
24
|
function num(value) {
|
|
24
|
-
return typeof value
|
|
25
|
+
return 'number' === typeof value ? value : undefined;
|
|
25
26
|
}
|
|
26
27
|
function record(value) {
|
|
27
|
-
return typeof value
|
|
28
|
+
return 'object' === typeof value && value !== null ? value : {};
|
|
28
29
|
}
|
|
29
30
|
function sourceLocation(value) {
|
|
30
|
-
if (typeof value !== 'object' ||
|
|
31
|
+
if (typeof value !== 'object' || null === value)
|
|
31
32
|
return undefined;
|
|
32
33
|
const obj = value;
|
|
33
34
|
if (typeof obj['file'] !== 'string' || typeof obj['line'] !== 'number')
|
|
@@ -48,15 +49,20 @@ function queryFromArgs(args) {
|
|
|
48
49
|
component: str(args['component']),
|
|
49
50
|
scope: str(args['scope']),
|
|
50
51
|
attrs: Array.isArray(args['attrs'])
|
|
51
|
-
? args['attrs'].filter((a) => typeof a
|
|
52
|
+
? args['attrs'].filter((a) => 'string' === typeof a)
|
|
52
53
|
: undefined,
|
|
53
54
|
source: sourceLocation(args['source']),
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
function inspect(ref) {
|
|
57
58
|
const el = refs.resolve(ref);
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
// THROW, exactly as executeAction does. Returning an error payload made it a SUCCESSFUL command
|
|
60
|
+
// result, which the server then handed to reticle_inspect's outputSchema (ref/role/name/states/
|
|
61
|
+
// visible all required) — so the MCP layer answered -32602 Output validation error for the most
|
|
62
|
+
// ordinary thing that follows a click. The wording matches actions.ts because the server's
|
|
63
|
+
// recovery table keys off /no longer resolves to an element/ to attach the stale-ref recovery.
|
|
64
|
+
if (null === el)
|
|
65
|
+
throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
|
|
60
66
|
const rect = el.getBoundingClientRect();
|
|
61
67
|
const component = identifyComponent(el);
|
|
62
68
|
const view = el.ownerDocument.defaultView;
|
|
@@ -74,8 +80,15 @@ function inspect(ref) {
|
|
|
74
80
|
visibility: cs.visibility,
|
|
75
81
|
}
|
|
76
82
|
: null;
|
|
83
|
+
// `describe()` carries the CHEAP source (the nearest babel-stamped ancestor), because it runs per
|
|
84
|
+
// element on paths that describe hundreds at once. inspect is a single-element path, so it takes
|
|
85
|
+
// the adapter's answer first — the component that actually RENDERED this element, not the nearest
|
|
86
|
+
// stamped host. `act` already did this; inspect did not, so the two tools disagreed about the same
|
|
87
|
+
// ref and inspect — the tool you reach for to ask where something lives — was the one saying null.
|
|
88
|
+
const source = formatSource(sourceFor(el, component?.source));
|
|
77
89
|
return {
|
|
78
90
|
...describe(el),
|
|
91
|
+
...(source !== undefined ? { source } : {}),
|
|
79
92
|
tag: el.tagName.toLowerCase(),
|
|
80
93
|
href: el.getAttribute('href') ?? undefined,
|
|
81
94
|
formAction: isButton(el) || isInput(el) ? (el.form?.getAttribute('action') ?? undefined) : undefined,
|
|
@@ -96,7 +109,7 @@ function isOccluded(el, rect) {
|
|
|
96
109
|
}
|
|
97
110
|
/** Narrowing guard: an adapter returned a ComponentStateResult (has a boolean `ok`). */
|
|
98
111
|
function isComponentStateResult(value) {
|
|
99
|
-
return (typeof value
|
|
112
|
+
return ('object' === typeof value && value !== null && 'ok' in value && 'boolean' === typeof value.ok);
|
|
100
113
|
}
|
|
101
114
|
const COMPONENT_UNAVAILABLE = {
|
|
102
115
|
ok: false,
|
|
@@ -132,6 +145,10 @@ function readState(ref, store, path, depth) {
|
|
|
132
145
|
// what keeps "the list is short" distinguishable from "I shortened the list".
|
|
133
146
|
...(projected.truncation === undefined ? {} : { truncation: projected.truncation }),
|
|
134
147
|
...('availableKeys' in selection ? { availableKeys: selection.availableKeys } : {}),
|
|
148
|
+
// How many keys there REALLY were, when the near-miss list is a sample. Without it, 50 names
|
|
149
|
+
// and no marker reads as "the key you asked for does not exist" — the strongest negative
|
|
150
|
+
// signal there is, and a false one when the key is simply number 51.
|
|
151
|
+
...('totalKeys' in selection ? { totalKeys: selection.totalKeys } : {}),
|
|
135
152
|
storeNames: names,
|
|
136
153
|
};
|
|
137
154
|
}
|
|
@@ -146,7 +163,7 @@ function readState(ref, store, path, depth) {
|
|
|
146
163
|
result.truncation = truncation;
|
|
147
164
|
if (ref !== undefined && ref.length > 0) {
|
|
148
165
|
const el = refs.resolve(ref);
|
|
149
|
-
if (
|
|
166
|
+
if (null === el) {
|
|
150
167
|
result.component = COMPONENT_UNAVAILABLE;
|
|
151
168
|
}
|
|
152
169
|
else {
|
|
@@ -172,11 +189,11 @@ function listAnimations() {
|
|
|
172
189
|
return { animations };
|
|
173
190
|
}
|
|
174
191
|
export function resolveNavigationUrl(rawUrl, baseUrl) {
|
|
175
|
-
if (rawUrl.length
|
|
192
|
+
if (0 === rawUrl.length || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_LENGTH)
|
|
176
193
|
return null;
|
|
177
194
|
try {
|
|
178
195
|
const url = new URL(rawUrl, baseUrl);
|
|
179
|
-
return
|
|
196
|
+
return 'http:' === url.protocol || 'https:' === url.protocol ? url.toString() : null;
|
|
180
197
|
}
|
|
181
198
|
catch {
|
|
182
199
|
return null;
|
|
@@ -191,14 +208,14 @@ export function createCommandRegistry() {
|
|
|
191
208
|
}));
|
|
192
209
|
reg.set(ReticleCommand.QUERY, (args) => {
|
|
193
210
|
const limit = args['limit'];
|
|
194
|
-
return runQuery(queryFromArgs(args), typeof limit
|
|
211
|
+
return runQuery(queryFromArgs(args), 'number' === typeof limit ? limit : undefined);
|
|
195
212
|
});
|
|
196
213
|
reg.set(ReticleCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args['query'])), str(args['state'])));
|
|
197
214
|
reg.set(ReticleCommand.ACT, (args) => {
|
|
198
215
|
const action = str(args['action']) ?? '';
|
|
199
216
|
if (action === ActionType.WEBMCP) {
|
|
200
217
|
const inner = record(args['args']);
|
|
201
|
-
return dispatchWebMcp(str(inner['tool']) ?? '', record(inner['params']), inner[DANGEROUS_ACTION_CONFIRM_ARG]
|
|
218
|
+
return dispatchWebMcp(str(inner['tool']) ?? '', record(inner['params']), true === inner[DANGEROUS_ACTION_CONFIRM_ARG]);
|
|
202
219
|
}
|
|
203
220
|
return executeAction(str(args['ref']) ?? '', action, record(args['args']));
|
|
204
221
|
});
|
|
@@ -206,14 +223,14 @@ export function createCommandRegistry() {
|
|
|
206
223
|
reg.set(ReticleCommand.INSPECT, (args) => inspect(str(args['ref']) ?? ''));
|
|
207
224
|
reg.set(ReticleCommand.ANIMATIONS, () => listAnimations());
|
|
208
225
|
reg.set(ReticleCommand.CLOCK, (args) => {
|
|
209
|
-
if (args['reset']
|
|
226
|
+
if (true === args['reset']) {
|
|
210
227
|
resetClock();
|
|
211
228
|
}
|
|
212
229
|
else {
|
|
213
|
-
if (args['freeze']
|
|
230
|
+
if (true === args['freeze'])
|
|
214
231
|
freezeClock();
|
|
215
232
|
const adv = args['advanceMs'];
|
|
216
|
-
if (typeof adv
|
|
233
|
+
if ('number' === typeof adv)
|
|
217
234
|
advanceClock(adv);
|
|
218
235
|
}
|
|
219
236
|
return { frozen: isClockFrozen() };
|
|
@@ -221,24 +238,24 @@ export function createCommandRegistry() {
|
|
|
221
238
|
reg.set(ReticleCommand.STATE_READ, (args) => readState(str(args['ref']), str(args['store']), str(args['path']), num(args['depth'])));
|
|
222
239
|
reg.set(ReticleCommand.STORAGE_READ, (args) => readStorage(str(args['area'])));
|
|
223
240
|
reg.set(ReticleCommand.CAPABILITIES, () => getCapabilities());
|
|
224
|
-
reg.set(ReticleCommand.CAPTURE, (args) => captureDesktopWindow(args['fullPage']
|
|
241
|
+
reg.set(ReticleCommand.CAPTURE, (args) => captureDesktopWindow(true === args['fullPage']));
|
|
225
242
|
reg.set(ReticleCommand.SCROLL, (args) => {
|
|
226
243
|
const dy = args['dy'];
|
|
227
244
|
const fraction = args['fraction'];
|
|
228
|
-
return scrollContainer(str(args['ref']), typeof dy
|
|
245
|
+
return scrollContainer(str(args['ref']), 'number' === typeof dy ? dy : undefined, 'number' === typeof fraction ? fraction : undefined);
|
|
229
246
|
});
|
|
230
247
|
reg.set(ReticleCommand.NAVIGATE, (args) => {
|
|
231
248
|
const rawUrl = str(args['url']);
|
|
232
|
-
if (rawUrl === undefined || rawUrl.length
|
|
249
|
+
if (rawUrl === undefined || 0 === rawUrl.length)
|
|
233
250
|
return { ok: false, reason: 'url required' };
|
|
234
251
|
const url = resolveNavigationUrl(rawUrl, window.location.href);
|
|
235
|
-
if (
|
|
252
|
+
if (null === url)
|
|
236
253
|
return { ok: false, reason: 'only http(s) navigation is allowed' };
|
|
237
254
|
window.location.assign(url);
|
|
238
255
|
return { ok: true, url };
|
|
239
256
|
});
|
|
240
257
|
reg.set(ReticleCommand.REFRESH, (args) => {
|
|
241
|
-
if (args['hard']
|
|
258
|
+
if (true === args['hard']) {
|
|
242
259
|
// Hard reload: navigate to self with a cache-busting param then replace history.
|
|
243
260
|
const url = new URL(window.location.href);
|
|
244
261
|
url.searchParams.set(RELOAD_CACHE_BUST_PARAM, String(Date.now()));
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { RedactionConfig } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* What `reticle.connect()` accepts.
|
|
4
|
+
*
|
|
5
|
+
* Split out of reticle.ts when that file crossed the 600-line cap — the same move `session-info.ts`
|
|
6
|
+
* made for the server's Session. A pure description of the SDK's public entry contract has no reason
|
|
7
|
+
* to live inside the class that happens to consume it, and separating them keeps the surface every
|
|
8
|
+
* integrating app actually reads reviewable on its own.
|
|
9
|
+
*/
|
|
10
|
+
export interface ReticleConnectOptions {
|
|
11
|
+
/** WS endpoint of the local bridge. Defaults to ws://localhost:<port><path>. */
|
|
12
|
+
url?: string;
|
|
13
|
+
/** Human-friendly session label so the agent can target the right tab. */
|
|
14
|
+
session?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Stable project identity, normally stamped by the build plugin (e.g. "acme-web-9f3c1d"). Lets the
|
|
17
|
+
* agent scope to the right app even when its dev server boots on an unexpected port. Optional.
|
|
18
|
+
*/
|
|
19
|
+
projectId?: string;
|
|
20
|
+
/** Browser/bridge pairing token. Required when either endpoint is non-localhost. */
|
|
21
|
+
token?: string;
|
|
22
|
+
/** Explicitly allow Reticle on a non-localhost page or bridge. Requires token. */
|
|
23
|
+
allowNonLocalhost?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Escape hatch for the production backstop. Reticle is dev-only and refuses to connect when the
|
|
26
|
+
* build reports NODE_ENV=production (an SSR healthcheck or a prod bundle opened locally would
|
|
27
|
+
* otherwise activate). The real fix is to gate the import behind `import.meta.env.DEV` so it's
|
|
28
|
+
* tree-shaken out; this flag only exists for the rare intentional prod diagnostic.
|
|
29
|
+
*/
|
|
30
|
+
allowInProduction?: boolean;
|
|
31
|
+
/** Show a small in-page status chip (connection + event count). */
|
|
32
|
+
overlay?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Capture request/response bodies on net.request events (dev-only; text-like content only,
|
|
35
|
+
* sensitive keys redacted, per-body capped). Off by default — bodies cost tokens and can carry PII.
|
|
36
|
+
*/
|
|
37
|
+
captureNetworkBodies?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The project root, so source paths report repo-relative instead of absolute.
|
|
40
|
+
*
|
|
41
|
+
* React's `_debugSource.fileName` is absolute; the babel stamp is repo-relative. Build plugins that
|
|
42
|
+
* can `define` a global set it themselves; frameworks that cannot (Next passes it through env,
|
|
43
|
+
* Astro through its own config) hand it here instead. Taking it as a connect OPTION is what keeps
|
|
44
|
+
* the generated app-side code plain JavaScript — the previous shape made the caller assign a global,
|
|
45
|
+
* and the TypeScript cast that needed shipped into a `.jsx` file and broke the build.
|
|
46
|
+
*/
|
|
47
|
+
root?: string;
|
|
48
|
+
/**
|
|
49
|
+
* The SDK's own package version, so a version-skewed pair against the daemon can name itself
|
|
50
|
+
* rather than surfacing as a bare `-32000`. Build plugins supply it automatically; pass it here
|
|
51
|
+
* only for a hand-wired connect that has no plugin.
|
|
52
|
+
*/
|
|
53
|
+
sdkVersion?: string;
|
|
54
|
+
/** Presenter mode: glow border, animated cursor, click/hover effects, narration HUD. */
|
|
55
|
+
present?: boolean;
|
|
56
|
+
/** Per-action pacing (ms) in presenter mode so a human can follow. Default 450. */
|
|
57
|
+
pace?: number;
|
|
58
|
+
/** Min ms each narration line stays visible before the next replaces it (presenter). Default 3000. */
|
|
59
|
+
narrationDwellMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Border behavior in presenter mode: 'session' (default) persists the border for the whole
|
|
62
|
+
* session; 'busy' restores the fade-after-idle behavior.
|
|
63
|
+
*/
|
|
64
|
+
border?: 'session' | 'busy';
|
|
65
|
+
/** Max accumulated activity-log rows before the oldest are pruned (presenter). Default 50. */
|
|
66
|
+
logMax?: number;
|
|
67
|
+
/**
|
|
68
|
+
* Mount the floating human-recorder toolbar (Record/Stop/Annotate).
|
|
69
|
+
* Default off — purely additive, dev-only.
|
|
70
|
+
*/
|
|
71
|
+
recorder?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Mount the "Flag a bug" annotator: the human clicks an element that looks wrong, types what's
|
|
74
|
+
* wrong, and Reticle emits a HUMAN_MARK the agent drains via reticle_review. Defaults to ON with the
|
|
75
|
+
* presenter (it's the human's side of the look→act→assert loop); pass `annotate: false` to suppress.
|
|
76
|
+
*/
|
|
77
|
+
annotate?: boolean;
|
|
78
|
+
/** Live-control: overridable ended-border fade delay (native timer). Default 4000. */
|
|
79
|
+
endedFadeMs?: number;
|
|
80
|
+
/** Session auto-end after this much agent idle (presenter). Default 5min; agent-tunable via reticle_session. */
|
|
81
|
+
idleEndMs?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Extend the redaction rules with your app's own vocabulary.
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* reticle.connect({
|
|
87
|
+
* redact: {
|
|
88
|
+
* keys: ['deviceSecretRef', /^partner[-_]?code$/i], // also redact these
|
|
89
|
+
* allow: ['designToken'], // stop redacting this false positive
|
|
90
|
+
* },
|
|
91
|
+
* });
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* Additive only — there is no way to replace the built-in rule, because a config that could would
|
|
95
|
+
* eventually ship an app that leaks. `keys` strings match a key name exactly (case-insensitively);
|
|
96
|
+
* a RegExp is tested against it. `allow` exempts a key from the DEFAULT rule and loses to `keys`.
|
|
97
|
+
*
|
|
98
|
+
* Literal `keys` strings also cross the bridge, so the daemon redacts them on the driven path,
|
|
99
|
+
* where request bodies are captured raw from the network stack and never pass through this SDK.
|
|
100
|
+
* RegExp entries and `allow` do NOT cross — see docs/usage.md ("Extending the redaction rules") for why, and what that means.
|
|
101
|
+
*/
|
|
102
|
+
redact?: RedactionConfig;
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/dom/a11y.js
CHANGED
|
@@ -45,15 +45,15 @@ function inputRole(input) {
|
|
|
45
45
|
const type = input.type.toLowerCase();
|
|
46
46
|
if (INPUT_TEXT_TYPES.has(type))
|
|
47
47
|
return 'textbox';
|
|
48
|
-
if (
|
|
48
|
+
if ('checkbox' === type)
|
|
49
49
|
return 'checkbox';
|
|
50
|
-
if (
|
|
50
|
+
if ('radio' === type)
|
|
51
51
|
return 'radio';
|
|
52
|
-
if (
|
|
52
|
+
if ('range' === type)
|
|
53
53
|
return 'slider';
|
|
54
|
-
if (
|
|
54
|
+
if ('number' === type)
|
|
55
55
|
return 'spinbutton';
|
|
56
|
-
if (
|
|
56
|
+
if ('submit' === type || 'button' === type || 'reset' === type)
|
|
57
57
|
return 'button';
|
|
58
58
|
return 'textbox';
|
|
59
59
|
}
|
|
@@ -115,7 +115,7 @@ function collapse(text) {
|
|
|
115
115
|
}
|
|
116
116
|
function labelledByText(el) {
|
|
117
117
|
const ids = el.getAttribute('aria-labelledby');
|
|
118
|
-
if (
|
|
118
|
+
if (null === ids)
|
|
119
119
|
return null;
|
|
120
120
|
const parts = [];
|
|
121
121
|
for (const id of ids.split(/\s+/)) {
|
|
@@ -144,7 +144,7 @@ function textWithoutHidden(node) {
|
|
|
144
144
|
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
145
145
|
return '';
|
|
146
146
|
const el = node;
|
|
147
|
-
if (el.getAttribute('aria-hidden')
|
|
147
|
+
if ('true' === el.getAttribute('aria-hidden'))
|
|
148
148
|
return '';
|
|
149
149
|
let out = '';
|
|
150
150
|
for (const child of el.childNodes)
|
|
@@ -191,9 +191,9 @@ export function getAccessibleName(el) {
|
|
|
191
191
|
}
|
|
192
192
|
function ariaBool(el, attr) {
|
|
193
193
|
const value = el.getAttribute(attr);
|
|
194
|
-
if (
|
|
194
|
+
if (null === value)
|
|
195
195
|
return undefined;
|
|
196
|
-
return
|
|
196
|
+
return 'true' === value;
|
|
197
197
|
}
|
|
198
198
|
/**
|
|
199
199
|
* The set of states relevant to assertions. `visible` is an O(depth) forced-style walk; callers that
|
|
@@ -203,12 +203,12 @@ export function getStates(el, visible = isVisible(el)) {
|
|
|
203
203
|
const states = [ElementState.PRESENT];
|
|
204
204
|
states.push(visible ? ElementState.VISIBLE : ElementState.HIDDEN);
|
|
205
205
|
const disabledProp = (isButton(el) || isInput(el) || isSelect(el) || isTextArea(el)) && el.disabled;
|
|
206
|
-
const disabled = disabledProp || ariaBool(el, 'aria-disabled')
|
|
206
|
+
const disabled = disabledProp || true === ariaBool(el, 'aria-disabled');
|
|
207
207
|
states.push(disabled ? ElementState.DISABLED : ElementState.ENABLED);
|
|
208
|
-
const checkedProp = isInput(el) && (
|
|
209
|
-
if (checkedProp || ariaBool(el, 'aria-checked')
|
|
208
|
+
const checkedProp = isInput(el) && ('checkbox' === el.type || 'radio' === el.type) && el.checked;
|
|
209
|
+
if (checkedProp || true === ariaBool(el, 'aria-checked'))
|
|
210
210
|
states.push(ElementState.CHECKED);
|
|
211
|
-
if (ariaBool(el, 'aria-expanded')
|
|
211
|
+
if (true === ariaBool(el, 'aria-expanded'))
|
|
212
212
|
states.push(ElementState.EXPANDED);
|
|
213
213
|
if (el.ownerDocument.activeElement === el)
|
|
214
214
|
states.push(ElementState.FOCUSED);
|
|
@@ -232,7 +232,7 @@ export function isSensitiveField(el) {
|
|
|
232
232
|
el.getAttribute('aria-label') ?? '',
|
|
233
233
|
];
|
|
234
234
|
const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
|
|
235
|
-
return ((isInput(el) && el.type.toLowerCase()
|
|
235
|
+
return ((isInput(el) && 'password' === el.type.toLowerCase()) ||
|
|
236
236
|
sensitiveAutocomplete ||
|
|
237
237
|
identifiers.some(isSensitiveKey));
|
|
238
238
|
}
|
|
@@ -247,19 +247,19 @@ export function getValue(el) {
|
|
|
247
247
|
}
|
|
248
248
|
/** Whether the element's OWN box hides it — one forced-style resolution, no ancestor walk. */
|
|
249
249
|
function selfHidden(el) {
|
|
250
|
-
if (el.getAttribute('aria-hidden')
|
|
250
|
+
if ('true' === el.getAttribute('aria-hidden'))
|
|
251
251
|
return true;
|
|
252
252
|
if (isHtmlElement(el) && el.hidden)
|
|
253
253
|
return true;
|
|
254
254
|
const view = el.ownerDocument.defaultView;
|
|
255
255
|
if (view !== null) {
|
|
256
256
|
const style = view.getComputedStyle(el);
|
|
257
|
-
if (style.display
|
|
258
|
-
style.visibility
|
|
259
|
-
style.visibility
|
|
257
|
+
if ('none' === style.display ||
|
|
258
|
+
'hidden' === style.visibility ||
|
|
259
|
+
'collapse' === style.visibility) {
|
|
260
260
|
return true;
|
|
261
261
|
}
|
|
262
|
-
if (Number.parseFloat(style.opacity || '1')
|
|
262
|
+
if (0 === Number.parseFloat(style.opacity || '1'))
|
|
263
263
|
return true;
|
|
264
264
|
}
|
|
265
265
|
return false;
|
|
@@ -282,7 +282,7 @@ export function isVisible(el, memo) {
|
|
|
282
282
|
// Each cached boolean already folds in that node's own aria-hidden/[hidden]/display/visibility/opacity,
|
|
283
283
|
// so inherited visibility composes by AND up the chain and a sibling short-circuits at the first
|
|
284
284
|
// cached ancestor.
|
|
285
|
-
const result = !selfHidden(el) && (
|
|
285
|
+
const result = !selfHidden(el) && (null === parent || isVisible(parent, memo));
|
|
286
286
|
if (memo !== undefined)
|
|
287
287
|
memo.set(el, result);
|
|
288
288
|
return result;
|
package/dist/dom/auto-anchor.js
CHANGED
|
@@ -32,7 +32,7 @@ export const AnchorStrategy = {
|
|
|
32
32
|
POSITION: 'position',
|
|
33
33
|
};
|
|
34
34
|
function nonEmpty(s) {
|
|
35
|
-
return typeof s
|
|
35
|
+
return 'string' === typeof s && s.length > 0;
|
|
36
36
|
}
|
|
37
37
|
/** Source as a compact `file:line` suffix; basename only to stay terse and path-independent. */
|
|
38
38
|
function sourceTag(source) {
|