@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,26 +1,6 @@
1
- /**
2
- * Gathers the text an action put on the page, from mutation records the observer already receives.
3
- *
4
- * `domMutatedWithin` counts records and throws their content away, so a failed login reports
5
- * `ok / settled / mutated` and reads exactly like a successful one. The message the app rendered
6
- * was passing through this callback the whole time.
7
- *
8
- * Deliberately reports what appeared, never what it means: no error/success classification, no
9
- * guessing which fragment matters. Truncated, because a route change can add a whole page.
10
- */
11
1
  export declare class AppearedText {
12
2
  #private;
13
3
  collect(records: readonly MutationRecord[]): void;
14
- /**
15
- * `{ appeared }` when the APP added text, `{}` otherwise — an absent key means it added none.
16
- *
17
- * `wrote` is the value the action itself just set, and is excluded: a textarea carries its value
18
- * in a child text node, so a controlled one re-rendering after the write mutates characterData
19
- * with the caller's own string. Handing that back is noise wearing the name of evidence, and
20
- * `valueChanged` already reports that the write landed. Exact-match only, so an app that quotes
21
- * your input inside a sentence of its own ("No results for zzz") is still reported — that is
22
- * the app talking.
23
- */
24
4
  effect(wrote?: string): {
25
5
  appeared?: string;
26
6
  };
@@ -1,34 +1,15 @@
1
1
  import { isIgnored } from '../dom/dom-ignore.js';
2
- /** Cap on the reported text. Enough for an error message, far short of a re-rendered page. */
3
2
  const APPEARED_MAX = 200;
4
- /** Separator between distinct fragments that appeared in the same window. */
5
3
  const JOIN = ' | ';
6
4
  const TEXT_NODE = 3;
7
- /** Any letter, in any script — Latin, Cyrillic, CJK, Arabic. */
8
5
  const HAS_LETTER = /\p{L}/u;
9
- /**
10
- * A fragment with no letters at all is not the app saying something.
11
- *
12
- * The Hostile fixture mutates a counter every 16ms, and clicking a button on that page reported
13
- * `appeared: "409"` — the ticker, not the action's effect. A count-up animation is exactly what
14
- * emits a bare number into the settle window, and a bare number carries no message a reader can
15
- * act on. Deliberately conservative: it drops only fragments with NO letters, so "status 500",
16
- * "3 items deleted" and "Could not save" all survive. An app whose only feedback is a naked
17
- * numeral loses it here — the snapshot still shows it, and that is the cheaper mistake.
18
- */
19
6
  function saysSomething(text) {
20
7
  return HAS_LETTER.test(text);
21
8
  }
22
- /**
23
- * Gathers the text an action put on the page, from mutation records the observer already receives.
24
- *
25
- * `domMutatedWithin` counts records and throws their content away, so a failed login reports
26
- * `ok / settled / mutated` and reads exactly like a successful one. The message the app rendered
27
- * was passing through this callback the whole time.
28
- *
29
- * Deliberately reports what appeared, never what it means: no error/success classification, no
30
- * guessing which fragment matters. Truncated, because a route change can add a whole page.
31
- */
9
+ const MAX_MESSAGE_ELEMENTS = 3;
10
+ function isRenderedView(node) {
11
+ return node instanceof Element && node.querySelectorAll('*').length > MAX_MESSAGE_ELEMENTS;
12
+ }
32
13
  export class AppearedText {
33
14
  #seen = new Set();
34
15
  #length = 0;
@@ -37,28 +18,19 @@ export class AppearedText {
37
18
  if (this.#full())
38
19
  return;
39
20
  if ('characterData' === record.type) {
40
- // The NEW value; oldValue is not requested, so this is what the reader now sees.
41
21
  this.#add(record.target.textContent, record.target.parentElement);
42
22
  continue;
43
23
  }
44
24
  for (const node of record.addedNodes) {
45
25
  if (this.#full())
46
26
  return;
27
+ if (isRenderedView(node))
28
+ continue;
47
29
  const owner = TEXT_NODE === node.nodeType ? node.parentElement : elementOf(node);
48
30
  this.#add(node.textContent, owner);
49
31
  }
50
32
  }
51
33
  }
52
- /**
53
- * `{ appeared }` when the APP added text, `{}` otherwise — an absent key means it added none.
54
- *
55
- * `wrote` is the value the action itself just set, and is excluded: a textarea carries its value
56
- * in a child text node, so a controlled one re-rendering after the write mutates characterData
57
- * with the caller's own string. Handing that back is noise wearing the name of evidence, and
58
- * `valueChanged` already reports that the write landed. Exact-match only, so an app that quotes
59
- * your input inside a sentence of its own ("No results for zzz") is still reported — that is
60
- * the app talking.
61
- */
62
34
  effect(wrote) {
63
35
  const said = [...this.#seen].filter((text) => text !== wrote);
64
36
  if (0 === said.length)
@@ -72,8 +44,6 @@ export class AppearedText {
72
44
  return this.#length > APPEARED_MAX;
73
45
  }
74
46
  #add(raw, owner) {
75
- // Reticle's own overlay mutates constantly (the "live" panel); reporting it as the app's
76
- // response would be worse than reporting nothing.
77
47
  if (null !== owner && isIgnored(owner))
78
48
  return;
79
49
  const text = (raw ?? '').replace(/\s+/g, ' ').trim();
@@ -0,0 +1,3 @@
1
+ export declare function dangerousActionContext(el: HTMLElement): string;
2
+ export declare function submitControlFor(el: HTMLElement): HTMLElement | null;
3
+ export declare function requiresDangerousConfirmation(text: string, role?: string): boolean;
@@ -0,0 +1,39 @@
1
+ import { isDangerousActionText } from '@reticlehq/core';
2
+ import { getAccessibleName } from '../dom/a11y.js';
3
+ const LABEL_VALUED_INPUT_TYPES = new Set([
4
+ 'submit',
5
+ 'button',
6
+ 'reset',
7
+ 'image',
8
+ ]);
9
+ function holdsUserText(el) {
10
+ if (el.isContentEditable)
11
+ return true;
12
+ if (el instanceof HTMLTextAreaElement)
13
+ return true;
14
+ if (el instanceof HTMLInputElement)
15
+ return !LABEL_VALUED_INPUT_TYPES.has(el.type.toLowerCase());
16
+ return false;
17
+ }
18
+ export function dangerousActionContext(el) {
19
+ const form = el.closest('form');
20
+ const ownText = holdsUserText(el)
21
+ ? ['', '']
22
+ : [el.textContent ?? '', el.getAttribute('value') ?? ''];
23
+ return [
24
+ getAccessibleName(el),
25
+ ...ownText,
26
+ el.getAttribute('title') ?? '',
27
+ el.getAttribute('aria-label') ?? '',
28
+ el.getAttribute('href') ?? '',
29
+ form?.getAttribute('action') ?? '',
30
+ ].join(' ');
31
+ }
32
+ const SUBMIT_CONTROL_SELECTOR = 'button[type="submit"], input[type="submit"], button:not([type]):not([type=""])';
33
+ export function submitControlFor(el) {
34
+ const found = el.closest('form')?.querySelector(SUBMIT_CONTROL_SELECTOR);
35
+ return found instanceof HTMLElement ? found : null;
36
+ }
37
+ export function requiresDangerousConfirmation(text, role) {
38
+ return isDangerousActionText(text, role);
39
+ }
@@ -1,18 +1,16 @@
1
- /** Outcome of one container scroll — enough for the server to drive a find loop. */
2
1
  interface ScrollResult {
3
- /** scrollTop actually moved (false ⇒ already at the end / not scrollable). */
4
2
  scrolled: boolean;
5
3
  scrollTop: number;
6
4
  scrollHeight: number;
7
5
  clientHeight: number;
8
- /** At (or within 1px of) the bottom — no more rows to reveal by scrolling down. */
9
6
  atEnd: boolean;
7
+ atStart: boolean;
8
+ scrollLeft: number;
9
+ scrollWidth: number;
10
+ clientWidth: number;
11
+ atLeftEnd: boolean;
12
+ atRightEnd: boolean;
10
13
  }
11
- /**
12
- * Scroll the container of `ref` (or the document) down by `dy` (default ~one viewport) so a
13
- * virtualized list mounts its next window of rows. If `fraction` (0–1) is supplied, jumps
14
- * directly to that fraction of scrollHeight instead of stepping — enables bisection for large
15
- * lists where a specific row index is known (targetIndex / totalCount = fraction).
16
- */
17
- export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number): ScrollResult;
14
+ export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
15
+ export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
18
16
  export {};
@@ -1,43 +1,54 @@
1
- import { refs } from '../dom/refs.js';
1
+ import { refs } from '../dom/addressing/refs.js';
2
2
  const FALLBACK_STEP_PX = 400;
3
3
  const VIEWPORT_FRACTION = 0.8;
4
- /** The nearest scrollable ancestor of `el` (inclusive), else the document scrolling element. */
5
- function nearestScrollable(el) {
4
+ function nearestScrollable(el, axis) {
6
5
  let cur = el;
7
6
  while (cur !== null && cur !== document.body) {
8
- const oy = getComputedStyle(cur).overflowY;
9
- if (('auto' === oy || 'scroll' === oy) && cur.scrollHeight > cur.clientHeight)
7
+ const style = getComputedStyle(cur);
8
+ const overflow = 'x' === axis ? style.overflowX : style.overflowY;
9
+ const scrollable = 'x' === axis ? cur.scrollWidth > cur.clientWidth : cur.scrollHeight > cur.clientHeight;
10
+ if (('auto' === overflow || 'scroll' === overflow) && scrollable)
10
11
  return cur;
11
12
  cur = cur.parentElement;
12
13
  }
13
14
  return document.scrollingElement ?? document.documentElement;
14
15
  }
15
- /**
16
- * Scroll the container of `ref` (or the document) down by `dy` (default ~one viewport) so a
17
- * virtualized list mounts its next window of rows. If `fraction` (0–1) is supplied, jumps
18
- * directly to that fraction of scrollHeight instead of stepping — enables bisection for large
19
- * lists where a specific row index is known (targetIndex / totalCount = fraction).
20
- */
21
- export function scrollContainer(ref, dy, fraction) {
16
+ export function scrollContainer(ref, dy, fraction, dx) {
22
17
  const base = ref !== undefined ? refs.resolve(ref) : null;
23
- const target = base instanceof Element
24
- ? nearestScrollable(base)
18
+ return scrollFrom(base instanceof Element ? base : undefined, dy, fraction, dx);
19
+ }
20
+ export function scrollFrom(base, dy, fraction, dx) {
21
+ const axis = dx !== undefined && dx !== 0 && (dy === undefined || 0 === dy) ? 'x' : 'y';
22
+ const target = base !== undefined
23
+ ? nearestScrollable(base, axis)
25
24
  : (document.scrollingElement ?? document.documentElement);
26
25
  const before = target.scrollTop;
26
+ const beforeLeft = target.scrollLeft;
27
27
  if (fraction !== undefined && fraction >= 0 && fraction <= 1) {
28
28
  target.scrollTop = Math.round(target.scrollHeight * fraction);
29
29
  }
30
30
  else {
31
- const step = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
32
- target.scrollTop = before + step;
31
+ if (dx !== undefined && dx !== 0)
32
+ target.scrollLeft = beforeLeft + dx;
33
+ if (dx === undefined || (dy !== undefined && 0 !== dy)) {
34
+ const step = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
35
+ target.scrollTop = before + step;
36
+ }
33
37
  }
34
38
  target.dispatchEvent(new Event('scroll', { bubbles: false }));
35
39
  const after = target.scrollTop;
40
+ const afterLeft = target.scrollLeft;
36
41
  return {
37
- scrolled: after !== before,
42
+ scrolled: after !== before || afterLeft !== beforeLeft,
38
43
  scrollTop: after,
39
44
  scrollHeight: target.scrollHeight,
40
45
  clientHeight: target.clientHeight,
41
46
  atEnd: after + target.clientHeight >= target.scrollHeight - 1,
47
+ atStart: after <= 1,
48
+ scrollLeft: afterLeft,
49
+ scrollWidth: target.scrollWidth,
50
+ clientWidth: target.clientWidth,
51
+ atLeftEnd: afterLeft <= 1,
52
+ atRightEnd: afterLeft + target.clientWidth >= target.scrollWidth - 1,
42
53
  };
43
54
  }
@@ -0,0 +1,2 @@
1
+ export declare function isSyntheticInput(): boolean;
2
+ export declare function asSyntheticInput<T>(dispatch: () => T): T;
@@ -0,0 +1,13 @@
1
+ let depth = 0;
2
+ export function isSyntheticInput() {
3
+ return depth > 0;
4
+ }
5
+ export function asSyntheticInput(dispatch) {
6
+ depth += 1;
7
+ try {
8
+ return dispatch();
9
+ }
10
+ finally {
11
+ depth -= 1;
12
+ }
13
+ }
@@ -1,33 +1,3 @@
1
- /**
2
- * Writing a value into a field, and the two preconditions that make it honest.
3
- *
4
- * Lifted out of actions.ts when that file hit the 600-line cap. It is a cohesive unit: every export
5
- * here exists so that a value write either lands as a real user's edit would, or is refused with a
6
- * reason — never forced through while reporting success.
7
- */
8
- /**
9
- * Set a value on a controlled input the way React expects (native setter + input event).
10
- * Returns the `input` event's dispatchEvent result (defaultPrevented of the primary event).
11
- */
12
1
  export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
13
- /**
14
- * Refuse to write to a field a person could not edit.
15
- *
16
- * `readonly` and `disabled` block USER input, not scripted assignment, so the prototype value setter
17
- * writes straight through both. Measured: filling a readonly input reported `ok:true` and
18
- * `valueChanged:true` with nothing in the effect block marking it read-only. That leaves the app in a
19
- * state no user could reach, and every conclusion drawn from what follows is about software nobody
20
- * can operate — the same reason the click path reports `occluded`, and the same choice Playwright
21
- * makes when it refuses to fill a non-editable element.
22
- */
23
- /**
24
- * A rich-text target is UNSUPPORTED, not a mistake by the caller.
25
- *
26
- * Every rich-text editor (TipTap, Quill, ProseMirror, Slate, Lexical) is `[contenteditable]`, and
27
- * these actions handle only input/textarea. `cannot fill a <div>` reads as "you picked the wrong
28
- * element" and sends the reader hunting for a better selector that does not exist. Support is not
29
- * faked: assigning textContent would update the DOM while the editor's own model kept the old value,
30
- * so the tool would report success for content the app will never submit.
31
- */
32
2
  export declare function assertNotRichText(el: HTMLElement, action: string): void;
33
3
  export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
@@ -1,18 +1,6 @@
1
1
  import { captureValueSetter } from '../patching/capture-method.js';
2
2
  import { valuePrototypeOf } from '../dom/realm.js';
3
- /**
4
- * Writing a value into a field, and the two preconditions that make it honest.
5
- *
6
- * Lifted out of actions.ts when that file hit the 600-line cap. It is a cohesive unit: every export
7
- * here exists so that a value write either lands as a real user's edit would, or is refused with a
8
- * reason — never forced through while reporting success.
9
- */
10
- /**
11
- * Set a value on a controlled input the way React expects (native setter + input event).
12
- * Returns the `input` event's dispatchEvent result (defaultPrevented of the primary event).
13
- */
14
3
  export function setNativeValue(el, value) {
15
- // Realm-aware: a controlled input inside a same-origin frame has its own prototype.
16
4
  const proto = valuePrototypeOf(el);
17
5
  const setter = proto === undefined ? undefined : captureValueSetter(proto);
18
6
  if (setter !== undefined) {
@@ -25,28 +13,7 @@ export function setNativeValue(el, value) {
25
13
  el.dispatchEvent(new Event('change', { bubbles: true }));
26
14
  return !notPrevented;
27
15
  }
28
- /**
29
- * Refuse to write to a field a person could not edit.
30
- *
31
- * `readonly` and `disabled` block USER input, not scripted assignment, so the prototype value setter
32
- * writes straight through both. Measured: filling a readonly input reported `ok:true` and
33
- * `valueChanged:true` with nothing in the effect block marking it read-only. That leaves the app in a
34
- * state no user could reach, and every conclusion drawn from what follows is about software nobody
35
- * can operate — the same reason the click path reports `occluded`, and the same choice Playwright
36
- * makes when it refuses to fill a non-editable element.
37
- */
38
- /**
39
- * A rich-text target is UNSUPPORTED, not a mistake by the caller.
40
- *
41
- * Every rich-text editor (TipTap, Quill, ProseMirror, Slate, Lexical) is `[contenteditable]`, and
42
- * these actions handle only input/textarea. `cannot fill a <div>` reads as "you picked the wrong
43
- * element" and sends the reader hunting for a better selector that does not exist. Support is not
44
- * faked: assigning textContent would update the DOM while the editor's own model kept the old value,
45
- * so the tool would report success for content the app will never submit.
46
- */
47
16
  export function assertNotRichText(el, action) {
48
- // The ATTRIBUTE, not `isContentEditable`: jsdom does not implement the property, so a test would
49
- // pass against a guard that never fires in the environment the unit suite runs in.
50
17
  const flag = el.getAttribute('contenteditable');
51
18
  if (el.isContentEditable || (flag !== null && flag !== 'false')) {
52
19
  throw new Error(`cannot ${action} a contenteditable element — rich-text editors keep their own document model, ` +
@@ -1,6 +1,4 @@
1
1
  export type CommandHandler = (args: Record<string, unknown>) => unknown;
2
- /** Query param appended on a hard reload to bypass the browser cache. */
3
2
  export declare const RELOAD_CACHE_BUST_PARAM = "_reticle_reload";
4
3
  export declare function resolveNavigationUrl(rawUrl: string, baseUrl: string): string | null;
5
- /** Map browser command names to handlers. Used by the transport on each COMMAND. */
6
4
  export declare function createCommandRegistry(): Map<string, CommandHandler>;
@@ -1,23 +1,23 @@
1
1
  import { ActionType, ComponentStateReason, DANGEROUS_ACTION_CONFIRM_ARG, ElementQuerySchema, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
2
2
  import { buildSnapshot } from '../dom/snapshot.js';
3
+ import { paintContextOf } from '../dom/paint-context.js';
3
4
  import { matchQuery, runQuery } from '../dom/query.js';
4
5
  import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/actions.js';
5
6
  import { describe } from '../dom/a11y.js';
6
- import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/source.js';
7
+ import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/addressing/source.js';
7
8
  import { themeReport } from '../dom/theme.js';
8
- import { refs } from '../dom/refs.js';
9
+ import { refs } from '../dom/addressing/refs.js';
9
10
  import { editEpoch } from '../edit-epoch.js';
10
11
  import { isButton, isInput } from '../dom/realm.js';
11
12
  import { hitTestOccluder } from '../dom/occlusion.js';
12
13
  import { readStorage } from '../observers/storage.js';
13
14
  import { captureDesktopWindow } from '../dom/desktop-capture.js';
14
- import { identifyComponent, readComponentState } from '../registry/adapters.js';
15
+ import { identifyComponent, readComponentState } from '../registry/stores/adapters.js';
15
16
  import { readStoresWithTruncation, readStoresRaw, storeNames } from '../registry/stores.js';
16
17
  import { sanitizeWithReport } from '../security/serialization.js';
17
18
  import { getCapabilities } from '../registry/capabilities.js';
18
19
  import { freezeClock, advanceClock, resetClock, isClockFrozen } from '../timers/clock.js';
19
20
  import { scrollContainer } from '../actions/scroll.js';
20
- /** Query param appended on a hard reload to bypass the browser cache. */
21
21
  export const RELOAD_CACHE_BUST_PARAM = '_reticle_reload';
22
22
  function str(value) {
23
23
  return 'string' === typeof value ? value : undefined;
@@ -49,10 +49,6 @@ function queryFromArgs(args) {
49
49
  alt: str(args['alt']),
50
50
  component: str(args['component']),
51
51
  scope: str(args['scope']),
52
- // The THIRD allowlist a query input has to appear in — tool schema, server forward, and here.
53
- // `self` was in the first two and missing from this one, so a live call returned zero matches on
54
- // an element that was plainly on the page, with no error to explain it. Same shape as the
55
- // `attrs` drop below it.
56
52
  self: true === args['self'] ? true : undefined,
57
53
  attrs: Array.isArray(args['attrs'])
58
54
  ? args['attrs'].filter((a) => 'string' === typeof a)
@@ -62,20 +58,12 @@ function queryFromArgs(args) {
62
58
  }
63
59
  function inspect(ref) {
64
60
  const el = refs.resolve(ref);
65
- // THROW, exactly as executeAction does. Returning an error payload made it a SUCCESSFUL command
66
- // result, which the server then handed to reticle_inspect's outputSchema (ref/role/name/states/
67
- // visible all required) — so the MCP layer answered -32602 Output validation error for the most
68
- // ordinary thing that follows a click. The wording matches actions.ts because the server's
69
- // recovery table keys off /no longer resolves to an element/ to attach the stale-ref recovery.
70
61
  if (null === el)
71
62
  throw new Error(editEpoch.staleRefMessage(ref));
72
63
  const rect = el.getBoundingClientRect();
73
64
  const component = identifyComponent(el);
74
65
  const view = el.ownerDocument.defaultView;
75
66
  const cs = view !== null ? view.getComputedStyle(el) : null;
76
- // Computed style the a11y tree is blind to — `cursor` (does it look interactive?), display/
77
- // visibility, and color/opacity — so a UI bug that leaves the element "present but unusable"
78
- // (dead cursor, invisible, recolored) is observable in one inspect call.
79
67
  const styles = cs !== null
80
68
  ? {
81
69
  color: cs.color,
@@ -86,16 +74,7 @@ function inspect(ref) {
86
74
  visibility: cs.visibility,
87
75
  }
88
76
  : null;
89
- // `describe()` carries the CHEAP source (the nearest babel-stamped ancestor), because it runs per
90
- // element on paths that describe hundreds at once. inspect is a single-element path, so it takes
91
- // the adapter's answer first — the component that actually RENDERED this element, not the nearest
92
- // stamped host. `act` already did this; inspect did not, so the two tools disagreed about the same
93
- // ref and inspect — the tool you reach for to ask where something lives — was the one saying null.
94
77
  const source = formatSource(sourceFor(el, component?.source));
95
- // Say WHY the source is missing rather than omitting the field and leaving the caller to guess.
96
- // No stamp anywhere in the document is decisive: the loader is not running, so no element will
97
- // have one this session, and the fix is a build-config change rather than anything about this
98
- // element. Only computed when `source` is already absent, so the ordinary path pays nothing.
99
78
  const sourceUnavailable = source !== undefined
100
79
  ? undefined
101
80
  : documentHasSourceStamps(el.ownerDocument)
@@ -106,31 +85,32 @@ function inspect(ref) {
106
85
  scrollHeight: el.scrollHeight,
107
86
  clientHeight: el.clientHeight,
108
87
  overflowY: cs?.overflowY ?? 'visible',
88
+ scrollLeft: el.scrollLeft,
89
+ scrollWidth: el.scrollWidth,
90
+ clientWidth: el.clientWidth,
91
+ overflowX: cs?.overflowX ?? 'visible',
109
92
  };
93
+ const paintContext = paintContextOf(el);
110
94
  return {
111
95
  ...describe(el),
112
96
  ...(source !== undefined ? { source } : {}),
97
+ ...(paintContext.length > 0 ? { paintContext } : {}),
113
98
  ...(sourceUnavailable !== undefined ? { sourceUnavailable } : {}),
114
99
  tag: el.tagName.toLowerCase(),
115
100
  href: el.getAttribute('href') ?? undefined,
116
101
  formAction: isButton(el) || isInput(el) ? (el.form?.getAttribute('action') ?? undefined) : undefined,
117
102
  formText: isButton(el) || isInput(el) ? (el.form?.textContent ?? undefined) : undefined,
118
103
  box: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
119
- // True when another element sits over this one's center point — the click would hit the overlay,
120
- // not this control (a z-index/overlay bug the DOM tree cannot show).
121
104
  occluded: isOccluded(el, rect),
122
105
  styles,
123
106
  scroll,
124
- // Theme compliance vs the app's design tokens (off-theme colors a DOM tool can't judge).
125
107
  theme: cs !== null ? themeReport(cs) : null,
126
108
  component,
127
109
  };
128
110
  }
129
- /** Whether a NON-Reticle element covers this one's center point (a transparent overlay / z-index bug). */
130
111
  function isOccluded(el, rect) {
131
112
  return hitTestOccluder(el, rect) !== null;
132
113
  }
133
- /** Narrowing guard: an adapter returned a ComponentStateResult (has a boolean `ok`). */
134
114
  function isComponentStateResult(value) {
135
115
  return ('object' === typeof value && value !== null && 'ok' in value && 'boolean' === typeof value.ok);
136
116
  }
@@ -138,21 +118,8 @@ const COMPONENT_UNAVAILABLE = {
138
118
  ok: false,
139
119
  reason: ComponentStateReason.UNAVAILABLE,
140
120
  };
141
- /**
142
- * Stores are the reliable path. The `ref` component read is bounded: a stale ref, no
143
- * adapter, or an adapter returning a non-conforming value all collapse to a structured
144
- * `{ ok: false, reason }` — never a raw (possibly circular) object that could hang serialization.
145
- *
146
- * `path`/`depth` are applied HERE, in the page, BEFORE the value crosses the transport — so a scoped
147
- * read of a huge store (e.g. `deployments.0.status` on a 500-row store) returns only the small
148
- * sub-tree and is never truncated. (Previously selection ran server-side, AFTER the whole store had
149
- * already been size-capped in transit, which silently lost any field after a large array.)
150
- */
151
121
  function readState(ref, store, path, depth) {
152
122
  const names = storeNames();
153
- // Scoped read: walk `path` into the RAW (uncapped) store, then sanitize only the selected sub-tree.
154
- // Selecting before the transport cap is what lets a deep/large path (row 250 of a 500-row array)
155
- // resolve — capping first would truncate the store before selection ever reached the row.
156
123
  if (path !== undefined || depth !== undefined) {
157
124
  const rawStores = readStoresRaw(store);
158
125
  const base = store !== undefined ? rawStores[store] : { stores: rawStores, storeNames: names };
@@ -164,13 +131,8 @@ function readState(ref, store, path, depth) {
164
131
  path,
165
132
  found: selection.found,
166
133
  value: projected.value,
167
- // Even a SCOPED read can hit the caps when the selected sub-tree is itself large; saying so is
168
- // what keeps "the list is short" distinguishable from "I shortened the list".
169
134
  ...(projected.truncation === undefined ? {} : { truncation: projected.truncation }),
170
135
  ...('availableKeys' in selection ? { availableKeys: selection.availableKeys } : {}),
171
- // How many keys there REALLY were, when the near-miss list is a sample. Without it, 50 names
172
- // and no marker reads as "the key you asked for does not exist" — the strongest negative
173
- // signal there is, and a false one when the key is simply number 51.
174
136
  ...('totalKeys' in selection ? { totalKeys: selection.totalKeys } : {}),
175
137
  storeNames: names,
176
138
  };
@@ -180,8 +142,6 @@ function readState(ref, store, path, depth) {
180
142
  stores,
181
143
  storeNames: names,
182
144
  };
183
- // The whole-store read is where a large store silently became a small one. Present only when a cap
184
- // actually fired, so an intact read is unchanged and the field's presence is the warning.
185
145
  if (truncation !== undefined)
186
146
  result.truncation = truncation;
187
147
  if (ref !== undefined && ref.length > 0) {
@@ -196,19 +156,6 @@ function readState(ref, store, path, depth) {
196
156
  }
197
157
  return result;
198
158
  }
199
- /**
200
- * Every animation, and WHICH element it drives.
201
- *
202
- * Reported from the field: a page with nine concurrent animations returned nine indistinguishable
203
- * `{playState, currentTime, duration}` rows. `a.effect` was read only for `getTiming()`, never for
204
- * its `target` — so the tool advertised "targets/timing" and shipped timing alone. A list you cannot
205
- * index is not a list, and the question this tool exists to answer is "is THAT thing still
206
- * animating".
207
- *
208
- * `describe()` is reused rather than inventing a target shape: it is what `reticle_query` returns,
209
- * so the descriptor carries a `ref` the agent can pass straight to `reticle_act` — a target you
210
- * cannot act on would be half an answer.
211
- */
212
159
  function listAnimations() {
213
160
  const doc = document;
214
161
  if (typeof doc.getAnimations !== 'function')
@@ -216,9 +163,6 @@ function listAnimations() {
216
163
  const animations = doc.getAnimations().map((a) => {
217
164
  const effect = a.effect;
218
165
  const timing = effect?.getTiming();
219
- // A KeyframeEffect's target is legitimately nullable, and only KeyframeEffect has one at all.
220
- // `null` is reported explicitly: omitting the key would make "this animation has no element"
221
- // indistinguishable from the bug above, which is the distinction the fix exists to draw.
222
166
  const target = effect?.target ?? null;
223
167
  return {
224
168
  playState: a.playState,
@@ -240,14 +184,11 @@ export function resolveNavigationUrl(rawUrl, baseUrl) {
240
184
  return null;
241
185
  }
242
186
  }
243
- /** Map browser command names to handlers. Used by the transport on each COMMAND. */
244
187
  export function createCommandRegistry() {
245
188
  const reg = new Map();
246
189
  reg.set(ReticleCommand.SNAPSHOT, (args) => buildSnapshot({
247
190
  scope: str(args['scope']),
248
191
  mode: str(args['mode']) ?? SnapshotMode.FULL,
249
- // Only the completion re-read sets this: it re-reads a branch the truncated walk stopped
250
- // BEFORE emitting, so that branch's own line has to come back with its descendants.
251
192
  includeRoot: true === args['includeRoot'],
252
193
  }));
253
194
  reg.set(ReticleCommand.QUERY, (args) => {
@@ -300,7 +241,6 @@ export function createCommandRegistry() {
300
241
  });
301
242
  reg.set(ReticleCommand.REFRESH, (args) => {
302
243
  if (true === args['hard']) {
303
- // Hard reload: navigate to self with a cache-busting param then replace history.
304
244
  const url = new URL(window.location.href);
305
245
  url.searchParams.set(RELOAD_CACHE_BUST_PARAM, String(Date.now()));
306
246
  window.location.replace(url.toString());