@reticlehq/browser 2.13.1 → 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
package/dist/reticle.js CHANGED
@@ -7,7 +7,7 @@ import { Transport } from './transport/transport.js';
7
7
  import { unreachableMessage } from './transport/unreachable-message.js';
8
8
  import { adapterNames } from './registry/adapters.js';
9
9
  import { registerCapabilities, setCapabilitiesListener, hasCapabilities, } from './registry/capabilities.js';
10
- import { installAllObservers } from './observers/install-all.js';
10
+ import { installAllObservers, runTeardowns } from './observers/install-all.js';
11
11
  import { installOverlay } from './presenter/overlay.js';
12
12
  import { Presenter, LOG_KIND, LOG_RESULT, } from './presenter/presenter.js';
13
13
  import { getPresenterSettings } from './presenter/presenter-settings.js';
@@ -17,18 +17,10 @@ import { resetClock } from './timers/clock.js';
17
17
  import { nativeWarn } from './timers/native-console.js';
18
18
  import { installRecorder } from './recorder/recorder.js';
19
19
  import { Annotator } from './review/annotator.js';
20
- /**
21
- * Runtime backstop for the dev-only SDK: block connecting when the build reports production, unless
22
- * explicitly overridden. Pure so it's testable; connect reads NODE_ENV safely (process may be absent
23
- * in a raw browser). This is defense-in-depth - the primary guard is the consumer gating the import
24
- * behind `import.meta.env.DEV` so the SDK is dead-code-eliminated from prod bundles entirely.
25
- */
26
20
  export function shouldBlockProduction(nodeEnv, allowInProduction) {
27
21
  return 'production' === nodeEnv && !allowInProduction;
28
22
  }
29
- export function connectionPolicy(pageHostname, bridgeUrl, allowNonLocalhost, token,
30
- /** The page's protocol. Desktop webviews (file:, app:, tauri:) are local despite a non-loopback host. */
31
- pageProtocol = 'http:') {
23
+ export function connectionPolicy(pageHostname, bridgeUrl, allowNonLocalhost, token, pageProtocol = 'http:') {
32
24
  let bridge;
33
25
  try {
34
26
  bridge = new URL(bridgeUrl);
@@ -63,19 +55,11 @@ pageProtocol = 'http:') {
63
55
  }
64
56
  return { allowed: true };
65
57
  }
66
- /** HUD summary when the SDK self-ends a session because the bridge (server/agent) became unreachable. */
67
58
  const BRIDGE_LOST_SUMMARY = 'Session ended - lost connection to Reticle (the agent is no longer running).';
68
- /**
69
- * Resolve the session label. An absent label or the `auto` sentinel yields a fresh per-tab id (via
70
- * the injected generator) so multi-tab / new-tab routes never collide; any other label is used
71
- * verbatim so tabs can intentionally share a session. `gen` is injected to keep this clock-free.
72
- */
73
59
  export function resolveSessionLabel(option, gen) {
74
60
  return option === undefined || option === SESSION_AUTO ? gen() : option;
75
61
  }
76
- // Re-exported from the protocol (the wire contract) so callers/tests can import it from the SDK too.
77
62
  export { RETICLE_URL_PARAM };
78
- /** Remove the `_reticle_reload` cache-buster a hard REFRESH left behind, via a native replaceState. */
79
63
  function stripReloadCacheBustParam() {
80
64
  try {
81
65
  const current = new URL(window.location.href);
@@ -85,13 +69,8 @@ function stripReloadCacheBustParam() {
85
69
  window.history.replaceState(window.history.state, '', current.toString());
86
70
  }
87
71
  catch {
88
- /* best-effort URL hygiene - never block connect() on it */
89
72
  }
90
73
  }
91
- /**
92
- * Extract Reticle identity overrides from a `location.search` string. Pure (takes the string, not the
93
- * window) so it's testable without a DOM. Explicit connect options still win over these.
94
- */
95
74
  export function reticleParamsFromSearch(search) {
96
75
  const params = new URLSearchParams(search);
97
76
  const out = {};
@@ -103,12 +82,6 @@ export function reticleParamsFromSearch(search) {
103
82
  out.projectId = projectId;
104
83
  return out;
105
84
  }
106
- /**
107
- * Resolve the session + project identity for a connection: an explicit, non-`auto` option wins;
108
- * otherwise a launcher-stamped URL param is used; otherwise undefined (caller generates a per-tab id).
109
- * Crucially `auto` is treated like "unset" so an app that passes the auto sentinel still lets a pooled
110
- * launcher correlate the lease via __reticle_session. Pure for testability.
111
- */
112
85
  export function resolveConnectIdentity(options, search) {
113
86
  const url = reticleParamsFromSearch(search);
114
87
  const explicitSession = options.session !== undefined && options.session !== SESSION_AUTO ? options.session : undefined;
@@ -118,11 +91,6 @@ export function resolveConnectIdentity(options, search) {
118
91
  projectId: projectId !== undefined && projectId.length > 0 ? projectId : undefined,
119
92
  };
120
93
  }
121
- /**
122
- * Assemble an event envelope. Pure: `seq` (monotonic per session) and `t` (elapsed clock) are
123
- * injected, never read here - so it is unit-testable and honors the clock-injection rule. The
124
- * causing action's id is attributed server-side by the settle window, so it is not stamped here.
125
- */
126
94
  export function buildEvent(args) {
127
95
  return {
128
96
  t: args.t,
@@ -130,34 +98,17 @@ export function buildEvent(args) {
130
98
  type: args.type,
131
99
  sessionId: args.sessionId,
132
100
  ref: args.ref,
133
- // Which document this was observed under, so the server can refuse evidence minted before a
134
- // navigation replaced the page. Stamped HERE because it is the one place every event passes
135
- // through: an observer added later would otherwise emit unstamped events, which read as
136
- // "current" by design and would reintroduce the defect silently for one event type.
137
101
  documentId: args.documentId,
138
- // Which round of source edits this was observed under. Stamped HERE for exactly the reason
139
- // documentId is, and omitted while nothing has hot-updated: absence already reads as "current"
140
- // downstream, so `NO_EDITS_OBSERVED` on the wire would be bytes spent saying "unknown".
141
102
  editEpoch: NO_EDITS_OBSERVED === args.editEpoch ? undefined : args.editEpoch,
142
103
  data: args.data,
143
104
  };
144
105
  }
145
- /**
146
- * The browser-side orchestrator. Wires observers -> events -> bridge, and bridge
147
- * commands -> handlers. Embedded in the host app (dev only).
148
- */
149
106
  export class Reticle {
150
107
  #transport;
151
108
  #registry = new Map();
152
109
  #teardowns = [];
153
110
  #connected = false;
154
111
  #session = 'default';
155
- /**
156
- * Minted once, here, because this class is constructed once per document: a full navigation tears
157
- * down the JavaScript context and builds a new one, so the id dies with the document it names. An
158
- * SPA route change does not reconstruct it, which is correct — same context, same in-flight
159
- * requests, same evidence.
160
- */
161
112
  #documentId = newDocumentId(Math.random);
162
113
  #start = 0;
163
114
  #overlay;
@@ -167,18 +118,16 @@ export class Reticle {
167
118
  #eventCount = 0;
168
119
  #token;
169
120
  #sdkVersion;
121
+ #captureBodies = false;
122
+ #sourceMapping = undefined;
170
123
  #projectId;
171
- /** App-declared extra redaction keys, announced in hello so the driven path honours them too. */
172
124
  #redactKeys = [];
173
- /** Act-row log handle for the in-flight act/act_sequence, so its outcome stamps the right row. */
174
125
  #actHandle;
175
126
  connect(options = {}) {
176
127
  if (this.#connected)
177
128
  return;
178
129
  if ('undefined' === typeof window || 'undefined' === typeof document)
179
130
  return;
180
- // Dev-only backstop: refuse to activate in a production build (SSR healthcheck, prod bundle opened
181
- // on localhost). `process` may not exist in a raw browser, so read NODE_ENV off globalThis.
182
131
  const proc = globalThis.process;
183
132
  const nodeEnv = proc?.env?.['NODE_ENV'];
184
133
  if (shouldBlockProduction(nodeEnv, true === options.allowInProduction)) {
@@ -186,9 +135,6 @@ export class Reticle {
186
135
  'import.meta.env.DEV, or pass allowInProduction:true to override.');
187
136
  return;
188
137
  }
189
- // A `hard` REFRESH left a cache-busting `_reticle_reload=<nonce>` in the address bar. Strip it now
190
- // (before the route observer installs, so it emits no spurious ROUTE_CHANGE) - otherwise every hard
191
- // reload permanently pollutes the URL the app and the agent see.
192
138
  stripReloadCacheBustParam();
193
139
  const url = options.url ?? bridgeWsUrl(RETICLE_DEFAULT_PORT);
194
140
  const policy = connectionPolicy(window.location.hostname, url, true === options.allowNonLocalhost, options.token, window.location.protocol);
@@ -196,24 +142,15 @@ export class Reticle {
196
142
  globalThis.console.warn(`[Reticle] ${policy.reason ?? 'connection blocked'}`);
197
143
  return;
198
144
  }
199
- // Publish the project root for the adapters' source relativisation. Done HERE, in library code,
200
- // so the app-side connect stays plain JavaScript - see ReticleConnectOptions.root.
201
145
  if (options.root !== undefined && options.root.length > 0) {
202
146
  globalThis[RETICLE_ROOT_GLOBAL] = options.root;
203
147
  }
204
- // The build plugin defines this; a hand-wired connect can pass it explicitly. Either way an
205
- // absent value means UNKNOWN, so the bridge never reports an unknown pair as in sync.
206
148
  const declaredVersion = options.sdkVersion ?? globalThis[RETICLE_SDK_VERSION_GLOBAL];
207
149
  this.#sdkVersion =
208
150
  'string' === typeof declaredVersion && declaredVersion.length > 0
209
151
  ? declaredVersion
210
152
  : undefined;
211
- // A pooled/headless launcher can stamp identity via namespaced URL params; explicit (non-auto)
212
- // options win, but the `auto` sentinel defers to the URL param so leases correlate.
213
153
  const identity = resolveConnectIdentity(options, window.location.search);
214
- // Remembered per TAB, so a reload rejoins the same session instead of appearing as a new one and
215
- // stranding the agent's handle - see session-continuity. An explicit id (connect option, or the
216
- // lease's URL param) still wins.
217
154
  const explicitSession = resolveSessionLabel(identity.session, () => '');
218
155
  this.#session = rememberSessionLabel(explicitSession.length > 0 ? explicitSession : undefined, 'undefined' === typeof globalThis.sessionStorage ? undefined : globalThis.sessionStorage, () => 'function' === typeof globalThis.crypto?.randomUUID
219
156
  ? `s${globalThis.crypto.randomUUID()}`
@@ -221,9 +158,6 @@ export class Reticle {
221
158
  this.#token =
222
159
  options.token !== undefined && options.token.length > 0 ? options.token : undefined;
223
160
  this.#projectId = identity.projectId;
224
- // Install the redaction rule BEFORE anything can emit. Every observer, the serializer and the
225
- // snapshot path read it ambiently, so a policy set even one line after installAllObservers would
226
- // leave the first events of the session redacted by a different rule than the rest.
227
161
  this.#applyRedaction(options.redact);
228
162
  this.#start = performance.now();
229
163
  this.#registry = createCommandRegistry();
@@ -231,42 +165,29 @@ export class Reticle {
231
165
  url,
232
166
  hello: () => this.#hello(),
233
167
  handleCommand: (command) => this.#handleCommand(command),
234
- // Show the presenter HUD as soon as the agent bridge connects - the user immediately sees
235
- // the glow border and narration panel, even before the first tool call lands.
236
168
  onConnected: () => this.#presenter?.sessionStart(),
237
- // Liveness fallback: if the bridge stays unreachable (the agent killed the server process),
238
- // no server-pushed end can arrive - so end the run we're presenting ourselves. A returning
239
- // agent revives it via the normal sessionStart path on its next command.
240
169
  onConnectionLost: () => {
241
- // Restore real timers. A frozen clock is driven by the agent through the bridge; once the
242
- // bridge is gone nothing can ever advance it, so leaving it installed pins Date.now and
243
- // queues every setTimeout into a scheduler that will never run. Concretely that kills every
244
- // lodash debounce/throttle in the app (now - lastCall stays 0), so search boxes, autosave
245
- // and resize handlers stop firing until a reload - with nothing on screen explaining why.
246
170
  resetClock();
247
171
  if (true === this.#presenter?.sessionActive) {
248
172
  this.#presenter.setState(SessionState.ENDED, BRIDGE_LOST_SUMMARY);
249
173
  }
250
174
  },
251
- // First-connect never succeeded ⇒ this page's socket is not opening at this URL. Say what the
252
- // page observed rather than guessing at the daemon, which it cannot see from in here.
253
175
  onUnreachable: ({ url: tried, attempts }) => {
254
176
  nativeWarn(unreachableMessage(tried, attempts));
255
- // And on screen, not only in a console nobody has open. An instrumented page with a dead
256
- // bridge looked exactly like a page with no Reticle in it, so the user could not tell a
257
- // daemon they forgot to start from an install that did not work.
258
177
  this.#presenter?.showUnreachable(tried, attempts);
259
178
  },
260
179
  });
261
- // Capabilities registered AFTER connect (which is when they are registered, by design) must
262
- // reach the bridge, or an app that declared its whole testable surface reads as having none.
263
180
  setCapabilitiesListener(() => this.#transport?.reannounce());
264
- // Contributors only, and reported in capabilities so a verdict drawn with it open is never
265
- // mistaken for an ordinary one. See connect-options.ts.
266
181
  setPresenterVisible(true === options.exposePresenter);
267
182
  const emit = this.#emit;
183
+ this.#captureBodies = true === options.captureNetworkBodies;
184
+ this.#sourceMapping =
185
+ 'boolean' === typeof options.sourceMapping ? options.sourceMapping : undefined;
268
186
  this.#teardowns = installAllObservers(emit, {
269
- captureBodies: true === options.captureNetworkBodies,
187
+ captureBodies: this.#captureBodies,
188
+ ...(options.captureErrorBodies === undefined
189
+ ? {}
190
+ : { captureErrorBodies: options.captureErrorBodies }),
270
191
  });
271
192
  if (true === options.overlay) {
272
193
  this.#overlay = installOverlay();
@@ -288,21 +209,16 @@ export class Reticle {
288
209
  if (options.idleEndMs !== undefined)
289
210
  presenterOptions.idleEndMs = options.idleEndMs;
290
211
  presenterOptions.sessionId = this.#session;
291
- // The panel calls this when the human pauses/resumes/ends or sends a message. We emit a
292
- // HUMAN_CONTROL event over the existing transport; #emit stamps `t` from the elapsed clock.
293
212
  presenterOptions.onControl = (intent) => this.#emit(EventType.HUMAN_CONTROL, intent.text !== undefined
294
213
  ? { kind: intent.kind, text: intent.text }
295
214
  : { kind: intent.kind });
296
215
  this.#presenter = new Presenter(presenterOptions);
297
- // Mount the overlay. The session (glow + HUD) activates on bridge connect via onConnected,
298
- // so the presenter is visible as soon as the agent is reachable - not just on first command.
299
216
  this.#presenter.mount();
300
217
  }
301
218
  if (true === options.recorder) {
302
219
  this.#recorder = installRecorder({ emit, now: () => Date.now() });
303
220
  this.#recorder.mount();
304
221
  }
305
- // The page annotator rides with the presenter (the human surface) unless explicitly off.
306
222
  if (options.annotate ?? options.present !== false) {
307
223
  const presenter = this.#presenter;
308
224
  this.#annotator = new Annotator({
@@ -317,65 +233,45 @@ export class Reticle {
317
233
  this.#transport.connect();
318
234
  this.#connected = true;
319
235
  }
320
- /** Whether the in-page SDK is connected to the bridge (read by createReticleEmitter, P5a). */
321
236
  get connected() {
322
237
  return this.#connected;
323
238
  }
324
- /** Surface an arbitrary app-domain observation the DOM can't express. */
325
239
  signal(name, data = {}) {
326
240
  this.#emit(EventType.SIGNAL, { name, data });
327
241
  }
328
- /** Report a framework/store state change the agent can observe and assert on. */
329
242
  state(name, value) {
330
243
  this.#emit(EventType.STATE_CHANGE, { name, value });
331
244
  }
332
- /**
333
- * Report an aggregated count of React commits (the @reticlehq/react render meter calls this on a
334
- * throttle). Emits a single RENDER_COMMIT event per window so commit storms are observable without a
335
- * per-render flood. Dev-only, like the whole SDK.
336
- */
337
245
  renderCommit(commits) {
338
246
  if (commits > 0)
339
247
  this.#emit(EventType.RENDER_COMMIT, { commits });
340
248
  }
341
- /** Advertise the app's testable surface so the agent learns it without reading source. */
342
249
  describe(input) {
343
- registerCapabilities(input); // the registry notifies the transport - see setCapabilitiesListener
250
+ registerCapabilities(input);
344
251
  }
345
- /** Live-control: end the session programmatically from the host app (drives the panel to ended). */
346
252
  endSession() {
347
253
  this.#presenter?.setState(SessionState.ENDED);
348
254
  }
349
- /**
350
- * Hand the SDK the page's hot-update channel, so a stale ref can say the code changed underneath it.
351
- *
352
- * `unknown` on purpose: the only caller that has one is the build integration, the shape it passes
353
- * is Vite's `import.meta.hot`, and the SDK must not depend on Vite — it ships to Next, Electron,
354
- * Tauri and plain pages. Anything that is not a subscribable channel is ignored, which is the
355
- * normal case: with no channel the epoch stays at `NO_EDITS_OBSERVED`, meaning "no edits OBSERVED",
356
- * never "no edits happened".
357
- */
358
255
  observeHotUpdates(hot) {
359
256
  editEpoch.observe(hot);
360
257
  }
361
258
  disconnect() {
362
259
  if (!this.#connected)
363
260
  return;
364
- for (const teardown of this.#teardowns)
365
- teardown();
261
+ runTeardowns(this.#emit, this.#teardowns);
366
262
  this.#teardowns = [];
367
263
  this.#transport?.close();
368
264
  this.#transport = undefined;
369
265
  this.#overlay?.destroy();
370
266
  this.#overlay = undefined;
371
- this.#presenter?.sessionEnd(); // fade the border out before tearing the overlay down
267
+ this.#presenter?.sessionEnd();
372
268
  this.#presenter?.destroy();
373
269
  this.#presenter = undefined;
374
270
  this.#recorder?.destroy();
375
271
  this.#recorder = undefined;
376
272
  this.#annotator?.destroy();
377
273
  this.#annotator = undefined;
378
- resetClock(); // restore any frozen timers
274
+ resetClock();
379
275
  this.#connected = false;
380
276
  }
381
277
  #emit = (type, data, ref) => {
@@ -389,29 +285,16 @@ export class Reticle {
389
285
  data,
390
286
  ref,
391
287
  });
392
- // Guarded because #emit runs INLINE IN THE APP'S CALL STACK: every monkey-patch calls it from
393
- // inside the function it replaced. An exception here does not surface as an SDK error - it
394
- // propagates out of history.pushState (crashing a router's navigate), out of localStorage.setItem
395
- // after the write already succeeded, or out of console.log before the message reaches the console.
396
- // A dev-only observability SDK must never be able to break the app it is observing.
397
288
  try {
398
289
  this.#transport?.sendEvent(event);
399
290
  this.#eventCount += 1;
400
291
  this.#overlay?.update({ connected: true, events: this.#eventCount });
401
- // On a route change, re-scope the HUD's replay-flow chips to the page we're now on.
402
292
  if (type === EventType.ROUTE_CHANGE)
403
293
  this.#presenter?.refilterFlows();
404
294
  }
405
295
  catch {
406
- /* observation is best-effort; the host app's control flow is not */
407
296
  }
408
297
  };
409
- /**
410
- * Resolve the app's redaction config into the ambient policy, and remember the part of it that
411
- * travels. A config of `undefined` is not "no policy" - it is the DEFAULT policy, installed
412
- * explicitly so a second connect() in the same page (HMR, a re-mount) cannot inherit a rule the
413
- * previous one set.
414
- */
415
298
  #applyRedaction(config) {
416
299
  setActiveRedactionPolicy(buildRedactionPolicy(config, nativeWarn));
417
300
  this.#redactKeys = wireRedactionKeys(config);
@@ -427,31 +310,25 @@ export class Reticle {
427
310
  adapters: adapterNames(),
428
311
  ...(this.#token === undefined ? {} : { token: this.#token }),
429
312
  hasCapabilities: hasCapabilities(),
430
- // Absent when no build plugin supplied one - "unknown", never "matching".
313
+ captureBodies: this.#captureBodies,
314
+ ...(this.#sourceMapping === undefined ? {} : { sourceMapping: this.#sourceMapping }),
431
315
  ...(this.#sdkVersion === undefined ? {} : { sdkVersion: this.#sdkVersion }),
432
- // Always present: derived from THIS build's core, so it needs no build plugin to supply it.
433
- // It is the half of the skew check that works on a hand-wired connect.
434
316
  contract: CONTRACT_FINGERPRINT,
435
317
  ...(0 === this.#redactKeys.length ? {} : { redactKeys: this.#redactKeys }),
436
318
  };
437
319
  }
438
320
  async #handleCommand(command) {
439
- // NARRATE: the agent tells the human what it's about to do / decide (presenter HUD).
440
321
  if (command.name === ReticleCommand.NARRATE) {
441
- this.#presenter?.sessionStart(); // first agent activity → reveal the glow + panel
322
+ this.#presenter?.sessionStart();
442
323
  this.#presenter?.narrate(str(command.args['text']), str(command.args['level'], 'info'));
443
324
  return { ok: true, result: { shown: this.#presenter !== undefined } };
444
325
  }
445
- // SESSION_CONFIG: the agent tunes the session for the app (currently the idle-end window).
446
326
  if (command.name === ReticleCommand.SESSION_CONFIG) {
447
327
  const idleEndMs = command.args['idleEndMs'];
448
328
  if ('number' === typeof idleEndMs)
449
329
  this.#presenter?.setIdleEndMs(idleEndMs);
450
330
  return { ok: true, result: { applied: this.#presenter !== undefined, idleEndMs } };
451
331
  }
452
- // Bridge → browser presenter pushes (PRESENTER state echo / FLOWS replay list). The presenter owns
453
- // the parsing; here we only report whether a panel was mounted to apply it. setState-only, so a
454
- // PRESENTER echo of a HUMAN_CONTROL can't loop back into a re-emit.
455
332
  if (command.name === ReticleCommand.PRESENTER ||
456
333
  command.name === ReticleCommand.FLOWS ||
457
334
  command.name === ReticleCommand.IMPACT) {
@@ -462,7 +339,7 @@ export class Reticle {
462
339
  if (handler === undefined) {
463
340
  return { ok: false, error: `unknown command '${command.name}'` };
464
341
  }
465
- this.#presenter?.sessionStart(); // first agent command → reveal the glow + panel
342
+ this.#presenter?.sessionStart();
466
343
  await this.#presentBefore(command);
467
344
  try {
468
345
  const result = await handler(command.args);
@@ -478,12 +355,11 @@ export class Reticle {
478
355
  this.#presenter?.scheduleIdle();
479
356
  }
480
357
  }
481
- /** Drive the presenter (cursor/effects/status) before the real action runs. */
482
358
  async #presentBefore(command) {
483
359
  const p = this.#presenter;
484
360
  if (p === undefined)
485
361
  return;
486
- p.setMode(modeForCommand(command.name)); // paint reading vs acting intent first
362
+ p.setMode(modeForCommand(command.name));
487
363
  this.#actHandle = undefined;
488
364
  if (command.name === ReticleCommand.ACT) {
489
365
  const ref = str(command.args['ref']);
@@ -497,7 +373,6 @@ export class Reticle {
497
373
  const s = step;
498
374
  const ref = str(s.ref);
499
375
  const label = refLabel(ref);
500
- // one log row per step; the last handle carries the sequence outcome glyph
501
376
  this.#actHandle = p.log(LOG_KIND.ACT, `${actionVerb(str(s.action))} ${label}`);
502
377
  await p.beforeAct(ref, str(s.action), label);
503
378
  }
@@ -1,17 +1,10 @@
1
1
  import { Z_OVERLAY } from '../presenter/presenter-styles.js';
2
- /** Markup + CSS for the in-page annotation HUD (markers, hover box, composer). */
3
2
  const MARK_ATTR = 'data-reticle-mark';
4
3
  const sel = (role) => `[${MARK_ATTR}="${role}"]`;
5
4
  export const MARK_PLACEHOLDER = 'What should change?';
6
5
  export const MARK_SUBMIT = 'Add';
7
6
  export const MARK_CANCEL = 'Cancel';
8
7
  export const MARK_PENDING_GLYPH = '+';
9
- /**
10
- * ABOVE the presenter overlay, because the overlay contains the page blocker annotate mode raises.
11
- * One below it, the shield painted over the composer: every click on Cancel or Add landed on the
12
- * blocker instead of the button, so the note could be neither saved nor dismissed. The root here is
13
- * pointer-events:none, so only the marks and the composer sit above the HUD - not a new shield.
14
- */
15
8
  const Z_MARK = Z_OVERLAY + 1;
16
9
  export const ANNOTATOR_CSS = `
17
10
  ${sel('root')}{position:fixed;inset:0;pointer-events:none;z-index:${String(Z_MARK)};}
@@ -1,19 +1,9 @@
1
1
  import { EventType } from '@reticlehq/core';
2
- /**
3
- * What the HUD is told about a mark the moment it is placed.
4
- *
5
- * The row used to get the note and the anchor's label - "generic: my feedback" - which names
6
- * neither the element nor which of several marks it is. The pin on the page is numbered, so the
7
- * row carries the same number, plus the source the anchor already resolved.
8
- */
9
- export interface MarkReport {
2
+ interface MarkReport {
10
3
  note: string;
11
- /** The anchor the agent will look the element up by, e.g. `[data-testid="deploy-submit"]`. */
12
4
  anchor: string;
13
5
  label: string;
14
- /** 1-based, matching the number drawn in the pin. */
15
6
  index: number;
16
- /** file:line, when the app is source-stamped. */
17
7
  source?: string;
18
8
  }
19
9
  export interface AnnotatorDeps {
@@ -34,9 +24,7 @@ export declare class Annotator {
34
24
  get active(): boolean;
35
25
  get markCount(): number;
36
26
  mount(): void;
37
- /** Drive marker/highlight chrome from the HUD accent swatch. */
38
27
  setAccent(hex: string): void;
39
- /** @deprecated HUD expand/collapse owns annotate mode; chrome buttons are hide/clear. */
40
28
  attachFlagButton(btn: HTMLElement): void;
41
29
  attachChrome(chrome: AnnotatorChrome): void;
42
30
  destroy(): void;
@@ -45,3 +33,4 @@ export declare class Annotator {
45
33
  syncAnchors(): void;
46
34
  }
47
35
  export declare function installAnnotator(deps: AnnotatorDeps): Annotator;
36
+ export {};
@@ -4,7 +4,6 @@ import { isReticleUi, isReticleOverlay } from '../dom/dom-ignore.js';
4
4
  import { resolveMarkAnchor } from './mark-anchor.js';
5
5
  import { nativeSetTimeout, nativeClearTimeout } from '../timers/native-timers.js';
6
6
  import { ANNOTATOR_CSS, ANNOTATOR_ROOT_HTML, MARK_CANCEL, MARK_PENDING_GLYPH, MARK_PLACEHOLDER, MARK_SUBMIT, } from './annotator-styles.js';
7
- /** The cursor must rest this long before the outline boxes the element under it - calm, not jumpy. */
8
7
  const HIGHLIGHT_REST_MS = 130;
9
8
  const SHAKE_MS = 250;
10
9
  const POP_W = 280;
@@ -85,12 +84,10 @@ export class Annotator {
85
84
  if (undefined !== this.#accent)
86
85
  this.setAccent(this.#accent);
87
86
  }
88
- /** Drive marker/highlight chrome from the HUD accent swatch. */
89
87
  setAccent(hex) {
90
88
  this.#accent = hex;
91
89
  this.#root?.style.setProperty('--reticle-mark-accent', hex);
92
90
  }
93
- /** @deprecated HUD expand/collapse owns annotate mode; chrome buttons are hide/clear. */
94
91
  attachFlagButton(btn) {
95
92
  this.attachChrome({ markersBtn: btn });
96
93
  }
@@ -211,18 +208,11 @@ export class Annotator {
211
208
  #handleClick(ev) {
212
209
  if (!this.#active)
213
210
  return;
214
- // A click Reticle dispatched is the agent driving the app, not a person placing a mark. Without
215
- // this, expanding the HUD silently disabled every `reticle_act` click while the action still
216
- // reported success — a false green in our own UI. See synthetic-input.ts.
217
211
  if (isSyntheticInput())
218
212
  return;
219
213
  const raw = ev.target;
220
214
  if (!(raw instanceof Element))
221
215
  return;
222
- // The page blocker is a full-viewport shield Reticle puts up WHILE annotating, so with it on
223
- // every click lands on Reticle's own UI and was discarded as such — annotate mode looked
224
- // completely dead, because the one thing standing between the user and the page is the thing
225
- // annotate mode turns on. Resolve what is underneath instead of giving up on the click.
226
216
  const target = raw.hasAttribute(BLOCKER_ATTR_NAME)
227
217
  ? pageElementAt(ev.clientX, ev.clientY)
228
218
  : raw;
@@ -274,9 +264,6 @@ export class Annotator {
274
264
  if (this.#hi === undefined)
275
265
  return;
276
266
  const raw = ev.target;
277
- // Same shield as in #handleClick: while annotating, a full-viewport blocker sits over the page,
278
- // so EVERY mousemove reports it as the target. Resolving only there left the outline suppressed
279
- // on every element - a crosshair with no indication of what it was on. See #handleClick.
280
267
  const target = raw instanceof Element && raw.hasAttribute(BLOCKER_ATTR_NAME)
281
268
  ? pageElementAt(ev.clientX, ev.clientY)
282
269
  : raw;
@@ -315,22 +302,11 @@ export class Annotator {
315
302
  this.#pendingKey = mark.anchor;
316
303
  this.#pendingTarget = mark.target;
317
304
  this.#hideHighlight();
318
- // Where the element is NOW, not where it was when the mark was made. The popover is
319
- // position:fixed, so it is placed in viewport coordinates, and the stored clientX/clientY are
320
- // viewport coordinates from an earlier scroll position. Reopening a mark after scrolling put the
321
- // note somewhere unrelated to the thing it annotates — further away the further you had scrolled.
322
305
  const at = this.#currentPoint(mark);
323
306
  this.#mountPopover(mark.label, at.x, at.y, mark.note);
324
307
  this.#dropPending(at.x, at.y);
325
308
  this.#paintSelection();
326
309
  }
327
- /**
328
- * The viewport point a mark's popover should open at.
329
- *
330
- * Prefers the element's live box, because that is what the note is ABOUT. Falls back to the stored
331
- * coordinates only when the element is gone, where there is nothing better and the mark is already
332
- * shown as stale.
333
- */
334
310
  #currentPoint(mark) {
335
311
  const el = mark.target;
336
312
  if (el !== undefined && el.isConnected) {
@@ -471,7 +447,6 @@ export class Annotator {
471
447
  this.#dropPin(resolved, note, x, y);
472
448
  this.#report(note, resolved.anchor, resolved.label, this.#marks.length, sourceLabel(resolved));
473
449
  }
474
- /** Hand the HUD everything it needs to name this mark in one row. */
475
450
  #report(note, anchor, label, index, source) {
476
451
  const mark = { note, anchor, label, index };
477
452
  if (source !== undefined)
@@ -560,12 +535,6 @@ export class Annotator {
560
535
  this.#repositionPopover();
561
536
  this.#paintSelection();
562
537
  }
563
- /**
564
- * Keep an OPEN popover attached to its element while the page scrolls.
565
- *
566
- * The pins were repositioned here and the popover was not, so scrolling with a note open slid the
567
- * note off its target and left it floating over unrelated content.
568
- */
569
538
  #repositionPopover() {
570
539
  const pop = this.#pop;
571
540
  const mark = this.#editing;
@@ -589,7 +558,6 @@ export class Annotator {
589
558
  this.#selBox.hidden = true;
590
559
  }
591
560
  }
592
- /** "file:line" for a resolved anchor, when the app carries source stamps. */
593
561
  function sourceLabel(resolved) {
594
562
  return resolved.source === undefined
595
563
  ? undefined
@@ -607,15 +575,7 @@ function isElementFixed(el) {
607
575
  const pos = getComputedStyle(el).position;
608
576
  return 'fixed' === pos || 'sticky' === pos;
609
577
  }
610
- /** The blocker Reticle raises over the page while annotating; see the click handler. */
611
578
  const BLOCKER_ATTR_NAME = 'data-reticle-blocker';
612
- /**
613
- * The topmost element of the HOST PAGE at a point, seeing past Reticle's own overlays.
614
- *
615
- * `elementsFromPoint` returns the whole stack topmost-first, so the page element is simply the first
616
- * entry that is not ours. Returns undefined where the API is unavailable (jsdom) or nothing but
617
- * Reticle UI is there, and the caller drops the click rather than guessing at a target.
618
- */
619
579
  function pageElementAt(x, y) {
620
580
  const from = document.elementsFromPoint.bind(document);
621
581
  if (from === undefined)
@@ -1,10 +1,4 @@
1
1
  import { MarkAnchorStrategy } from '@reticlehq/core';
2
- /**
3
- * The element address carried by a human review mark: a re-resolvable anchor (auto-anchor's most
4
- * durable tier for this element), its durability strategy, a human-legible label, and — crucially —
5
- * the source file:line the agent should open. Source is reported INDEPENDENTLY of the anchor tier:
6
- * a role-anchored element still carries its babel-stamped source so the agent can jump to the code.
7
- */
8
2
  export interface MarkAnchor {
9
3
  anchor: string;
10
4
  strategy: MarkAnchorStrategy;
@@ -14,9 +8,4 @@ export interface MarkAnchor {
14
8
  line: number;
15
9
  };
16
10
  }
17
- /**
18
- * Resolve a clicked element into the address a human review mark carries. Reuses the same
19
- * auto-anchor machinery a recorded flow uses, so a mark pins to the element the way Reticle addresses
20
- * it everywhere else — and always reports the source file:line when one is available.
21
- */
22
11
  export declare function resolveMarkAnchor(el: Element): MarkAnchor;
@@ -3,26 +3,18 @@ import { AnchorStrategy, synthesizeAnchor } from '../dom/auto-anchor.js';
3
3
  import { getAccessibleName, getRole } from '../dom/a11y.js';
4
4
  import { identifyComponent } from '../registry/adapters.js';
5
5
  import { sourceFor } from '../dom/source.js';
6
- /** Attribute names — defined locally per the recorder/query convention (no shared free string). */
7
6
  const TESTID_ATTR = 'data-testid';
8
- /** AnchorStrategy and MarkAnchorStrategy share string values; map explicitly (no cross-cast). */
9
7
  const STRATEGY = {
10
8
  [AnchorStrategy.TESTID]: MarkAnchorStrategy.TESTID,
11
9
  [AnchorStrategy.COMPONENT]: MarkAnchorStrategy.COMPONENT,
12
10
  [AnchorStrategy.ROLE]: MarkAnchorStrategy.ROLE,
13
11
  [AnchorStrategy.POSITION]: MarkAnchorStrategy.POSITION,
14
12
  };
15
- /** A short, human-legible label for the element: `role "name"`, falling back to role / tag name. */
16
13
  function labelFor(el, role, name) {
17
14
  if (name.length > 0)
18
15
  return role.length > 0 ? `${role} "${name}"` : `"${name}"`;
19
16
  return role.length > 0 ? role : el.tagName.toLowerCase();
20
17
  }
21
- /**
22
- * Resolve a clicked element into the address a human review mark carries. Reuses the same
23
- * auto-anchor machinery a recorded flow uses, so a mark pins to the element the way Reticle addresses
24
- * it everywhere else — and always reports the source file:line when one is available.
25
- */
26
18
  export function resolveMarkAnchor(el) {
27
19
  const testid = el.getAttribute(TESTID_ATTR) ?? undefined;
28
20
  const info = identifyComponent(el);