@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
package/dist/dom/blind-spots.js
CHANGED
|
@@ -10,7 +10,7 @@ export function classifyCrossOriginFrames(frames) {
|
|
|
10
10
|
}
|
|
11
11
|
/** Probe the live document: count iframes whose document is unreachable (cross-origin). */
|
|
12
12
|
export function countCrossOriginFrames() {
|
|
13
|
-
if (typeof document
|
|
13
|
+
if ('undefined' === typeof document)
|
|
14
14
|
return 0;
|
|
15
15
|
const frames = [];
|
|
16
16
|
for (const frame of document.querySelectorAll('iframe')) {
|
package/dist/dom/chart.js
CHANGED
|
@@ -54,12 +54,12 @@ export function hasNonFiniteCoordinate(value) {
|
|
|
54
54
|
/** True when the geometry encloses no area — no coordinates, or every coordinate pair identical. */
|
|
55
55
|
export function isDegenerate(value) {
|
|
56
56
|
const nums = geometryNumbers(value).filter((n) => Number.isFinite(n));
|
|
57
|
-
if (nums.length
|
|
57
|
+
if (0 === nums.length)
|
|
58
58
|
return true;
|
|
59
59
|
// A polyline of one repeated point draws nothing. Compare pairs, not raw numbers, so a legitimately
|
|
60
60
|
// flat line (varying x, constant y) is NOT flagged — that is a real chart of constant data.
|
|
61
|
-
const xs = nums.filter((_, i) => i % 2
|
|
62
|
-
const ys = nums.filter((_, i) => i % 2
|
|
61
|
+
const xs = nums.filter((_, i) => 0 === i % 2);
|
|
62
|
+
const ys = nums.filter((_, i) => 1 === i % 2);
|
|
63
63
|
const allSame = (arr) => arr.every((v) => v === arr[0]);
|
|
64
64
|
return xs.length > 1 && allSame(xs) && allSame(ys);
|
|
65
65
|
}
|
|
@@ -74,14 +74,14 @@ export function inspectChart(root) {
|
|
|
74
74
|
for (const el of Array.from(root.querySelectorAll(tag))) {
|
|
75
75
|
for (const attr of GEOMETRY_ATTRS) {
|
|
76
76
|
const value = el.getAttribute(attr);
|
|
77
|
-
if (
|
|
77
|
+
if (null === value)
|
|
78
78
|
continue;
|
|
79
79
|
examined += 1;
|
|
80
80
|
const sample = value.length > SAMPLE_MAX ? `${value.slice(0, SAMPLE_MAX)}…` : value;
|
|
81
81
|
if (hasNonFiniteCoordinate(value)) {
|
|
82
82
|
findings.push({ kind: 'non-finite-coordinates', tag, attr, sample });
|
|
83
83
|
}
|
|
84
|
-
else if (value.trim().length
|
|
84
|
+
else if (0 === value.trim().length) {
|
|
85
85
|
findings.push({ kind: 'empty-geometry', tag, attr, sample });
|
|
86
86
|
}
|
|
87
87
|
else if (isDegenerate(value)) {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* (`reticle-tauri`). Absent that, this returns `{ ok: false }` and the tool reports no-provider
|
|
21
21
|
* rather than guessing.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
interface CaptureResult {
|
|
24
24
|
ok: boolean;
|
|
25
25
|
/**
|
|
26
26
|
* Filesystem path of the captured PNG, written by the desktop shell.
|
|
@@ -34,3 +34,4 @@ export interface CaptureResult {
|
|
|
34
34
|
reason?: string;
|
|
35
35
|
}
|
|
36
36
|
export declare function captureDesktopWindow(fullPage?: boolean): Promise<CaptureResult>;
|
|
37
|
+
export {};
|
|
@@ -48,8 +48,8 @@ function tauriCapture() {
|
|
|
48
48
|
return undefined;
|
|
49
49
|
const invoke = internals.invoke.bind(internals);
|
|
50
50
|
return async (fullPage) => {
|
|
51
|
-
const path = await invoke(RETICLE_TAURI_CAPTURE_COMMAND, { fullPage:
|
|
52
|
-
return typeof path
|
|
51
|
+
const path = await invoke(RETICLE_TAURI_CAPTURE_COMMAND, { fullPage: true === fullPage });
|
|
52
|
+
return 'string' === typeof path ? path : null;
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
@@ -94,13 +94,13 @@ async function withReticleUiHidden(capture) {
|
|
|
94
94
|
}
|
|
95
95
|
export async function captureDesktopWindow(fullPage = false) {
|
|
96
96
|
const channel = window[RETICLE_IPC_GLOBAL];
|
|
97
|
-
const capture = typeof channel?.capture
|
|
97
|
+
const capture = 'function' === typeof channel?.capture ? channel.capture : tauriCapture();
|
|
98
98
|
if (capture === undefined) {
|
|
99
99
|
return { ok: false, reason: 'no desktop capture helper installed' };
|
|
100
100
|
}
|
|
101
101
|
try {
|
|
102
102
|
const path = await withReticleUiHidden(() => capture(fullPage));
|
|
103
|
-
return typeof path
|
|
103
|
+
return 'string' === typeof path && path.length > 0
|
|
104
104
|
? { ok: true, path }
|
|
105
105
|
: { ok: false, reason: 'capture returned no image' };
|
|
106
106
|
}
|
package/dist/dom/occlusion.js
CHANGED
|
@@ -7,13 +7,13 @@ import { isReticleUi } from './dom-ignore.js';
|
|
|
7
7
|
* we never false-positive an occlusion. Reticle's own HUD is never treated as an occluder.
|
|
8
8
|
*/
|
|
9
9
|
export function hitTestOccluder(el, rect) {
|
|
10
|
-
if (rect.width
|
|
10
|
+
if (0 === rect.width || 0 === rect.height)
|
|
11
11
|
return null;
|
|
12
12
|
const doc = el.ownerDocument;
|
|
13
13
|
if (typeof doc.elementFromPoint !== 'function')
|
|
14
14
|
return null;
|
|
15
15
|
const top = doc.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
16
|
-
if (
|
|
16
|
+
if (null === top || isReticleUi(top))
|
|
17
17
|
return null;
|
|
18
18
|
const lands = top === el || el.contains(top) || top.contains(el);
|
|
19
19
|
return lands ? null : top;
|
package/dist/dom/query.js
CHANGED
|
@@ -106,7 +106,13 @@ function findIn(container, query) {
|
|
|
106
106
|
// value is the component name;.source (if present) still takes precedence inside.
|
|
107
107
|
return findByComponent(container, { ...query, component: query.component ?? value });
|
|
108
108
|
default:
|
|
109
|
-
return []
|
|
109
|
+
// THROW, never `return []`. An unsupported strategy answering "no matches" is
|
|
110
|
+
// indistinguishable from the element genuinely being absent, so `by:'css'` — the first thing
|
|
111
|
+
// anyone arriving from Playwright reaches for — reported a page with a <body> as empty. The
|
|
112
|
+
// server now rejects unknown strategies at the schema; this is the same guarantee for every
|
|
113
|
+
// other path in (replay, reticle_run, an internal caller), because a false negative invented
|
|
114
|
+
// by the tool is the exact failure this product exists to prevent.
|
|
115
|
+
throw new Error(`unsupported query strategy '${String(by)}' — use one of: ${Object.values(QueryBy).join(', ')}`);
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
// Auto-anchor (component / source) — checked before the role/text fields so a query carrying
|
|
@@ -184,7 +190,7 @@ function embeddedRootsUnder(root) {
|
|
|
184
190
|
if (!isFrame(el))
|
|
185
191
|
continue;
|
|
186
192
|
const body = readableFrameBody(el);
|
|
187
|
-
if (
|
|
193
|
+
if (null === body)
|
|
188
194
|
continue; // cross-origin — declared, not searched
|
|
189
195
|
found.push(body);
|
|
190
196
|
walk(body, depth + 1);
|
|
@@ -218,7 +224,7 @@ function findCandidates(query) {
|
|
|
218
224
|
const { container, scopeMissing } = resolveContainer(query.scope);
|
|
219
225
|
// A given-but-missing scope searches NOTHING — never the whole page. The empty result plus the
|
|
220
226
|
// scopeMissing flag is what keeps "gone scope" distinct from "absent element".
|
|
221
|
-
if (
|
|
227
|
+
if (null === container)
|
|
222
228
|
return { candidates: [], scopeMissing: true };
|
|
223
229
|
const seen = new Set();
|
|
224
230
|
const out = [];
|
|
@@ -259,7 +265,7 @@ function projectAttrs(el, keys) {
|
|
|
259
265
|
const out = {};
|
|
260
266
|
for (const key of keys.slice(0, ATTR_KEYS_MAX)) {
|
|
261
267
|
const raw = el.getAttribute(key);
|
|
262
|
-
if (
|
|
268
|
+
if (null === raw)
|
|
263
269
|
continue;
|
|
264
270
|
out[key] = isSensitiveKey(key) ? REDACTED_VALUE : raw.slice(0, ATTR_VALUE_MAX);
|
|
265
271
|
}
|
|
@@ -286,16 +292,15 @@ const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
|
|
|
286
292
|
* "how many?" stay exact while the answer stays affordable.
|
|
287
293
|
*/
|
|
288
294
|
export function matchQuery(query, state, limit = MAX_DESCRIBED) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
295
|
+
// NO blanket try/catch here. It used to turn ANY exception during candidate-finding into
|
|
296
|
+
// `elements = []`, which is the same lie as the `default` arm above: a query that could not run
|
|
297
|
+
// reported that the element is not on the page. The one failure it was plausibly guarding —
|
|
298
|
+
// a scope selector that matches nothing — is already handled explicitly and distinctly, as
|
|
299
|
+
// `scopeMissing`, so what remained was a net that could only convert real faults into false
|
|
300
|
+
// negatives.
|
|
301
|
+
const found = findCandidates(query);
|
|
302
|
+
const elements = found.candidates;
|
|
303
|
+
const scopeMissing = found.scopeMissing;
|
|
299
304
|
// One visibility cache for the whole (synchronous) query pass. isVisible is an O(depth) forced-style
|
|
300
305
|
// walk; the state filter runs it over EVERY candidate (the count must be exact) — on a match-heavy
|
|
301
306
|
// page (e.g. a 3k-row grid) that is tens of thousands of getComputedStyle calls on the host's main
|
|
@@ -306,7 +311,7 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED) {
|
|
|
306
311
|
const described = filtered.slice(0, Math.max(0, Math.min(limit, MAX_DESCRIBED)));
|
|
307
312
|
const descriptors = described.map((el) => {
|
|
308
313
|
const base = describe(el, visMemo);
|
|
309
|
-
if (attrs === undefined || attrs.length
|
|
314
|
+
if (attrs === undefined || 0 === attrs.length)
|
|
310
315
|
return base;
|
|
311
316
|
const projected = projectAttrs(el, attrs);
|
|
312
317
|
return projected === undefined ? base : { ...base, attrs: projected };
|
|
@@ -316,13 +321,18 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED) {
|
|
|
316
321
|
count: filtered.length,
|
|
317
322
|
elements: descriptors,
|
|
318
323
|
...(scopeMissing ? { scopeMissing: true } : {}),
|
|
324
|
+
// On a MISS, carry the same diagnosis `runQuery` has always returned. MATCH is the command every
|
|
325
|
+
// PREDICATE uses, so without this a failed assertion was a dead end ("no element matched") while
|
|
326
|
+
// the identical failure through reticle_query listed the testids that ARE present. Computed only
|
|
327
|
+
// when there is nothing to report, so the hot path pays nothing.
|
|
328
|
+
...(0 === filtered.length ? { hint: buildEmptyHint(query) } : {}),
|
|
319
329
|
};
|
|
320
330
|
}
|
|
321
331
|
/** Resolve `aria-labelledby` (one or more element IDs) to the referenced elements' text — a bare ID is
|
|
322
332
|
* not a human-readable name. Undefined when unset or nothing resolves. */
|
|
323
333
|
function resolveLabelledBy(el) {
|
|
324
334
|
const ids = el.getAttribute('aria-labelledby');
|
|
325
|
-
if (
|
|
335
|
+
if (null === ids)
|
|
326
336
|
return undefined;
|
|
327
337
|
const text = ids
|
|
328
338
|
.split(/\s+/)
|
|
@@ -426,8 +436,8 @@ function buildEmptyHint(query) {
|
|
|
426
436
|
*/
|
|
427
437
|
export function runQuery(query, limit) {
|
|
428
438
|
const result = matchQuery(query, undefined, limit);
|
|
429
|
-
const scopeFields = result.scopeMissing
|
|
430
|
-
if (result.elements.length
|
|
439
|
+
const scopeFields = true === result.scopeMissing ? { scopeMissing: true } : {};
|
|
440
|
+
if (0 === result.elements.length) {
|
|
431
441
|
return {
|
|
432
442
|
elements: result.elements,
|
|
433
443
|
count: result.count,
|
package/dist/dom/realm.js
CHANGED
|
@@ -15,12 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
/** The constructor named `name` in the realm that owns `node`, or undefined if there isn't one. */
|
|
17
17
|
function ctorIn(node, name) {
|
|
18
|
-
if (
|
|
18
|
+
if (null === node || typeof node !== 'object')
|
|
19
19
|
return undefined;
|
|
20
20
|
// A Document owns itself; every other node reports an ownerDocument.
|
|
21
21
|
const doc = node.ownerDocument ?? node;
|
|
22
22
|
const view = doc.defaultView;
|
|
23
|
-
if (
|
|
23
|
+
if (null === view || view === undefined)
|
|
24
24
|
return undefined;
|
|
25
25
|
return view[name];
|
|
26
26
|
}
|
|
@@ -32,17 +32,17 @@ function ctorIn(node, name) {
|
|
|
32
32
|
*/
|
|
33
33
|
function isIn(node, name, ambient) {
|
|
34
34
|
const ctor = ctorIn(node, name) ?? ambient;
|
|
35
|
-
return typeof ctor
|
|
35
|
+
return 'function' === typeof ctor && node instanceof ctor;
|
|
36
36
|
}
|
|
37
|
-
export const isElement = (n) => isIn(n, 'Element', typeof Element
|
|
38
|
-
export const isImage = (n) => isIn(n, 'HTMLImageElement', typeof HTMLImageElement
|
|
39
|
-
export const isHtmlElement = (n) => isIn(n, 'HTMLElement', typeof HTMLElement
|
|
40
|
-
export const isInput = (n) => isIn(n, 'HTMLInputElement', typeof HTMLInputElement
|
|
41
|
-
export const isTextArea = (n) => isIn(n, 'HTMLTextAreaElement', typeof HTMLTextAreaElement
|
|
42
|
-
export const isSelect = (n) => isIn(n, 'HTMLSelectElement', typeof HTMLSelectElement
|
|
43
|
-
export const isButton = (n) => isIn(n, 'HTMLButtonElement', typeof HTMLButtonElement
|
|
44
|
-
export const isForm = (n) => isIn(n, 'HTMLFormElement', typeof HTMLFormElement
|
|
45
|
-
export const isFrame = (n) => isIn(n, 'HTMLIFrameElement', typeof HTMLIFrameElement
|
|
37
|
+
export const isElement = (n) => isIn(n, 'Element', 'undefined' === typeof Element ? undefined : Element);
|
|
38
|
+
export const isImage = (n) => isIn(n, 'HTMLImageElement', 'undefined' === typeof HTMLImageElement ? undefined : HTMLImageElement);
|
|
39
|
+
export const isHtmlElement = (n) => isIn(n, 'HTMLElement', 'undefined' === typeof HTMLElement ? undefined : HTMLElement);
|
|
40
|
+
export const isInput = (n) => isIn(n, 'HTMLInputElement', 'undefined' === typeof HTMLInputElement ? undefined : HTMLInputElement);
|
|
41
|
+
export const isTextArea = (n) => isIn(n, 'HTMLTextAreaElement', 'undefined' === typeof HTMLTextAreaElement ? undefined : HTMLTextAreaElement);
|
|
42
|
+
export const isSelect = (n) => isIn(n, 'HTMLSelectElement', 'undefined' === typeof HTMLSelectElement ? undefined : HTMLSelectElement);
|
|
43
|
+
export const isButton = (n) => isIn(n, 'HTMLButtonElement', 'undefined' === typeof HTMLButtonElement ? undefined : HTMLButtonElement);
|
|
44
|
+
export const isForm = (n) => isIn(n, 'HTMLFormElement', 'undefined' === typeof HTMLFormElement ? undefined : HTMLFormElement);
|
|
45
|
+
export const isFrame = (n) => isIn(n, 'HTMLIFrameElement', 'undefined' === typeof HTMLIFrameElement ? undefined : HTMLIFrameElement);
|
|
46
46
|
/**
|
|
47
47
|
* The prototype whose native value setter drives `el` — React and friends install their own accessor
|
|
48
48
|
* on the instance, so a value write has to go through the prototype's. It must come from the
|
|
@@ -54,5 +54,5 @@ export function valuePrototypeOf(el) {
|
|
|
54
54
|
if (typeof ctor !== 'function')
|
|
55
55
|
return undefined;
|
|
56
56
|
const proto = ctor.prototype;
|
|
57
|
-
return typeof proto
|
|
57
|
+
return 'object' === typeof proto && proto !== null ? proto : undefined;
|
|
58
58
|
}
|
package/dist/dom/refs.d.ts
CHANGED
|
@@ -17,6 +17,20 @@ import { type Ref } from '@reticlehq/core';
|
|
|
17
17
|
* ref is one agent turn, and this is far more than any one turn produces.
|
|
18
18
|
*/
|
|
19
19
|
export declare const MAX_TRACKED_REFS = 10000;
|
|
20
|
+
/**
|
|
21
|
+
* A ref, made safe to interpolate into an error message.
|
|
22
|
+
*
|
|
23
|
+
* A ref Reticle mints is `e7` — three characters. A ref a CALLER passes is whatever they typed, and
|
|
24
|
+
* an agent guessing at the schema can pass 100KB of it. Every stale-ref message interpolates that
|
|
25
|
+
* value directly, so the caller's own bad argument came back as a 100KB error.
|
|
26
|
+
*
|
|
27
|
+
* The server caps error messages centrally, but it cannot help here: the browser's transport
|
|
28
|
+
* serializer truncates from the TAIL first, which severs "… no longer resolves to an element" — the
|
|
29
|
+
* exact substring the server's recovery table matches on. A stale ref then arrives classified as a
|
|
30
|
+
* possible Reticle defect. So the echo is bounded at the source, where the untrusted value enters
|
|
31
|
+
* the message.
|
|
32
|
+
*/
|
|
33
|
+
export declare function echoRef(ref: string): string;
|
|
20
34
|
export declare class RefRegistry {
|
|
21
35
|
#private;
|
|
22
36
|
/** How many reverse entries are currently retained. Exposed so the bound can be asserted. */
|
package/dist/dom/refs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asRef } from '@reticlehq/core';
|
|
1
|
+
import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* Stable, human-meaningful element handles. Each element gets a ref like `e7`; the same
|
|
4
4
|
* element keeps its ref across snapshots, and a ref re-resolves to its element as long as
|
|
@@ -17,6 +17,25 @@ import { asRef } from '@reticlehq/core';
|
|
|
17
17
|
* ref is one agent turn, and this is far more than any one turn produces.
|
|
18
18
|
*/
|
|
19
19
|
export const MAX_TRACKED_REFS = 10000;
|
|
20
|
+
/** Marks a ref that was too long to echo in full. */
|
|
21
|
+
const REF_ELISION = '…';
|
|
22
|
+
/**
|
|
23
|
+
* A ref, made safe to interpolate into an error message.
|
|
24
|
+
*
|
|
25
|
+
* A ref Reticle mints is `e7` — three characters. A ref a CALLER passes is whatever they typed, and
|
|
26
|
+
* an agent guessing at the schema can pass 100KB of it. Every stale-ref message interpolates that
|
|
27
|
+
* value directly, so the caller's own bad argument came back as a 100KB error.
|
|
28
|
+
*
|
|
29
|
+
* The server caps error messages centrally, but it cannot help here: the browser's transport
|
|
30
|
+
* serializer truncates from the TAIL first, which severs "… no longer resolves to an element" — the
|
|
31
|
+
* exact substring the server's recovery table matches on. A stale ref then arrives classified as a
|
|
32
|
+
* possible Reticle defect. So the echo is bounded at the source, where the untrusted value enters
|
|
33
|
+
* the message.
|
|
34
|
+
*/
|
|
35
|
+
export function echoRef(ref) {
|
|
36
|
+
const max = TRANSPORT_LIMITS.MAX_REF_LENGTH;
|
|
37
|
+
return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
|
|
38
|
+
}
|
|
20
39
|
/**
|
|
21
40
|
* Full-sweep cadence. The deref-sweep is O(retained), and a page legitimately holding ≥ MAX_TRACKED_REFS
|
|
22
41
|
* live elements (a 10k-node dashboard) can never get under the cap that way, so running it on EVERY mint
|
|
@@ -65,7 +65,7 @@ function sweep() {
|
|
|
65
65
|
}
|
|
66
66
|
while (roots.size > MAX_TRACKED_ROOTS) {
|
|
67
67
|
const oldest = roots.values().next();
|
|
68
|
-
if (oldest.done
|
|
68
|
+
if (true === oldest.done)
|
|
69
69
|
break;
|
|
70
70
|
roots.delete(oldest.value);
|
|
71
71
|
}
|
|
@@ -144,7 +144,7 @@ export function installShadowRegistry() {
|
|
|
144
144
|
const sweep = (node) => {
|
|
145
145
|
for (const el of node.querySelectorAll('*')) {
|
|
146
146
|
const root = el.shadowRoot;
|
|
147
|
-
if (
|
|
147
|
+
if (null === root)
|
|
148
148
|
continue;
|
|
149
149
|
record(root, el); // idempotent — a root the patch already caught is skipped inside
|
|
150
150
|
sweep(root);
|
package/dist/dom/snapshot.js
CHANGED
|
@@ -31,7 +31,7 @@ const TEXT_MAX = 80;
|
|
|
31
31
|
function directText(el) {
|
|
32
32
|
let out = '';
|
|
33
33
|
for (const node of el.childNodes) {
|
|
34
|
-
if (node.nodeType
|
|
34
|
+
if (3 === node.nodeType /* Node.TEXT_NODE */)
|
|
35
35
|
out += node.textContent ?? '';
|
|
36
36
|
}
|
|
37
37
|
const collapsed = out.replace(/\s+/g, ' ').trim();
|
|
@@ -43,7 +43,7 @@ function skipEarly(el) {
|
|
|
43
43
|
return true;
|
|
44
44
|
if (isIgnored(el))
|
|
45
45
|
return true; // Reticle overlay + known dev overlays
|
|
46
|
-
if (el.getAttribute('aria-hidden')
|
|
46
|
+
if ('true' === el.getAttribute('aria-hidden'))
|
|
47
47
|
return true;
|
|
48
48
|
if (el instanceof HTMLElement && el.hidden)
|
|
49
49
|
return true;
|
|
@@ -75,13 +75,13 @@ function formatTextLine(depth, text) {
|
|
|
75
75
|
* snapshot is blind to it; this line makes it visible. Empty for non-container elements.
|
|
76
76
|
*/
|
|
77
77
|
function layoutSignature(style) {
|
|
78
|
-
if (
|
|
78
|
+
if (null === style)
|
|
79
79
|
return '';
|
|
80
80
|
const display = style.display;
|
|
81
81
|
// Grid track templates are the high-signal CLS case (column/row count + sizing) and there are
|
|
82
82
|
// few grid containers per page. Flex is intentionally excluded: nearly every row is a flex
|
|
83
83
|
// box, so signing them all floods the snapshot for little regression value.
|
|
84
|
-
if (
|
|
84
|
+
if ('grid' === display || 'inline-grid' === display) {
|
|
85
85
|
const cols = style.gridTemplateColumns;
|
|
86
86
|
return cols !== '' && cols !== 'none' ? `grid-cols:${cols}` : 'grid';
|
|
87
87
|
}
|
|
@@ -130,7 +130,7 @@ function walk(parent, depth, ctx) {
|
|
|
130
130
|
// display-none skip and the layout signature — was two forced style resolutions per node.
|
|
131
131
|
const view = child.ownerDocument.defaultView;
|
|
132
132
|
const style = view !== null ? view.getComputedStyle(child) : null;
|
|
133
|
-
if (style !== null && style.display
|
|
133
|
+
if (style !== null && 'none' === style.display)
|
|
134
134
|
continue;
|
|
135
135
|
const role = getRole(child);
|
|
136
136
|
const name = getAccessibleName(child);
|
|
@@ -138,14 +138,14 @@ function walk(parent, depth, ctx) {
|
|
|
138
138
|
// A generic, unnamed container's own text content — only consulted outside INTERACTIVE mode,
|
|
139
139
|
// so the actionable-only view stays lean while FULL/meaningful views see content regressions.
|
|
140
140
|
const lean = ctx.mode === SnapshotMode.INTERACTIVE;
|
|
141
|
-
const text = !lean &&
|
|
141
|
+
const text = !lean && 'generic' === role && 0 === name.length ? directText(child) : '';
|
|
142
142
|
// Layout signature for grid/flex containers — makes CLS/layout regressions visible.
|
|
143
143
|
const layout = lean ? '' : layoutSignature(style);
|
|
144
144
|
const meaningful = interactive || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
|
|
145
145
|
const include = lean ? interactive : meaningful;
|
|
146
146
|
if (include) {
|
|
147
147
|
ctx.nodes += 1;
|
|
148
|
-
ctx.lines.push(text.length > 0 && name.length
|
|
148
|
+
ctx.lines.push(text.length > 0 && 0 === name.length && 0 === layout.length
|
|
149
149
|
? formatTextLine(depth, text)
|
|
150
150
|
: formatLine(child, depth, role, name, layout));
|
|
151
151
|
walk(child, depth + 1, ctx);
|
package/dist/dom/source.js
CHANGED
|
@@ -24,14 +24,14 @@ import { DATA_RETICLE_SOURCE_ATTR } from '@reticlehq/core';
|
|
|
24
24
|
const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
|
|
25
25
|
/** Parse a `data-reticle-source="file:line:column"` value into `{ file, line }` (column dropped). */
|
|
26
26
|
export function parseSourceAttr(value) {
|
|
27
|
-
if (
|
|
27
|
+
if (null === value)
|
|
28
28
|
return undefined;
|
|
29
29
|
const m = /^(.*):(\d+):(\d+)$/.exec(value);
|
|
30
|
-
if (
|
|
30
|
+
if (null === m)
|
|
31
31
|
return undefined;
|
|
32
32
|
const file = m[1];
|
|
33
33
|
const line = Number(m[2]);
|
|
34
|
-
if (file === undefined || file.length
|
|
34
|
+
if (file === undefined || 0 === file.length || !Number.isFinite(line))
|
|
35
35
|
return undefined;
|
|
36
36
|
return { file, line };
|
|
37
37
|
}
|
package/dist/dom/theme.js
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
let cached = null;
|
|
12
12
|
/** Resolve any CSS color string to canonical computed `rgb...)`/`rgba...)`, or null if not a color. */
|
|
13
13
|
function toRgb(value) {
|
|
14
|
-
if (value.length
|
|
14
|
+
if (0 === value.length)
|
|
15
15
|
return null;
|
|
16
16
|
const probe = document.createElement('span');
|
|
17
17
|
probe.style.color = '';
|
|
18
18
|
probe.style.color = value; // invalid colors leave it empty (the setter rejects them)
|
|
19
|
-
if (probe.style.color
|
|
19
|
+
if ('' === probe.style.color)
|
|
20
20
|
return null;
|
|
21
21
|
probe.style.position = 'absolute';
|
|
22
22
|
probe.style.pointerEvents = 'none';
|
|
@@ -36,7 +36,7 @@ function collectTokens() {
|
|
|
36
36
|
catch {
|
|
37
37
|
continue;
|
|
38
38
|
}
|
|
39
|
-
if (
|
|
39
|
+
if (null === rules)
|
|
40
40
|
continue;
|
|
41
41
|
for (const rule of Array.from(rules)) {
|
|
42
42
|
if (!(rule instanceof CSSStyleRule))
|
|
@@ -66,15 +66,15 @@ function palette() {
|
|
|
66
66
|
}
|
|
67
67
|
/** True for a color that carries no visual weight (fully transparent) — never flagged off-theme. */
|
|
68
68
|
function isTransparent(rgb) {
|
|
69
|
-
return
|
|
69
|
+
return 'rgba(0, 0, 0, 0)' === rgb || 'transparent' === rgb;
|
|
70
70
|
}
|
|
71
71
|
/** Report an element's theme compliance from its computed style. */
|
|
72
72
|
export function themeReport(cs) {
|
|
73
73
|
const p = palette();
|
|
74
74
|
const colorToken = p.byColor.get(cs.color) ?? null;
|
|
75
75
|
const backgroundToken = p.byColor.get(cs.backgroundColor) ?? null;
|
|
76
|
-
const colorOff = !isTransparent(cs.color) &&
|
|
77
|
-
const bgOff = !isTransparent(cs.backgroundColor) &&
|
|
76
|
+
const colorOff = !isTransparent(cs.color) && null === colorToken;
|
|
77
|
+
const bgOff = !isTransparent(cs.backgroundColor) && null === backgroundToken;
|
|
78
78
|
return {
|
|
79
79
|
colorToken,
|
|
80
80
|
backgroundToken,
|
package/dist/dom/virtualized.js
CHANGED
|
@@ -26,7 +26,7 @@ function rowHost(container) {
|
|
|
26
26
|
let host = container;
|
|
27
27
|
for (let depth = 0; depth < 3; depth += 1) {
|
|
28
28
|
const children = Array.from(host.children).filter((c) => c instanceof HTMLElement);
|
|
29
|
-
const only = children.length
|
|
29
|
+
const only = 1 === children.length ? children[0] : undefined;
|
|
30
30
|
if (only === undefined)
|
|
31
31
|
return host;
|
|
32
32
|
// A spacer is a single child that fills (or over-fills) the scroll area.
|
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ export { registerAdapter, identifyComponent, readComponentState, elementHasHover
|
|
|
8
8
|
export type { ReticleAdapter, ComponentInfo, ComponentSource } from './registry/adapters.js';
|
|
9
9
|
export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation, } from './registry/stores.js';
|
|
10
10
|
export type { StoreGetter, StoreLike, StoreSubscribe } from './registry/stores.js';
|
|
11
|
-
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, pushStore, } from './registry/store-adapters.js';
|
|
12
|
-
export type { QuerySnapshot } from './registry/store-adapters.js';
|
|
13
|
-
export { registerCapabilities, getCapabilities, hasCapabilities } from './registry/capabilities.js';
|
|
11
|
+
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
|
|
12
|
+
export type { QuerySnapshot, RecoilAtomSnapshot } from './registry/store-adapters.js';
|
|
13
|
+
export { registerCapabilities, getCapabilities, hasCapabilities, setCapabilitiesListener, } from './registry/capabilities.js';
|
|
14
14
|
export type { Capabilities, CapabilitiesInput, CapabilityFlow } from './registry/capabilities.js';
|
|
15
15
|
export { createReticleEmitter } from './registry/emitter.js';
|
|
16
16
|
export type { ReticleEmitter, EmitterTarget, CreateReticleEmitterOptions, } from './registry/emitter.js';
|
package/dist/index.js
CHANGED
|
@@ -18,9 +18,9 @@ export { registerStore, unregisterStore, storeNames, readStores, readStoresWithT
|
|
|
18
18
|
// Adapters for state libraries that do not natively expose `{getState, subscribe}`. zustand and Redux
|
|
19
19
|
// need none of these; TanStack Query is the one that matters most, because a stale-cache bug fires no
|
|
20
20
|
// request and renders a plausible value, so the cache is the only witness.
|
|
21
|
-
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, pushStore, } from './registry/store-adapters.js';
|
|
21
|
+
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
|
|
22
22
|
// Capability registry: the app self-describes its testable surface via reticle.describe.
|
|
23
|
-
export { registerCapabilities, getCapabilities, hasCapabilities } from './registry/capabilities.js';
|
|
23
|
+
export { registerCapabilities, getCapabilities, hasCapabilities, setCapabilitiesListener, } from './registry/capabilities.js';
|
|
24
24
|
// SDK helpers: adopt the recommended integration patterns without boilerplate.
|
|
25
25
|
export { createReticleEmitter } from './registry/emitter.js';
|
|
26
26
|
export { commitAndSignal } from './registry/commit-and-signal.js';
|
|
@@ -51,10 +51,10 @@ export function countClosedShadowRoots() {
|
|
|
51
51
|
*/
|
|
52
52
|
export function isCrossOriginFrame(frame) {
|
|
53
53
|
const src = frame.getAttribute('src');
|
|
54
|
-
if (
|
|
54
|
+
if (null === src || 0 === src.length)
|
|
55
55
|
return false;
|
|
56
56
|
try {
|
|
57
|
-
return frame.contentDocument
|
|
57
|
+
return null === frame.contentDocument;
|
|
58
58
|
}
|
|
59
59
|
catch {
|
|
60
60
|
return true; // SecurityError on access is itself proof of cross-origin
|
|
@@ -12,7 +12,7 @@ const METHOD_EVENT = {
|
|
|
12
12
|
function stringifyArgs(args) {
|
|
13
13
|
return args
|
|
14
14
|
.map((a) => {
|
|
15
|
-
if (typeof a
|
|
15
|
+
if ('string' === typeof a)
|
|
16
16
|
return a;
|
|
17
17
|
if (a instanceof Error)
|
|
18
18
|
return a.message;
|
|
@@ -23,7 +23,7 @@ function stringifyArgs(args) {
|
|
|
23
23
|
/** Stacks can be long; cap so a deep async trace never blows the event budget. */
|
|
24
24
|
const MAX_STACK_LEN = TRANSPORT_LIMITS.MAX_STACK_LENGTH;
|
|
25
25
|
function capStack(stack) {
|
|
26
|
-
if (stack === undefined || stack.length
|
|
26
|
+
if (stack === undefined || 0 === stack.length)
|
|
27
27
|
return undefined;
|
|
28
28
|
return stack.length > MAX_STACK_LEN ? stack.slice(0, MAX_STACK_LEN) : stack;
|
|
29
29
|
}
|
|
@@ -47,7 +47,7 @@ export function installConsole(emit) {
|
|
|
47
47
|
const callOriginal = original.bind(console);
|
|
48
48
|
const wrapper = (...args) => {
|
|
49
49
|
// Only console.error carries a stack — the diagnosis case; log/warn stay lean.
|
|
50
|
-
const stack =
|
|
50
|
+
const stack = 'error' === method ? firstErrorStack(args) : undefined;
|
|
51
51
|
emit(METHOD_EVENT[method], {
|
|
52
52
|
message: stringifyArgs(args),
|
|
53
53
|
...(stack === undefined ? {} : { stack }),
|
package/dist/observers/dom.js
CHANGED
|
@@ -9,7 +9,7 @@ import { refs } from '../dom/refs.js';
|
|
|
9
9
|
*/
|
|
10
10
|
function regionKeyOf(target) {
|
|
11
11
|
const el = isElement(target) ? target : null;
|
|
12
|
-
if (
|
|
12
|
+
if (null === el)
|
|
13
13
|
return undefined;
|
|
14
14
|
const labelled = el.closest('[data-testid]');
|
|
15
15
|
return labelled?.getAttribute('data-testid') ?? refs.refFor(el);
|
|
@@ -37,7 +37,7 @@ const WATCHED_ATTRS = [
|
|
|
37
37
|
/** Attribute/text values are capped per event — style/src can be huge and would bloat the ledger. */
|
|
38
38
|
const MAX_ATTR_VALUE_LEN = 120;
|
|
39
39
|
function capValue(value) {
|
|
40
|
-
if (
|
|
40
|
+
if (null === value)
|
|
41
41
|
return undefined;
|
|
42
42
|
return value.length > MAX_ATTR_VALUE_LEN ? `${value.slice(0, MAX_ATTR_VALUE_LEN)}…` : value;
|
|
43
43
|
}
|
|
@@ -82,7 +82,7 @@ export function installDom(emit) {
|
|
|
82
82
|
// (it can include not-yet-inspected noise) but `dropped > 0` honestly means "this batch was capped".
|
|
83
83
|
let dropped = 0;
|
|
84
84
|
for (const record of records) {
|
|
85
|
-
if (record.type
|
|
85
|
+
if ('attributes' === record.type) {
|
|
86
86
|
const target = record.target;
|
|
87
87
|
if (isElement(target) && record.attributeName !== null && !isReticleOverlay(target)) {
|
|
88
88
|
if (changed >= MAX_PER_BATCH) {
|
|
@@ -101,7 +101,7 @@ export function installDom(emit) {
|
|
|
101
101
|
}
|
|
102
102
|
continue;
|
|
103
103
|
}
|
|
104
|
-
if (record.type
|
|
104
|
+
if ('characterData' === record.type) {
|
|
105
105
|
// In-place text change inside an existing subtree (wizard steps, inline edits) —
|
|
106
106
|
// childList-only would miss this.
|
|
107
107
|
const parent = record.target.parentElement;
|
|
@@ -158,7 +158,7 @@ export function installDom(emit) {
|
|
|
158
158
|
emit(EventType.DOM_ADDED, { role, name, region: regionKeyOf(record.target) }, ref);
|
|
159
159
|
if (DIALOG_ROLES.has(role) ||
|
|
160
160
|
LIVE_ROLES.has(role) ||
|
|
161
|
-
node.getAttribute('aria-modal')
|
|
161
|
+
'true' === node.getAttribute('aria-modal')) {
|
|
162
162
|
if (isVisible(node))
|
|
163
163
|
emit(EventType.VISIBLE_SHOWN, { role, name }, ref);
|
|
164
164
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Emit, Teardown } from './types.js';
|
|
2
|
-
|
|
2
|
+
interface DownloadOptions {
|
|
3
3
|
/** Include a content preview. Gated for the same reason request bodies are: exports hold PII. */
|
|
4
4
|
capturePreview?: boolean;
|
|
5
5
|
}
|
|
6
6
|
export declare function installDownload(emit: Emit, opts?: DownloadOptions): Teardown;
|
|
7
|
+
export {};
|
|
@@ -63,7 +63,7 @@ export function installDownload(emit, opts = {}) {
|
|
|
63
63
|
// The LINE COUNT is reported even without preview capture: it is a number, not content, and it
|
|
64
64
|
// is the whole tell for "25 rows in a file the toast calls 128".
|
|
65
65
|
data['lines'] = text.split('\n').filter((l) => l.length > 0).length;
|
|
66
|
-
if (opts.capturePreview
|
|
66
|
+
if (true === opts.capturePreview) {
|
|
67
67
|
const { body, truncated } = projectBody(text.slice(0, PREVIEW_CHARS), record.type);
|
|
68
68
|
data['preview'] = body;
|
|
69
69
|
if (truncated)
|
|
@@ -79,7 +79,7 @@ export function installDownload(emit, opts = {}) {
|
|
|
79
79
|
const onClick = (event) => {
|
|
80
80
|
const target = event.target;
|
|
81
81
|
const anchor = target instanceof Element ? target.closest('a[download]') : null;
|
|
82
|
-
if (
|
|
82
|
+
if (null === anchor)
|
|
83
83
|
return;
|
|
84
84
|
report(anchor.getAttribute('href') ?? '', anchor.getAttribute('download') ?? undefined);
|
|
85
85
|
};
|
package/dist/observers/focus.js
CHANGED
|
@@ -9,7 +9,7 @@ export function focusLabel(el) {
|
|
|
9
9
|
return name.length > 0 ? `${role} "${name}"` : role;
|
|
10
10
|
}
|
|
11
11
|
function toBody(el) {
|
|
12
|
-
return
|
|
12
|
+
return null === el || el === document.body;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Track element focus movement. Focus dropping to <body> after an act — a modal that closed and stranded
|
package/dist/observers/frames.js
CHANGED