@reticlehq/browser 2.13.0 → 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,70 +1,23 @@
1
- /**
2
- * Selectors for Reticle's own presenter overlay (cursor, HUD, glow) + the annotator's
3
- * UI (`data-reticle-mark`) - never observed/snapshotted. The annotator mounts by DEFAULT with the
4
- * presenter, so omitting its selector here leaked annotation chrome into every snapshot.
5
- */
6
1
  export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark],[data-reticle-blocker]';
7
- /** Known third-party dev overlays to keep out of snapshots (Agentation, Next dev UI). */
8
2
  const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
9
3
  let extraIgnore = '';
10
- /**
11
- * Whether Reticle's OWN UI is visible to snapshots and queries.
12
- *
13
- * Off, always, unless an app opts in — and the opt-in exists for exactly one situation: the app
14
- * under test IS Reticle. A HUD change is otherwise the only kind of change Reticle cannot be used to
15
- * check, because the panel that renders it is invisible to every tool that could look at it.
16
- *
17
- * It stays off by default because the reason for hiding it is sound: an agent that can drive
18
- * Reticle's own interface can fabricate its own impact report, and a snapshot full of Reticle chrome
19
- * is noise in every other app on earth. This is a hatch for contributors, not a setting.
20
- *
21
- * Third-party dev overlays stay ignored either way — they are somebody else's furniture and were
22
- * never the thing being verified.
23
- */
24
4
  let presenterVisible = false;
25
- /**
26
- * Make Reticle's own presenter visible to snapshots and queries. Contributors only.
27
- *
28
- * Deliberately a function rather than a field on the ignore set, so the one place that decides
29
- * "is this ours" also decides "may it be seen", and no caller can half-apply it.
30
- */
31
5
  export function setPresenterVisible(visible) {
32
6
  presenterVisible = visible;
33
7
  }
34
- /** Whether the hatch is open — reported in the app's capabilities so a verdict is never mistaken. */
35
8
  export function isPresenterVisible() {
36
9
  return presenterVisible;
37
10
  }
38
- /** Let the host app add selectors to exclude from snapshots (e.g. its own dev widgets). */
39
11
  export function setIgnoreSelectors(selectors) {
40
12
  extraIgnore = selectors.join(',');
41
13
  }
42
- /** True if the element is part of Reticle's own presenter overlay. */
43
14
  export function isReticleOverlay(el) {
44
15
  return el.closest(RETICLE_OVERLAY) !== null;
45
16
  }
46
- /**
47
- * True iff the element is part of Reticle's OWN UI - the presenter overlay, the HUD, the synthetic
48
- * cursor, the glow, or the annotator's marks - or lives inside one of them.
49
- *
50
- * The rule used to be "any ancestor carries a data-reticle* attribute", which is wrong twice over.
51
- * `data-reticle-mark-active` sits on <html> while annotate mode is live, so the whole document
52
- * answered yes; and `data-reticle-source` is stamped by the Vite/Babel plugins on every element the
53
- * APP renders, so in an instrumented app - which is the only kind there is - most of the page
54
- * answered yes too. Those attributes describe page content; they do not make it ours.
55
- *
56
- * Two things read this and both failed silently. `pageElementAt` (annotator) skipped every stamped
57
- * element and anchored the note to the outermost unstamped ancestor, i.e. the app shell instead of
58
- * the control under the cursor. `occlusion.ts` reads a yes as "nothing to report", so occlusion
59
- * detection - a bug class Reticle advertises catching - came back clean wherever the stamp reached.
60
- */
61
17
  export function isReticleUi(node) {
62
18
  return node !== null && node.closest(RETICLE_OVERLAY) !== null;
63
19
  }
64
- /** True if the element should be excluded from snapshots/queries (Reticle overlay or dev overlay). */
65
20
  export function isIgnored(el) {
66
- // With the hatch open, Reticle's own overlay drops out of the ignore set — but the third-party
67
- // dev overlays do not. They are somebody else's furniture and were never the thing being verified.
68
21
  const ours = presenterVisible ? '' : RETICLE_OVERLAY;
69
22
  const sel = [ours, DEV_OVERLAYS, extraIgnore].filter((part) => part.length > 0).join(',');
70
23
  return el.closest(sel) !== null;
@@ -1,8 +1 @@
1
- /**
2
- * Center-point hit-test, shared by click-geometry (actions) and occlusion detection (commands) so the
3
- * two can't drift. Returns the top NON-Reticle element covering `el`'s center, or null when nothing
4
- * foreign is on top — i.e. `el` itself, an ancestor wrapping it, or a descendant is on top (all "not
5
- * occluded"). A zero-area or unlayouted box (jsdom / detached node) returns null: we cannot tell, so
6
- * we never false-positive an occlusion. Reticle's own HUD is never treated as an occluder.
7
- */
8
1
  export declare function hitTestOccluder(el: Element, rect: DOMRect): Element | null;
@@ -1,11 +1,4 @@
1
1
  import { isReticleUi } from './dom-ignore.js';
2
- /**
3
- * Center-point hit-test, shared by click-geometry (actions) and occlusion detection (commands) so the
4
- * two can't drift. Returns the top NON-Reticle element covering `el`'s center, or null when nothing
5
- * foreign is on top — i.e. `el` itself, an ancestor wrapping it, or a descendant is on top (all "not
6
- * occluded"). A zero-area or unlayouted box (jsdom / detached node) returns null: we cannot tell, so
7
- * we never false-positive an occlusion. Reticle's own HUD is never treated as an occluder.
8
- */
9
2
  export function hitTestOccluder(el, rect) {
10
3
  if (0 === rect.width || 0 === rect.height)
11
4
  return null;
@@ -1,18 +1,4 @@
1
1
  import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
2
- /** Longest attribute value returned; one enormous href must not blow the response budget. */
3
2
  export declare const ATTR_VALUE_MAX = 512;
4
- /**
5
- * Match an element predicate against the live DOM.
6
- *
7
- * `count` is every match; `elements` is the described prefix. Keeping those separate is what lets
8
- * "how many?" stay exact while the answer stays affordable.
9
- */
10
3
  export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number): MatchResult;
11
- /**
12
- * Resolve a query to descriptors for the `query` MCP tool.
13
- *
14
- * `count` is carried through deliberately: the server reports match totals from it rather than from
15
- * the array, because the array is capped in transit. Dropping it here - which this function used to
16
- * do - silently put the server back to counting survivors and calling that the answer.
17
- */
18
4
  export declare function runQuery(query: ElementQuery, limit?: number): QueryResult;
package/dist/dom/query.js CHANGED
@@ -2,7 +2,7 @@ import { DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSP
2
2
  import { isFrame, isHtmlElement, isInput, isSelect, isTextArea } from './realm.js';
3
3
  import { capturedRootOf } from './shadow-registry.js';
4
4
  import { getAccessibleName, getRole, describe, getStates, isInViewport, isVisible, } from './a11y.js';
5
- import { isIgnored } from './dom-ignore.js';
5
+ import { isIgnored, isReticleOverlay } from './dom-ignore.js';
6
6
  import { isSensitiveKey } from '../security/serialization.js';
7
7
  import { declaredTestids } from '../registry/capabilities.js';
8
8
  import { identifyComponent } from '../registry/adapters.js';
@@ -10,31 +10,12 @@ import { refs } from './refs.js';
10
10
  const TESTID_ATTR = 'data-testid';
11
11
  const SOURCE_ATTR = DATA_RETICLE_SOURCE_ATTR;
12
12
  const MAX_PRESENT_TESTIDS = 12;
13
- /** Bound the fiber-walk fallback so a component-name query can't scan an unbounded DOM. */
14
13
  const MAX_COMPONENT_CANDIDATES = 2000;
15
- /** Likely-actionable elements considered when resolving a component anchor without a source stamp. */
16
14
  const COMPONENT_CANDIDATE_SELECTOR = `[${SOURCE_ATTR}], [${TESTID_ATTR}], button, a, input, select, textarea, [role]`;
17
- /**
18
- * Every candidate a semantic locator may match: the container ITSELF first, then its descendants.
19
- *
20
- * The container's own text, role or name was matched by every engine this replaces - a scoped
21
- * `{ text: "Saved" }` against `<div id="status">Saved</div>` found #status itself - so dropping
22
- * the root here would turn exactly those scoped queries into silent zero-match answers: the worst
23
- * failure mode, indistinguishable from the element being absent. `self: true` stays the way to
24
- * reach an UNLABELLED root (it skips the predicate entirely); this keeps a root that DOES satisfy
25
- * the predicate findable without a second spelling.
26
- */
27
15
  function elementsUnder(container) {
28
- // Embedded roots include ShadowRoots, which are DocumentFragments: they have no attributes or
29
- // tag to match on, so only a true Element root joins the candidates.
30
16
  const self = Node.ELEMENT_NODE === container.nodeType ? [container] : [];
31
17
  return [...self, ...Array.from(container.querySelectorAll('*'))];
32
18
  }
33
- /**
34
- * Match user-visible prose the way the public query API has always promised: trim and collapse
35
- * whitespace, compare canonically equivalent Unicode in NFC, and keep fuzzy text queries
36
- * case-insensitive. Attribute identifiers such as testid stay exact and do not pass through here.
37
- */
38
19
  function normaliseVisibleText(value) {
39
20
  return value.replace(/\s+/g, ' ').trim().normalize('NFC');
40
21
  }
@@ -60,15 +41,6 @@ function directText(el) {
60
41
  .map((node) => node.textContent ?? '')
61
42
  .join('');
62
43
  }
63
- /**
64
- * Elements whose accessible name comes from author-supplied naming rather than free subtree
65
- * content - the set `by: label` addresses.
66
- *
67
- * Form controls plus anything explicitly named with aria-label/aria-labelledby covers the common
68
- * case. `button`, `meter`, `output` and `progress` are included because their names arrive on
69
- * attributes (`value`) or from their caption, which is precisely what a label query is looking
70
- * for; leaving them out would silently drop buttons from label searches.
71
- */
72
44
  function semanticNameTarget(el) {
73
45
  if (isInput(el) || isTextArea(el) || isSelect(el))
74
46
  return true;
@@ -80,12 +52,6 @@ function semanticNameTarget(el) {
80
52
  el.hasAttribute('aria-label') ||
81
53
  el.hasAttribute('aria-labelledby'));
82
54
  }
83
- /**
84
- * Resolve a scope to its container. A container of `null` means a scope was GIVEN but resolved to
85
- * nothing (unmounted, or a selector that matches no element) - the caller must NOT fall back to the
86
- * whole page, or a scoped query silently widens into a phantom match. `scope === undefined` (no scope)
87
- * legitimately searches the body.
88
- */
89
55
  function resolveContainer(scope) {
90
56
  if (scope === undefined)
91
57
  return { container: document.body, scopeMissing: false };
@@ -98,16 +64,9 @@ function resolveContainer(scope) {
98
64
  return { container: found, scopeMissing: false };
99
65
  }
100
66
  catch {
101
- // invalid selector - treat as a missing scope, never a whole-page search
102
67
  }
103
68
  return { container: null, scopeMissing: true };
104
69
  }
105
- /**
106
- * Resolve an element by its SOURCE location - the precise, granular auto-anchor. The babel plugin
107
- * stamps `data-reticle-source="file:line:column"` on host elements, so a line-level starts-with match
108
- * pins the exact JSX element with a single fast attribute selector (no fiber walk). Column is
109
- * ignored so a small column drift doesn't unbind the anchor.
110
- */
111
70
  function findBySource(container, source) {
112
71
  const prefix = `${source.file}:${source.line}:`.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
113
72
  try {
@@ -117,12 +76,6 @@ function findBySource(container, source) {
117
76
  return [];
118
77
  }
119
78
  }
120
- /**
121
- * Resolve by component display name when no source stamp is available: scan a bounded set of
122
- * likely-actionable elements and keep those whose NEAREST enclosing component (via the registered
123
- * framework adapter) matches. Coarser than source (one component renders many hosts) - used as a
124
- * fallback / for frameworks without a source plugin.
125
- */
126
79
  function findByComponentName(container, component) {
127
80
  const out = [];
128
81
  let scanned = 0;
@@ -136,7 +89,6 @@ function findByComponentName(container, component) {
136
89
  }
137
90
  return out;
138
91
  }
139
- /** Auto-anchor resolution: source (precise) first, then component name (coarse fallback). */
140
92
  function findByComponent(container, query) {
141
93
  if (query.source !== undefined) {
142
94
  const bySource = findBySource(container, query.source);
@@ -148,13 +100,6 @@ function findByComponent(container, query) {
148
100
  }
149
101
  return [];
150
102
  }
151
- /**
152
- * Role + name, matched with the same local accessibility engine used to describe results.
153
- *
154
- * This intentionally makes Reticle's reported role and name the source of truth. If an element is
155
- * described as `textbox "Search User"`, the resolver must accept that exact pair, not a different
156
- * role or name from a second library.
157
- */
158
103
  function queryByRoleAndName(container, role, name) {
159
104
  return elementsUnder(container).filter((el) => getRole(el) === role && (name === undefined || exactVisibleText(getAccessibleName(el), name)));
160
105
  }
@@ -179,11 +124,9 @@ function queryByAlt(container, value) {
179
124
  return alt !== null && fuzzyVisibleText(alt, value);
180
125
  });
181
126
  }
182
- /** Run the appropriate local query against ONE root, light DOM or a shadow root. */
183
127
  function findIn(container, query) {
184
128
  const by = query.by;
185
129
  const value = query.value;
186
- // Explicit `by`+`value` form.
187
130
  if (by !== undefined && value !== undefined) {
188
131
  switch (by) {
189
132
  case QueryBy.ROLE:
@@ -199,25 +142,14 @@ function findIn(container, query) {
199
142
  case QueryBy.ALT:
200
143
  return queryByAlt(container, value);
201
144
  case QueryBy.COMPONENT:
202
- // value is the component name;.source (if present) still takes precedence inside.
203
145
  return findByComponent(container, { ...query, component: query.component ?? value });
204
146
  default:
205
- // THROW, never `return []`. An unsupported strategy answering "no matches" is
206
- // indistinguishable from the element genuinely being absent, so `by:'css'` - the first thing
207
- // anyone arriving from Playwright reaches for - reported a page with a <body> as empty. The
208
- // server now rejects unknown strategies at the schema; this is the same guarantee for every
209
- // other path in (replay, reticle_run, an internal caller), because a false negative invented
210
- // by the tool is the exact failure this product exists to prevent.
211
147
  throw new Error(`unsupported query strategy '${String(by)}' - use one of: ${Object.values(QueryBy).join(', ')}`);
212
148
  }
213
149
  }
214
- // Auto-anchor (component / source) - checked before the role/text fields so a query carrying
215
- // both a component anchor and an incidental role resolves by the more durable anchor.
216
150
  if (query.component !== undefined || query.source !== undefined) {
217
151
  return findByComponent(container, query);
218
152
  }
219
- // Structured form (role+name, or any single field). Same round-trip guarantee as the by+value
220
- // spelling above - the two forms must not disagree about what is findable.
221
153
  if (query.role !== undefined) {
222
154
  return queryByRoleAndName(container, query.role, query.name);
223
155
  }
@@ -233,19 +165,6 @@ function findIn(container, query) {
233
165
  return queryByAlt(container, query.alt);
234
166
  return [];
235
167
  }
236
- /**
237
- * Every open shadow root at or beneath `root`, in document order.
238
- *
239
- * A closed root is deliberately unreachable - `element.shadowRoot` is null by design, and the SDK
240
- * reports that as a blind spot rather than pretending to see through it.
241
- */
242
- /**
243
- * A same-origin `<iframe>`'s body, or null when the document is unreachable.
244
- *
245
- * Cross-origin access THROWS in some engines and yields null in others, so both are handled - and
246
- * both mean the same thing here: not ours to read. That case is not a silent skip, it is reported
247
- * separately as a declared blind spot.
248
- */
249
168
  function readableFrameBody(frame) {
250
169
  try {
251
170
  return frame.contentDocument?.body ?? null;
@@ -254,27 +173,15 @@ function readableFrameBody(frame) {
254
173
  return null;
255
174
  }
256
175
  }
257
- /** How deep to follow frames-inside-frames before giving up. Real consoles nest one, maybe two. */
258
176
  const FRAME_DEPTH_MAX = 3;
259
- /**
260
- * Every root beneath the scope that the page can read but `querySelectorAll` will not reach:
261
- * open shadow roots, and the documents of SAME-ORIGIN iframes.
262
- *
263
- * The iframe half was measured missing: a same-origin frame whose text the page could read straight
264
- * off `contentDocument` returned zero matches from `query`, and coverage said nothing about it - the
265
- * worst combination, an absent-element answer for content that is right there. Cross-origin frames
266
- * are a genuinely different case and stay unread; they are declared instead.
267
- */
268
177
  function embeddedRootsUnder(root) {
269
178
  const found = [];
270
179
  const walk = (node, depth) => {
271
180
  for (const el of node.querySelectorAll('*')) {
272
181
  const shadow = el.shadowRoot ?? capturedRootOf(el);
273
182
  if (shadow !== null) {
274
- // A ShadowRoot is a DocumentFragment. The local query helpers only call querySelectorAll on
275
- // the container, and every call site only reads from it.
276
183
  found.push(shadow);
277
- walk(shadow, depth); // nested web components
184
+ walk(shadow, depth);
278
185
  continue;
279
186
  }
280
187
  if (depth >= FRAME_DEPTH_MAX)
@@ -283,7 +190,7 @@ function embeddedRootsUnder(root) {
283
190
  continue;
284
191
  const body = readableFrameBody(el);
285
192
  if (null === body)
286
- continue; // cross-origin - declared, not searched
193
+ continue;
287
194
  found.push(body);
288
195
  walk(body, depth + 1);
289
196
  }
@@ -291,42 +198,16 @@ function embeddedRootsUnder(root) {
291
198
  walk(root, 0);
292
199
  return found;
293
200
  }
294
- /**
295
- * NOTE ON COST. The walk above is a `querySelectorAll('*')` over the container, and `reticle_query` is
296
- * a hot-path tool, so every query on every app pays it - including the overwhelming majority that hold
297
- * no web components.
298
- *
299
- * A cache invalidated by a MutationObserver was written and then REMOVED, because it is not sound:
300
- * `attachShadow` on an element that is already in the document mutates only the shadow tree, which an
301
- * observer watching documentElement's subtree never sees. The cached "no shadow roots here" would
302
- * persist and the content would be silently missed - a false negative indistinguishable from a genuinely
303
- * absent element, which is the exact bug the piercing was added to fix.
304
- *
305
- * Correctness wins until there is a measurement saying the walk actually matters. The allocation is
306
- * avoided (iterate the live NodeList rather than copying it); the traversal itself stays.
307
- */
308
- /**
309
- * Run the query against the light DOM AND every open shadow root beneath the scope.
310
- *
311
- * A single querySelectorAll call only walks the root it starts from, so a control inside a web
312
- * component returned zero matches on a completely healthy page - a miss indistinguishable from a
313
- * genuinely absent element. The snapshot has always pierced open roots; this makes `query` agree
314
- * with it.
315
- */
316
201
  function findCandidates(query) {
317
202
  const { container, scopeMissing } = resolveContainer(query.scope);
318
- // A given-but-missing scope searches NOTHING - never the whole page. The empty result plus the
319
- // scopeMissing flag is what keeps "gone scope" distinct from "absent element".
320
203
  if (null === container)
321
204
  return { candidates: [], scopeMissing: true };
322
- // `self: true` asks for the container ITSELF, which every other path excludes by construction.
323
- // A layout element with no role, name, testid or own text is unreachable by any semantic locator,
324
- // and it is routinely the element carrying the handler - "click the empty space in this row" is a
325
- // real user action that was simply not expressible. Requires a scope: without one there is no root
326
- // to return, and answering `document.body` would be a wrong answer wearing the shape of one.
327
205
  if (true === query.self) {
328
206
  if (query.scope === undefined)
329
207
  return { candidates: [], scopeMissing };
208
+ if (query.text !== undefined && !fuzzyVisibleText(container.textContent ?? '', query.text)) {
209
+ return { candidates: [], scopeMissing };
210
+ }
330
211
  return { candidates: isIgnored(container) ? [] : [container], scopeMissing };
331
212
  }
332
213
  const seen = new Set();
@@ -334,14 +215,7 @@ function findCandidates(query) {
334
215
  const collect = (els) => {
335
216
  for (const el of els) {
336
217
  if (seen.has(el))
337
- continue; // reachable from both host and root - count once
338
- // Reticle's OWN UI is not part of the app under test. `snapshot` has always excluded it; query
339
- // did not, so the presenter panel and the annotator answered `by: role` like app controls -
340
- // measured on a real merchant dashboard, 7 of the 40 buttons an agent could see were ours
341
- // ("Pause", "End", "Chat", "Export"). Two of those
342
- // share a NAME with a real control on that page, so an agent resolving "Export" could drive the
343
- // observer instead of the app and then reason about the result. Filtered here, at the one
344
- // collection point, so every entry path (by role/text/testid, shadow roots, scoped) is covered.
218
+ continue;
345
219
  if (isIgnored(el))
346
220
  continue;
347
221
  seen.add(el);
@@ -353,17 +227,8 @@ function findCandidates(query) {
353
227
  collect(findIn(embedded, query));
354
228
  return { candidates: out, scopeMissing };
355
229
  }
356
- /** Longest attribute value returned; one enormous href must not blow the response budget. */
357
230
  export const ATTR_VALUE_MAX = 512;
358
- /** Most attributes projected per element - a guard against a caller asking for everything. */
359
231
  const ATTR_KEYS_MAX = 12;
360
- /**
361
- * Read the requested attributes off an element.
362
- *
363
- * Absent attributes are OMITTED rather than returned empty, so "not present" and "present but blank"
364
- * stay distinguishable. Credential-bearing names are redacted with the same rule the network and
365
- * storage paths use - a projection API must not become an exfiltration path.
366
- */
367
232
  function projectAttrs(el, keys) {
368
233
  const out = {};
369
234
  for (const key of keys.slice(0, ATTR_KEYS_MAX)) {
@@ -375,50 +240,27 @@ function projectAttrs(el, keys) {
375
240
  return Object.keys(out).length > 0 ? out : undefined;
376
241
  }
377
242
  function inState(el, state, memo) {
378
- // inViewport is computed on demand rather than listed in getStates, so it stays assertable without
379
- // adding a state to every element in every snapshot (it would bloat the wire and churn every
380
- // describe). The predicate path is the only consumer that needs it. (#398)
381
243
  if (ElementState.IN_VIEWPORT === state)
382
244
  return isInViewport(el, memo);
383
245
  return getStates(el, isVisible(el, memo)).includes(state);
384
246
  }
385
- /**
386
- * How many matches are DESCRIBED, however many are found.
387
- *
388
- * describe() is the expensive part of a query: it resolves the accessible name and forces a style
389
- * computation for every ancestor to decide visibility. Describing every match on a page with
390
- * thousands of them freezes the main thread for seconds - and then the wire discards all but the
391
- * first `MAX_COLLECTION_ITEMS` anyway, so the work past that point was never observable by anyone.
392
- * Matching one to the other means the cost of a broad query is bounded by the transport rather than
393
- * by the size of the page.
394
- */
395
247
  const MAX_DESCRIBED = TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS;
396
- /**
397
- * Match an element predicate against the live DOM.
398
- *
399
- * `count` is every match; `elements` is the described prefix. Keeping those separate is what lets
400
- * "how many?" stay exact while the answer stays affordable.
401
- */
402
248
  export function matchQuery(query, state, limit = MAX_DESCRIBED) {
403
- // NO blanket try/catch here. It used to turn ANY exception during candidate-finding into
404
- // `elements = []`, which is the same lie as the `default` arm above: a query that could not run
405
- // reported that the element is not on the page. The one failure it was plausibly guarding -
406
- // a scope selector that matches nothing - is already handled explicitly and distinctly, as
407
- // `scopeMissing`, so what remained was a net that could only convert real faults into false
408
- // negatives.
409
249
  const found = findCandidates(query);
410
250
  const elements = found.candidates;
411
251
  const scopeMissing = found.scopeMissing;
412
- // One visibility cache for the whole (synchronous) query pass. isVisible is an O(depth) forced-style
413
- // walk; the state filter runs it over EVERY candidate (the count must be exact) - on a match-heavy
414
- // page (e.g. a 3k-row grid) that is tens of thousands of getComputedStyle calls on the host's main
415
- // thread. The memo makes each element's ancestors resolve once, then short-circuit for every sibling.
416
252
  const visMemo = new Map();
417
253
  const filtered = state === undefined ? elements : elements.filter((el) => inState(el, state, visMemo));
418
254
  const attrs = query.attrs;
419
255
  const described = filtered.slice(0, Math.max(0, Math.min(limit, MAX_DESCRIBED)));
256
+ const stampViewport = described.length > 1;
420
257
  const descriptors = described.map((el) => {
421
- const base = describe(el, visMemo);
258
+ let base = describe(el, visMemo);
259
+ if (stampViewport &&
260
+ isInViewport(el, visMemo) &&
261
+ !base.states.includes(ElementState.IN_VIEWPORT)) {
262
+ base = { ...base, states: [...base.states, ElementState.IN_VIEWPORT] };
263
+ }
422
264
  if (attrs === undefined || 0 === attrs.length)
423
265
  return base;
424
266
  const projected = projectAttrs(el, attrs);
@@ -429,15 +271,9 @@ export function matchQuery(query, state, limit = MAX_DESCRIBED) {
429
271
  count: filtered.length,
430
272
  elements: descriptors,
431
273
  ...(scopeMissing ? { scopeMissing: true } : {}),
432
- // On a MISS, carry the same diagnosis `runQuery` has always returned. MATCH is the command every
433
- // PREDICATE uses, so without this a failed assertion was a dead end ("no element matched") while
434
- // the identical failure through reticle_query listed the testids that ARE present. Computed only
435
- // when there is nothing to report, so the hot path pays nothing.
436
274
  ...(0 === filtered.length ? { hint: buildEmptyHint(query) } : {}),
437
275
  };
438
276
  }
439
- /** Resolve `aria-labelledby` (one or more element IDs) to the referenced elements' text - a bare ID is
440
- * not a human-readable name. Undefined when unset or nothing resolves. */
441
277
  function resolveLabelledBy(el) {
442
278
  const ids = el.getAttribute('aria-labelledby');
443
279
  if (null === ids)
@@ -449,10 +285,7 @@ function resolveLabelledBy(el) {
449
285
  .join(' ');
450
286
  return text.length > 0 ? text : undefined;
451
287
  }
452
- /** Structural clusters of the page - the successor to the raw testid list in zero-match hints. */
453
288
  function buildPresentRegions(query) {
454
- // For the diagnostic hint we WANT the page's orientation even when the scope is gone, so fall back
455
- // to the body here (the match path above already reported scopeMissing, so this can't mask it).
456
289
  const container = resolveContainer(query.scope).container ?? document.body;
457
290
  const regions = [];
458
291
  const CONTAINER_ROLES = [
@@ -476,8 +309,10 @@ function buildPresentRegions(query) {
476
309
  for (const role of CONTAINER_ROLES) {
477
310
  const containers = queryByRoleAndName(container, role, undefined);
478
311
  for (const el of containers) {
312
+ if (isReticleOverlay(el))
313
+ continue;
479
314
  const name = el.getAttribute('aria-label') ??
480
- resolveLabelledBy(el) ?? // aria-labelledby is an element ID - resolve it to the referenced TEXT
315
+ resolveLabelledBy(el) ??
481
316
  el.getAttribute('data-testid') ??
482
317
  undefined;
483
318
  const children = el.querySelectorAll('[role]');
@@ -504,29 +339,12 @@ function buildPresentRegions(query) {
504
339
  }
505
340
  return regions;
506
341
  }
507
- /**
508
- * The tightest single element whose SUBTREE text carries `wanted`, when no element's OWN text does.
509
- *
510
- * `by: text` matches an element's direct text nodes (`directText`), so a label rendered across
511
- * several child nodes — `v-html`, an inline `<span>` per word, a highlighted substring — exists on
512
- * the page and matches nothing. The two channels disagree by construction: `act_and_wait` reports
513
- * `appeared` by concatenating an added subtree, so the string an agent is most likely to copy into
514
- * its next predicate is exactly the shape this query cannot find.
515
- *
516
- * That failure is indistinguishable from the element being absent, which is the expensive half: the
517
- * agent reads "no element matched", concludes the app did not render, and reports a bug against
518
- * correct code.
519
- *
520
- * Returns the DEEPEST container rather than the first, because every ancestor up to `<body>` also
521
- * contains the string and naming `<body>` is not a locator. The deepest one is the tightest scope
522
- * that still holds the whole string, which is the one worth pasting back.
523
- */
524
342
  function splitTextOwner(container, wanted) {
525
343
  let best;
526
344
  let bestDepth = -1;
527
345
  for (const el of elementsUnder(container)) {
528
346
  if (isIgnored(el))
529
- continue; // never point an agent at Reticle's own UI
347
+ continue;
530
348
  if (!fuzzyVisibleText(el.textContent ?? '', wanted))
531
349
  continue;
532
350
  let depth = 0;
@@ -539,20 +357,44 @@ function splitTextOwner(container, wanted) {
539
357
  }
540
358
  return best;
541
359
  }
542
- /** The text this query searched for, in either spelling, or undefined when it searched by something else. */
543
360
  function wantedTextOf(query) {
544
361
  if (query.text !== undefined)
545
362
  return query.text;
546
363
  return QueryBy.TEXT === query.by ? query.value : undefined;
547
364
  }
548
- /** Diagnostic hint for a zero-match query: what testids ARE present in the searched scope. */
365
+ const MAX_NAME_NEAR_MISSES = 5;
366
+ function nameNearMisses(container, query) {
367
+ const role = QueryBy.ROLE === query.by ? query.value : undefined;
368
+ const wanted = query.name;
369
+ if (role === undefined || wanted === undefined || 0 === wanted.length)
370
+ return [];
371
+ const target = normaliseVisibleText(wanted).toLowerCase();
372
+ if (0 === target.length)
373
+ return [];
374
+ const out = [];
375
+ for (const el of elementsUnder(container)) {
376
+ if (isIgnored(el) || getRole(el) !== role)
377
+ continue;
378
+ const name = normaliseVisibleText(getAccessibleName(el));
379
+ if (0 === name.length)
380
+ continue;
381
+ const folded = name.toLowerCase();
382
+ if (!folded.includes(target) && !target.includes(folded))
383
+ continue;
384
+ if (!out.includes(name))
385
+ out.push(name);
386
+ if (out.length >= MAX_NAME_NEAR_MISSES)
387
+ break;
388
+ }
389
+ return out;
390
+ }
549
391
  function buildEmptyHint(query) {
550
392
  const container = resolveContainer(query.scope).container ?? document.body;
551
393
  const all = container.querySelectorAll(`[${TESTID_ATTR}]`);
552
394
  const present = [];
553
395
  for (const el of Array.from(all)) {
554
396
  if (isIgnored(el))
555
- continue; // the "what IS here" hint must not advertise Reticle's own UI either
397
+ continue;
556
398
  const id = el.getAttribute(TESTID_ATTR);
557
399
  if (id !== null && id.length > 0 && !present.includes(id)) {
558
400
  present.push(id);
@@ -560,8 +402,6 @@ function buildEmptyHint(query) {
560
402
  break;
561
403
  }
562
404
  }
563
- // DECLARED, not observed: see declaredTestids. Every present testid is also an observed one, so
564
- // the merged list would make this flag true for any page that has a testid at all.
565
405
  const registered = declaredTestids();
566
406
  const knownEmptyState = present.some((id) => registered.includes(id));
567
407
  const route = `${location.pathname}${location.search}`;
@@ -571,23 +411,17 @@ function buildEmptyHint(query) {
571
411
  presentRegions: buildPresentRegions(query),
572
412
  knownEmptyState,
573
413
  };
574
- // Only for a text search, and only when the string IS here — otherwise this is an ordinary miss
575
- // and the extra clause would lengthen every failure to say nothing.
576
414
  const wanted = wantedTextOf(query);
577
415
  if (wanted !== undefined) {
578
416
  const owner = splitTextOwner(container, wanted);
579
417
  if (owner !== undefined)
580
418
  hint.splitText = describe(owner);
581
419
  }
420
+ const near = nameNearMisses(container, query);
421
+ if (near.length > 0)
422
+ hint.nameNearMiss = near;
582
423
  return hint;
583
424
  }
584
- /**
585
- * Resolve a query to descriptors for the `query` MCP tool.
586
- *
587
- * `count` is carried through deliberately: the server reports match totals from it rather than from
588
- * the array, because the array is capped in transit. Dropping it here - which this function used to
589
- * do - silently put the server back to counting survivors and calling that the answer.
590
- */
591
425
  export function runQuery(query, limit) {
592
426
  const result = matchQuery(query, undefined, limit);
593
427
  const scopeFields = true === result.scopeMissing ? { scopeMissing: true } : {};
@@ -1,18 +1,3 @@
1
- /**
2
- * Cross-realm type tests.
3
- *
4
- * `instanceof` compares against ONE realm's constructor. An element inside a same-origin `<iframe>`
5
- * lives in that document's realm, so `frameButton instanceof HTMLElement` — evaluated with the top
6
- * window's `HTMLElement` — is FALSE for a perfectly ordinary button.
7
- *
8
- * That is not theoretical. Once `query` learned to descend into same-origin frames, it returned the
9
- * frame's button correctly and `act` then rejected the ref with "is not an HTMLElement": found, and
10
- * unusable. A find you cannot act on is worse than a miss, because it reads as a product bug in the
11
- * app rather than in the tool.
12
- *
13
- * These resolve the constructor from the NODE's own document instead, so an element is measured
14
- * against its own realm. Same-realm nodes behave exactly as `instanceof` did.
15
- */
16
1
  export declare const isElement: (n: unknown) => n is Element;
17
2
  export declare const isImage: (n: unknown) => n is HTMLImageElement;
18
3
  export declare const isHtmlElement: (n: unknown) => n is HTMLElement;
@@ -22,9 +7,4 @@ export declare const isSelect: (n: unknown) => n is HTMLSelectElement;
22
7
  export declare const isButton: (n: unknown) => n is HTMLButtonElement;
23
8
  export declare const isForm: (n: unknown) => n is HTMLFormElement;
24
9
  export declare const isFrame: (n: unknown) => n is HTMLIFrameElement;
25
- /**
26
- * The prototype whose native value setter drives `el` — React and friends install their own accessor
27
- * on the instance, so a value write has to go through the prototype's. It must come from the
28
- * ELEMENT's realm for the same reason as above.
29
- */
30
10
  export declare function valuePrototypeOf(el: Element): object | undefined;