@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,12 +1,11 @@
1
1
  import { EventType, TRANSPORT_LIMITS } from '@reticlehq/core';
2
+ import { observeSafely } from './types.js';
2
3
  import { safeStringify } from '../security/serialization.js';
3
4
  import { requireCapturedMethod } from '../util/captured-method.js';
4
5
  const METHOD_EVENT = {
5
6
  log: EventType.CONSOLE_LOG,
6
7
  warn: EventType.CONSOLE_WARN,
7
8
  error: EventType.CONSOLE_ERROR,
8
- // info/debug are captured for the raw console channel but excluded from summaries/deviation reports
9
- // (low signal — most apps chatter here). Lean: no stack, like log/warn.
10
9
  info: EventType.CONSOLE_INFO,
11
10
  debug: EventType.CONSOLE_DEBUG,
12
11
  };
@@ -21,14 +20,12 @@ function stringifyArgs(args) {
21
20
  })
22
21
  .join(' ');
23
22
  }
24
- /** Stacks can be long; cap so a deep async trace never blows the event budget. */
25
23
  const MAX_STACK_LEN = TRANSPORT_LIMITS.MAX_STACK_LENGTH;
26
24
  function capStack(stack) {
27
25
  if (stack === undefined || 0 === stack.length)
28
26
  return undefined;
29
27
  return stack.length > MAX_STACK_LEN ? stack.slice(0, MAX_STACK_LEN) : stack;
30
28
  }
31
- /** The stack of the first Error argument, if any — the single biggest diagnosis upgrade, near-zero cost. */
32
29
  function firstErrorStack(args) {
33
30
  for (const arg of args) {
34
31
  if (arg instanceof Error)
@@ -36,58 +33,72 @@ function firstErrorStack(args) {
36
33
  }
37
34
  return undefined;
38
35
  }
39
- /** Patch console.{log,warn,error} and window error events. Reversible. */
40
36
  export function installConsole(emit) {
41
37
  const methods = ['log', 'warn', 'error', 'info', 'debug'];
42
38
  const originals = new Map();
43
39
  const patched = new Map();
44
40
  for (const method of methods) {
45
- // Store the true original for teardown identity; call through a bound copy. Read as a stored
46
- // VALUE rather than a method reference — see capturedMethod for why that distinction is the
47
- // honest way to say "I am detaching this deliberately and putting it back".
48
41
  const original = requireCapturedMethod(console, method);
49
42
  originals.set(method, original);
50
43
  const callOriginal = original.bind(console);
51
44
  const wrapper = (...args) => {
52
- // Only console.error carries a stack — the diagnosis case; log/warn stay lean.
53
- const stack = 'error' === method ? firstErrorStack(args) : undefined;
54
- emit(METHOD_EVENT[method], {
55
- message: stringifyArgs(args),
56
- ...(stack === undefined ? {} : { stack }),
57
- });
58
45
  callOriginal(...args);
46
+ observeSafely(() => {
47
+ const stack = 'error' === method ? firstErrorStack(args) : undefined;
48
+ emit(METHOD_EVENT[method], {
49
+ message: stringifyArgs(args),
50
+ ...(stack === undefined ? {} : { stack }),
51
+ });
52
+ });
59
53
  };
60
54
  patched.set(method, wrapper);
61
55
  console[method] = wrapper;
62
56
  }
63
57
  const onError = (event) => {
64
- const stack = capStack(event.error instanceof Error ? event.error.stack : undefined);
65
- emit(EventType.ERROR_UNCAUGHT, {
66
- message: event.message,
67
- source: event.filename,
68
- line: event.lineno,
69
- ...(stack === undefined ? {} : { stack }),
58
+ if (!(event instanceof ErrorEvent)) {
59
+ const target = event.target;
60
+ if (!(target instanceof Element) || target === document)
61
+ return;
62
+ const tag = target.tagName.toLowerCase();
63
+ const url = target.getAttribute('src') ?? target.getAttribute('href') ?? '';
64
+ observeSafely(() => {
65
+ emit(EventType.ERROR_UNCAUGHT, {
66
+ message: `<${tag}> failed to load${0 === url.length ? '' : `: ${url}`}`,
67
+ kind: 'resource',
68
+ ...(0 === url.length ? {} : { source: url }),
69
+ });
70
+ });
71
+ return;
72
+ }
73
+ observeSafely(() => {
74
+ const stack = capStack(event.error instanceof Error ? event.error.stack : undefined);
75
+ emit(EventType.ERROR_UNCAUGHT, {
76
+ message: event.message,
77
+ source: event.filename,
78
+ line: event.lineno,
79
+ ...(stack === undefined ? {} : { stack }),
80
+ });
70
81
  });
71
82
  };
72
83
  const onRejection = (event) => {
73
- const reason = event.reason;
74
- const stack = capStack(reason instanceof Error ? reason.stack : undefined);
75
- emit(EventType.ERROR_UNCAUGHT, {
76
- message: reason instanceof Error ? reason.message : String(reason),
77
- kind: 'unhandledrejection',
78
- ...(stack === undefined ? {} : { stack }),
84
+ observeSafely(() => {
85
+ const reason = event.reason;
86
+ const stack = capStack(reason instanceof Error ? reason.stack : undefined);
87
+ emit(EventType.ERROR_UNCAUGHT, {
88
+ message: reason instanceof Error ? reason.message : String(reason),
89
+ kind: 'unhandledrejection',
90
+ ...(stack === undefined ? {} : { stack }),
91
+ });
79
92
  });
80
93
  };
81
- window.addEventListener('error', onError);
94
+ window.addEventListener('error', onError, true);
82
95
  window.addEventListener('unhandledrejection', onRejection);
83
96
  return () => {
84
97
  for (const [method, original] of originals) {
85
- // Restore only if console[method] still holds our wrapper — a logging SDK (Sentry, LogRocket)
86
- // that wrapped console AFTER connect() must keep its instrumentation on teardown.
87
98
  if (console[method] === patched.get(method))
88
99
  console[method] = original;
89
100
  }
90
- window.removeEventListener('error', onError);
101
+ window.removeEventListener('error', onError, true);
91
102
  window.removeEventListener('unhandledrejection', onRejection);
92
103
  };
93
104
  }
@@ -1,12 +1,2 @@
1
- import type { Emit, Teardown } from './types.js';
2
- /**
3
- * The page asked for ANOTHER browsing context — `window.open` — so the consequence of whatever was
4
- * just clicked may live where this document's SDK cannot follow. An OAuth sign-in is the archetype:
5
- * the POST succeeds, the popup carries the whole flow, and the original tab legitimately never
6
- * changes (#508). Without this event the contradiction layer reads that shape as response-ignored,
7
- * accusing the client of ignoring a response it handed to a window nobody can see.
8
- *
9
- * The wrap preserves the call exactly: same arguments, same return value, and a null/undefined
10
- * first argument (the "open a blank tab" form) still emits, since a blank tab can navigate anywhere.
11
- */
1
+ import { type Emit, type Teardown } from './types.js';
12
2
  export declare function installContextOpen(emit: Emit): Teardown;
@@ -1,25 +1,21 @@
1
1
  import { EventType } from '@reticlehq/core';
2
2
  import { captureMethod } from '../patching/capture-method.js';
3
- /**
4
- * The page asked for ANOTHER browsing context — `window.open` — so the consequence of whatever was
5
- * just clicked may live where this document's SDK cannot follow. An OAuth sign-in is the archetype:
6
- * the POST succeeds, the popup carries the whole flow, and the original tab legitimately never
7
- * changes (#508). Without this event the contradiction layer reads that shape as response-ignored,
8
- * accusing the client of ignoring a response it handed to a window nobody can see.
9
- *
10
- * The wrap preserves the call exactly: same arguments, same return value, and a null/undefined
11
- * first argument (the "open a blank tab" form) still emits, since a blank tab can navigate anywhere.
12
- */
3
+ import { observeSafely } from './types.js';
13
4
  export function installContextOpen(emit) {
14
5
  const originalOpen = captureMethod(window, 'open');
15
6
  if (originalOpen === undefined)
16
7
  return () => { };
17
8
  const openPatch = function (href, target, features) {
18
- emit(EventType.CONTEXT_OPENED, { ...(href === undefined ? {} : { href }) });
19
- return originalOpen.call(this, href, target, features);
9
+ const opened = originalOpen.call(this, href, target, features);
10
+ observeSafely(() => {
11
+ emit(EventType.CONTEXT_OPENED, { ...(href === undefined ? {} : { href }) });
12
+ });
13
+ return opened;
20
14
  };
21
- window.open = openPatch;
15
+ const patchedOpen = openPatch;
16
+ window.open = patchedOpen;
22
17
  return () => {
23
- window.open = originalOpen;
18
+ if (window.open === patchedOpen)
19
+ window.open = originalOpen;
24
20
  };
25
21
  }
@@ -0,0 +1,3 @@
1
+ import { type Emit, type Teardown } from './types.js';
2
+ export declare function isDeparture(href: string, downloadAttr: boolean, here: string): boolean;
3
+ export declare function installDeparture(emit: Emit): Teardown;
@@ -0,0 +1,52 @@
1
+ import { EventType, NetInitiator } from '@reticlehq/core';
2
+ import { observeSafely } from './types.js';
3
+ const NON_NAVIGATING = /^(javascript|mailto|tel|sms|blob|data):/i;
4
+ function anchorFor(target) {
5
+ if (!(target instanceof Element))
6
+ return undefined;
7
+ const anchor = target.closest('a');
8
+ return anchor instanceof HTMLAnchorElement ? anchor : undefined;
9
+ }
10
+ export function isDeparture(href, downloadAttr, here) {
11
+ if (0 === href.length || NON_NAVIGATING.test(href))
12
+ return false;
13
+ if (downloadAttr)
14
+ return true;
15
+ try {
16
+ const target = new URL(href, here);
17
+ const current = new URL(here);
18
+ if (target.origin === current.origin) {
19
+ return target.pathname !== current.pathname || target.search !== current.search;
20
+ }
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ export function installDeparture(emit) {
28
+ if ('undefined' === typeof document)
29
+ return () => undefined;
30
+ let seq = 0;
31
+ const onClick = (event) => {
32
+ observeSafely(() => {
33
+ const anchor = anchorFor(event.target);
34
+ if (anchor === undefined)
35
+ return;
36
+ const href = anchor.getAttribute('href') ?? '';
37
+ const here = document.location.href;
38
+ if (!isDeparture(href, anchor.hasAttribute('download'), here))
39
+ return;
40
+ seq += 1;
41
+ emit(EventType.NET_PENDING, {
42
+ id: `nav-${String(seq)}`,
43
+ method: 'GET',
44
+ url: new URL(href, here).toString(),
45
+ initiator: NetInitiator.NAVIGATION,
46
+ ...(anchor.hasAttribute('download') ? { download: true } : {}),
47
+ });
48
+ });
49
+ };
50
+ document.addEventListener('click', onClick, true);
51
+ return () => document.removeEventListener('click', onClick, true);
52
+ }
@@ -0,0 +1,2 @@
1
+ import { type Emit, type Teardown } from './types.js';
2
+ export declare function installDialogs(emit: Emit): Teardown;
@@ -0,0 +1,39 @@
1
+ import { EventType } from '@reticlehq/core';
2
+ import { observeSafely } from './types.js';
3
+ import { isSyntheticInput } from '../actions/synthetic/synthetic-input.js';
4
+ const DECLINED = { alert: undefined, confirm: false, prompt: null };
5
+ const KINDS = ['alert', 'confirm', 'prompt'];
6
+ export function installDialogs(emit) {
7
+ const slots = window;
8
+ const originals = new Map();
9
+ const patched = new Map();
10
+ for (const kind of KINDS) {
11
+ const original = slots[kind];
12
+ if ('function' !== typeof original)
13
+ continue;
14
+ originals.set(kind, original);
15
+ const callOriginal = original.bind(window);
16
+ const wrapper = (...args) => {
17
+ if (!isSyntheticInput())
18
+ return callOriginal(...args);
19
+ const answered = DECLINED[kind];
20
+ const message = args[0];
21
+ observeSafely(() => {
22
+ emit(EventType.DIALOG_OPENED, {
23
+ kind,
24
+ ...('string' === typeof message ? { message } : {}),
25
+ ...(answered === undefined ? {} : { answered }),
26
+ });
27
+ });
28
+ return answered;
29
+ };
30
+ patched.set(kind, wrapper);
31
+ slots[kind] = wrapper;
32
+ }
33
+ return () => {
34
+ for (const [kind, original] of originals) {
35
+ if (slots[kind] === patched.get(kind))
36
+ slots[kind] = original;
37
+ }
38
+ };
39
+ }
@@ -1,12 +1,6 @@
1
1
  import { EventType, TruncationChannel } from '@reticlehq/core';
2
2
  import { getAccessibleName, getRole, isVisible } from '../dom/a11y.js';
3
- import { refs } from '../dom/refs.js';
4
- /**
5
- * A STABLE identity for the region a mutation happened in. Ambient learning cannot key on the element
6
- * ref for a churning list: every appended row is a NEW element (fresh ref) and a removed one has no ref
7
- * at all, so per-ref counts never accumulate and the region is never recognized as ambient. The
8
- * container persists across ticks, so its testid (or ref) is the identity that does accumulate.
9
- */
3
+ import { refs } from '../dom/addressing/refs.js';
10
4
  function regionKeyOf(target) {
11
5
  const el = isElement(target) ? target : null;
12
6
  if (null === el)
@@ -28,13 +22,11 @@ const WATCHED_ATTRS = [
28
22
  'aria-selected',
29
23
  'aria-checked',
30
24
  'data-state',
31
- // Widened: visual + resource + form-value attributes. Values are capped (they can be long).
32
25
  'style',
33
26
  'src',
34
27
  'href',
35
28
  'value',
36
29
  ];
37
- /** Attribute/text values are capped per event — style/src can be huge and would bloat the ledger. */
38
30
  const MAX_ATTR_VALUE_LEN = 120;
39
31
  function capValue(value) {
40
32
  if (null === value)
@@ -43,24 +35,34 @@ function capValue(value) {
43
35
  }
44
36
  const DIALOG_ROLES = new Set(['dialog', 'alertdialog']);
45
37
  const LIVE_ROLES = new Set(['alert', 'status']);
46
- /** Max meaningful added/removed nodes reported per mutation batch (backpressure). */
47
38
  const MAX_PER_BATCH = 40;
48
39
  function isMeaningful(role, name) {
49
40
  return role !== 'generic' || name.length > 0;
50
41
  }
51
- /** Observe DOM mutations and emit semantic (not raw) events. */
52
- /**
53
- * The trimmed text carried by a node list, or undefined when it carries none.
54
- *
55
- * Whitespace-only nodes are ignored deliberately: a re-render that reshuffles indentation would
56
- * otherwise emit a text change on every layout node, which is noise the timeline cannot afford.
57
- */
42
+ const MOUNT_SCAN_LIMIT = 40;
43
+ function describeMount(node) {
44
+ const role = getRole(node);
45
+ const name = getAccessibleName(node);
46
+ if (isMeaningful(role, name))
47
+ return { role, name, el: node };
48
+ let scanned = 0;
49
+ for (const child of node.querySelectorAll('*')) {
50
+ if (scanned >= MOUNT_SCAN_LIMIT)
51
+ return undefined;
52
+ scanned += 1;
53
+ if (isReticleOverlay(child))
54
+ continue;
55
+ const childRole = getRole(child);
56
+ const childName = getAccessibleName(child);
57
+ if (isMeaningful(childRole, childName))
58
+ return { role: childRole, name: childName, el: child };
59
+ }
60
+ return undefined;
61
+ }
58
62
  const TEXT_NODE = 3;
59
63
  function textOf(nodes) {
60
64
  let out = '';
61
65
  for (const node of nodes) {
62
- // The numeric nodeType, not `Node.TEXT_NODE`: the global `Node` constructor is not guaranteed to
63
- // exist wherever this runs, and referencing it threw a ReferenceError rather than degrading.
64
66
  if (node.nodeType !== TEXT_NODE)
65
67
  continue;
66
68
  out += node.nodeValue ?? '';
@@ -69,17 +71,11 @@ function textOf(nodes) {
69
71
  return trimmed.length > 0 ? trimmed : undefined;
70
72
  }
71
73
  export function installDom(emit) {
72
- // Installed here rather than in the bootstrap list because this observer is the registry's reason
73
- // for existing, and the ordering matters: a root attached between the two would be missed by both.
74
- // It is also the only chance to capture a CLOSED root, which attachShadow returns exactly once.
75
74
  const stopRegistry = installShadowRegistry();
76
75
  const observer = new MutationObserver((records) => {
77
76
  let added = 0;
78
77
  let removed = 0;
79
78
  let changed = 0;
80
- // Count element nodes dropped purely because a per-batch cap was already reached. The cap is on
81
- // MEANINGFUL events, so it is only hit after a real flood — the count is a raw over-estimate
82
- // (it can include not-yet-inspected noise) but `dropped > 0` honestly means "this batch was capped".
83
79
  let dropped = 0;
84
80
  for (const record of records) {
85
81
  if ('attributes' === record.type) {
@@ -90,7 +86,6 @@ export function installDom(emit) {
90
86
  continue;
91
87
  }
92
88
  changed += 1;
93
- // Old value (attributeOldValue) + capped new value — a diff, not just a reading.
94
89
  const value = capValue(target.getAttribute(record.attributeName));
95
90
  const old = capValue(record.oldValue);
96
91
  emit(EventType.DOM_ATTR, {
@@ -102,8 +97,6 @@ export function installDom(emit) {
102
97
  continue;
103
98
  }
104
99
  if ('characterData' === record.type) {
105
- // In-place text change inside an existing subtree (wizard steps, inline edits) —
106
- // childList-only would miss this.
107
100
  const parent = record.target.parentElement;
108
101
  if (parent !== null && !isReticleOverlay(parent)) {
109
102
  if (changed >= MAX_PER_BATCH) {
@@ -117,14 +110,6 @@ export function installDom(emit) {
117
110
  }
118
111
  continue;
119
112
  }
120
- // A TEXT node arriving or leaving is a text change, and it is the ordinary one. `characterData`
121
- // fires only when an EXISTING text node's `data` is edited in place; `el.textContent = x`,
122
- // `innerText`, and React replacing a child all REPLACE the node instead — a childList mutation
123
- // carrying a Text node, which the Element-only loops below skip. Measured on a live page:
124
- // `firstChild.data = x` emitted `dom.text`, while `textContent`, `innerText` and
125
- // `appendChild(createTextNode())` emitted NOTHING. So the most common visible change an app
126
- // makes — updating text — produced no event at all, `reticle_observe` reported "nothing
127
- // happened" over a screen that had visibly changed, and `settled` went quiet immediately.
128
113
  if (textOf(record.addedNodes) !== undefined || textOf(record.removedNodes) !== undefined) {
129
114
  const parent = isElement(record.target) ? record.target : null;
130
115
  if (parent !== null && !isReticleOverlay(parent)) {
@@ -132,8 +117,6 @@ export function installDom(emit) {
132
117
  dropped += 1;
133
118
  else {
134
119
  changed += 1;
135
- // The element's text AFTER the mutation, so a cleared node reports '' rather than the
136
- // text that just left — the reader wants what is on screen now.
137
120
  const text = (parent.textContent ?? '').trim().slice(0, 80);
138
121
  const old = capValue(textOf(record.removedNodes) ?? null);
139
122
  emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
@@ -149,12 +132,12 @@ export function installDom(emit) {
149
132
  }
150
133
  if (isReticleOverlay(node))
151
134
  continue;
152
- const role = getRole(node);
153
- const name = getAccessibleName(node);
154
- if (!isMeaningful(role, name))
135
+ const mounted = describeMount(node);
136
+ if (mounted === undefined)
155
137
  continue;
138
+ const { role, name } = mounted;
156
139
  added += 1;
157
- const ref = refs.refFor(node);
140
+ const ref = refs.refFor(mounted.el);
158
141
  emit(EventType.DOM_ADDED, { role, name, region: regionKeyOf(record.target) }, ref);
159
142
  if (DIALOG_ROLES.has(role) ||
160
143
  LIVE_ROLES.has(role) ||
@@ -172,17 +155,14 @@ export function installDom(emit) {
172
155
  }
173
156
  if (isReticleOverlay(node))
174
157
  continue;
175
- const role = getRole(node);
176
- const name = getAccessibleName(node);
177
- if (!isMeaningful(role, name))
158
+ const gone = describeMount(node);
159
+ if (gone === undefined)
178
160
  continue;
161
+ const { role, name } = gone;
179
162
  removed += 1;
180
- // A removed node has no ref (it is gone), so the CONTAINER is the only stable identity — and it
181
- // is what ambient learning needs to recognize a churning region.
182
163
  emit(EventType.DOM_REMOVED, { role, name, region: regionKeyOf(record.target) });
183
164
  }
184
165
  }
185
- // Never silent: a capped batch tells the ledger its DOM counts understate reality.
186
166
  if (dropped > 0)
187
167
  emit(EventType.TRUNCATED, { channel: TruncationChannel.DOM, dropped });
188
168
  });
@@ -196,13 +176,9 @@ export function installDom(emit) {
196
176
  characterDataOldValue: true,
197
177
  };
198
178
  observer.observe(document.documentElement, options);
199
- // A MutationObserver does NOT cross a shadow boundary, so every root needs its own observation.
200
- // Without this, a click that changed text inside a web component produced an empty observe window —
201
- // the same shape as an app that ignored the click entirely.
202
179
  for (const root of capturedRoots())
203
180
  observer.observe(root, options);
204
181
  const unsubscribe = onShadowRoot((root) => observer.observe(root, options));
205
- // Same-origin frame documents are separate node trees for the same reason, with the same symptom.
206
182
  const stopFrames = observeSameOriginFrames((body) => observer.observe(body, options));
207
183
  return () => {
208
184
  stopFrames();
@@ -1,6 +1,5 @@
1
- import type { Emit, Teardown } from './types.js';
1
+ import { type Emit, type Teardown } from './types.js';
2
2
  interface DownloadOptions {
3
- /** Include a content preview. Gated for the same reason request bodies are: exports hold PII. */
4
3
  capturePreview?: boolean;
5
4
  }
6
5
  export declare function installDownload(emit: Emit, opts?: DownloadOptions): Teardown;
@@ -1,58 +1,37 @@
1
1
  import { EventType } from '@reticlehq/core';
2
2
  import { captureMethod } from '../patching/capture-method.js';
3
- import { projectBody } from './network-body.js';
4
- /**
5
- * Files the app PRODUCES — the one artifact class an outside-the-browser tool cannot inspect.
6
- *
7
- * A generated export never crosses the network: the app builds a Blob, hands it to
8
- * `URL.createObjectURL`, and clicks an anchor. There is no request to intercept, and Playwright MCP
9
- * has no tool that returns a downloaded file at all. Reticle runs inside the page, where the Blob is
10
- * constructed, so the export is observable before it reaches the disk.
11
- *
12
- * Measured on a real merchant dashboard: one Export click produced a CSV wrong three ways at once —
13
- * 25 data rows in a file named `transactions-128-rows.csv`, an unquoted `3 Aug, 10:00 pm` that
14
- * shifts every column after it, and an `amount_inr` header over rows denominated in USD — while the
15
- * click itself reported a clean success and every other channel agreed. The benchmark's ground truth
16
- * records those three as missed by BOTH toolchains, for the same reason: nobody opened the file.
17
- */
18
- /** Text-like types worth counting lines in and previewing. Anything else reports type + size only. */
3
+ import { projectBody } from './net-detail/network-body.js';
4
+ import { observeSafely, observeValue } from './types.js';
19
5
  const TEXTUAL = /text\/|json|csv|xml|ndjson|javascript/i;
20
- /** Lines are reported for text files; the preview is capped and redacted by projectBody. */
21
6
  const PREVIEW_CHARS = 2000;
22
7
  export function installDownload(emit, opts = {}) {
23
- // jsdom does not implement `createObjectURL`, and plenty of users run this SDK under jsdom in their
24
- // own test suite. Reaching for `.bind` on an absent function threw during `connect()` — an
25
- // observability layer taking the host app down with it, which is the one failure mode it may never
26
- // have. No object URLs means no generated files to observe, so this simply stands down.
27
8
  if (typeof URL.createObjectURL !== 'function')
28
9
  return () => undefined;
29
10
  const objectUrl = URL.createObjectURL.bind(URL);
30
11
  const known = new Map();
31
- /**
32
- * `createObjectURL` is where the file becomes real. The filename is NOT here — it lives on the
33
- * anchor that gets clicked — so the blob is remembered against its URL and reported when either
34
- * the anchor fires or, failing that, on a microtask so a download that is never clicked (a
35
- * generated preview, an <img> src) is still visible.
36
- */
37
12
  const originalCreate = captureMethod(URL, 'createObjectURL');
38
- URL.createObjectURL = function reticleObservedObjectUrl(obj) {
13
+ const patchedCreate = function reticleObservedObjectUrl(obj) {
39
14
  const url = objectUrl(obj);
40
- if (typeof Blob !== 'undefined' && obj instanceof Blob) {
41
- // Started here, awaited at report time — never blocking the app's own download path.
42
- const text = TEXTUAL.test(obj.type)
43
- ? obj.text().catch(() => undefined)
44
- : Promise.resolve(undefined);
45
- known.set(url, { type: obj.type, size: obj.size, text });
46
- }
15
+ observeSafely(() => {
16
+ if (typeof Blob !== 'undefined' && obj instanceof Blob) {
17
+ const text = TEXTUAL.test(obj.type)
18
+ ? obj.text().catch(() => undefined)
19
+ : Promise.resolve(undefined);
20
+ known.set(url, { type: obj.type, size: obj.size, text });
21
+ }
22
+ });
47
23
  return url;
48
24
  };
25
+ URL.createObjectURL = patchedCreate;
49
26
  const report = (url, filename) => {
50
27
  const record = known.get(url);
51
28
  if (record === undefined)
52
29
  return;
53
30
  known.delete(url);
54
31
  void record.text.then((text) => {
55
- emitDownload(record, filename, text);
32
+ observeSafely(() => {
33
+ emitDownload(record, filename, text);
34
+ });
56
35
  });
57
36
  };
58
37
  const emitDownload = (record, filename, text) => {
@@ -60,8 +39,6 @@ export function installDownload(emit, opts = {}) {
60
39
  if (filename !== undefined && filename.length > 0)
61
40
  data['filename'] = filename;
62
41
  if (text !== undefined) {
63
- // The LINE COUNT is reported even without preview capture: it is a number, not content, and it
64
- // is the whole tell for "25 rows in a file the toast calls 128".
65
42
  data['lines'] = text.split('\n').filter((l) => l.length > 0).length;
66
43
  if (true === opts.capturePreview) {
67
44
  const { body, truncated } = projectBody(text.slice(0, PREVIEW_CHARS), record.type);
@@ -72,32 +49,38 @@ export function installDownload(emit, opts = {}) {
72
49
  }
73
50
  emit(EventType.DOWNLOAD, data);
74
51
  };
75
- /**
76
- * An anchor click carries the NAME. Captured at the document level so it sees the click whether the
77
- * app dispatches it programmatically or a human does, and whether or not the anchor is in the DOM.
78
- */
79
52
  const onClick = (event) => {
80
- const target = event.target;
81
- const anchor = target instanceof Element ? target.closest('a[download]') : null;
82
- if (null === anchor)
83
- return;
84
- report(anchor.getAttribute('href') ?? '', anchor.getAttribute('download') ?? undefined);
53
+ observeSafely(() => {
54
+ const target = event.target;
55
+ const anchor = target instanceof Element ? target.closest('a[download]') : null;
56
+ if (null === anchor)
57
+ return;
58
+ report(anchor.getAttribute('href') ?? '', anchor.getAttribute('download') ?? undefined);
59
+ });
85
60
  };
86
61
  document.addEventListener('click', onClick, true);
87
- // A programmatic `a.click()` on an anchor never inserted into the document dispatches no bubbling
88
- // event this listener can see, so the anchor path is patched too.
89
62
  const originalClick = captureMethod(HTMLAnchorElement.prototype, 'click');
90
- HTMLAnchorElement.prototype.click = function reticleObservedAnchorClick() {
91
- if (this.hasAttribute('download')) {
92
- report(this.getAttribute('href') ?? '', this.getAttribute('download') ?? undefined);
93
- }
63
+ const patchedClick = function reticleObservedAnchorClick() {
64
+ const pending = observeValue(() => this.hasAttribute('download')
65
+ ? {
66
+ href: this.getAttribute('href') ?? '',
67
+ name: this.getAttribute('download') ?? undefined,
68
+ }
69
+ : undefined);
94
70
  originalClick.call(this);
71
+ observeSafely(() => {
72
+ if (pending !== undefined)
73
+ report(pending.href, pending.name);
74
+ });
95
75
  };
76
+ HTMLAnchorElement.prototype.click = patchedClick;
96
77
  return () => {
97
- // A REAL restore, so teardown leaves the page exactly as it was found.
98
78
  document.removeEventListener('click', onClick, true);
99
- URL.createObjectURL = originalCreate;
100
- HTMLAnchorElement.prototype.click = originalClick;
79
+ if (URL.createObjectURL === patchedCreate)
80
+ URL.createObjectURL = originalCreate;
81
+ if (HTMLAnchorElement.prototype.click === patchedClick) {
82
+ HTMLAnchorElement.prototype.click = originalClick;
83
+ }
101
84
  known.clear();
102
85
  };
103
86
  }
@@ -1,9 +1,3 @@
1
1
  import type { Emit, Teardown } from './types.js';
2
- /** A human-legible label for a focused element, or undefined for body/none. Pure; exported for testing. */
3
2
  export declare function focusLabel(el: EventTarget | null): string | undefined;
4
- /**
5
- * Track element focus movement. Focus dropping to <body> after an act — a modal that closed and stranded
6
- * focus, a button that vanished — is a real accessibility/UX regression no screenshot names. Emits
7
- * FOCUS_CHANGE {to, from, toBody}. Reversible.
8
- */
9
3
  export declare function installFocus(emit: Emit): Teardown;
@@ -1,6 +1,5 @@
1
1
  import { EventType } from '@reticlehq/core';
2
2
  import { getAccessibleName, getRole } from '../dom/a11y.js';
3
- /** A human-legible label for a focused element, or undefined for body/none. Pure; exported for testing. */
4
3
  export function focusLabel(el) {
5
4
  if (!(el instanceof Element) || el === document.body)
6
5
  return undefined;
@@ -11,11 +10,6 @@ export function focusLabel(el) {
11
10
  function toBody(el) {
12
11
  return null === el || el === document.body;
13
12
  }
14
- /**
15
- * Track element focus movement. Focus dropping to <body> after an act — a modal that closed and stranded
16
- * focus, a button that vanished — is a real accessibility/UX regression no screenshot names. Emits
17
- * FOCUS_CHANGE {to, from, toBody}. Reversible.
18
- */
19
13
  export function installFocus(emit) {
20
14
  const ac = new AbortController();
21
15
  const { signal } = ac;
@@ -31,7 +25,6 @@ export function installFocus(emit) {
31
25
  });
32
26
  };
33
27
  const onFocusOut = (event) => {
34
- // Focus left with nothing gaining it → it dropped to the body (the regression case).
35
28
  if (event.relatedTarget !== null)
36
29
  return;
37
30
  const from = last;