@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.
Files changed (79) hide show
  1. package/dist/actions/actions-dom.js +10 -10
  2. package/dist/actions/actions.d.ts +13 -0
  3. package/dist/actions/actions.js +45 -15
  4. package/dist/actions/scroll.js +1 -1
  5. package/dist/commands/commands.js +39 -22
  6. package/dist/connect-options.d.ts +103 -0
  7. package/dist/connect-options.js +1 -0
  8. package/dist/dom/a11y.js +20 -20
  9. package/dist/dom/auto-anchor.js +1 -1
  10. package/dist/dom/blind-spots.js +1 -1
  11. package/dist/dom/chart.js +5 -5
  12. package/dist/dom/desktop-capture.d.ts +2 -1
  13. package/dist/dom/desktop-capture.js +4 -4
  14. package/dist/dom/occlusion.js +2 -2
  15. package/dist/dom/query.js +28 -18
  16. package/dist/dom/realm.js +13 -13
  17. package/dist/dom/refs.d.ts +14 -0
  18. package/dist/dom/refs.js +20 -1
  19. package/dist/dom/shadow-registry.js +2 -2
  20. package/dist/dom/snapshot.js +7 -7
  21. package/dist/dom/source.js +3 -3
  22. package/dist/dom/theme.js +6 -6
  23. package/dist/dom/virtualized.js +1 -1
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +2 -2
  26. package/dist/observers/blind-spots.js +2 -2
  27. package/dist/observers/console.js +3 -3
  28. package/dist/observers/dom.js +5 -5
  29. package/dist/observers/download.d.ts +2 -1
  30. package/dist/observers/download.js +2 -2
  31. package/dist/observers/focus.js +1 -1
  32. package/dist/observers/frames.js +1 -1
  33. package/dist/observers/health.js +70 -2
  34. package/dist/observers/install-all.d.ts +2 -1
  35. package/dist/observers/ipc.d.ts +2 -7
  36. package/dist/observers/ipc.js +7 -7
  37. package/dist/observers/network-body.js +2 -2
  38. package/dist/observers/network-redact.js +5 -5
  39. package/dist/observers/network-stream.js +2 -2
  40. package/dist/observers/network.d.ts +3 -3
  41. package/dist/observers/network.js +9 -9
  42. package/dist/observers/perf.js +1 -1
  43. package/dist/observers/scroll.js +1 -1
  44. package/dist/observers/state.js +1 -1
  45. package/dist/observers/storage.js +7 -7
  46. package/dist/presenter/presenter-brand.d.ts +19 -0
  47. package/dist/presenter/presenter-brand.js +21 -0
  48. package/dist/presenter/presenter-config.d.ts +17 -0
  49. package/dist/presenter/presenter-config.js +21 -0
  50. package/dist/presenter/presenter-controls.d.ts +1 -11
  51. package/dist/presenter/presenter-controls.js +6 -7
  52. package/dist/presenter/presenter-log.js +2 -2
  53. package/dist/presenter/presenter-run-state.js +1 -1
  54. package/dist/presenter/presenter-styles.d.ts +1 -1
  55. package/dist/presenter/presenter-styles.js +20 -9
  56. package/dist/presenter/presenter-tally.js +3 -3
  57. package/dist/presenter/presenter-test-helpers.d.ts +2 -1
  58. package/dist/presenter/presenter-test-helpers.js +2 -2
  59. package/dist/presenter/presenter-verbs.js +16 -11
  60. package/dist/presenter/presenter.js +13 -10
  61. package/dist/recorder/recorder.js +10 -9
  62. package/dist/registry/capabilities.d.ts +2 -0
  63. package/dist/registry/capabilities.js +16 -0
  64. package/dist/registry/store-adapters.d.ts +127 -1
  65. package/dist/registry/store-adapters.js +178 -0
  66. package/dist/registry/stores.js +3 -3
  67. package/dist/reticle-presenter-helpers.js +3 -3
  68. package/dist/reticle.d.ts +2 -57
  69. package/dist/reticle.js +57 -14
  70. package/dist/review/annotator.js +7 -7
  71. package/dist/security/serialization.js +9 -9
  72. package/dist/session-continuity.d.ts +17 -0
  73. package/dist/session-continuity.js +42 -0
  74. package/dist/timers/clock.js +4 -4
  75. package/dist/timers/native-console.js +1 -1
  76. package/dist/timers/native-timers.js +4 -4
  77. package/dist/transport/transport.d.ts +10 -0
  78. package/dist/transport/transport.js +19 -5
  79. 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 === 'function')
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 (win === null)
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 top === null
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 === 'function') {
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 === 'function') {
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 === 'function') {
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 === 'object' && entry !== null) {
90
+ if ('object' === typeof entry && entry !== null) {
91
91
  const e = entry;
92
- if (typeof e.mime === 'string' && typeof e.value === 'string')
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 === 'function' && type.startsWith('pointer')) {
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 === 'function') {
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
  }
@@ -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 === 'string' ? value : fallback;
9
+ return 'string' === typeof value ? value : fallback;
10
10
  }
11
11
  function requireElement(ref) {
12
12
  const el = refs.resolve(ref);
13
- if (el === null)
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 && name.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') === '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 (active === null || active === el.ownerDocument.body)
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'] === 'number' ? args['holdMs'] : 0;
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
- el.value = args['value'];
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 (form === null)
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);
@@ -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 ((oy === 'auto' || oy === 'scroll') && cur.scrollHeight > cur.clientHeight)
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 === 'string' ? value : undefined;
22
+ return 'string' === typeof value ? value : undefined;
22
23
  }
23
24
  function num(value) {
24
- return typeof value === 'number' ? value : undefined;
25
+ return 'number' === typeof value ? value : undefined;
25
26
  }
26
27
  function record(value) {
27
- return typeof value === 'object' && value !== null ? value : {};
28
+ return 'object' === typeof value && value !== null ? value : {};
28
29
  }
29
30
  function sourceLocation(value) {
30
- if (typeof value !== 'object' || value === null)
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 === 'string')
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
- if (el === null)
59
- return { error: `ref '${ref}' no longer resolves` };
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 === 'object' && value !== null && 'ok' in value && typeof value.ok === 'boolean');
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 (el === null) {
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 === 0 || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_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 url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : null;
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 === 'number' ? limit : undefined);
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] === true);
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'] === true) {
226
+ if (true === args['reset']) {
210
227
  resetClock();
211
228
  }
212
229
  else {
213
- if (args['freeze'] === true)
230
+ if (true === args['freeze'])
214
231
  freezeClock();
215
232
  const adv = args['advanceMs'];
216
- if (typeof adv === 'number')
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'] === true));
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 === 'number' ? dy : undefined, typeof fraction === 'number' ? fraction : undefined);
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 === 0)
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 (url === null)
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'] === true) {
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 (type === 'checkbox')
48
+ if ('checkbox' === type)
49
49
  return 'checkbox';
50
- if (type === 'radio')
50
+ if ('radio' === type)
51
51
  return 'radio';
52
- if (type === 'range')
52
+ if ('range' === type)
53
53
  return 'slider';
54
- if (type === 'number')
54
+ if ('number' === type)
55
55
  return 'spinbutton';
56
- if (type === 'submit' || type === 'button' || type === 'reset')
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 (ids === null)
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') === 'true')
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 (value === null)
194
+ if (null === value)
195
195
  return undefined;
196
- return value === 'true';
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') === true;
206
+ const disabled = disabledProp || true === ariaBool(el, 'aria-disabled');
207
207
  states.push(disabled ? ElementState.DISABLED : ElementState.ENABLED);
208
- const checkedProp = isInput(el) && (el.type === 'checkbox' || el.type === 'radio') && el.checked;
209
- if (checkedProp || ariaBool(el, 'aria-checked') === true)
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') === true)
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() === 'password') ||
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') === 'true')
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 === 'none' ||
258
- style.visibility === 'hidden' ||
259
- style.visibility === 'collapse') {
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') === 0)
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) && (parent === null || isVisible(parent, memo));
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;
@@ -32,7 +32,7 @@ export const AnchorStrategy = {
32
32
  POSITION: 'position',
33
33
  };
34
34
  function nonEmpty(s) {
35
- return typeof s === 'string' && s.length > 0;
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) {