@reticlehq/browser 2.13.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/actions/actions-dom.d.ts +8 -20
  2. package/dist/actions/actions-dom.js +70 -69
  3. package/dist/actions/actions-press.d.ts +14 -0
  4. package/dist/actions/actions-press.js +123 -0
  5. package/dist/actions/actions.d.ts +1 -89
  6. package/dist/actions/actions.js +64 -365
  7. package/dist/actions/appeared-text.d.ts +0 -20
  8. package/dist/actions/appeared-text.js +6 -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 +8 -10
  12. package/dist/actions/scroll.js +28 -17
  13. package/dist/actions/synthetic/synthetic-input.d.ts +2 -0
  14. package/dist/actions/synthetic/synthetic-input.js +13 -0
  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 +10 -70
  19. package/dist/connect-options.d.ts +2 -96
  20. package/dist/dom/a11y.d.ts +0 -29
  21. package/dist/dom/a11y.js +3 -90
  22. package/dist/dom/addressing/auto-anchor.d.ts +25 -0
  23. package/dist/dom/addressing/auto-anchor.js +41 -0
  24. package/dist/dom/addressing/refs.d.ts +14 -0
  25. package/dist/dom/addressing/refs.js +114 -0
  26. package/dist/dom/addressing/source.d.ts +9 -0
  27. package/dist/dom/addressing/source.js +29 -0
  28. package/dist/dom/chart.d.ts +0 -61
  29. package/dist/dom/chart.js +0 -60
  30. package/dist/dom/desktop-capture.d.ts +0 -30
  31. package/dist/dom/desktop-capture.js +1 -73
  32. package/dist/dom/dom-ignore.d.ts +0 -30
  33. package/dist/dom/dom-ignore.js +1 -48
  34. package/dist/dom/occlusion.d.ts +0 -7
  35. package/dist/dom/occlusion.js +31 -12
  36. package/dist/dom/paint-context.d.ts +6 -0
  37. package/dist/dom/paint-context.js +38 -0
  38. package/dist/dom/query.d.ts +0 -14
  39. package/dist/dom/query.js +50 -216
  40. package/dist/dom/realm.d.ts +0 -20
  41. package/dist/dom/realm.js +0 -28
  42. package/dist/dom/shadow-registry.d.ts +0 -33
  43. package/dist/dom/shadow-registry.js +2 -72
  44. package/dist/dom/snapshot.d.ts +2 -43
  45. package/dist/dom/snapshot.js +27 -152
  46. package/dist/dom/theme.d.ts +0 -37
  47. package/dist/dom/theme.js +2 -52
  48. package/dist/dom/virtualized.d.ts +0 -17
  49. package/dist/dom/virtualized.js +0 -27
  50. package/dist/edit-epoch.d.ts +1 -23
  51. package/dist/edit-epoch.js +1 -53
  52. package/dist/index.d.ts +4 -5
  53. package/dist/index.js +4 -28
  54. package/dist/observers/animation.d.ts +0 -1
  55. package/dist/observers/animation.js +1 -4
  56. package/dist/observers/blind-spots.d.ts +0 -26
  57. package/dist/observers/blind-spots.js +3 -62
  58. package/dist/observers/console.d.ts +1 -2
  59. package/dist/observers/console.js +41 -30
  60. package/dist/observers/context-open.d.ts +1 -11
  61. package/dist/observers/context-open.js +10 -14
  62. package/dist/observers/departure.d.ts +3 -0
  63. package/dist/observers/departure.js +52 -0
  64. package/dist/observers/dialogs.d.ts +2 -0
  65. package/dist/observers/dialogs.js +39 -0
  66. package/dist/observers/dom.js +28 -52
  67. package/dist/observers/download.d.ts +1 -2
  68. package/dist/observers/download.js +39 -56
  69. package/dist/observers/focus.d.ts +0 -6
  70. package/dist/observers/focus.js +0 -7
  71. package/dist/observers/frames.d.ts +0 -9
  72. package/dist/observers/frames.js +1 -28
  73. package/dist/observers/health.d.ts +0 -5
  74. package/dist/observers/health.js +2 -34
  75. package/dist/observers/install-all.d.ts +2 -0
  76. package/dist/observers/install-all.js +26 -27
  77. package/dist/observers/ipc.d.ts +0 -25
  78. package/dist/observers/ipc.js +0 -65
  79. package/dist/observers/navigation.d.ts +2 -0
  80. package/dist/observers/navigation.js +33 -0
  81. package/dist/observers/net-detail/network-body.d.ts +12 -0
  82. package/dist/observers/net-detail/network-body.js +71 -0
  83. package/dist/observers/net-detail/network-redact.d.ts +1 -0
  84. package/dist/observers/net-detail/network-redact.js +1 -0
  85. package/dist/observers/network-stream.d.ts +0 -21
  86. package/dist/observers/network-stream.js +3 -46
  87. package/dist/observers/network.d.ts +3 -27
  88. package/dist/observers/network.js +178 -295
  89. package/dist/observers/perf.d.ts +0 -10
  90. package/dist/observers/perf.js +0 -16
  91. package/dist/observers/route.d.ts +1 -2
  92. package/dist/observers/route.js +13 -19
  93. package/dist/observers/scroll.d.ts +0 -1
  94. package/dist/observers/scroll.js +2 -11
  95. package/dist/observers/sdk-failure.d.ts +1 -9
  96. package/dist/observers/sdk-failure.js +1 -30
  97. package/dist/observers/state.d.ts +1 -11
  98. package/dist/observers/state.js +13 -41
  99. package/dist/observers/storage.d.ts +0 -14
  100. package/dist/observers/storage.js +10 -34
  101. package/dist/observers/types.d.ts +0 -15
  102. package/dist/observers/types.js +0 -14
  103. package/dist/patching/capture-method.d.ts +0 -24
  104. package/dist/patching/capture-method.js +0 -26
  105. package/dist/presenter/chrome/layers.d.ts +2 -0
  106. package/dist/presenter/chrome/layers.js +2 -0
  107. package/dist/presenter/chrome/log-kinds.d.ts +12 -0
  108. package/dist/presenter/chrome/log-kinds.js +7 -0
  109. package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -1
  110. package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -1
  111. package/dist/presenter/chrome/presenter-brand.d.ts +3 -0
  112. package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -21
  113. package/dist/presenter/chrome/presenter-effects.d.ts +4 -0
  114. package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -10
  115. package/dist/presenter/chrome/presenter-hud-chrome.d.ts +7 -0
  116. package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -10
  117. package/dist/presenter/chrome/presenter-log.d.ts +17 -0
  118. package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +5 -54
  119. package/dist/presenter/chrome/presenter-report-copy.d.ts +51 -0
  120. package/dist/presenter/chrome/presenter-report-copy.js +95 -0
  121. package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
  122. package/dist/presenter/chrome/presenter-safe-html.js +16 -0
  123. package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -5
  124. package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -5
  125. package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -7
  126. package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -8
  127. package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -8
  128. package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -6
  129. package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -6
  130. package/dist/presenter/presenter-account-styles.d.ts +1 -0
  131. package/dist/presenter/presenter-account-styles.js +99 -0
  132. package/dist/presenter/presenter-account.d.ts +40 -0
  133. package/dist/presenter/presenter-account.js +178 -0
  134. package/dist/presenter/presenter-config.d.ts +10 -107
  135. package/dist/presenter/presenter-config.js +5 -92
  136. package/dist/presenter/presenter-controls.d.ts +1 -35
  137. package/dist/presenter/presenter-controls.js +10 -133
  138. package/dist/presenter/presenter-dock-layout.d.ts +0 -3
  139. package/dist/presenter/presenter-dock-layout.js +1 -5
  140. package/dist/presenter/presenter-drag.d.ts +2 -42
  141. package/dist/presenter/presenter-drag.js +3 -76
  142. package/dist/presenter/presenter-glow.d.ts +0 -21
  143. package/dist/presenter/presenter-glow.js +2 -28
  144. package/dist/presenter/presenter-hud-position.d.ts +18 -0
  145. package/dist/presenter/presenter-hud-position.js +50 -0
  146. package/dist/presenter/presenter-report-styles.d.ts +0 -6
  147. package/dist/presenter/presenter-report-styles.js +22 -7
  148. package/dist/presenter/presenter-report.d.ts +5 -7
  149. package/dist/presenter/presenter-report.js +44 -95
  150. package/dist/presenter/presenter-run-state.d.ts +1 -8
  151. package/dist/presenter/presenter-run-state.js +1 -6
  152. package/dist/presenter/presenter-settings-styles.d.ts +0 -1
  153. package/dist/presenter/presenter-settings-styles.js +2 -2
  154. package/dist/presenter/presenter-settings.d.ts +5 -23
  155. package/dist/presenter/presenter-settings.js +21 -33
  156. package/dist/presenter/presenter-shell-styles.js +23 -20
  157. package/dist/presenter/presenter-shell.d.ts +6 -18
  158. package/dist/presenter/presenter-shell.js +26 -77
  159. package/dist/presenter/presenter-styles.d.ts +2 -10
  160. package/dist/presenter/presenter-styles.js +6 -12
  161. package/dist/presenter/presenter-workspace.d.ts +4 -4
  162. package/dist/presenter/presenter-workspace.js +30 -10
  163. package/dist/presenter/presenter.d.ts +3 -63
  164. package/dist/presenter/presenter.js +29 -132
  165. package/dist/presenter/tour/tour-view.d.ts +33 -0
  166. package/dist/presenter/tour/tour-view.js +188 -0
  167. package/dist/presenter/tour/tour.d.ts +25 -0
  168. package/dist/presenter/tour/tour.js +245 -0
  169. package/dist/recorder/recorder-styles.d.ts +0 -6
  170. package/dist/recorder/recorder-styles.js +0 -6
  171. package/dist/recorder/recorder.d.ts +0 -34
  172. package/dist/recorder/recorder.js +6 -49
  173. package/dist/registry/auto-testids.d.ts +0 -19
  174. package/dist/registry/auto-testids.js +1 -21
  175. package/dist/registry/capabilities.d.ts +0 -31
  176. package/dist/registry/capabilities.js +1 -46
  177. package/dist/registry/commit-and-signal.d.ts +0 -6
  178. package/dist/registry/commit-and-signal.js +1 -7
  179. package/dist/registry/declared-channels.d.ts +2 -0
  180. package/dist/registry/declared-channels.js +14 -0
  181. package/dist/registry/declared-platform.d.ts +4 -0
  182. package/dist/registry/declared-platform.js +34 -0
  183. package/dist/registry/domains.d.ts +0 -8
  184. package/dist/registry/domains.js +0 -7
  185. package/dist/registry/emitter.d.ts +0 -14
  186. package/dist/registry/emitter.js +1 -11
  187. package/dist/registry/store-adapters.d.ts +0 -186
  188. package/dist/registry/store-adapters.js +1 -164
  189. package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -10
  190. package/dist/registry/{adapters.js → stores/adapters.js} +0 -10
  191. package/dist/registry/stores.d.ts +0 -33
  192. package/dist/registry/stores.js +1 -60
  193. package/dist/reticle-presenter-helpers.d.ts +0 -13
  194. package/dist/reticle-presenter-helpers.js +10 -20
  195. package/dist/reticle-singleton.d.ts +2 -0
  196. package/dist/reticle-singleton.js +3 -0
  197. package/dist/reticle.d.ts +1 -52
  198. package/dist/reticle.js +115 -180
  199. package/dist/review/annotator-styles.js +1 -8
  200. package/dist/review/annotator.d.ts +2 -13
  201. package/dist/review/annotator.js +2 -42
  202. package/dist/review/mark-anchor.d.ts +0 -11
  203. package/dist/review/mark-anchor.js +3 -11
  204. package/dist/security/serialization.d.ts +0 -20
  205. package/dist/security/serialization.js +1 -66
  206. package/dist/session-continuity.d.ts +0 -16
  207. package/dist/session-continuity.js +1 -20
  208. package/dist/timers/clock.d.ts +1 -14
  209. package/dist/timers/clock.js +59 -60
  210. package/dist/timers/console/native-console.js +5 -0
  211. package/dist/timers/native/native-timers.d.ts +11 -0
  212. package/dist/timers/native/native-timers.js +48 -0
  213. package/dist/transport/transport.d.ts +0 -51
  214. package/dist/transport/transport.js +5 -131
  215. package/dist/transport/unreachable-message.d.ts +0 -17
  216. package/dist/transport/unreachable-message.js +0 -17
  217. package/dist/util/captured-method.d.ts +0 -35
  218. package/dist/util/captured-method.js +0 -35
  219. package/package.json +5 -4
  220. package/dist/actions/synthetic-input.d.ts +0 -9
  221. package/dist/actions/synthetic-input.js +0 -40
  222. package/dist/dom/auto-anchor.d.ts +0 -65
  223. package/dist/dom/auto-anchor.js +0 -74
  224. package/dist/dom/refs.d.ts +0 -90
  225. package/dist/dom/refs.js +0 -245
  226. package/dist/dom/source.d.ts +0 -53
  227. package/dist/dom/source.js +0 -74
  228. package/dist/observers/network-body.d.ts +0 -35
  229. package/dist/observers/network-body.js +0 -168
  230. package/dist/observers/network-redact.d.ts +0 -20
  231. package/dist/observers/network-redact.js +0 -80
  232. package/dist/presenter/presenter-brand.d.ts +0 -22
  233. package/dist/presenter/presenter-effects.d.ts +0 -13
  234. package/dist/presenter/presenter-hud-chrome.d.ts +0 -17
  235. package/dist/presenter/presenter-log.d.ts +0 -64
  236. package/dist/presenter/presenter-report-copy.d.ts +0 -112
  237. package/dist/presenter/presenter-report-copy.js +0 -156
  238. package/dist/presenter/presenter-test-helpers.d.ts +0 -16
  239. package/dist/presenter/presenter-test-helpers.js +0 -41
  240. package/dist/timers/native-console.js +0 -9
  241. package/dist/timers/native-timers.d.ts +0 -36
  242. package/dist/timers/native-timers.js +0 -78
  243. /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
  244. /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
@@ -1,63 +1,11 @@
1
- /**
2
- * Every shadow root this SDK has seen created, open or closed.
3
- *
4
- * Two problems are solved by the same record.
5
- *
6
- * **Observation.** A `MutationObserver` on `documentElement` does NOT cross a shadow boundary, so a
7
- * click that changes text inside a web component produced an entirely empty observe window. Measured
8
- * on a console with a shadow-DOM alert badge: the click landed, the badge changed, and `observe`
9
- * reported zero DOM events — indistinguishable from an app that ignored the click. The DOM observer
10
- * needs a list of roots to attach to, and no API enumerates them.
11
- *
12
- * **Closed roots.** `element.shadowRoot` is null forever for `mode: 'closed'`, so a closed root is
13
- * unreadable AFTER the fact — but `attachShadow` still RETURNS it to its caller. Patching that call
14
- * captures the root at the one instant it is exposed. This only works when the SDK is installed
15
- * before the component upgrades, which is why hosts are tracked individually rather than assumed:
16
- * whatever was missed is still declared as a blind spot, and only what was genuinely captured stops
17
- * being one.
18
- */
19
1
  import { captureMethod } from '../patching/capture-method.js';
20
- /**
21
- * How many installs are live.
22
- *
23
- * The state below is module-level, and a dev-only SDK gets installed more than once: Vite re-runs the
24
- * module on hot reload, so `connect()` can run again before the previous instance is torn down.
25
- * Measured: install, subscribe, install again, tear down the SECOND one — and the FIRST instance's
26
- * subscribers were gone, so the DOM observer silently stopped being told about new shadow roots for
27
- * the rest of the session. Shared state must only be cleared by the last install standing.
28
- */
29
2
  let installs = 0;
30
- /**
31
- * Retained shadow roots, weakly.
32
- *
33
- * This was a strong `Set` and it never shrank. Measured: a virtualized list that renders and removes
34
- * 5,000 web-component rows left 5,000 roots retained — each pinning its whole shadow subtree, and
35
- * pinning its host through `root.host`, so nothing the list discarded could ever be collected. A
36
- * design system's rows ARE web components, so the most ordinary data-heavy page is the worst case,
37
- * and this SDK is meant to sit in a dev session all day.
38
- *
39
- * Same shape `RefRegistry` already uses for element handles: hold weakly, bound the bookkeeping,
40
- * sweep the dead entries periodically rather than on every write.
41
- */
42
3
  const roots = new Set();
43
- /**
44
- * Membership, separately and weakly.
45
- *
46
- * The weak entries above are for ENUMERATION and cannot answer "have I seen this root" without
47
- * dereferencing all of them — which turns every `attachShadow` into a scan of every root so far, and
48
- * a 5,000-row list into 25 million comparisons. A WeakSet answers it in O(1) and retains nothing.
49
- */
50
4
  const known = new WeakSet();
51
5
  const hosts = new WeakSet();
52
- /**
53
- * Bookkeeping ceiling. Weak references make the ROOTS collectable; this bounds the entries pointing
54
- * at them, which are not. Sized far above any real page's live component count.
55
- */
56
6
  export const MAX_TRACKED_ROOTS = 4000;
57
- /** Dead-entry sweeps are O(retained), so they run once per this many roots rather than per root. */
58
7
  export const SWEEP_EVERY = 250;
59
8
  let sinceSweep = 0;
60
- /** Drop entries whose root has been collected, then any excess by age (Set keeps insertion order). */
61
9
  function sweep() {
62
10
  for (const ref of roots) {
63
11
  if (ref.deref() === undefined)
@@ -70,7 +18,6 @@ function sweep() {
70
18
  roots.delete(oldest.value);
71
19
  }
72
20
  }
73
- /** The live roots behind the weak entries. */
74
21
  function liveRoots() {
75
22
  const out = [];
76
23
  for (const ref of roots) {
@@ -82,10 +29,6 @@ function liveRoots() {
82
29
  }
83
30
  const listeners = new Set();
84
31
  function record(root, host) {
85
- // Idempotent. A hot reload leaves one patch wrapping another, so a single `attachShadow` travels
86
- // through both and reports twice; the initial sweep can also meet a root the patch already caught.
87
- // Subscribers must see each root exactly once — they attach an observer per root, and counting one
88
- // twice is how a "reported once" guarantee quietly becomes "reported per install".
89
32
  if (known.has(root))
90
33
  return;
91
34
  known.add(root);
@@ -99,15 +42,12 @@ function record(root, host) {
99
42
  for (const listener of listeners)
100
43
  listener(root);
101
44
  }
102
- /** Shadow roots captured so far. Includes closed roots when the patch was installed in time. */
103
45
  export function capturedRoots() {
104
46
  return liveRoots();
105
47
  }
106
- /** Whether this element's shadow root was captured — i.e. whether its contents are reachable. */
107
48
  export function isCaptured(host) {
108
49
  return hosts.has(host);
109
50
  }
110
- /** The captured root for a host, or null. Non-null for closed roots the patch caught in time. */
111
51
  export function capturedRootOf(host) {
112
52
  if (!hosts.has(host))
113
53
  return null;
@@ -116,21 +56,12 @@ export function capturedRootOf(host) {
116
56
  return root;
117
57
  return null;
118
58
  }
119
- /** Run `listener` for every root captured from now on. Returns an unsubscribe. */
120
59
  export function onShadowRoot(listener) {
121
60
  listeners.add(listener);
122
61
  return () => listeners.delete(listener);
123
62
  }
124
- /**
125
- * Patch `attachShadow` to record each root as it is created, and sweep the roots that already exist.
126
- *
127
- * The sweep can only find OPEN roots (there is no other way in), so a closed root attached before
128
- * install stays unreachable — correctly, and it is declared as such.
129
- */
130
63
  export function installShadowRegistry() {
131
64
  const proto = Element.prototype;
132
- // Through captureMethod, like every other patch site here: reading a method to hold and restore it
133
- // is the one thing `unbound-method` exists to flag, and the intention is named there once.
134
65
  const original = captureMethod(proto, 'attachShadow');
135
66
  if (typeof original !== 'function')
136
67
  return () => undefined;
@@ -146,19 +77,18 @@ export function installShadowRegistry() {
146
77
  const root = el.shadowRoot;
147
78
  if (null === root)
148
79
  continue;
149
- record(root, el); // idempotent — a root the patch already caught is skipped inside
80
+ record(root, el);
150
81
  sweep(root);
151
82
  }
152
83
  };
153
84
  if (document.documentElement !== null)
154
85
  sweep(document.documentElement);
155
86
  return () => {
156
- // Restore only if nobody patched over us; clobbering a later patch is worse than leaving ours.
157
87
  if (captureMethod(proto, 'attachShadow') === patched)
158
88
  proto.attachShadow = original;
159
89
  installs -= 1;
160
90
  if (installs > 0)
161
- return; // another install is still using this state
91
+ return;
162
92
  roots.clear();
163
93
  listeners.clear();
164
94
  };
@@ -2,12 +2,7 @@ import { SnapshotMode } from '@reticlehq/core';
2
2
  interface SnapshotStatus {
3
3
  route: string;
4
4
  title: string;
5
- /** Open dialogs, OMITTED when there are none — absence means "no dialog is up". */
6
5
  visibleDialogs?: string[];
7
- /**
8
- * Present ONLY when a whole-page snapshot came back near-empty because an open overlay
9
- * aria-hidden the rest of the page (#397). Absence means the page is not hidden behind an overlay.
10
- */
11
6
  overlayHidingPage?: string;
12
7
  }
13
8
  export interface SnapshotResult {
@@ -15,32 +10,11 @@ export interface SnapshotResult {
15
10
  status: SnapshotStatus;
16
11
  nodes: number;
17
12
  truncated: boolean;
18
- /**
19
- * True when a `scope` was given but resolved to nothing. The snapshot then covers NOTHING rather
20
- * than silently falling back to the whole page — an agent snapshotting "the modal" after it closed
21
- * must not receive the entire page as if it were the modal.
22
- */
23
13
  scopeMissing?: boolean;
24
- /**
25
- * How many MEANINGFUL nodes the lean walk passed over, present only in a lean mode and only when
26
- * non-zero. This is what stops an empty `interactive` tree reading as an empty page — the mode is
27
- * a role filter, and a production app whose controls carry no roles yields nothing while being
28
- * full of things to drive. It shipped untyped: the value was spread into the result and every
29
- * consumer had to reach for it without a type, including the tool that builds the note from it.
30
- */
31
14
  leanSkipped?: number;
32
- /**
33
- * Refs of the subtree roots the walk never entered, present ONLY when `truncated`. This is the
34
- * cut's own frontier: re-snapshot each with `{ scope: ref, includeRoot: true }` and the union is
35
- * the whole tree. Without it `truncated` says only THAT the read stopped, never WHERE, so nobody
36
- * could finish it — and a reader who does not finish it must not conclude anything is ABSENT.
37
- */
15
+ hiddenSkipped?: number;
16
+ domElements?: number;
38
17
  unread?: string[];
39
- /**
40
- * True when the frontier itself did not fit in `MAX_UNREAD_BRANCHES`, so `unread` is a prefix of
41
- * the branches that were cut. Completion is then impossible from this read, and the difference
42
- * between "impossible" and "not attempted" is the whole point of saying so.
43
- */
44
18
  unreadOverflow?: true;
45
19
  }
46
20
  interface SnapshotOptions {
@@ -48,23 +22,8 @@ interface SnapshotOptions {
48
22
  mode?: SnapshotMode | undefined;
49
23
  maxNodes?: number | undefined;
50
24
  maxDepth?: number | undefined;
51
- /**
52
- * Emit the scope element's OWN line before its descendants. Off by default (a scoped snapshot has
53
- * always been the subtree UNDER the scope); the completion re-read needs it, because the branch it
54
- * is re-reading is exactly the node the truncated walk stopped before emitting.
55
- */
56
25
  includeRoot?: boolean | undefined;
57
26
  }
58
- /**
59
- * Cap on how many cut branches one snapshot names.
60
- *
61
- * A hostile or merely huge DOM can cut thousands of siblings at once (a non-virtualized grid does it
62
- * routinely), and a frontier that large is neither affordable to send nor useful to re-read one at a
63
- * time. Fifty is well above what a real page's cut produces — the frontier is the branches at the
64
- * node cap, not the nodes themselves — and small enough that the list stays a hint rather than a
65
- * second snapshot. Past it the read declares `unreadOverflow` instead of quietly sending fewer.
66
- */
67
27
  export declare const MAX_UNREAD_BRANCHES = 50;
68
- /** Build the semantic accessibility snapshot of the page or a subtree. */
69
28
  export declare function buildSnapshot(options?: SnapshotOptions): SnapshotResult;
70
29
  export {};
@@ -2,7 +2,7 @@ import { ElementState, SnapshotMode } from '@reticlehq/core';
2
2
  import { capturedRootOf } from './shadow-registry.js';
3
3
  import { isFrame } from './realm.js';
4
4
  import { getAccessibleName, getRole, getStates, getValue, isVisible } from './a11y.js';
5
- import { refs } from './refs.js';
5
+ import { refs } from './addressing/refs.js';
6
6
  import { isIgnored, isReticleOverlay } from './dom-ignore.js';
7
7
  const INTERACTIVE = new Set([
8
8
  'button',
@@ -20,62 +20,35 @@ const INTERACTIVE = new Set([
20
20
  'option',
21
21
  ]);
22
22
  const SKIP_TAGS = new Set(['script', 'style', 'noscript', 'template', 'head', 'meta', 'link']);
23
- /** Roles whose whole purpose is announcing a change; both imply an implicit aria-live. */
24
23
  const ANNOUNCE_ROLES = new Set(['alert', 'status']);
25
- /**
26
- * Is this element the app SAYING something changed?
27
- *
28
- * INTERACTIVE mode keeps only actionable elements, which silently drops the one node that explains
29
- * why an action did nothing — the error the app just rendered. An agent told to prefer the cheaper
30
- * mode was structurally blind to the failure it had caused: the click reports settled with a DOM
31
- * mutation, and the lean snapshot shows the same controls as before. Live regions exist precisely
32
- * to announce state changes to a consumer that cannot see the screen, which is exactly the agent,
33
- * so they are the principled exception — and a bounded one, since a page has very few.
34
- */
35
24
  function announces(el, role) {
36
25
  if (ANNOUNCE_ROLES.has(role))
37
26
  return true;
38
27
  const live = el.getAttribute('aria-live');
39
28
  return live !== null && 'off' !== live;
40
29
  }
41
- /** Cap on inlined text content so a verbose node can't blow up the snapshot. */
42
30
  const TEXT_MAX = 80;
43
- /**
44
- * Concatenated DIRECT text of an element (its own text nodes, not descendants' — those are
45
- * captured when their own element is walked, so no duplication). Collapsed + truncated.
46
- * This is what makes a silent removal of non-interactive content (e.g. a KPI card) visible:
47
- * the accessibility role tree alone omits generic containers' text.
48
- */
49
31
  function directText(el) {
50
32
  let out = '';
51
33
  for (const node of el.childNodes) {
52
- if (3 === node.nodeType /* Node.TEXT_NODE */)
34
+ if (3 === node.nodeType)
53
35
  out += node.textContent ?? '';
54
36
  }
55
37
  const collapsed = out.replace(/\s+/g, ' ').trim();
56
38
  return collapsed.length > TEXT_MAX ? `${collapsed.slice(0, TEXT_MAX)}…` : collapsed;
57
39
  }
58
- /**
59
- * Cap on how many cut branches one snapshot names.
60
- *
61
- * A hostile or merely huge DOM can cut thousands of siblings at once (a non-virtualized grid does it
62
- * routinely), and a frontier that large is neither affordable to send nor useful to re-read one at a
63
- * time. Fifty is well above what a real page's cut produces — the frontier is the branches at the
64
- * node cap, not the nodes themselves — and small enough that the list stays a hint rather than a
65
- * second snapshot. Past it the read declares `unreadOverflow` instead of quietly sending fewer.
66
- */
67
40
  export const MAX_UNREAD_BRANCHES = 50;
68
- /** Cheap skips that need no computed style (tags, overlays, aria-hidden, [hidden]). */
41
+ function hiddenByMarkup(el) {
42
+ if ('true' === el.getAttribute('aria-hidden'))
43
+ return true;
44
+ return el instanceof HTMLElement && el.hidden;
45
+ }
69
46
  function skipEarly(el) {
70
47
  if (SKIP_TAGS.has(el.tagName.toLowerCase()))
71
48
  return true;
72
49
  if (isIgnored(el))
73
- return true; // Reticle overlay + known dev overlays
74
- if ('true' === el.getAttribute('aria-hidden'))
75
50
  return true;
76
- if (el instanceof HTMLElement && el.hidden)
77
- return true;
78
- return false;
51
+ return hiddenByMarkup(el);
79
52
  }
80
53
  function stateSuffix(el) {
81
54
  const states = getStates(el).filter((s) => s === ElementState.DISABLED ||
@@ -84,42 +57,30 @@ function stateSuffix(el) {
84
57
  s === ElementState.FOCUSED);
85
58
  return states.length > 0 ? ` [${states.join(',')}]` : '';
86
59
  }
87
- function formatLine(el, depth, role, name, layout) {
60
+ function formatLine(el, depth, role, name, layout, ownText = '') {
88
61
  const indent = ' '.repeat(depth);
89
62
  const value = getValue(el);
90
- // A testid stands in for the accessible name when there is none. Including a role-less control and
91
- // then printing it as a bare `- generic` is worse than leaving it out: it is visible and it cannot
92
- // be addressed. This is the name its author gave it.
93
63
  const label = name;
94
64
  const namePart = label.length > 0 ? ` "${label}"` : '';
95
65
  const refPart = INTERACTIVE.has(role) || label.length > 0 ? ` (ref=${refs.refFor(el)})` : '';
96
66
  const valuePart = value !== undefined && value.length > 0 ? ` [value="${value}"]` : '';
97
67
  const layoutPart = layout.length > 0 ? ` [${layout}]` : '';
98
- return `${indent}- ${role}${namePart}${refPart}${valuePart}${layoutPart}${stateSuffix(el)}`;
68
+ const textPart = ownText.length > 0 ? ` [text="${ownText}"]` : '';
69
+ return `${indent}- ${role}${namePart}${refPart}${valuePart}${textPart}${layoutPart}${stateSuffix(el)}`;
99
70
  }
100
- /** A generic container's own text content, with no ref (kept lean — text isn't actionable). */
101
71
  function formatTextLine(depth, text) {
102
72
  return `${' '.repeat(depth)}- text "${text}"`;
103
73
  }
104
- /**
105
- * Compact signature of an element's own layout when it is a grid/flex container. A layout
106
- * regression (e.g. grid columns 2 -> 3) leaves the role+text tree identical, so a role-only
107
- * snapshot is blind to it; this line makes it visible. Empty for non-container elements.
108
- */
109
74
  function layoutSignature(style) {
110
75
  if (null === style)
111
76
  return '';
112
77
  const display = style.display;
113
- // Grid track templates are the high-signal CLS case (column/row count + sizing) and there are
114
- // few grid containers per page. Flex is intentionally excluded: nearly every row is a flex
115
- // box, so signing them all floods the snapshot for little regression value.
116
78
  if ('grid' === display || 'inline-grid' === display) {
117
79
  const cols = style.gridTemplateColumns;
118
80
  return cols !== '' && cols !== 'none' ? `grid-cols:${cols}` : 'grid';
119
81
  }
120
82
  return '';
121
83
  }
122
- /** Record a cut branch on the frontier, declaring an overflow rather than shortening it silently. */
123
84
  function recordUnread(branches, ctx) {
124
85
  for (const branch of branches) {
125
86
  if (ctx.unread.length >= MAX_UNREAD_BRANCHES) {
@@ -129,23 +90,11 @@ function recordUnread(branches, ctx) {
129
90
  ctx.unread.push(refs.refFor(branch));
130
91
  }
131
92
  }
132
- /**
133
- * The children to walk under `parent`, piercing boundaries a plain `.children` misses: an OPEN shadow
134
- * root renders as part of the element (web components / design systems), and a SAME-ORIGIN iframe's
135
- * body is real content. Cross-origin frames throw on access and are skipped by design. Without this,
136
- * an entire category of modern apps is invisible to reticle_snapshot (systematic false negative).
137
- */
138
93
  function pierceChildren(parent) {
139
94
  const out = [...parent.children];
140
- // A CLOSED root reports `shadowRoot === null` forever, but the registry captured it at the moment
141
- // `attachShadow` returned it. `query` has been able to reach that content since; the snapshot could
142
- // not, so the two tools disagreed about what is on the page — and the snapshot is the one an agent
143
- // reads to decide what to query for, which makes its omission the one that actually costs.
144
95
  const shadow = parent.shadowRoot ?? capturedRootOf(parent);
145
96
  if (shadow !== null)
146
97
  out.push(...shadow.children);
147
- // Realm-aware: an iframe nested INSIDE another frame's body belongs to that frame's realm, so the
148
- // top window's constructor never matches it and nested embeds were silently never pierced.
149
98
  if (isFrame(parent)) {
150
99
  try {
151
100
  const body = parent.contentDocument?.body;
@@ -153,84 +102,39 @@ function pierceChildren(parent) {
153
102
  out.push(...body.children);
154
103
  }
155
104
  catch {
156
- /* cross-origin frame — inaccessible by design */
157
105
  }
158
106
  }
159
107
  return out;
160
108
  }
161
- /** Emit one element (if it earns a line) and descend into it. Split out of `walk` so the completion
162
- * re-read can start AT a branch root — the node the truncated walk stopped just before emitting. */
163
109
  function visit(child, depth, ctx, inLive) {
164
- if (skipEarly(child))
110
+ if (skipEarly(child)) {
111
+ if (hiddenByMarkup(child))
112
+ ctx.hiddenSkipped += 1;
165
113
  return;
166
- // Resolve computed style ONCE per node (the dominant snapshot cost) and thread it into both the
167
- // display-none skip and the layout signature — was two forced style resolutions per node.
114
+ }
168
115
  const view = child.ownerDocument.defaultView;
169
116
  const style = view !== null ? view.getComputedStyle(child) : null;
170
- if (style !== null && 'none' === style.display)
117
+ if (style !== null && 'none' === style.display) {
118
+ ctx.hiddenSkipped += 1;
171
119
  return;
120
+ }
172
121
  const role = getRole(child);
173
122
  const name = getAccessibleName(child);
174
123
  const interactive = INTERACTIVE.has(role);
175
- // Announcements are exempt from leanness, and so is everything inside one: a live region whose
176
- // message sits in a child element would otherwise be included as a contentless `- generic`.
177
124
  const announce = inLive || announces(child, role);
178
125
  const lean = ctx.mode === SnapshotMode.INTERACTIVE && !announce;
179
- // A generic, unnamed container's own text content — only consulted outside INTERACTIVE mode,
180
- // so the actionable-only view stays lean while FULL/meaningful views see content regressions.
181
- const text = !lean && 'generic' === role && 0 === name.length ? directText(child) : '';
182
- // Layout signature for grid/flex containers — makes CLS/layout regressions visible.
126
+ const text = !lean && 0 === name.length ? directText(child) : '';
183
127
  const layout = lean ? '' : layoutSignature(style);
184
- // Actionable WITHOUT an interactive role, which is most of the real web.
185
- //
186
- // `interactive` is a role test, and a large share of production UI carries no roles at all.
187
- // Measured on MarkText, a production Electron editor: `mode:"interactive"` returned an EMPTY tree
188
- // where `mode:"full"` found 47 nodes, because its whole block picker is `<div>`s. An empty tree is
189
- // indistinguishable from an empty page, and the tool description recommends this mode as the
190
- // default — so the cheaper view told an agent there was nothing to drive.
191
- //
192
- // A `data-testid` ONLY. It is a handle its author put there to be driven, and it marks one element
193
- // rather than a subtree.
194
- //
195
- // `cursor: pointer` was tried here as a second signal — it is how an app tells a HUMAN that a
196
- // thing is clickable — and measured on MarkText it was a disaster: interactive went from 0 nodes
197
- // and 72 tokens to 180 nodes and 870, against a FULL snapshot of 47 nodes and 424. The pointer
198
- // cursor is inherited, so every wrapper div inside a clickable row matched, and the lean mode
199
- // became twice the size of the complete one while adding nothing but nameless `- generic` lines.
200
- // The mode's whole claim is that it is smaller; a signal that cannot tell a control from its
201
- // ancestors cannot be used to decide what a control is.
202
- // `data-testid` was tried here as a second signal, on the premise that it is a handle its author
203
- // put there to be driven. Measured against our own instrumented bench app, the premise is false:
204
- // the lean tree on its dashboard came to 16 nodes and 175 tokens, and EIGHT were display elements
205
- // — kpi-deploys, kpi-success, kpi-p95, kpi-services, area-chart, activity-feed, brand. Half the
206
- // "actionable" view was things nothing can act on, and the mode roughly doubled to carry them; the
207
- // benchmark read it as a 4% verification-efficiency regression. A testid marks what a TEST cares
208
- // about, which is a superset of what a driver can use.
209
- //
210
- // It did not even help the case it shipped for: MarkText's controls carry no testid. `leanSkipped`
211
- // is what fixed that — an empty tree that says how many it passed over is no longer an empty page.
212
- //
213
- // `cursor: pointer` was tried too and was worse: interactive went from 0 nodes and 72 tokens to
214
- // 180 and 870 against a FULL snapshot of 47/424, because the pointer cursor is inherited and every
215
- // wrapper div inside a clickable row matched. A signal that cannot tell a control from its
216
- // ancestors cannot decide what a control is.
217
128
  const actionable = interactive;
218
- // A testid still makes an element MEANINGFUL, so `full` keeps it and can name and address it. It
219
- // just is not evidence that the element is ACTIONABLE, which is the only question lean mode asks.
220
129
  const meaningful = actionable || role !== 'generic' || name.length > 0 || text.length > 0 || layout.length > 0;
221
130
  const include = lean ? actionable : meaningful;
222
- // Counted, not just skipped. An empty INTERACTIVE tree is indistinguishable from an empty page,
223
- // and on a real app the difference is everything: MarkText's block picker is 20+ live controls
224
- // that carry no role, no testid and no pointer cursor, so the lean view is empty while the app is
225
- // full of things to drive. Knowing how many were passed over is what turns "" into "look again in
226
- // full mode" — see the note the snapshot tool builds from this.
227
131
  if (lean && !include && meaningful)
228
132
  ctx.leanSkipped += 1;
229
133
  if (include) {
230
134
  ctx.nodes += 1;
231
- ctx.lines.push(text.length > 0 && 0 === name.length && 0 === layout.length
135
+ ctx.lines.push('generic' === role && text.length > 0 && 0 === layout.length
232
136
  ? formatTextLine(depth, text)
233
- : formatLine(child, depth, role, name, layout));
137
+ : formatLine(child, depth, role, name, layout, text));
234
138
  walk(child, depth + 1, ctx, announce);
235
139
  }
236
140
  else {
@@ -244,8 +148,6 @@ function walk(parent, depth, ctx, inLive = false) {
244
148
  for (let index = 0; index < children.length; index += 1) {
245
149
  if (ctx.nodes >= ctx.maxNodes) {
246
150
  ctx.truncated = true;
247
- // Everything from here on is unread, not absent. Naming the frontier is what makes the cut
248
- // recoverable — see `unread` on SnapshotResult.
249
151
  recordUnread(children.slice(index), ctx);
250
152
  return;
251
153
  }
@@ -254,16 +156,6 @@ function walk(parent, depth, ctx, inLive = false) {
254
156
  visit(child, depth, ctx, inLive);
255
157
  }
256
158
  }
257
- /**
258
- * The APP's open dialogs. Reticle's own panels are excluded, always.
259
- *
260
- * The HUD's chat and report panels carry `role="dialog"` because that is the correct role for what
261
- * they are — but they are OUR surface, not the application's. Once the presenter became visible to
262
- * the tool surface (so that Reticle can be used to check its own HUD), every snapshot of every page
263
- * started reporting `visibleDialogs: ["Reticle agent chat"]`, telling the agent a modal was up when
264
- * the app had none. An agent that believes a dialog is open dismisses it before doing anything else,
265
- * which is a wasted action at best and a dismissed REAL dialog at worst.
266
- */
267
159
  function collectDialogs(root) {
268
160
  const nodes = root.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
269
161
  const names = [];
@@ -275,40 +167,22 @@ function collectDialogs(root) {
275
167
  }
276
168
  return names;
277
169
  }
278
- /**
279
- * Status for a snapshot, with uninformative defaults OMITTED.
280
- *
281
- * `reticle_act` already follows this rule — "fields at their uninformative default are omitted so a
282
- * clean action collapses to its consequence" — and snapshot did not, so every response carried
283
- * `visibleDialogs: []` whether or not a dialog existed. An empty array is the overwhelmingly common
284
- * case, so the field was almost pure repetition; a reader learns nothing from its presence and
285
- * everything from it.
286
- */
287
- /**
288
- * A focus-trap modal (Radix and friends) marks every sibling of the portal root aria-hidden. The
289
- * snapshot walk correctly skips aria-hidden subtrees, so a whole-page snapshot can return only the
290
- * overlay's own nodes and nothing else -- indistinguishable from "this page is empty". If the
291
- * overlay's cleanup is stuck (common on a throttled tab), every later action then dispatches with no
292
- * effect because the overlay genuinely captures pointer events. Say so instead of returning a
293
- * near-empty tree with no explanation. (#397)
294
- */
295
170
  function overlayHidingPage(root) {
296
- // Only meaningful for a whole-page snapshot; a scoped snapshot is the subtree the caller chose.
297
171
  if (root !== document.body)
298
172
  return undefined;
299
173
  const dialogs = document.body.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]');
300
174
  let modal;
175
+ let hiddenCandidate;
301
176
  for (const d of dialogs) {
302
- // Never OUR overlay. Reticle's HUD must not be able to explain the app's absence with itself:
303
- // that turns "the page did not render" into "an overlay is covering it", which sends the reader
304
- // to dismiss a panel that was never the problem.
305
177
  if (isReticleOverlay(d))
306
178
  continue;
307
179
  if (isVisible(d)) {
308
180
  modal = d;
309
181
  break;
310
182
  }
183
+ hiddenCandidate ??= d;
311
184
  }
185
+ modal ??= hiddenCandidate;
312
186
  if (modal === undefined)
313
187
  return undefined;
314
188
  const modalEl = modal;
@@ -331,13 +205,11 @@ function buildStatus(root) {
331
205
  ...(overlay !== undefined ? { overlayHidingPage: overlay } : {}),
332
206
  };
333
207
  }
334
- /** Build the semantic accessibility snapshot of the page or a subtree. */
335
208
  export function buildSnapshot(options = {}) {
336
209
  const mode = options.mode ?? SnapshotMode.FULL;
337
210
  const scopeEl = options.scope !== undefined
338
211
  ? (refs.resolve(options.scope) ?? document.querySelector(options.scope))
339
212
  : document.body;
340
- // A given-but-missing scope snapshots NOTHING and says so — never a silent whole-page fallback.
341
213
  const scopeMissing = options.scope !== undefined && !(scopeEl instanceof Element);
342
214
  const root = scopeEl instanceof Element ? scopeEl : document.body;
343
215
  const status = buildStatus(root);
@@ -351,6 +223,7 @@ export function buildSnapshot(options = {}) {
351
223
  lines: [],
352
224
  nodes: 0,
353
225
  leanSkipped: 0,
226
+ hiddenSkipped: 0,
354
227
  truncated: false,
355
228
  mode,
356
229
  maxNodes: options.maxNodes ?? 400,
@@ -367,7 +240,9 @@ export function buildSnapshot(options = {}) {
367
240
  status,
368
241
  nodes: ctx.nodes,
369
242
  truncated: ctx.truncated,
243
+ ...(0 === ctx.nodes ? { domElements: root.querySelectorAll('*').length } : {}),
370
244
  ...(ctx.leanSkipped > 0 ? { leanSkipped: ctx.leanSkipped } : {}),
245
+ ...(ctx.hiddenSkipped > 0 ? { hiddenSkipped: ctx.hiddenSkipped } : {}),
371
246
  ...(ctx.unread.length > 0 ? { unread: ctx.unread } : {}),
372
247
  ...(ctx.unreadOverflow ? { unreadOverflow: true } : {}),
373
248
  };
@@ -1,48 +1,11 @@
1
- /**
2
- * Design-token (theme) awareness — the in-source answer to "is this color on-theme?". A DOM/a11y
3
- * tool sees a color rendered; it does not know the app's INTENDED palette. Reticle reads the design
4
- * tokens the app declares as CSS custom properties, resolves them AS THE PAGE CURRENTLY RENDERS
5
- * THEM, and reports, per element, which tokens its color/background map to or whether the value is
6
- * an off-palette hardcoded one. That is a theme-compliance signal no outside-the-page tool has
7
- * without re-deriving the palette itself.
8
- *
9
- * Conservative by design: only a SET, opaque color with no matching token is flagged off-theme, and
10
- * every matching token name is returned — so the agent can judge, not just trust a flag.
11
- *
12
- * Two defects made this field untrustworthy in BOTH directions before it was rewritten:
13
- * - the reverse map was color → ONE name, first writer wins, so a button styled with a brand token
14
- * was told its background was a focus-border token because the two resolve to the same hex. The
15
- * name is the part an agent reasons about, and it was arbitrary among the candidates.
16
- * - the palette was built from the RAW `:root` declaration text and cached for the page lifetime,
17
- * on the reasoning that a theme rarely changes at runtime. A human toggling the theme between
18
- * two inspect calls left every lookup keyed on the previous theme's colors, so a `<summary>` that
19
- * had just been changed to a text token reported a surface token. The reporter read that as their
20
- * edit not applying and went hunting for a theme-scope bug that did not exist.
21
- * Hence: no cache, values resolved through `getComputedStyle` so the active theme wins, and the
22
- * scope that was active at capture time reported alongside, so two results minutes apart are
23
- * comparable at all.
24
- */
25
1
  interface ThemeReport {
26
- /**
27
- * The ONE design token matching the text color, or null. Null both when nothing matches and when
28
- * several tokens share the color: the previous behaviour picked an arbitrary winner and the caller
29
- * could not tell, which is the whole defect. Read `colorTokens` for the full answer; this field
30
- * stays for callers that only ever wanted the unambiguous case.
31
- */
32
2
  colorToken: string | null;
33
- /** Every design token whose resolved value is the text color. Empty when the color is off-palette. */
34
3
  colorTokens: string[];
35
- /** The one design token matching the background color, or null (same abstain-on-tie rule). */
36
4
  backgroundToken: string | null;
37
- /** Every design token whose resolved value is the background color. */
38
5
  backgroundTokens: string[];
39
- /** True when a set, opaque color matches NO design token — a theme violation worth surfacing. */
40
6
  offTheme: boolean;
41
- /** How many color tokens resolved under the active theme (0 ⇒ offTheme is never asserted). */
42
7
  tokenCount: number;
43
- /** Theme scope active at capture time, e.g. `.dark`, so two results taken apart are comparable. */
44
8
  themeScope: string | null;
45
9
  }
46
- /** Report an element's theme compliance from its computed style. */
47
10
  export declare function themeReport(cs: CSSStyleDeclaration): ThemeReport;
48
11
  export {};