@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
@@ -4,12 +4,6 @@ interface ClickGeometry {
4
4
  scrolledIntoView: boolean;
5
5
  }
6
6
  export declare const NO_GEOMETRY: ClickGeometry;
7
- /**
8
- * Full click as a real user produces it: pointerdown -> mousedown -> focus -> pointerup -> mouseup
9
- * -> click. A bare `click` event skips pointer- and focus-gated handlers. Returns the click event's
10
- * `defaultPrevented` so the probe is unchanged. Focus only moves for focusable targets (tabIndex>=0),
11
- * so a plain <div> click still reports focusMoved=null.
12
- */
13
7
  export declare function fireClickSequence(el: HTMLElement, hold?: {
14
8
  ms: number;
15
9
  sleep: (ms: number) => Promise<void>;
@@ -18,22 +12,16 @@ export declare function fireClickSequence(el: HTMLElement, hold?: {
18
12
  prevented: boolean;
19
13
  heldMs: number;
20
14
  }>;
21
- /**
22
- * Click-like geometry honesty: scroll an off-viewport target into view, then hit-test the click
23
- * point. Synthetic dispatch always reaches the target regardless — this is purely so the agent
24
- * learns when the target is off-screen or visually blocked instead of getting a false "it worked".
25
- */
26
15
  export declare function clickGeometry(el: HTMLElement): ClickGeometry;
27
16
  export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
28
- /** Enter/leave pointer events are non-bubbling per spec; keep them so to avoid double-firing. */
29
17
  export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null): void;
30
- /**
31
- * Pointer-based drag (dnd-kit / react-beautiful-dnd) + best-effort HTML5 DnD. Async: yields a
32
- * frame between phases so React commits state between steps (fixes stale-closure handlers).
33
- *
34
- * Dispatches along a PATH with real client coordinates and a held button. Without those a
35
- * geometry-based library resolves the drop target as the source, so the action reports success over
36
- * an app that did not change — a false green produced by the tool rather than caught by it.
37
- */
38
18
  export declare function dragElement(source: HTMLElement, target: HTMLElement | null, data: unknown): Promise<boolean>;
19
+ export declare function fireTapSequence(el: HTMLElement, hold: {
20
+ ms: number;
21
+ sleep: (ms: number) => Promise<void>;
22
+ now: () => number;
23
+ } | undefined): Promise<{
24
+ prevented: boolean;
25
+ heldMs: number;
26
+ }>;
39
27
  export {};
@@ -1,18 +1,12 @@
1
- import { refs } from '../dom/refs.js';
1
+ import { refs } from '../dom/addressing/refs.js';
2
2
  import { hitTestOccluder } from '../dom/occlusion.js';
3
- import { nativeFrame } from '../timers/native-timers.js';
4
- import { asSyntheticInput } from './synthetic-input.js';
3
+ import { nativeFrame } from '../timers/native/native-timers.js';
4
+ import { asSyntheticInput } from './synthetic/synthetic-input.js';
5
5
  export const NO_GEOMETRY = {
6
6
  occluded: false,
7
7
  occludedBy: null,
8
8
  scrolledIntoView: false,
9
9
  };
10
- /**
11
- * Full click as a real user produces it: pointerdown -> mousedown -> focus -> pointerup -> mouseup
12
- * -> click. A bare `click` event skips pointer- and focus-gated handlers. Returns the click event's
13
- * `defaultPrevented` so the probe is unchanged. Focus only moves for focusable targets (tabIndex>=0),
14
- * so a plain <div> click still reports focusMoved=null.
15
- */
16
10
  export async function fireClickSequence(el, hold) {
17
11
  const doc = el.ownerDocument;
18
12
  const from = doc.activeElement ?? doc.body;
@@ -20,14 +14,6 @@ export async function fireClickSequence(el, hold) {
20
14
  asSyntheticInput(() => el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })));
21
15
  if (el.tabIndex >= 0 && 'function' === typeof el.focus)
22
16
  el.focus();
23
- // The gap that makes hold-to-confirm driveable. Everything between down and up used to be
24
- // synchronous, so a control whose contract is "the button is down for N ms" could not be
25
- // expressed at all — the reported case cancelled its own confirm on the mouseup that arrived 7ms
26
- // later. `drag` already splits the pair across a gap; this is that shape with a timer.
27
- //
28
- // The ACHIEVED hold is measured and returned rather than echoed back: `holdMs: 1200` against a
29
- // 1200ms animation is a race by construction, and a caller needs to tell "held 1200" from
30
- // "held 1204". A backgrounded tab throttles timers, so this can legitimately overshoot by a lot.
31
17
  let heldMs = 0;
32
18
  if (hold !== undefined && hold.ms > 0) {
33
19
  const startedAt = hold.now();
@@ -36,17 +22,12 @@ export async function fireClickSequence(el, hold) {
36
22
  }
37
23
  firePointer(el, 'pointerup', from);
38
24
  asSyntheticInput(() => el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })));
39
- // Marked as ours so the annotator's capture-phase listener lets it through. Only the CHECK branch
40
- // used to do this, so the ordinary click action — which is this function — was swallowed whole in
41
- // annotate mode while still reporting `dispatched: true`.
42
25
  const notPrevented = asSyntheticInput(() => el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })));
43
26
  return { prevented: !notPrevented, heldMs };
44
27
  }
45
- /** A box has layout we can reason about (jsdom returns an all-zero box — nothing to hit-test). */
46
28
  function isMeasurable(rect) {
47
29
  return rect.width > 0 || rect.height > 0;
48
30
  }
49
- /** The center of `rect` falls outside the visible viewport. */
50
31
  function isOffViewport(el, rect) {
51
32
  const win = el.ownerDocument.defaultView;
52
33
  if (null === win)
@@ -55,18 +36,12 @@ function isOffViewport(el, rect) {
55
36
  const cy = rect.top + rect.height / 2;
56
37
  return cx < 0 || cy < 0 || cx > win.innerWidth || cy > win.innerHeight;
57
38
  }
58
- /** Hit-test the center: occluded iff the top NON-Reticle element is a foreign subtree (not target/ancestor/descendant). */
59
39
  function hitTest(el, rect) {
60
40
  const top = hitTestOccluder(el, rect);
61
41
  return null === top
62
42
  ? { occluded: false, occludedBy: null }
63
43
  : { occluded: true, occludedBy: refs.refFor(top) };
64
44
  }
65
- /**
66
- * Click-like geometry honesty: scroll an off-viewport target into view, then hit-test the click
67
- * point. Synthetic dispatch always reaches the target regardless — this is purely so the agent
68
- * learns when the target is off-screen or visually blocked instead of getting a false "it worked".
69
- */
70
45
  export function clickGeometry(el) {
71
46
  if (typeof el.getBoundingClientRect !== 'function')
72
47
  return NO_GEOMETRY;
@@ -91,7 +66,6 @@ export function firePointer(el, type, relatedTarget = null) {
91
66
  }
92
67
  });
93
68
  }
94
- /** Enter/leave pointer events are non-bubbling per spec; keep them so to avoid double-firing. */
95
69
  export function firePointerNonBubbling(el, type, relatedTarget = null) {
96
70
  asSyntheticInput(() => {
97
71
  if ('function' === typeof PointerEvent) {
@@ -106,7 +80,6 @@ function makeDataTransfer(data) {
106
80
  if (typeof DataTransfer !== 'function')
107
81
  return null;
108
82
  const dt = new DataTransfer();
109
- // data: { mime, value } or [{ mime, value }, …]
110
83
  const entries = Array.isArray(data) ? data : data !== undefined ? [data] : [];
111
84
  for (const entry of entries) {
112
85
  if ('object' === typeof entry && entry !== null) {
@@ -117,46 +90,20 @@ function makeDataTransfer(data) {
117
90
  }
118
91
  return dt;
119
92
  }
120
- /** `buttons` bitmask for the primary button: 1 while held, 0 once released. */
121
93
  const BUTTON_HELD = 1;
122
94
  const BUTTON_RELEASED = 0;
123
- /**
124
- * How many intermediate moves a drag emits.
125
- *
126
- * Enough to cross any realistic `activationConstraint: { distance: N }` while a sensor is watching,
127
- * and few enough that a drag stays a handful of frames rather than an animation.
128
- */
129
95
  const DRAG_STEPS = 5;
130
- /** The centre of an element in client coordinates — what a real pointer would be over. */
131
96
  function centreOf(el) {
132
97
  const box = el.getBoundingClientRect();
133
98
  return { x: box.left + box.width / 2, y: box.top + box.height / 2 };
134
99
  }
135
- /** A point `t` of the way from `a` to `b`, rounded — pointer coordinates are integers in practice. */
136
100
  function lerp(a, b, t) {
137
101
  return { x: Math.round(a.x + (b.x - a.x) * t), y: Math.round(a.y + (b.y - a.y) * t) };
138
102
  }
139
- /**
140
- * Pointer-based drag (dnd-kit / react-beautiful-dnd) + best-effort HTML5 DnD. Async: yields a
141
- * frame between phases so React commits state between steps (fixes stale-closure handlers).
142
- *
143
- * Dispatches along a PATH with real client coordinates and a held button. Without those a
144
- * geometry-based library resolves the drop target as the source, so the action reports success over
145
- * an app that did not change — a false green produced by the tool rather than caught by it.
146
- */
147
103
  export async function dragElement(source, target, data) {
148
104
  const dest = target ?? source;
149
105
  const from = centreOf(source);
150
106
  const to = centreOf(dest);
151
- /**
152
- * Dispatch ONE pointer/mouse pair with real coordinates and button state.
153
- *
154
- * Everything here was missing before, and each omission breaks a different, standard library
155
- * pattern: without coordinates a geometry-based collision resolver sees a zero delta and reports
156
- * the source as its own drop target; without `buttons: 1` the usual "was the mouse released?"
157
- * guard (`event.buttons === 0`) bails out mid-drag. Boundary events (`over`/`out` bubbling,
158
- * `enter`/`leave` not) do not bubble exactly when a real pointer's would not.
159
- */
160
107
  const fire = (el, type, at, buttons, related) => {
161
108
  const bubbles = !(type.endsWith('enter') || type.endsWith('leave'));
162
109
  const init = {
@@ -177,29 +124,18 @@ export async function dragElement(source, target, data) {
177
124
  el.dispatchEvent(new MouseEvent(type, init));
178
125
  }
179
126
  };
180
- /** Is this path point inside an element's box? Rects are cached; jsdom reports zeros otherwise. */
181
127
  const sourceRect = source.getBoundingClientRect();
182
128
  const destRect = target !== null ? dest.getBoundingClientRect() : null;
183
129
  const crosses = (rect, p) => p.x >= rect.left && p.x <= rect.right && p.y >= rect.top && p.y <= rect.bottom;
184
130
  fire(source, 'pointerdown', from, BUTTON_HELD);
185
131
  fire(source, 'mousedown', from, BUTTON_HELD);
186
132
  await nativeFrame();
187
- // A path, not a jump. `activationConstraint: { distance: N }` is the standard way to keep a
188
- // draggable card clickable, and a sensor only starts a drag once it has SEEN the pointer travel
189
- // that far — which a single move from A to B never shows it.
190
- //
191
- // The crossing announces itself: the first step outside the source fires out/leave on it, and
192
- // the first step inside the destination fires over/enter on it, both with the button still held.
193
- // Without these, React never synthesises `onMouseEnter` on the destination (it derives that from
194
- // delegated `mouseover`/`mouseout`), so drag-to-select grids never extend their selection.
195
133
  let leftSource = false;
196
134
  let enteredDest = false;
197
135
  for (let step = 1; step <= DRAG_STEPS; step += 1) {
198
136
  const at = lerp(from, to, step / DRAG_STEPS);
199
137
  if (!leftSource && !crosses(sourceRect, at)) {
200
138
  leftSource = true;
201
- // relatedTarget is the element the pointer crossed TO/FROM; React's enter/leave synthesis
202
- // reads it off the delegated over/out pair to know which boundary was crossed.
203
139
  fire(source, 'pointerout', at, BUTTON_HELD, dest);
204
140
  fire(source, 'mouseout', at, BUTTON_HELD, dest);
205
141
  fire(source, 'pointerleave', at, BUTTON_HELD, dest);
@@ -216,8 +152,6 @@ export async function dragElement(source, target, data) {
216
152
  fire(dest, 'mousemove', at, BUTTON_HELD);
217
153
  await nativeFrame();
218
154
  }
219
- // A short hop whose sampled steps never land inside the destination box still crossed into it;
220
- // announce the arrival rather than silently skipping the pair.
221
155
  if (!enteredDest && destRect !== null) {
222
156
  fire(dest, 'pointerover', to, BUTTON_HELD, source);
223
157
  fire(dest, 'mouseover', to, BUTTON_HELD, source);
@@ -242,3 +176,70 @@ export async function dragElement(source, target, data) {
242
176
  }
243
177
  return dropPrevented;
244
178
  }
179
+ export async function fireTapSequence(el, hold) {
180
+ const touches = touchListFor(el);
181
+ firePointerTouch(el, 'pointerdown');
182
+ asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchstart', touches)));
183
+ if (el.tabIndex >= 0 && 'function' === typeof el.focus)
184
+ el.focus();
185
+ let heldMs = 0;
186
+ if (hold !== undefined && hold.ms > 0) {
187
+ const started = hold.now();
188
+ await hold.sleep(hold.ms);
189
+ heldMs = hold.now() - started;
190
+ }
191
+ asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchend', [])));
192
+ firePointerTouch(el, 'pointerup');
193
+ const clicked = asSyntheticInput(() => el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })));
194
+ return { prevented: !clicked, heldMs };
195
+ }
196
+ function firePointerTouch(el, type) {
197
+ asSyntheticInput(() => {
198
+ if ('function' === typeof PointerEvent) {
199
+ el.dispatchEvent(new PointerEvent(type, {
200
+ bubbles: true,
201
+ cancelable: true,
202
+ pointerType: 'touch',
203
+ isPrimary: true,
204
+ width: 23,
205
+ height: 23,
206
+ }));
207
+ return;
208
+ }
209
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true }));
210
+ });
211
+ }
212
+ function touchListFor(el) {
213
+ if ('function' !== typeof Touch || 'function' !== typeof el.getBoundingClientRect)
214
+ return [];
215
+ const rect = el.getBoundingClientRect();
216
+ try {
217
+ return [
218
+ new Touch({
219
+ identifier: 0,
220
+ target: el,
221
+ clientX: rect.left + rect.width / 2,
222
+ clientY: rect.top + rect.height / 2,
223
+ }),
224
+ ];
225
+ }
226
+ catch {
227
+ return [];
228
+ }
229
+ }
230
+ function makeTouchEvent(type, touches) {
231
+ if ('function' === typeof TouchEvent) {
232
+ try {
233
+ return new TouchEvent(type, {
234
+ bubbles: true,
235
+ cancelable: true,
236
+ touches,
237
+ targetTouches: touches,
238
+ changedTouches: touches,
239
+ });
240
+ }
241
+ catch {
242
+ }
243
+ }
244
+ return new Event(type, { bubbles: true, cancelable: true });
245
+ }
@@ -0,0 +1,14 @@
1
+ export declare function pressKey(args: Record<string, unknown>): string;
2
+ export declare function pressModifiers(args: Record<string, unknown>): {
3
+ metaKey: boolean;
4
+ ctrlKey: boolean;
5
+ shiftKey: boolean;
6
+ altKey: boolean;
7
+ };
8
+ export declare function pressCode(args: Record<string, unknown>, key: string): string;
9
+ export declare function focusedOrDocument(): HTMLElement;
10
+ export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
11
+ export declare function pressKeys(args: Record<string, unknown>): string[];
12
+ export declare function holdKey(el: HTMLElement, key: string, code: string, mods: Record<string, boolean>, ms: number): Promise<void>;
13
+ export declare function pressCombo(el: HTMLElement, keys: readonly string[], mods: Record<string, boolean>, holdMs: number): Promise<boolean>;
14
+ export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
@@ -0,0 +1,123 @@
1
+ import { ActionType, isGlobalPress, pressKeyFromArgs } from '@reticlehq/core';
2
+ import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
+ import { nativeSetTimeout } from '../timers/native/native-timers.js';
4
+ const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
5
+ import { isHtmlElement } from '../dom/realm.js';
6
+ function asString(value, fallback = '') {
7
+ return 'string' === typeof value ? value : fallback;
8
+ }
9
+ export function pressKey(args) {
10
+ return pressKeyFromArgs(args);
11
+ }
12
+ export function pressModifiers(args) {
13
+ const raw = args['modifiers'];
14
+ const names = Array.isArray(raw) ? raw.map((m) => asString(m).toLowerCase()) : [];
15
+ const has = (...aliases) => aliases.some((a) => names.includes(a));
16
+ return {
17
+ metaKey: has('meta', 'cmd', 'command', 'super', 'win'),
18
+ ctrlKey: has('control', 'ctrl'),
19
+ shiftKey: has('shift'),
20
+ altKey: has('alt', 'option', 'opt'),
21
+ };
22
+ }
23
+ export function pressCode(args, key) {
24
+ const explicit = args['code'];
25
+ if ('string' === typeof explicit && 0 < explicit.length)
26
+ return explicit;
27
+ if (' ' === key)
28
+ return 'Space';
29
+ if (1 === key.length) {
30
+ if (/[a-z]/i.test(key))
31
+ return `Key${key.toUpperCase()}`;
32
+ if (/[0-9]/.test(key))
33
+ return `Digit${key}`;
34
+ return '';
35
+ }
36
+ return /^[A-Z][A-Za-z0-9]*$/.test(key) && KNOWN_NAMED_KEYS.has(key) ? key : '';
37
+ }
38
+ const KNOWN_NAMED_KEYS = new Set([
39
+ 'Enter',
40
+ 'Escape',
41
+ 'Tab',
42
+ 'Backspace',
43
+ 'Delete',
44
+ 'Home',
45
+ 'End',
46
+ 'PageUp',
47
+ 'PageDown',
48
+ 'ArrowUp',
49
+ 'ArrowDown',
50
+ 'ArrowLeft',
51
+ 'ArrowRight',
52
+ 'Insert',
53
+ 'F1',
54
+ 'F2',
55
+ 'F3',
56
+ 'F4',
57
+ 'F5',
58
+ 'F6',
59
+ 'F7',
60
+ 'F8',
61
+ 'F9',
62
+ 'F10',
63
+ 'F11',
64
+ 'F12',
65
+ ]);
66
+ const NO_DOCUMENT_TO_PRESS = 'no document to press into';
67
+ export function focusedOrDocument() {
68
+ const active = document.activeElement;
69
+ if (isHtmlElement(active) && document.body !== active)
70
+ return active;
71
+ if (isHtmlElement(document.body))
72
+ return document.body;
73
+ const root = document.documentElement;
74
+ if (isHtmlElement(root))
75
+ return root;
76
+ throw new Error(NO_DOCUMENT_TO_PRESS);
77
+ }
78
+ export function isReflessDocumentPress(ref, action, args) {
79
+ return 0 === ref.length && isGlobalPress(args) && ActionType.PRESS === action;
80
+ }
81
+ export function pressKeys(args) {
82
+ const raw = args['keys'];
83
+ if (!Array.isArray(raw))
84
+ return [];
85
+ return raw.map((k) => asString(k)).filter((k) => k.length > 0);
86
+ }
87
+ const KEY_REPEAT_MS = 33;
88
+ const KEY_REPEAT_DELAY_MS = 500;
89
+ export async function holdKey(el, key, code, mods, ms) {
90
+ const started = Date.now();
91
+ if (ms > KEY_REPEAT_DELAY_MS)
92
+ await sleep(KEY_REPEAT_DELAY_MS);
93
+ while (Date.now() - started < ms) {
94
+ asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', {
95
+ key,
96
+ code,
97
+ bubbles: true,
98
+ cancelable: true,
99
+ repeat: true,
100
+ ...mods,
101
+ })));
102
+ await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
103
+ }
104
+ }
105
+ export async function pressCombo(el, keys, mods, holdMs) {
106
+ let prevented = false;
107
+ for (const key of keys) {
108
+ const code = pressCode({}, key);
109
+ const ok = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
110
+ if (!ok)
111
+ prevented = true;
112
+ }
113
+ if (holdMs > 0)
114
+ await sleep(holdMs);
115
+ for (const key of [...keys].reverse()) {
116
+ const code = pressCode({}, key);
117
+ asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
118
+ }
119
+ return prevented;
120
+ }
121
+ export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
122
+ 'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
123
+ 'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
@@ -1,94 +1,30 @@
1
1
  import { SettleReason } from '@reticlehq/core';
2
- /**
3
- * Best-effort evidence of whether/why an action landed, so the agent can separate
4
- * "my action missed" vs "app didn't react" vs "tool didn't dispatch". All probes are
5
- * cheap and best-effort (see docs/usage.md).
6
- */
7
2
  interface ActionEffect {
8
- /** We reached dispatch (no throw before it). Typed `true`: if we never dispatch we throw. */
9
3
  dispatched: true;
10
- /** Ref resolved to a still-connected element at read time. */
11
4
  targetMatched: boolean;
12
- /** a11y isVisible(el) at the start of the action. */
13
5
  visible: boolean;
14
- /** Not disabled / aria-disabled at the start of the action. */
15
6
  enabled: boolean;
16
- /** The primary cancelable event's dispatchEvent returned false (handler called preventDefault). */
17
7
  defaultPrevented: boolean;
18
- /** "<prevRef>-><newRef>" if document.activeElement changed, else null. body counts as null. */
19
8
  focusMoved: string | null;
20
- /** fill/type/clear only: input value before !== after; else false. */
21
9
  valueChanged: boolean;
22
- /** Mutation records counted by a short-lived MutationObserver (one microtask + rAF window). */
23
10
  domMutatedWithin: number;
24
- /**
25
- * Text the action put on the page, truncated. OMITTED when it added none.
26
- *
27
- * `domMutatedWithin` says the click did SOMETHING; it cannot say the app rejected you. A login
28
- * that fails returns ok/settled/mutated and reads exactly like one that succeeded, so an agent
29
- * moves on and asserts against a page it misread. The observer already receives these records
30
- * and was keeping only `.length` — the message was being thrown away, not gathered.
31
- *
32
- * Deliberately NOT a verdict: this is what appeared, not what it means. Absence means no text
33
- * was added, which is why a silent class toggle omits the key rather than reporting "".
34
- */
35
11
  appeared?: string;
36
- /**
37
- * Click-like only: the center hit-tested to a foreign element (an overlay is on top). Synthetic
38
- * dispatch STILL delivered the event to the target, but a real user could not click it — treat
39
- * the target as visually blocked. `false` when not click-like or not hit-testable (no layout).
40
- */
41
12
  occluded: boolean;
42
- /** Ref of the element actually on top at the click point when `occluded`, else null. */
43
13
  occludedBy: string | null;
44
- /** Click-like only: the target was off-viewport, so it was scrolled into view before dispatch. */
45
14
  scrolledIntoView: boolean;
46
- /**
47
- * How long the pointer was ACTUALLY held down, for a click with `args.holdMs`. Absent otherwise.
48
- *
49
- * Measured, not echoed. `holdMs: 1200` against a 1200ms animation is a race by construction, so a
50
- * caller needs to tell "held 1200" from "held 1204" — and a backgrounded tab throttles timers, so
51
- * the achieved hold can legitimately be much longer than the one requested.
52
- */
53
15
  heldMs?: number;
54
- /**
55
- * check/uncheck only: the control ALREADY read as the requested state, so NOTHING was dispatched.
56
- * OMITTED otherwise (the uninformative default).
57
- *
58
- * Not clicking is right — clicking an already-checked box would uncheck it. But the branch that
59
- * skips the click reads the DOM PROPERTY, and that is not evidence about what the APPLICATION
60
- * holds. They come apart exactly when it matters: a default-checked input the app has not
61
- * committed, a property written earlier by something else, a control read mid-render. Then `check`
62
- * no-ops, the framework never hears, and every later read agrees because every later read is also
63
- * reading the DOM — reported from the field as a form whose flag was unset in the database while
64
- * Reticle confirmed it checked.
65
- *
66
- * So this is a caveat, not a failure: the requested state is what the DOM shows, and Reticle did
67
- * not put it there. Assert the app's own state (a signal, a request, a derived control) rather than
68
- * the box.
69
- */
70
16
  alreadyAtValue?: true;
71
17
  }
72
18
  interface ActionResult {
73
19
  ok: true;
74
20
  ref: string;
75
21
  action: string;
76
- /** We reached dispatch without throwing. Mirror of effect.dispatched for top-level reads. */
77
22
  dispatched: boolean;
78
- /** A real animation frame fired within the settle budget (false = fallback timer fired). */
79
23
  settled: boolean;
80
- /** Why settle did not complete on a real frame: SettleReason.TIMEOUT when the budget fallback fired, else null. */
81
24
  settleReason: SettleReason | null;
82
25
  effect: ActionEffect;
83
- /** data-testid of the resolved element, when present — lets the server compile a ref-stable replay step. */
84
26
  testid?: string;
85
- /**
86
- * Auto-anchor fallback (attached ONLY when there is no testid): the element's nearest component
87
- * name + source location, so the server can compile a stable `component` anchor instead of a
88
- * degraded role one — addressing the element across replays with zero hand-added testids.
89
- */
90
27
  component?: string;
91
- /** Role + accessible name — the anchor that identifies an INSTANCE, not a JSX site. */
92
28
  role?: string;
93
29
  name?: string;
94
30
  source?: {
@@ -96,28 +32,11 @@ interface ActionResult {
96
32
  line: number;
97
33
  column?: number;
98
34
  };
99
- /** Best-effort caveat the agent should heed (e.g. synthetic hover may not fire enter/leave). */
100
35
  warning?: string;
101
36
  }
102
- /**
103
- * Dispatch the action's events. Returns `defaultPrevented` of the primary cancelable event
104
- * (false for actions whose primary event is non-cancelable or unobservable). Drag/hover-hold
105
- * await internally; the probe wrapper builds the result once they resolve.
106
- */
107
- /**
108
- * The longest a single action may hold the pointer down, in milliseconds.
109
- *
110
- * An unbounded `holdMs` is a tool call that never returns, which is the failure the transport layer
111
- * is shaped around. 30s is far above any real hold-to-confirm (the reported case was 1.2s) and far
112
- * below a hang.
113
- */
114
37
  export declare const MAX_HOLD_MS = 30000;
115
- /**
116
- * Execute a single action against a ref and probe for best-effort evidence of effect.
117
- * Always async: the MutationObserver read needs a microtask + rAF after dispatch.
118
- */
38
+ export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
119
39
  export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
120
- /** Best-effort WebMCP passthrough: call a navigator.modelContext tool if the site exposes one. */
121
40
  export declare function dispatchWebMcp(tool: string, params: Record<string, unknown>, confirmDangerous?: boolean): Promise<unknown>;
122
41
  export interface ActionStep {
123
42
  ref: string;
@@ -127,15 +46,9 @@ export interface ActionStep {
127
46
  interface SequenceStepResult {
128
47
  ref: string;
129
48
  action: string;
130
- /** per-step dispatch/settle outcome (see ActionResult). */
131
49
  dispatched: boolean;
132
50
  settled: boolean;
133
51
  settleReason: SettleReason | null;
134
- /**
135
- * The SAME anchors a single act reports. Reporting only the testid meant a testid-less app
136
- * compiled every sub-step to a volatile ref, so the saved flow carried the degraded sentinel and
137
- * every replay drifted.
138
- */
139
52
  testid?: string;
140
53
  component?: string;
141
54
  role?: string;
@@ -145,7 +58,6 @@ interface SequenceStepResult {
145
58
  line: number;
146
59
  column?: number;
147
60
  };
148
- /** best-effort caveat for this step (e.g. synthetic hover may not fire enter/leave). */
149
61
  warning?: string;
150
62
  }
151
63
  export declare function executeSequence(steps: ActionStep[]): Promise<{