@reticlehq/browser 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/dist/actions/actions-dom.d.ts +0 -20
  2. package/dist/actions/actions-dom.js +0 -66
  3. package/dist/actions/actions-press.d.ts +10 -0
  4. package/dist/actions/actions-press.js +77 -0
  5. package/dist/actions/actions.d.ts +0 -89
  6. package/dist/actions/actions.js +24 -358
  7. package/dist/actions/appeared-text.d.ts +0 -20
  8. package/dist/actions/appeared-text.js +0 -36
  9. package/dist/actions/danger-context.d.ts +3 -0
  10. package/dist/actions/danger-context.js +39 -0
  11. package/dist/actions/scroll.d.ts +0 -9
  12. package/dist/actions/scroll.js +0 -7
  13. package/dist/actions/synthetic-input.d.ts +0 -7
  14. package/dist/actions/synthetic-input.js +0 -27
  15. package/dist/actions/value-input.d.ts +0 -30
  16. package/dist/actions/value-input.js +0 -33
  17. package/dist/commands/commands.d.ts +0 -2
  18. package/dist/commands/commands.js +0 -67
  19. package/dist/connect-options.d.ts +2 -96
  20. package/dist/dom/a11y.d.ts +0 -29
  21. package/dist/dom/a11y.js +1 -88
  22. package/dist/dom/auto-anchor.d.ts +0 -40
  23. package/dist/dom/auto-anchor.js +0 -33
  24. package/dist/dom/chart.d.ts +0 -61
  25. package/dist/dom/chart.js +0 -60
  26. package/dist/dom/desktop-capture.d.ts +0 -30
  27. package/dist/dom/desktop-capture.js +0 -72
  28. package/dist/dom/dom-ignore.d.ts +0 -30
  29. package/dist/dom/dom-ignore.js +0 -47
  30. package/dist/dom/occlusion.d.ts +0 -7
  31. package/dist/dom/occlusion.js +0 -7
  32. package/dist/dom/query.d.ts +0 -14
  33. package/dist/dom/query.js +48 -214
  34. package/dist/dom/realm.d.ts +0 -20
  35. package/dist/dom/realm.js +0 -28
  36. package/dist/dom/refs.d.ts +0 -76
  37. package/dist/dom/refs.js +2 -133
  38. package/dist/dom/shadow-registry.d.ts +0 -33
  39. package/dist/dom/shadow-registry.js +2 -72
  40. package/dist/dom/snapshot.d.ts +2 -43
  41. package/dist/dom/snapshot.js +26 -151
  42. package/dist/dom/source.d.ts +0 -44
  43. package/dist/dom/source.js +0 -45
  44. package/dist/dom/theme.d.ts +0 -37
  45. package/dist/dom/theme.js +2 -52
  46. package/dist/dom/virtualized.d.ts +0 -17
  47. package/dist/dom/virtualized.js +0 -27
  48. package/dist/edit-epoch.d.ts +0 -22
  49. package/dist/edit-epoch.js +0 -52
  50. package/dist/index.js +1 -23
  51. package/dist/observers/animation.d.ts +0 -1
  52. package/dist/observers/animation.js +0 -3
  53. package/dist/observers/blind-spots.d.ts +0 -26
  54. package/dist/observers/blind-spots.js +2 -61
  55. package/dist/observers/console.d.ts +1 -2
  56. package/dist/observers/console.js +41 -30
  57. package/dist/observers/context-open.d.ts +1 -11
  58. package/dist/observers/context-open.js +10 -14
  59. package/dist/observers/departure.d.ts +3 -0
  60. package/dist/observers/departure.js +52 -0
  61. package/dist/observers/dialogs.d.ts +2 -0
  62. package/dist/observers/dialogs.js +39 -0
  63. package/dist/observers/dom.js +0 -44
  64. package/dist/observers/download.d.ts +1 -2
  65. package/dist/observers/download.js +38 -55
  66. package/dist/observers/focus.d.ts +0 -6
  67. package/dist/observers/focus.js +0 -7
  68. package/dist/observers/frames.d.ts +0 -9
  69. package/dist/observers/frames.js +1 -28
  70. package/dist/observers/health.d.ts +0 -5
  71. package/dist/observers/health.js +1 -33
  72. package/dist/observers/install-all.d.ts +2 -0
  73. package/dist/observers/install-all.js +26 -27
  74. package/dist/observers/ipc.d.ts +0 -25
  75. package/dist/observers/ipc.js +0 -65
  76. package/dist/observers/navigation.d.ts +2 -0
  77. package/dist/observers/navigation.js +33 -0
  78. package/dist/observers/network-body.d.ts +5 -28
  79. package/dist/observers/network-body.js +11 -108
  80. package/dist/observers/network-redact.d.ts +1 -20
  81. package/dist/observers/network-redact.js +1 -80
  82. package/dist/observers/network-stream.d.ts +0 -21
  83. package/dist/observers/network-stream.js +1 -44
  84. package/dist/observers/network.d.ts +2 -26
  85. package/dist/observers/network.js +177 -294
  86. package/dist/observers/perf.d.ts +0 -10
  87. package/dist/observers/perf.js +0 -16
  88. package/dist/observers/route.d.ts +1 -2
  89. package/dist/observers/route.js +13 -19
  90. package/dist/observers/scroll.d.ts +0 -1
  91. package/dist/observers/scroll.js +0 -9
  92. package/dist/observers/sdk-failure.d.ts +1 -9
  93. package/dist/observers/sdk-failure.js +1 -30
  94. package/dist/observers/state.d.ts +1 -11
  95. package/dist/observers/state.js +13 -41
  96. package/dist/observers/storage.d.ts +0 -14
  97. package/dist/observers/storage.js +10 -34
  98. package/dist/observers/types.d.ts +0 -15
  99. package/dist/observers/types.js +0 -14
  100. package/dist/patching/capture-method.d.ts +0 -24
  101. package/dist/patching/capture-method.js +0 -26
  102. package/dist/presenter/overlay.d.ts +0 -1
  103. package/dist/presenter/overlay.js +0 -1
  104. package/dist/presenter/presenter-brand.d.ts +0 -19
  105. package/dist/presenter/presenter-brand.js +0 -21
  106. package/dist/presenter/presenter-config.d.ts +1 -105
  107. package/dist/presenter/presenter-config.js +2 -91
  108. package/dist/presenter/presenter-controls.d.ts +0 -29
  109. package/dist/presenter/presenter-controls.js +3 -131
  110. package/dist/presenter/presenter-dock-layout.d.ts +0 -3
  111. package/dist/presenter/presenter-dock-layout.js +0 -4
  112. package/dist/presenter/presenter-drag.d.ts +2 -24
  113. package/dist/presenter/presenter-drag.js +0 -26
  114. package/dist/presenter/presenter-effects.d.ts +0 -9
  115. package/dist/presenter/presenter-effects.js +0 -9
  116. package/dist/presenter/presenter-glow.d.ts +0 -21
  117. package/dist/presenter/presenter-glow.js +1 -27
  118. package/dist/presenter/presenter-heroicons-data.d.ts +0 -8
  119. package/dist/presenter/presenter-heroicons-data.js +0 -8
  120. package/dist/presenter/presenter-hud-chrome.d.ts +0 -10
  121. package/dist/presenter/presenter-hud-chrome.js +0 -10
  122. package/dist/presenter/presenter-icons.d.ts +0 -6
  123. package/dist/presenter/presenter-icons.js +0 -6
  124. package/dist/presenter/presenter-log.d.ts +0 -38
  125. package/dist/presenter/presenter-log.js +1 -45
  126. package/dist/presenter/presenter-report-copy.d.ts +5 -66
  127. package/dist/presenter/presenter-report-copy.js +5 -66
  128. package/dist/presenter/presenter-report-styles.d.ts +0 -6
  129. package/dist/presenter/presenter-report-styles.js +6 -6
  130. package/dist/presenter/presenter-report.d.ts +4 -7
  131. package/dist/presenter/presenter-report.js +18 -76
  132. package/dist/presenter/presenter-run-state.d.ts +0 -7
  133. package/dist/presenter/presenter-run-state.js +0 -5
  134. package/dist/presenter/presenter-settings-styles.d.ts +0 -1
  135. package/dist/presenter/presenter-settings-styles.js +0 -1
  136. package/dist/presenter/presenter-settings.d.ts +1 -23
  137. package/dist/presenter/presenter-settings.js +1 -31
  138. package/dist/presenter/presenter-shell-styles.js +2 -5
  139. package/dist/presenter/presenter-shell.d.ts +2 -18
  140. package/dist/presenter/presenter-shell.js +1 -73
  141. package/dist/presenter/presenter-styles.d.ts +0 -9
  142. package/dist/presenter/presenter-styles.js +0 -9
  143. package/dist/presenter/presenter-tally.d.ts +0 -5
  144. package/dist/presenter/presenter-tally.js +0 -4
  145. package/dist/presenter/presenter-test-helpers.d.ts +0 -2
  146. package/dist/presenter/presenter-test-helpers.js +0 -2
  147. package/dist/presenter/presenter-verbs.js +0 -7
  148. package/dist/presenter/presenter-workspace.d.ts +0 -4
  149. package/dist/presenter/presenter-workspace.js +1 -8
  150. package/dist/presenter/presenter.d.ts +0 -60
  151. package/dist/presenter/presenter.js +4 -118
  152. package/dist/recorder/recorder-styles.d.ts +0 -6
  153. package/dist/recorder/recorder-styles.js +0 -6
  154. package/dist/recorder/recorder.d.ts +0 -34
  155. package/dist/recorder/recorder.js +0 -49
  156. package/dist/registry/adapters.d.ts +0 -10
  157. package/dist/registry/adapters.js +0 -10
  158. package/dist/registry/auto-testids.d.ts +0 -19
  159. package/dist/registry/auto-testids.js +1 -21
  160. package/dist/registry/capabilities.d.ts +0 -31
  161. package/dist/registry/capabilities.js +1 -46
  162. package/dist/registry/commit-and-signal.d.ts +0 -6
  163. package/dist/registry/commit-and-signal.js +1 -7
  164. package/dist/registry/domains.d.ts +0 -8
  165. package/dist/registry/domains.js +0 -7
  166. package/dist/registry/emitter.d.ts +0 -14
  167. package/dist/registry/emitter.js +0 -10
  168. package/dist/registry/store-adapters.d.ts +0 -186
  169. package/dist/registry/store-adapters.js +0 -163
  170. package/dist/registry/stores.d.ts +0 -33
  171. package/dist/registry/stores.js +0 -59
  172. package/dist/reticle-presenter-helpers.d.ts +0 -13
  173. package/dist/reticle-presenter-helpers.js +0 -14
  174. package/dist/reticle.d.ts +1 -52
  175. package/dist/reticle.js +20 -145
  176. package/dist/review/annotator-styles.js +0 -7
  177. package/dist/review/annotator.d.ts +2 -13
  178. package/dist/review/annotator.js +0 -40
  179. package/dist/review/mark-anchor.d.ts +0 -11
  180. package/dist/review/mark-anchor.js +0 -8
  181. package/dist/security/serialization.d.ts +0 -20
  182. package/dist/security/serialization.js +1 -66
  183. package/dist/session-continuity.d.ts +0 -16
  184. package/dist/session-continuity.js +1 -20
  185. package/dist/timers/clock.d.ts +1 -14
  186. package/dist/timers/clock.js +59 -60
  187. package/dist/timers/native-console.js +0 -4
  188. package/dist/timers/native-timers.d.ts +0 -25
  189. package/dist/timers/native-timers.js +3 -33
  190. package/dist/transport/transport.d.ts +0 -51
  191. package/dist/transport/transport.js +3 -129
  192. package/dist/transport/unreachable-message.d.ts +0 -17
  193. package/dist/transport/unreachable-message.js +0 -17
  194. package/dist/util/captured-method.d.ts +0 -35
  195. package/dist/util/captured-method.js +0 -35
  196. package/package.json +2 -2
@@ -1,27 +1,5 @@
1
- /**
2
- * Rows a scroll container has reserved space for but has NOT rendered.
3
- *
4
- * `BlindSpotKind.VIRTUALIZED_UNMOUNTED` has had a label on the server since it was defined and
5
- * nothing ever emitted it — a declared blind spot that could not fire, shaped exactly like the most
6
- * common data-heavy UI there is. Measured on a 10,000-row console: 29 rows in the DOM, and a verdict
7
- * that reported full coverage. An assertion like "no shipment is held" was a claim about 29 rows,
8
- * delivered as a claim about all of them.
9
- *
10
- * The detector measures EMPTY RESERVED SPACE rather than guessing from child counts. A virtualizer
11
- * sizes its scroll area for the whole list and renders only a window, so the area above the first
12
- * child and below the last is space the container has promised and not filled. An ordinary list —
13
- * even one with large gaps or margins between rows — spans its own scroll height, so it measures
14
- * zero and never reports.
15
- */
16
- /** Below this, the empty space is padding or a sticky header rather than a virtualized remainder. */
17
1
  const UNMOUNTED_RATIO = 0.25;
18
- /** A container has to be meaningfully scrollable before "what is not rendered" is even a question. */
19
2
  const MIN_SCROLL_RATIO = 1.2;
20
- /**
21
- * Virtualizers usually put one full-height SPACER inside the scroller and position rows absolutely
22
- * inside it, so the scroller's own children are just that spacer. Descend through it to reach the
23
- * rows — without this the spacer measures as filling the container and nothing is ever detected.
24
- */
25
3
  function rowHost(container) {
26
4
  let host = container;
27
5
  for (let depth = 0; depth < 3; depth += 1) {
@@ -29,14 +7,12 @@ function rowHost(container) {
29
7
  const only = 1 === children.length ? children[0] : undefined;
30
8
  if (only === undefined)
31
9
  return host;
32
- // A spacer is a single child that fills (or over-fills) the scroll area.
33
10
  if (only.offsetHeight < host.scrollHeight * 0.9)
34
11
  return host;
35
12
  host = only;
36
13
  }
37
14
  return host;
38
15
  }
39
- /** How many rows a container is holding space for but has not rendered. 0 when it is not virtualized. */
40
16
  export function unmountedRowsIn(container) {
41
17
  const scrollHeight = container.scrollHeight;
42
18
  const clientHeight = container.clientHeight;
@@ -58,7 +34,6 @@ export function unmountedRowsIn(container) {
58
34
  bottom = rowBottom;
59
35
  totalHeight += row.offsetHeight;
60
36
  }
61
- // Space the container reserved and left empty: above the first row plus below the last.
62
37
  const empty = Math.max(0, top) + Math.max(0, scrollHeight - bottom);
63
38
  if (empty < scrollHeight * UNMOUNTED_RATIO)
64
39
  return 0;
@@ -67,11 +42,9 @@ export function unmountedRowsIn(container) {
67
42
  return 0;
68
43
  return Math.round(empty / averageRow);
69
44
  }
70
- /** Total unmounted rows across every scroll container on the page. */
71
45
  export function countUnmountedRows() {
72
46
  let total = 0;
73
47
  for (const element of document.querySelectorAll('*')) {
74
- // Cheap gate first: only elements that actually scroll are worth measuring.
75
48
  if (element.scrollHeight <= element.clientHeight)
76
49
  continue;
77
50
  total += unmountedRowsIn(element);
@@ -1,32 +1,10 @@
1
1
  import { type RefRegistry } from './dom/refs.js';
2
2
  export declare class EditEpoch {
3
3
  #private;
4
- /** The registry is injected for the same reason the clock is: the interesting property is the
5
- * RELATION between when a ref was minted and when an edit landed, and a global has no seam. */
6
4
  constructor(registry: RefRegistry);
7
- /**
8
- * How many hot updates this document has applied.
9
- *
10
- * `NO_EDITS_OBSERVED` means exactly that and never "none happened": most pages Reticle instruments
11
- * have no channel that could report one, so absence here is UNKNOWN.
12
- */
13
5
  get current(): number;
14
- /** Subscribe to a hot-update channel if one was handed in. Anything else is silently ignored —
15
- * no channel is the normal case, not an error. */
16
6
  observe(hot: unknown): void;
17
- /** Record that an update was applied, naming the files it changed (which may be none). */
18
7
  applied(files: readonly string[]): void;
19
- /**
20
- * The refusal for a ref that no longer resolves — saying WHY when the page can tell us.
21
- *
22
- * The generic wording is untouched and stays at the front: it is already tuned, and the server's
23
- * recovery table matches on it. When the ref predates the last hot update the diagnosis is
24
- * appended, because "you edited TripCard.tsx and it re-rendered" is a next step and "that ref is
25
- * stale" is a dead end. A ref minted AFTER the last update gets the generic message unchanged —
26
- * blaming an edit for an ordinary post-click stale ref would be a confident wrong answer, which is
27
- * worse than the vague right one.
28
- */
29
8
  staleRefMessage(ref: string): string;
30
9
  }
31
- /** The epoch of the page, paired with the process-wide ref registry it has to reason against. */
32
10
  export declare const editEpoch: EditEpoch;
@@ -1,38 +1,9 @@
1
1
  import { NO_EDITS_OBSERVED } from '@reticlehq/core';
2
2
  import { echoRef, refs } from './dom/refs.js';
3
- /**
4
- * What the page can tell us about its own source being edited underneath it.
5
- *
6
- * The agent driving Reticle edits source and re-verifies in a loop. When an edit lands, the dev
7
- * server hot-updates the module and the framework re-renders: DOM nodes are REPLACED, every ref the
8
- * agent holds points at a detached node — and there was no navigation, so the document is the same
9
- * one and `documentId` never moves. The agent got a generic "that ref is stale" and could not tell
10
- * its own edit landing from a bug in the app it was verifying.
11
- *
12
- * DELIBERATELY NOT A BUILD INTEGRATION. Nothing here watches files, reads the user's repo or knows
13
- * what a module graph is. The page is told by its own dev server that it hot-updated; this records
14
- * that it happened and what it named, and that is the whole feature.
15
- */
16
- /**
17
- * Vite's "the update has been applied" event, on a hot context.
18
- *
19
- * `vite:afterUpdate` rather than `vite:beforeUpdate`: the point of the epoch is that the DOM has
20
- * been replaced, and before the update it has not been yet.
21
- *
22
- * The SDK does NOT depend on Vite — it ships to Next, Electron, Tauri and plain pages, and it cannot
23
- * read `import.meta.hot` of its own module in any of them (a dependency is pre-bundled by Vite's
24
- * optimizer and gets no hot context at all). The channel is handed IN by whoever has one, and this
25
- * name is the only Vite-shaped thing in the SDK.
26
- */
27
3
  const HOT_UPDATE_APPLIED = 'vite:afterUpdate';
28
- /** How many changed files a refusal names before it stops being a diagnosis and starts being a list. */
29
4
  const MAX_NAMED_FILES = 3;
30
- /** The exact substring the server's recovery table keys off. Changing it silently unclassifies. */
31
5
  const STALE_REF_REFUSAL = 'no longer resolves to an element';
32
- /** The clause that turns a dead end into a diagnosis, and the one the server's table matches on. */
33
6
  const CODE_CHANGED = 'the code changed underneath it';
34
- /** Anything object-shaped, as a bag of unknowns. The payload comes from the page, so nothing about
35
- * its shape is guaranteed and every field has to be checked before it is read. */
36
7
  function fields(value) {
37
8
  return null !== value && 'object' === typeof value
38
9
  ? value
@@ -41,7 +12,6 @@ function fields(value) {
41
12
  function isHotLike(value) {
42
13
  return 'function' === typeof fields(value)?.['on'];
43
14
  }
44
- /** The file paths a hot-update payload named, narrowed from `unknown`. */
45
15
  function updatedPaths(payload) {
46
16
  const updates = fields(payload)?.['updates'];
47
17
  if (!Array.isArray(updates))
@@ -58,43 +28,22 @@ export class EditEpoch {
58
28
  #epoch = NO_EDITS_OBSERVED;
59
29
  #files = [];
60
30
  #refs;
61
- /** The registry is injected for the same reason the clock is: the interesting property is the
62
- * RELATION between when a ref was minted and when an edit landed, and a global has no seam. */
63
31
  constructor(registry) {
64
32
  this.#refs = registry;
65
33
  }
66
- /**
67
- * How many hot updates this document has applied.
68
- *
69
- * `NO_EDITS_OBSERVED` means exactly that and never "none happened": most pages Reticle instruments
70
- * have no channel that could report one, so absence here is UNKNOWN.
71
- */
72
34
  get current() {
73
35
  return this.#epoch;
74
36
  }
75
- /** Subscribe to a hot-update channel if one was handed in. Anything else is silently ignored —
76
- * no channel is the normal case, not an error. */
77
37
  observe(hot) {
78
38
  if (!isHotLike(hot))
79
39
  return;
80
40
  hot.on(HOT_UPDATE_APPLIED, (payload) => this.applied(updatedPaths(payload)));
81
41
  }
82
- /** Record that an update was applied, naming the files it changed (which may be none). */
83
42
  applied(files) {
84
43
  this.#epoch += 1;
85
44
  this.#files = files.slice(0, MAX_NAMED_FILES);
86
45
  this.#refs.markEdited();
87
46
  }
88
- /**
89
- * The refusal for a ref that no longer resolves — saying WHY when the page can tell us.
90
- *
91
- * The generic wording is untouched and stays at the front: it is already tuned, and the server's
92
- * recovery table matches on it. When the ref predates the last hot update the diagnosis is
93
- * appended, because "you edited TripCard.tsx and it re-rendered" is a next step and "that ref is
94
- * stale" is a dead end. A ref minted AFTER the last update gets the generic message unchanged —
95
- * blaming an edit for an ordinary post-click stale ref would be a confident wrong answer, which is
96
- * worse than the vague right one.
97
- */
98
47
  staleRefMessage(ref) {
99
48
  const generic = `ref '${echoRef(ref)}' ${STALE_REF_REFUSAL}`;
100
49
  if (!this.#refs.mintedBeforeLastEdit(ref))
@@ -104,5 +53,4 @@ export class EditEpoch {
104
53
  'replaced the node. Query again for a fresh ref: this is your own edit landing, not the app failing.');
105
54
  }
106
55
  }
107
- /** The epoch of the page, paired with the process-wide ref registry it has to reason against. */
108
56
  export const editEpoch = new EditEpoch(refs);
package/dist/index.js CHANGED
@@ -1,39 +1,17 @@
1
1
  import { Reticle } from './reticle.js';
2
- /**
3
- * The singleton embedded in the host app: `import { reticle } from '@reticlehq/browser'`.
4
- * Persisted on a global so HMR module re-evaluation reuses the same (already-connected)
5
- * instance instead of creating a second bridge connection (feedback #7).
6
- */
7
2
  const globalStore = globalThis;
8
3
  export const reticle = (globalStore.__reticleInstance ??= new Reticle());
9
4
  export { Reticle } from './reticle.js';
10
- // Session label sentinel: pass as `session` (or omit it) to get a unique per-tab id (no collisions).
11
5
  export { SESSION_AUTO } from '@reticlehq/core';
12
- // Exclude your own dev widgets from snapshots/observers.
13
6
  export { setIgnoreSelectors } from './dom/dom-ignore.js';
14
- // Adapter API (used by @reticlehq/react and other framework adapters).
15
7
  export { registerAdapter, identifyComponent, readComponentState, elementHasHoverHandlers, adapterNames, } from './registry/adapters.js';
16
- // Store registry: pull live framework/store state on demand.
17
- export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation,
18
- // Which name a store SOURCE is registered under. The React adapter discovers stores from the fiber
19
- // tree and needs this to tell "the app already wired this exact store" from "this is another one" —
20
- // a question neither the name nor the read value can answer.
21
- sourceOwner, markAdapterSource, } from './registry/stores.js';
22
- // Adapters for state libraries that do not natively expose `{getState, subscribe}`. zustand and Redux
23
- // need none of these; TanStack Query is the one that matters most, because a stale-cache bug fires no
24
- // request and renders a plausible value, so the cache is the only witness.
8
+ export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation, sourceOwner, markAdapterSource, } from './registry/stores.js';
25
9
  export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
26
- // Capability registry: the app self-describes its testable surface via reticle.describe.
27
10
  export { registerCapabilities, getCapabilities, hasCapabilities, setCapabilitiesListener, } from './registry/capabilities.js';
28
- // SDK helpers: adopt the recommended integration patterns without boilerplate.
29
11
  export { createReticleEmitter } from './registry/emitter.js';
30
12
  export { commitAndSignal } from './registry/commit-and-signal.js';
31
13
  export { registerReticleDomain } from './registry/domains.js';
32
- // Lower-level building blocks (useful for tests and advanced embedding).
33
14
  export { buildSnapshot } from './dom/snapshot.js';
34
- // Chart inspection. Surfaced automatically on any element descriptor that contains faulty plot
35
- // geometry (see ElementDescriptor.chart), and exported for direct use — canvasChartData is the only
36
- // route to a canvas chart's data, since its pixels are not DOM.
37
15
  export { inspectChart, canvasChartData } from './dom/chart.js';
38
16
  export { matchQuery, runQuery } from './dom/query.js';
39
17
  export { executeAction, executeSequence } from './actions/actions.js';
@@ -1,3 +1,2 @@
1
1
  import type { Emit, Teardown } from './types.js';
2
- /** Observe CSS animations + transitions and emit anim.start / anim.end. */
3
2
  export declare function installAnimation(emit: Emit): Teardown;
@@ -1,14 +1,11 @@
1
1
  import { EventType } from '@reticlehq/core';
2
2
  import { refs } from '../dom/refs.js';
3
3
  import { isReticleOverlay } from '../dom/dom-ignore.js';
4
- /** Observe CSS animations + transitions and emit anim.start / anim.end. */
5
4
  export function installAnimation(emit) {
6
5
  const ac = new AbortController();
7
6
  const { signal } = ac;
8
7
  const onStart = (event) => {
9
8
  const target = event.target;
10
- // Skip Reticle's own HUD keyframes (reticle-pulse/reticle-shimmer/…) so observe/record never
11
- // self-pollute the agent's view of the app (matches the DOM observer's overlay filter).
12
9
  if (target instanceof Element && !isReticleOverlay(target)) {
13
10
  emit(EventType.ANIM_START, { name: event.animationName }, refs.refFor(target));
14
11
  }
@@ -1,31 +1,5 @@
1
1
  import type { Emit, Teardown } from './types.js';
2
- /**
3
- * Blind-spot sensor. The SDK instruments the DOM, but a cross-origin iframe is a wall it cannot
4
- * see through — its contents, network, and console are invisible. Left unstated, a green verdict would
5
- * imply the whole page was verified. This detects cross-origin frames and emits a BLIND_SPOT event so the
6
- * server can mark results `coverage: partial` instead of lying by omission. This sensor owns the three
7
- * kinds cheaply and reliably detectable from the page: cross-origin frames, closed shadow roots, and
8
- * virtualized-unmounted rows.
9
- */
10
- /**
11
- * Elements rendering content nobody outside them can read.
12
- *
13
- * `attachShadow({mode:'closed'})` makes `el.shadowRoot` null FOREVER, for every caller — Reticle,
14
- * Playwright's accessibility snapshot, a screenshot-reading model, all of them. There is no API that
15
- * recovers it, so the only honest behaviour is to say the region exists and is unreadable.
16
- *
17
- * It is not directly detectable either, so this infers it: a custom element (a dash in the tag name is
18
- * the spec's own rule) that has NO light-DOM children and no text, yet occupies layout space, is
19
- * painting from somewhere. Nothing else produces that combination — an empty custom element that
20
- * renders nothing measures zero and is skipped.
21
- */
22
2
  export declare function countClosedShadowRoots(): number;
23
- /**
24
- * Whether a frame is cross-origin (unobservable). A same-origin frame exposes its `contentDocument`; a
25
- * cross-origin one returns null (or THROWS a SecurityError on access). A src-less / about:blank frame is
26
- * same-origin and simply not navigated — not a blind spot.
27
- */
28
3
  export declare function isCrossOriginFrame(frame: HTMLIFrameElement): boolean;
29
- /** How many of the given frames are cross-origin (unobservable). */
30
4
  export declare function countCrossOriginFrames(frames: readonly HTMLIFrameElement[]): number;
31
5
  export declare function installBlindSpots(emit: Emit): Teardown;
@@ -3,35 +3,13 @@ import { countUnmountedRows } from '../dom/virtualized.js';
3
3
  import { isCaptured } from '../dom/shadow-registry.js';
4
4
  import { sameOriginFrameBodies } from './frames.js';
5
5
  import { nativeClearTimeout, nativeSetTimeout } from '../timers/native-timers.js';
6
- /**
7
- * Blind-spot sensor. The SDK instruments the DOM, but a cross-origin iframe is a wall it cannot
8
- * see through — its contents, network, and console are invisible. Left unstated, a green verdict would
9
- * imply the whole page was verified. This detects cross-origin frames and emits a BLIND_SPOT event so the
10
- * server can mark results `coverage: partial` instead of lying by omission. This sensor owns the three
11
- * kinds cheaply and reliably detectable from the page: cross-origin frames, closed shadow roots, and
12
- * virtualized-unmounted rows.
13
- */
14
- /**
15
- * Elements rendering content nobody outside them can read.
16
- *
17
- * `attachShadow({mode:'closed'})` makes `el.shadowRoot` null FOREVER, for every caller — Reticle,
18
- * Playwright's accessibility snapshot, a screenshot-reading model, all of them. There is no API that
19
- * recovers it, so the only honest behaviour is to say the region exists and is unreadable.
20
- *
21
- * It is not directly detectable either, so this infers it: a custom element (a dash in the tag name is
22
- * the spec's own rule) that has NO light-DOM children and no text, yet occupies layout space, is
23
- * painting from somewhere. Nothing else produces that combination — an empty custom element that
24
- * renders nothing measures zero and is skipped.
25
- */
26
6
  export function countClosedShadowRoots() {
27
7
  let count = 0;
28
8
  for (const el of document.querySelectorAll('*')) {
29
9
  if (!el.tagName.includes('-'))
30
10
  continue;
31
11
  if (el.shadowRoot !== null)
32
- continue; // open — readable, not a blind spot
33
- // A closed root attached AFTER the SDK installed was captured from attachShadow's return value,
34
- // so it is readable too. Only what was genuinely missed is declared.
12
+ continue;
35
13
  if (isCaptured(el))
36
14
  continue;
37
15
  if (el.children.length > 0)
@@ -44,11 +22,6 @@ export function countClosedShadowRoots() {
44
22
  }
45
23
  return count;
46
24
  }
47
- /**
48
- * Whether a frame is cross-origin (unobservable). A same-origin frame exposes its `contentDocument`; a
49
- * cross-origin one returns null (or THROWS a SecurityError on access). A src-less / about:blank frame is
50
- * same-origin and simply not navigated — not a blind spot.
51
- */
52
25
  export function isCrossOriginFrame(frame) {
53
26
  const src = frame.getAttribute('src');
54
27
  if (null === src || 0 === src.length)
@@ -57,10 +30,9 @@ export function isCrossOriginFrame(frame) {
57
30
  return null === frame.contentDocument;
58
31
  }
59
32
  catch {
60
- return true; // SecurityError on access is itself proof of cross-origin
33
+ return true;
61
34
  }
62
35
  }
63
- /** How many of the given frames are cross-origin (unobservable). */
64
36
  export function countCrossOriginFrames(frames) {
65
37
  let count = 0;
66
38
  for (const frame of frames)
@@ -71,31 +43,11 @@ export function countCrossOriginFrames(frames) {
71
43
  function currentCount() {
72
44
  return countCrossOriginFrames(Array.from(document.querySelectorAll('iframe')));
73
45
  }
74
- /**
75
- * Same-origin frames — observed for DOM, NOT for network.
76
- *
77
- * Their `fetch` lives in the frame's own realm, which the top realm's patch never sees. Left
78
- * undeclared, `settled` can read true while a frame request is still in flight.
79
- */
80
46
  function uninstrumentedFrameCount() {
81
47
  return sameOriginFrameBodies(document).length;
82
48
  }
83
- /**
84
- * Watch the page for cross-origin iframes and emit BLIND_SPOT whenever the count CHANGES (install +
85
- * on any added/removed frame). Only the delta is emitted — never a per-mutation flood — so a static
86
- * embed reports once. Reversible: disconnects the observer on teardown.
87
- */
88
- /**
89
- * How long added/removed nodes are allowed to pile up before the frame count is re-checked.
90
- *
91
- * A blind spot is a coverage caveat on a verdict, not a live signal — noticing an embed a fraction of
92
- * a second later costs nothing, while checking on every mutation costs a DOM scan per render.
93
- */
94
49
  const RECHECK_DEBOUNCE_MS = 250;
95
50
  export function installBlindSpots(emit) {
96
- // Every kind is counted the same way and emitted on CHANGE only, so a static embed or a steady list
97
- // reports once instead of flooding. Virtualized rows matter most: without them an assertion over a
98
- // 10,000-row grid is a claim about the ~29 on screen, delivered as full coverage.
99
51
  const sensors = [
100
52
  { kind: BlindSpotKind.CROSS_ORIGIN_IFRAME, count: currentCount },
101
53
  { kind: BlindSpotKind.UNINSTRUMENTED_FRAME, count: uninstrumentedFrameCount },
@@ -114,17 +66,6 @@ export function installBlindSpots(emit) {
114
66
  }
115
67
  };
116
68
  report();
117
- // Coalesce rather than gate.
118
- //
119
- // This used to try to skip unrelated churn by testing each added/removed node with
120
- // `n.querySelector('iframe')` — but that is a full subtree scan per mutated node, so a React commit
121
- // mounting a 500-node panel paid a 500-node scan to decide whether to do a document scan. The gate
122
- // cost more than the work, and it scaled with exactly the thing a large app does most.
123
- //
124
- // Since report() emits only when the count CHANGES, running it late and rarely is free. One
125
- // debounced document scan per quarter-second replaces per-node scanning, and it is also more
126
- // correct: the old gate's `querySelector` missed an iframe added inside a subtree whose own root
127
- // was a text or comment node.
128
69
  let pending;
129
70
  const observer = new MutationObserver(() => {
130
71
  if (pending !== undefined)
@@ -1,3 +1,2 @@
1
- import type { Emit, Teardown } from './types.js';
2
- /** Patch console.{log,warn,error} and window error events. Reversible. */
1
+ import { type Emit, type Teardown } from './types.js';
3
2
  export declare function installConsole(emit: Emit): Teardown;
@@ -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;