@reticlehq/browser 2.13.1 → 2.14.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 (196) hide show
  1. package/dist/actions/actions-dom.d.ts +0 -20
  2. package/dist/actions/actions-dom.js +0 -66
  3. package/dist/actions/actions-press.d.ts +10 -0
  4. package/dist/actions/actions-press.js +77 -0
  5. package/dist/actions/actions.d.ts +0 -89
  6. package/dist/actions/actions.js +24 -358
  7. package/dist/actions/appeared-text.d.ts +0 -20
  8. package/dist/actions/appeared-text.js +0 -36
  9. package/dist/actions/danger-context.d.ts +3 -0
  10. package/dist/actions/danger-context.js +39 -0
  11. package/dist/actions/scroll.d.ts +0 -9
  12. package/dist/actions/scroll.js +0 -7
  13. package/dist/actions/synthetic-input.d.ts +0 -7
  14. package/dist/actions/synthetic-input.js +0 -27
  15. package/dist/actions/value-input.d.ts +0 -30
  16. package/dist/actions/value-input.js +0 -33
  17. package/dist/commands/commands.d.ts +0 -2
  18. package/dist/commands/commands.js +0 -67
  19. package/dist/connect-options.d.ts +2 -96
  20. package/dist/dom/a11y.d.ts +0 -29
  21. package/dist/dom/a11y.js +1 -88
  22. package/dist/dom/auto-anchor.d.ts +0 -40
  23. package/dist/dom/auto-anchor.js +0 -33
  24. package/dist/dom/chart.d.ts +0 -61
  25. package/dist/dom/chart.js +0 -60
  26. package/dist/dom/desktop-capture.d.ts +0 -30
  27. package/dist/dom/desktop-capture.js +0 -72
  28. package/dist/dom/dom-ignore.d.ts +0 -30
  29. package/dist/dom/dom-ignore.js +0 -47
  30. package/dist/dom/occlusion.d.ts +0 -7
  31. package/dist/dom/occlusion.js +0 -7
  32. package/dist/dom/query.d.ts +0 -14
  33. package/dist/dom/query.js +48 -214
  34. package/dist/dom/realm.d.ts +0 -20
  35. package/dist/dom/realm.js +0 -28
  36. package/dist/dom/refs.d.ts +0 -76
  37. package/dist/dom/refs.js +2 -133
  38. package/dist/dom/shadow-registry.d.ts +0 -33
  39. package/dist/dom/shadow-registry.js +2 -72
  40. package/dist/dom/snapshot.d.ts +2 -43
  41. package/dist/dom/snapshot.js +26 -151
  42. package/dist/dom/source.d.ts +0 -44
  43. package/dist/dom/source.js +0 -45
  44. package/dist/dom/theme.d.ts +0 -37
  45. package/dist/dom/theme.js +2 -52
  46. package/dist/dom/virtualized.d.ts +0 -17
  47. package/dist/dom/virtualized.js +0 -27
  48. package/dist/edit-epoch.d.ts +0 -22
  49. package/dist/edit-epoch.js +0 -52
  50. package/dist/index.js +1 -23
  51. package/dist/observers/animation.d.ts +0 -1
  52. package/dist/observers/animation.js +0 -3
  53. package/dist/observers/blind-spots.d.ts +0 -26
  54. package/dist/observers/blind-spots.js +2 -61
  55. package/dist/observers/console.d.ts +1 -2
  56. package/dist/observers/console.js +41 -30
  57. package/dist/observers/context-open.d.ts +1 -11
  58. package/dist/observers/context-open.js +10 -14
  59. package/dist/observers/departure.d.ts +3 -0
  60. package/dist/observers/departure.js +52 -0
  61. package/dist/observers/dialogs.d.ts +2 -0
  62. package/dist/observers/dialogs.js +39 -0
  63. package/dist/observers/dom.js +0 -44
  64. package/dist/observers/download.d.ts +1 -2
  65. package/dist/observers/download.js +38 -55
  66. package/dist/observers/focus.d.ts +0 -6
  67. package/dist/observers/focus.js +0 -7
  68. package/dist/observers/frames.d.ts +0 -9
  69. package/dist/observers/frames.js +1 -28
  70. package/dist/observers/health.d.ts +0 -5
  71. package/dist/observers/health.js +1 -33
  72. package/dist/observers/install-all.d.ts +2 -0
  73. package/dist/observers/install-all.js +26 -27
  74. package/dist/observers/ipc.d.ts +0 -25
  75. package/dist/observers/ipc.js +0 -65
  76. package/dist/observers/navigation.d.ts +2 -0
  77. package/dist/observers/navigation.js +33 -0
  78. package/dist/observers/network-body.d.ts +5 -28
  79. package/dist/observers/network-body.js +11 -108
  80. package/dist/observers/network-redact.d.ts +1 -20
  81. package/dist/observers/network-redact.js +1 -80
  82. package/dist/observers/network-stream.d.ts +0 -21
  83. package/dist/observers/network-stream.js +1 -44
  84. package/dist/observers/network.d.ts +2 -26
  85. package/dist/observers/network.js +177 -294
  86. package/dist/observers/perf.d.ts +0 -10
  87. package/dist/observers/perf.js +0 -16
  88. package/dist/observers/route.d.ts +1 -2
  89. package/dist/observers/route.js +13 -19
  90. package/dist/observers/scroll.d.ts +0 -1
  91. package/dist/observers/scroll.js +0 -9
  92. package/dist/observers/sdk-failure.d.ts +1 -9
  93. package/dist/observers/sdk-failure.js +1 -30
  94. package/dist/observers/state.d.ts +1 -11
  95. package/dist/observers/state.js +13 -41
  96. package/dist/observers/storage.d.ts +0 -14
  97. package/dist/observers/storage.js +10 -34
  98. package/dist/observers/types.d.ts +0 -15
  99. package/dist/observers/types.js +0 -14
  100. package/dist/patching/capture-method.d.ts +0 -24
  101. package/dist/patching/capture-method.js +0 -26
  102. package/dist/presenter/overlay.d.ts +0 -1
  103. package/dist/presenter/overlay.js +0 -1
  104. package/dist/presenter/presenter-brand.d.ts +0 -19
  105. package/dist/presenter/presenter-brand.js +0 -21
  106. package/dist/presenter/presenter-config.d.ts +1 -105
  107. package/dist/presenter/presenter-config.js +2 -91
  108. package/dist/presenter/presenter-controls.d.ts +0 -29
  109. package/dist/presenter/presenter-controls.js +3 -131
  110. package/dist/presenter/presenter-dock-layout.d.ts +0 -3
  111. package/dist/presenter/presenter-dock-layout.js +0 -4
  112. package/dist/presenter/presenter-drag.d.ts +2 -24
  113. package/dist/presenter/presenter-drag.js +0 -26
  114. package/dist/presenter/presenter-effects.d.ts +0 -9
  115. package/dist/presenter/presenter-effects.js +0 -9
  116. package/dist/presenter/presenter-glow.d.ts +0 -21
  117. package/dist/presenter/presenter-glow.js +1 -27
  118. package/dist/presenter/presenter-heroicons-data.d.ts +0 -8
  119. package/dist/presenter/presenter-heroicons-data.js +0 -8
  120. package/dist/presenter/presenter-hud-chrome.d.ts +0 -10
  121. package/dist/presenter/presenter-hud-chrome.js +0 -10
  122. package/dist/presenter/presenter-icons.d.ts +0 -6
  123. package/dist/presenter/presenter-icons.js +0 -6
  124. package/dist/presenter/presenter-log.d.ts +0 -38
  125. package/dist/presenter/presenter-log.js +1 -45
  126. package/dist/presenter/presenter-report-copy.d.ts +5 -66
  127. package/dist/presenter/presenter-report-copy.js +5 -66
  128. package/dist/presenter/presenter-report-styles.d.ts +0 -6
  129. package/dist/presenter/presenter-report-styles.js +6 -6
  130. package/dist/presenter/presenter-report.d.ts +4 -7
  131. package/dist/presenter/presenter-report.js +18 -76
  132. package/dist/presenter/presenter-run-state.d.ts +0 -7
  133. package/dist/presenter/presenter-run-state.js +0 -5
  134. package/dist/presenter/presenter-settings-styles.d.ts +0 -1
  135. package/dist/presenter/presenter-settings-styles.js +0 -1
  136. package/dist/presenter/presenter-settings.d.ts +1 -23
  137. package/dist/presenter/presenter-settings.js +1 -31
  138. package/dist/presenter/presenter-shell-styles.js +2 -5
  139. package/dist/presenter/presenter-shell.d.ts +2 -18
  140. package/dist/presenter/presenter-shell.js +1 -73
  141. package/dist/presenter/presenter-styles.d.ts +0 -9
  142. package/dist/presenter/presenter-styles.js +0 -9
  143. package/dist/presenter/presenter-tally.d.ts +0 -5
  144. package/dist/presenter/presenter-tally.js +0 -4
  145. package/dist/presenter/presenter-test-helpers.d.ts +0 -2
  146. package/dist/presenter/presenter-test-helpers.js +0 -2
  147. package/dist/presenter/presenter-verbs.js +0 -7
  148. package/dist/presenter/presenter-workspace.d.ts +0 -4
  149. package/dist/presenter/presenter-workspace.js +1 -8
  150. package/dist/presenter/presenter.d.ts +0 -60
  151. package/dist/presenter/presenter.js +4 -118
  152. package/dist/recorder/recorder-styles.d.ts +0 -6
  153. package/dist/recorder/recorder-styles.js +0 -6
  154. package/dist/recorder/recorder.d.ts +0 -34
  155. package/dist/recorder/recorder.js +0 -49
  156. package/dist/registry/adapters.d.ts +0 -10
  157. package/dist/registry/adapters.js +0 -10
  158. package/dist/registry/auto-testids.d.ts +0 -19
  159. package/dist/registry/auto-testids.js +1 -21
  160. package/dist/registry/capabilities.d.ts +0 -31
  161. package/dist/registry/capabilities.js +1 -46
  162. package/dist/registry/commit-and-signal.d.ts +0 -6
  163. package/dist/registry/commit-and-signal.js +1 -7
  164. package/dist/registry/domains.d.ts +0 -8
  165. package/dist/registry/domains.js +0 -7
  166. package/dist/registry/emitter.d.ts +0 -14
  167. package/dist/registry/emitter.js +0 -10
  168. package/dist/registry/store-adapters.d.ts +0 -186
  169. package/dist/registry/store-adapters.js +0 -163
  170. package/dist/registry/stores.d.ts +0 -33
  171. package/dist/registry/stores.js +0 -59
  172. package/dist/reticle-presenter-helpers.d.ts +0 -13
  173. package/dist/reticle-presenter-helpers.js +0 -14
  174. package/dist/reticle.d.ts +1 -52
  175. package/dist/reticle.js +20 -145
  176. package/dist/review/annotator-styles.js +0 -7
  177. package/dist/review/annotator.d.ts +2 -13
  178. package/dist/review/annotator.js +0 -40
  179. package/dist/review/mark-anchor.d.ts +0 -11
  180. package/dist/review/mark-anchor.js +0 -8
  181. package/dist/security/serialization.d.ts +0 -20
  182. package/dist/security/serialization.js +1 -66
  183. package/dist/session-continuity.d.ts +0 -16
  184. package/dist/session-continuity.js +1 -20
  185. package/dist/timers/clock.d.ts +1 -14
  186. package/dist/timers/clock.js +59 -60
  187. package/dist/timers/native-console.js +0 -4
  188. package/dist/timers/native-timers.d.ts +0 -25
  189. package/dist/timers/native-timers.js +3 -33
  190. package/dist/transport/transport.d.ts +0 -51
  191. package/dist/transport/transport.js +3 -129
  192. package/dist/transport/unreachable-message.d.ts +0 -17
  193. package/dist/transport/unreachable-message.js +0 -17
  194. package/dist/util/captured-method.d.ts +0 -35
  195. package/dist/util/captured-method.js +0 -35
  196. package/package.json +2 -2
@@ -1,26 +1,6 @@
1
- /**
2
- * Gathers the text an action put on the page, from mutation records the observer already receives.
3
- *
4
- * `domMutatedWithin` counts records and throws their content away, so a failed login reports
5
- * `ok / settled / mutated` and reads exactly like a successful one. The message the app rendered
6
- * was passing through this callback the whole time.
7
- *
8
- * Deliberately reports what appeared, never what it means: no error/success classification, no
9
- * guessing which fragment matters. Truncated, because a route change can add a whole page.
10
- */
11
1
  export declare class AppearedText {
12
2
  #private;
13
3
  collect(records: readonly MutationRecord[]): void;
14
- /**
15
- * `{ appeared }` when the APP added text, `{}` otherwise — an absent key means it added none.
16
- *
17
- * `wrote` is the value the action itself just set, and is excluded: a textarea carries its value
18
- * in a child text node, so a controlled one re-rendering after the write mutates characterData
19
- * with the caller's own string. Handing that back is noise wearing the name of evidence, and
20
- * `valueChanged` already reports that the write landed. Exact-match only, so an app that quotes
21
- * your input inside a sentence of its own ("No results for zzz") is still reported — that is
22
- * the app talking.
23
- */
24
4
  effect(wrote?: string): {
25
5
  appeared?: string;
26
6
  };
@@ -1,34 +1,11 @@
1
1
  import { isIgnored } from '../dom/dom-ignore.js';
2
- /** Cap on the reported text. Enough for an error message, far short of a re-rendered page. */
3
2
  const APPEARED_MAX = 200;
4
- /** Separator between distinct fragments that appeared in the same window. */
5
3
  const JOIN = ' | ';
6
4
  const TEXT_NODE = 3;
7
- /** Any letter, in any script — Latin, Cyrillic, CJK, Arabic. */
8
5
  const HAS_LETTER = /\p{L}/u;
9
- /**
10
- * A fragment with no letters at all is not the app saying something.
11
- *
12
- * The Hostile fixture mutates a counter every 16ms, and clicking a button on that page reported
13
- * `appeared: "409"` — the ticker, not the action's effect. A count-up animation is exactly what
14
- * emits a bare number into the settle window, and a bare number carries no message a reader can
15
- * act on. Deliberately conservative: it drops only fragments with NO letters, so "status 500",
16
- * "3 items deleted" and "Could not save" all survive. An app whose only feedback is a naked
17
- * numeral loses it here — the snapshot still shows it, and that is the cheaper mistake.
18
- */
19
6
  function saysSomething(text) {
20
7
  return HAS_LETTER.test(text);
21
8
  }
22
- /**
23
- * Gathers the text an action put on the page, from mutation records the observer already receives.
24
- *
25
- * `domMutatedWithin` counts records and throws their content away, so a failed login reports
26
- * `ok / settled / mutated` and reads exactly like a successful one. The message the app rendered
27
- * was passing through this callback the whole time.
28
- *
29
- * Deliberately reports what appeared, never what it means: no error/success classification, no
30
- * guessing which fragment matters. Truncated, because a route change can add a whole page.
31
- */
32
9
  export class AppearedText {
33
10
  #seen = new Set();
34
11
  #length = 0;
@@ -37,7 +14,6 @@ export class AppearedText {
37
14
  if (this.#full())
38
15
  return;
39
16
  if ('characterData' === record.type) {
40
- // The NEW value; oldValue is not requested, so this is what the reader now sees.
41
17
  this.#add(record.target.textContent, record.target.parentElement);
42
18
  continue;
43
19
  }
@@ -49,16 +25,6 @@ export class AppearedText {
49
25
  }
50
26
  }
51
27
  }
52
- /**
53
- * `{ appeared }` when the APP added text, `{}` otherwise — an absent key means it added none.
54
- *
55
- * `wrote` is the value the action itself just set, and is excluded: a textarea carries its value
56
- * in a child text node, so a controlled one re-rendering after the write mutates characterData
57
- * with the caller's own string. Handing that back is noise wearing the name of evidence, and
58
- * `valueChanged` already reports that the write landed. Exact-match only, so an app that quotes
59
- * your input inside a sentence of its own ("No results for zzz") is still reported — that is
60
- * the app talking.
61
- */
62
28
  effect(wrote) {
63
29
  const said = [...this.#seen].filter((text) => text !== wrote);
64
30
  if (0 === said.length)
@@ -72,8 +38,6 @@ export class AppearedText {
72
38
  return this.#length > APPEARED_MAX;
73
39
  }
74
40
  #add(raw, owner) {
75
- // Reticle's own overlay mutates constantly (the "live" panel); reporting it as the app's
76
- // response would be worse than reporting nothing.
77
41
  if (null !== owner && isIgnored(owner))
78
42
  return;
79
43
  const text = (raw ?? '').replace(/\s+/g, ' ').trim();
@@ -0,0 +1,3 @@
1
+ export declare function dangerousActionContext(el: HTMLElement): string;
2
+ export declare function submitControlFor(el: HTMLElement): HTMLElement | null;
3
+ export declare function requiresDangerousConfirmation(text: string, role?: string): boolean;
@@ -0,0 +1,39 @@
1
+ import { isDangerousActionText } from '@reticlehq/core';
2
+ import { getAccessibleName } from '../dom/a11y.js';
3
+ const LABEL_VALUED_INPUT_TYPES = new Set([
4
+ 'submit',
5
+ 'button',
6
+ 'reset',
7
+ 'image',
8
+ ]);
9
+ function holdsUserText(el) {
10
+ if (el.isContentEditable)
11
+ return true;
12
+ if (el instanceof HTMLTextAreaElement)
13
+ return true;
14
+ if (el instanceof HTMLInputElement)
15
+ return !LABEL_VALUED_INPUT_TYPES.has(el.type.toLowerCase());
16
+ return false;
17
+ }
18
+ export function dangerousActionContext(el) {
19
+ const form = el.closest('form');
20
+ const ownText = holdsUserText(el)
21
+ ? ['', '']
22
+ : [el.textContent ?? '', el.getAttribute('value') ?? ''];
23
+ return [
24
+ getAccessibleName(el),
25
+ ...ownText,
26
+ el.getAttribute('title') ?? '',
27
+ el.getAttribute('aria-label') ?? '',
28
+ el.getAttribute('href') ?? '',
29
+ form?.getAttribute('action') ?? '',
30
+ ].join(' ');
31
+ }
32
+ const SUBMIT_CONTROL_SELECTOR = 'button[type="submit"], input[type="submit"], button:not([type]):not([type=""])';
33
+ export function submitControlFor(el) {
34
+ const found = el.closest('form')?.querySelector(SUBMIT_CONTROL_SELECTOR);
35
+ return found instanceof HTMLElement ? found : null;
36
+ }
37
+ export function requiresDangerousConfirmation(text, role) {
38
+ return isDangerousActionText(text, role);
39
+ }
@@ -1,18 +1,9 @@
1
- /** Outcome of one container scroll — enough for the server to drive a find loop. */
2
1
  interface ScrollResult {
3
- /** scrollTop actually moved (false ⇒ already at the end / not scrollable). */
4
2
  scrolled: boolean;
5
3
  scrollTop: number;
6
4
  scrollHeight: number;
7
5
  clientHeight: number;
8
- /** At (or within 1px of) the bottom — no more rows to reveal by scrolling down. */
9
6
  atEnd: boolean;
10
7
  }
11
- /**
12
- * Scroll the container of `ref` (or the document) down by `dy` (default ~one viewport) so a
13
- * virtualized list mounts its next window of rows. If `fraction` (0–1) is supplied, jumps
14
- * directly to that fraction of scrollHeight instead of stepping — enables bisection for large
15
- * lists where a specific row index is known (targetIndex / totalCount = fraction).
16
- */
17
8
  export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number): ScrollResult;
18
9
  export {};
@@ -1,7 +1,6 @@
1
1
  import { refs } from '../dom/refs.js';
2
2
  const FALLBACK_STEP_PX = 400;
3
3
  const VIEWPORT_FRACTION = 0.8;
4
- /** The nearest scrollable ancestor of `el` (inclusive), else the document scrolling element. */
5
4
  function nearestScrollable(el) {
6
5
  let cur = el;
7
6
  while (cur !== null && cur !== document.body) {
@@ -12,12 +11,6 @@ function nearestScrollable(el) {
12
11
  }
13
12
  return document.scrollingElement ?? document.documentElement;
14
13
  }
15
- /**
16
- * Scroll the container of `ref` (or the document) down by `dy` (default ~one viewport) so a
17
- * virtualized list mounts its next window of rows. If `fraction` (0–1) is supplied, jumps
18
- * directly to that fraction of scrollHeight instead of stepping — enables bisection for large
19
- * lists where a specific row index is known (targetIndex / totalCount = fraction).
20
- */
21
14
  export function scrollContainer(ref, dy, fraction) {
22
15
  const base = ref !== undefined ? refs.resolve(ref) : null;
23
16
  const target = base instanceof Element
@@ -1,9 +1,2 @@
1
- /** True while a Reticle-dispatched input event is being delivered. */
2
1
  export declare function isSyntheticInput(): boolean;
3
- /**
4
- * Run `dispatch` marked as Reticle's own.
5
- *
6
- * `finally` rather than a trailing decrement: a listener that throws must not leave the flag stuck
7
- * on, which would silently disable annotation for the rest of the page's life.
8
- */
9
2
  export declare function asSyntheticInput<T>(dispatch: () => T): T;
@@ -1,34 +1,7 @@
1
- /**
2
- * Is the input event currently being dispatched one that RETICLE made?
3
- *
4
- * The annotator listens for clicks in the capture phase and cancels them, because a click in
5
- * annotate mode is a request to place a mark rather than a request to press a button. That is right
6
- * for a person and wrong for the agent: `reticle_act` dispatches its clicks into the same document,
7
- * so with the HUD expanded every agent click was swallowed while the action still reported
8
- * `dispatched: true`. Measured on this repo's own fixture — the same click that produced
9
- * `POST /api/login -> 200` with annotate off produced no network at all with it on.
10
- *
11
- * `isTrusted` looks like the answer and is not usable: it is false for everything `dispatchEvent`
12
- * creates, which is also how every test in this package drives the annotator, so gating on it would
13
- * make annotation untestable and would still not distinguish Reticle's clicks from the page's own
14
- * synthetic ones.
15
- *
16
- * So Reticle marks its own. Dispatch is synchronous, so a plain depth counter held across the call
17
- * is exact: any listener running inside it — capture phase included — is running because of us.
18
- * Nested actions are counted rather than flagged, so an inner action finishing does not clear the
19
- * mark for the outer one still in flight.
20
- */
21
1
  let depth = 0;
22
- /** True while a Reticle-dispatched input event is being delivered. */
23
2
  export function isSyntheticInput() {
24
3
  return depth > 0;
25
4
  }
26
- /**
27
- * Run `dispatch` marked as Reticle's own.
28
- *
29
- * `finally` rather than a trailing decrement: a listener that throws must not leave the flag stuck
30
- * on, which would silently disable annotation for the rest of the page's life.
31
- */
32
5
  export function asSyntheticInput(dispatch) {
33
6
  depth += 1;
34
7
  try {
@@ -1,33 +1,3 @@
1
- /**
2
- * Writing a value into a field, and the two preconditions that make it honest.
3
- *
4
- * Lifted out of actions.ts when that file hit the 600-line cap. It is a cohesive unit: every export
5
- * here exists so that a value write either lands as a real user's edit would, or is refused with a
6
- * reason — never forced through while reporting success.
7
- */
8
- /**
9
- * Set a value on a controlled input the way React expects (native setter + input event).
10
- * Returns the `input` event's dispatchEvent result (defaultPrevented of the primary event).
11
- */
12
1
  export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
13
- /**
14
- * Refuse to write to a field a person could not edit.
15
- *
16
- * `readonly` and `disabled` block USER input, not scripted assignment, so the prototype value setter
17
- * writes straight through both. Measured: filling a readonly input reported `ok:true` and
18
- * `valueChanged:true` with nothing in the effect block marking it read-only. That leaves the app in a
19
- * state no user could reach, and every conclusion drawn from what follows is about software nobody
20
- * can operate — the same reason the click path reports `occluded`, and the same choice Playwright
21
- * makes when it refuses to fill a non-editable element.
22
- */
23
- /**
24
- * A rich-text target is UNSUPPORTED, not a mistake by the caller.
25
- *
26
- * Every rich-text editor (TipTap, Quill, ProseMirror, Slate, Lexical) is `[contenteditable]`, and
27
- * these actions handle only input/textarea. `cannot fill a <div>` reads as "you picked the wrong
28
- * element" and sends the reader hunting for a better selector that does not exist. Support is not
29
- * faked: assigning textContent would update the DOM while the editor's own model kept the old value,
30
- * so the tool would report success for content the app will never submit.
31
- */
32
2
  export declare function assertNotRichText(el: HTMLElement, action: string): void;
33
3
  export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
@@ -1,18 +1,6 @@
1
1
  import { captureValueSetter } from '../patching/capture-method.js';
2
2
  import { valuePrototypeOf } from '../dom/realm.js';
3
- /**
4
- * Writing a value into a field, and the two preconditions that make it honest.
5
- *
6
- * Lifted out of actions.ts when that file hit the 600-line cap. It is a cohesive unit: every export
7
- * here exists so that a value write either lands as a real user's edit would, or is refused with a
8
- * reason — never forced through while reporting success.
9
- */
10
- /**
11
- * Set a value on a controlled input the way React expects (native setter + input event).
12
- * Returns the `input` event's dispatchEvent result (defaultPrevented of the primary event).
13
- */
14
3
  export function setNativeValue(el, value) {
15
- // Realm-aware: a controlled input inside a same-origin frame has its own prototype.
16
4
  const proto = valuePrototypeOf(el);
17
5
  const setter = proto === undefined ? undefined : captureValueSetter(proto);
18
6
  if (setter !== undefined) {
@@ -25,28 +13,7 @@ export function setNativeValue(el, value) {
25
13
  el.dispatchEvent(new Event('change', { bubbles: true }));
26
14
  return !notPrevented;
27
15
  }
28
- /**
29
- * Refuse to write to a field a person could not edit.
30
- *
31
- * `readonly` and `disabled` block USER input, not scripted assignment, so the prototype value setter
32
- * writes straight through both. Measured: filling a readonly input reported `ok:true` and
33
- * `valueChanged:true` with nothing in the effect block marking it read-only. That leaves the app in a
34
- * state no user could reach, and every conclusion drawn from what follows is about software nobody
35
- * can operate — the same reason the click path reports `occluded`, and the same choice Playwright
36
- * makes when it refuses to fill a non-editable element.
37
- */
38
- /**
39
- * A rich-text target is UNSUPPORTED, not a mistake by the caller.
40
- *
41
- * Every rich-text editor (TipTap, Quill, ProseMirror, Slate, Lexical) is `[contenteditable]`, and
42
- * these actions handle only input/textarea. `cannot fill a <div>` reads as "you picked the wrong
43
- * element" and sends the reader hunting for a better selector that does not exist. Support is not
44
- * faked: assigning textContent would update the DOM while the editor's own model kept the old value,
45
- * so the tool would report success for content the app will never submit.
46
- */
47
16
  export function assertNotRichText(el, action) {
48
- // The ATTRIBUTE, not `isContentEditable`: jsdom does not implement the property, so a test would
49
- // pass against a guard that never fires in the environment the unit suite runs in.
50
17
  const flag = el.getAttribute('contenteditable');
51
18
  if (el.isContentEditable || (flag !== null && flag !== 'false')) {
52
19
  throw new Error(`cannot ${action} a contenteditable element — rich-text editors keep their own document model, ` +
@@ -1,6 +1,4 @@
1
1
  export type CommandHandler = (args: Record<string, unknown>) => unknown;
2
- /** Query param appended on a hard reload to bypass the browser cache. */
3
2
  export declare const RELOAD_CACHE_BUST_PARAM = "_reticle_reload";
4
3
  export declare function resolveNavigationUrl(rawUrl: string, baseUrl: string): string | null;
5
- /** Map browser command names to handlers. Used by the transport on each COMMAND. */
6
4
  export declare function createCommandRegistry(): Map<string, CommandHandler>;
@@ -17,7 +17,6 @@ import { sanitizeWithReport } from '../security/serialization.js';
17
17
  import { getCapabilities } from '../registry/capabilities.js';
18
18
  import { freezeClock, advanceClock, resetClock, isClockFrozen } from '../timers/clock.js';
19
19
  import { scrollContainer } from '../actions/scroll.js';
20
- /** Query param appended on a hard reload to bypass the browser cache. */
21
20
  export const RELOAD_CACHE_BUST_PARAM = '_reticle_reload';
22
21
  function str(value) {
23
22
  return 'string' === typeof value ? value : undefined;
@@ -49,10 +48,6 @@ function queryFromArgs(args) {
49
48
  alt: str(args['alt']),
50
49
  component: str(args['component']),
51
50
  scope: str(args['scope']),
52
- // The THIRD allowlist a query input has to appear in — tool schema, server forward, and here.
53
- // `self` was in the first two and missing from this one, so a live call returned zero matches on
54
- // an element that was plainly on the page, with no error to explain it. Same shape as the
55
- // `attrs` drop below it.
56
51
  self: true === args['self'] ? true : undefined,
57
52
  attrs: Array.isArray(args['attrs'])
58
53
  ? args['attrs'].filter((a) => 'string' === typeof a)
@@ -62,20 +57,12 @@ function queryFromArgs(args) {
62
57
  }
63
58
  function inspect(ref) {
64
59
  const el = refs.resolve(ref);
65
- // THROW, exactly as executeAction does. Returning an error payload made it a SUCCESSFUL command
66
- // result, which the server then handed to reticle_inspect's outputSchema (ref/role/name/states/
67
- // visible all required) — so the MCP layer answered -32602 Output validation error for the most
68
- // ordinary thing that follows a click. The wording matches actions.ts because the server's
69
- // recovery table keys off /no longer resolves to an element/ to attach the stale-ref recovery.
70
60
  if (null === el)
71
61
  throw new Error(editEpoch.staleRefMessage(ref));
72
62
  const rect = el.getBoundingClientRect();
73
63
  const component = identifyComponent(el);
74
64
  const view = el.ownerDocument.defaultView;
75
65
  const cs = view !== null ? view.getComputedStyle(el) : null;
76
- // Computed style the a11y tree is blind to — `cursor` (does it look interactive?), display/
77
- // visibility, and color/opacity — so a UI bug that leaves the element "present but unusable"
78
- // (dead cursor, invisible, recolored) is observable in one inspect call.
79
66
  const styles = cs !== null
80
67
  ? {
81
68
  color: cs.color,
@@ -86,16 +73,7 @@ function inspect(ref) {
86
73
  visibility: cs.visibility,
87
74
  }
88
75
  : null;
89
- // `describe()` carries the CHEAP source (the nearest babel-stamped ancestor), because it runs per
90
- // element on paths that describe hundreds at once. inspect is a single-element path, so it takes
91
- // the adapter's answer first — the component that actually RENDERED this element, not the nearest
92
- // stamped host. `act` already did this; inspect did not, so the two tools disagreed about the same
93
- // ref and inspect — the tool you reach for to ask where something lives — was the one saying null.
94
76
  const source = formatSource(sourceFor(el, component?.source));
95
- // Say WHY the source is missing rather than omitting the field and leaving the caller to guess.
96
- // No stamp anywhere in the document is decisive: the loader is not running, so no element will
97
- // have one this session, and the fix is a build-config change rather than anything about this
98
- // element. Only computed when `source` is already absent, so the ordinary path pays nothing.
99
77
  const sourceUnavailable = source !== undefined
100
78
  ? undefined
101
79
  : documentHasSourceStamps(el.ownerDocument)
@@ -116,21 +94,16 @@ function inspect(ref) {
116
94
  formAction: isButton(el) || isInput(el) ? (el.form?.getAttribute('action') ?? undefined) : undefined,
117
95
  formText: isButton(el) || isInput(el) ? (el.form?.textContent ?? undefined) : undefined,
118
96
  box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
119
- // True when another element sits over this one's center point — the click would hit the overlay,
120
- // not this control (a z-index/overlay bug the DOM tree cannot show).
121
97
  occluded: isOccluded(el, rect),
122
98
  styles,
123
99
  scroll,
124
- // Theme compliance vs the app's design tokens (off-theme colors a DOM tool can't judge).
125
100
  theme: cs !== null ? themeReport(cs) : null,
126
101
  component,
127
102
  };
128
103
  }
129
- /** Whether a NON-Reticle element covers this one's center point (a transparent overlay / z-index bug). */
130
104
  function isOccluded(el, rect) {
131
105
  return hitTestOccluder(el, rect) !== null;
132
106
  }
133
- /** Narrowing guard: an adapter returned a ComponentStateResult (has a boolean `ok`). */
134
107
  function isComponentStateResult(value) {
135
108
  return ('object' === typeof value && value !== null && 'ok' in value && 'boolean' === typeof value.ok);
136
109
  }
@@ -138,21 +111,8 @@ const COMPONENT_UNAVAILABLE = {
138
111
  ok: false,
139
112
  reason: ComponentStateReason.UNAVAILABLE,
140
113
  };
141
- /**
142
- * Stores are the reliable path. The `ref` component read is bounded: a stale ref, no
143
- * adapter, or an adapter returning a non-conforming value all collapse to a structured
144
- * `{ ok: false, reason }` — never a raw (possibly circular) object that could hang serialization.
145
- *
146
- * `path`/`depth` are applied HERE, in the page, BEFORE the value crosses the transport — so a scoped
147
- * read of a huge store (e.g. `deployments.0.status` on a 500-row store) returns only the small
148
- * sub-tree and is never truncated. (Previously selection ran server-side, AFTER the whole store had
149
- * already been size-capped in transit, which silently lost any field after a large array.)
150
- */
151
114
  function readState(ref, store, path, depth) {
152
115
  const names = storeNames();
153
- // Scoped read: walk `path` into the RAW (uncapped) store, then sanitize only the selected sub-tree.
154
- // Selecting before the transport cap is what lets a deep/large path (row 250 of a 500-row array)
155
- // resolve — capping first would truncate the store before selection ever reached the row.
156
116
  if (path !== undefined || depth !== undefined) {
157
117
  const rawStores = readStoresRaw(store);
158
118
  const base = store !== undefined ? rawStores[store] : { stores: rawStores, storeNames: names };
@@ -164,13 +124,8 @@ function readState(ref, store, path, depth) {
164
124
  path,
165
125
  found: selection.found,
166
126
  value: projected.value,
167
- // Even a SCOPED read can hit the caps when the selected sub-tree is itself large; saying so is
168
- // what keeps "the list is short" distinguishable from "I shortened the list".
169
127
  ...(projected.truncation === undefined ? {} : { truncation: projected.truncation }),
170
128
  ...('availableKeys' in selection ? { availableKeys: selection.availableKeys } : {}),
171
- // How many keys there REALLY were, when the near-miss list is a sample. Without it, 50 names
172
- // and no marker reads as "the key you asked for does not exist" — the strongest negative
173
- // signal there is, and a false one when the key is simply number 51.
174
129
  ...('totalKeys' in selection ? { totalKeys: selection.totalKeys } : {}),
175
130
  storeNames: names,
176
131
  };
@@ -180,8 +135,6 @@ function readState(ref, store, path, depth) {
180
135
  stores,
181
136
  storeNames: names,
182
137
  };
183
- // The whole-store read is where a large store silently became a small one. Present only when a cap
184
- // actually fired, so an intact read is unchanged and the field's presence is the warning.
185
138
  if (truncation !== undefined)
186
139
  result.truncation = truncation;
187
140
  if (ref !== undefined && ref.length > 0) {
@@ -196,19 +149,6 @@ function readState(ref, store, path, depth) {
196
149
  }
197
150
  return result;
198
151
  }
199
- /**
200
- * Every animation, and WHICH element it drives.
201
- *
202
- * Reported from the field: a page with nine concurrent animations returned nine indistinguishable
203
- * `{playState, currentTime, duration}` rows. `a.effect` was read only for `getTiming()`, never for
204
- * its `target` — so the tool advertised "targets/timing" and shipped timing alone. A list you cannot
205
- * index is not a list, and the question this tool exists to answer is "is THAT thing still
206
- * animating".
207
- *
208
- * `describe()` is reused rather than inventing a target shape: it is what `reticle_query` returns,
209
- * so the descriptor carries a `ref` the agent can pass straight to `reticle_act` — a target you
210
- * cannot act on would be half an answer.
211
- */
212
152
  function listAnimations() {
213
153
  const doc = document;
214
154
  if (typeof doc.getAnimations !== 'function')
@@ -216,9 +156,6 @@ function listAnimations() {
216
156
  const animations = doc.getAnimations().map((a) => {
217
157
  const effect = a.effect;
218
158
  const timing = effect?.getTiming();
219
- // A KeyframeEffect's target is legitimately nullable, and only KeyframeEffect has one at all.
220
- // `null` is reported explicitly: omitting the key would make "this animation has no element"
221
- // indistinguishable from the bug above, which is the distinction the fix exists to draw.
222
159
  const target = effect?.target ?? null;
223
160
  return {
224
161
  playState: a.playState,
@@ -240,14 +177,11 @@ export function resolveNavigationUrl(rawUrl, baseUrl) {
240
177
  return null;
241
178
  }
242
179
  }
243
- /** Map browser command names to handlers. Used by the transport on each COMMAND. */
244
180
  export function createCommandRegistry() {
245
181
  const reg = new Map();
246
182
  reg.set(ReticleCommand.SNAPSHOT, (args) => buildSnapshot({
247
183
  scope: str(args['scope']),
248
184
  mode: str(args['mode']) ?? SnapshotMode.FULL,
249
- // Only the completion re-read sets this: it re-reads a branch the truncated walk stopped
250
- // BEFORE emitting, so that branch's own line has to come back with its descendants.
251
185
  includeRoot: true === args['includeRoot'],
252
186
  }));
253
187
  reg.set(ReticleCommand.QUERY, (args) => {
@@ -300,7 +234,6 @@ export function createCommandRegistry() {
300
234
  });
301
235
  reg.set(ReticleCommand.REFRESH, (args) => {
302
236
  if (true === args['hard']) {
303
- // Hard reload: navigate to self with a cache-busting param then replace history.
304
237
  const url = new URL(window.location.href);
305
238
  url.searchParams.set(RELOAD_CACHE_BUST_PARAM, String(Date.now()));
306
239
  window.location.replace(url.toString());
@@ -1,120 +1,26 @@
1
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
2
  export interface ReticleConnectOptions {
11
- /** WS endpoint of the local bridge. Defaults to ws://localhost:<port><path>. */
12
3
  url?: string;
13
- /** Human-friendly session label so the agent can target the right tab. */
14
4
  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
5
  projectId?: string;
20
- /**
21
- * Browser/bridge pairing token. Required whenever the daemon has one, which is the ordinary case:
22
- * it auto-provisions `~/.reticle/pairing-token` on first start and then refuses a hello without it,
23
- * including on localhost. Non-localhost additionally needs `allowNonLocalhost`.
24
- */
25
6
  token?: string;
26
- /** Explicitly allow Reticle on a non-localhost page or bridge. Requires token. */
27
7
  allowNonLocalhost?: boolean;
28
- /**
29
- * Escape hatch for the production backstop. Reticle is dev-only and refuses to connect when the
30
- * build reports NODE_ENV=production (an SSR healthcheck or a prod bundle opened locally would
31
- * otherwise activate). The real fix is to gate the import behind `import.meta.env.DEV` so it's
32
- * tree-shaken out; this flag only exists for the rare intentional prod diagnostic.
33
- */
34
8
  allowInProduction?: boolean;
35
- /** Show a small in-page status chip (connection + event count). */
36
9
  overlay?: boolean;
37
- /**
38
- * Capture request/response bodies on net.request events (dev-only; text-like content only,
39
- * sensitive keys redacted, per-body capped). Off by default - bodies cost tokens and can carry PII.
40
- */
41
10
  captureNetworkBodies?: boolean;
42
- /**
43
- * Make Reticle's OWN presenter visible to snapshots and queries. CONTRIBUTORS ONLY.
44
- *
45
- * The presenter is hidden from every tool by design, for a good reason: an agent that can drive
46
- * Reticle's own interface can fabricate its own impact report, and Reticle chrome in a snapshot is
47
- * noise in every other app on earth.
48
- *
49
- * The cost of that rule is that a HUD change is the only kind of change Reticle cannot be used to
50
- * check — the panel rendering it is invisible to everything that could look at it. This hatch
51
- * exists for exactly that case: the app under test IS Reticle. It reports itself in the app's
52
- * capabilities, so a verdict drawn with it open can never be mistaken for an ordinary one.
53
- */
11
+ sourceMapping?: boolean;
12
+ captureErrorBodies?: boolean;
54
13
  exposePresenter?: boolean;
55
- /**
56
- * The project root, so source paths report repo-relative instead of absolute.
57
- *
58
- * React's `_debugSource.fileName` is absolute; the babel stamp is repo-relative. Build plugins that
59
- * can `define` a global set it themselves; frameworks that cannot (Next passes it through env,
60
- * Astro through its own config) hand it here instead. Taking it as a connect OPTION is what keeps
61
- * the generated app-side code plain JavaScript - the previous shape made the caller assign a global,
62
- * and the TypeScript cast that needed shipped into a `.jsx` file and broke the build.
63
- */
64
14
  root?: string;
65
- /**
66
- * The SDK's own package version, so a version-skewed pair against the daemon can name itself
67
- * rather than surfacing as a bare `-32000`. Build plugins supply it automatically; pass it here
68
- * only for a hand-wired connect that has no plugin.
69
- */
70
15
  sdkVersion?: string;
71
- /** Presenter mode: glow border, animated cursor, click/hover effects, narration HUD. */
72
16
  present?: boolean;
73
- /** Per-action pacing (ms) in presenter mode so a human can follow. Default 450. */
74
17
  pace?: number;
75
- /** Min ms each narration line stays visible before the next replaces it (presenter). Default 3000. */
76
18
  narrationDwellMs?: number;
77
- /**
78
- * Border behavior in presenter mode: 'session' (default) persists the border for the whole
79
- * session; 'busy' restores the fade-after-idle behavior.
80
- */
81
19
  border?: 'session' | 'busy';
82
- /** Max accumulated activity-log rows before the oldest are pruned (presenter). Default 50. */
83
20
  logMax?: number;
84
- /**
85
- * Mount the floating human-recorder toolbar (Record/Stop/Annotate).
86
- * Default off - purely additive, dev-only.
87
- */
88
21
  recorder?: boolean;
89
- /**
90
- * Mount the page annotator: expanding the HUD enters click-to-annotate mode, and Reticle emits
91
- * a HUMAN_MARK the agent drains via reticle_review. Defaults to ON with the presenter; pass
92
- * `annotate: false` to suppress.
93
- */
94
22
  annotate?: boolean;
95
- /** Live-control: overridable ended-border fade delay (native timer). Default 4000. */
96
23
  endedFadeMs?: number;
97
- /** Session auto-end after this much agent idle (presenter). Default 5min; agent-tunable via reticle_session. */
98
24
  idleEndMs?: number;
99
- /**
100
- * Extend the redaction rules with your app's own vocabulary.
101
- *
102
- * ```ts
103
- * reticle.connect({
104
- * redact: {
105
- * keys: ['deviceSecretRef', /^partner[-_]?code$/i], // also redact these
106
- * allow: ['designToken'], // stop redacting this false positive
107
- * },
108
- * });
109
- * ```
110
- *
111
- * Additive only - there is no way to replace the built-in rule, because a config that could would
112
- * eventually ship an app that leaks. `keys` strings match a key name exactly (case-insensitively);
113
- * a RegExp is tested against it. `allow` exempts a key from the DEFAULT rule and loses to `keys`.
114
- *
115
- * Literal `keys` strings also cross the bridge, so the daemon redacts them on the driven path,
116
- * where request bodies are captured raw from the network stack and never pass through this SDK.
117
- * RegExp entries and `allow` do NOT cross - see docs/usage.md ("Extending the redaction rules") for why, and what that means.
118
- */
119
25
  redact?: RedactionConfig;
120
26
  }
@@ -1,38 +1,9 @@
1
1
  import { ElementState, type ElementDescriptor } from '@reticlehq/core';
2
- /** Compute the ARIA role (explicit wins, else implicit from the tag). */
3
2
  export declare function getRole(el: Element): string;
4
3
  export declare function getAccessibleName(el: Element): string;
5
- /**
6
- * The set of states relevant to assertions. `visible` is an O(depth) forced-style walk; callers that
7
- * already computed it (describe) pass it in so it isn't resolved twice per element.
8
- */
9
4
  export declare function getStates(el: Element, visible?: boolean): ElementState[];
10
- /**
11
- * True when a form field holds a secret the SDK must never capture verbatim — a password input, a
12
- * sensitive `autocomplete` (cc-number, one-time-code, …), or a name/id/testid/aria-label that trips
13
- * `isSensitiveKey`. The single source of truth for both live-snapshot redaction (`getValue`) and the
14
- * flow recorder's fill-value redaction, so recorded flows never persist typed passwords/OTPs/keys.
15
- */
16
5
  export declare function isSensitiveField(el: Element): boolean;
17
6
  export declare function getValue(el: Element): string | undefined;
18
- /**
19
- * Whether the element is actually visible (not display:none/hidden/aria-hidden/opacity:0), walking to
20
- * root. This is an O(depth) forced-style walk PER node; `memo` (optional, scoped to ONE synchronous
21
- * query pass) caches the full inherited result per element so a broad state-filtered query stops
22
- * re-resolving getComputedStyle up the same ancestor chain for every sibling. Sound because the DOM is
23
- * static for the pass's duration — the cache MUST be a per-call Map, never module-level (that would go
24
- * stale the instant the app mutates, the same trap the shadow-root note in query.ts documents).
25
- */
26
- /**
27
- * True when the element is inside the viewport right now: visible AND its bounding box intersects
28
- * the window. This is what makes a scroll assertable (#398) — content below the fold of a scrolling
29
- * container is `visible`/`present` before any scroll, so only a viewport-intersection check can tell
30
- * "scrolled into view" from "was always in the DOM". Uses getBoundingClientRect (viewport-relative,
31
- * already accounts for scroll position), not an IntersectionObserver, so it stays synchronous inside
32
- * the predicate pass.
33
- */
34
7
  export declare function isInViewport(el: Element, memo?: Map<Element, boolean>): boolean;
35
8
  export declare function isVisible(el: Element, memo?: Map<Element, boolean>): boolean;
36
- /** Build the compact descriptor surfaced to the agent. `memo` (optional) shares the per-call
37
- * visibility cache with the query's state filter so ancestors aren't re-walked per element. */
38
9
  export declare function describe(el: Element, memo?: Map<Element, boolean>): ElementDescriptor;