@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
package/dist/reticle.d.ts CHANGED
@@ -2,40 +2,17 @@ import { EventType, RETICLE_URL_PARAM, type ReticleEvent } from '@reticlehq/core
2
2
  import { type CapabilitiesInput } from './registry/capabilities.js';
3
3
  import type { ReticleConnectOptions } from './connect-options.js';
4
4
  export type { ReticleConnectOptions };
5
- /**
6
- * Runtime backstop for the dev-only SDK: block connecting when the build reports production, unless
7
- * explicitly overridden. Pure so it's testable; connect reads NODE_ENV safely (process may be absent
8
- * in a raw browser). This is defense-in-depth - the primary guard is the consumer gating the import
9
- * behind `import.meta.env.DEV` so the SDK is dead-code-eliminated from prod bundles entirely.
10
- */
11
5
  export declare function shouldBlockProduction(nodeEnv: string | undefined, allowInProduction: boolean): boolean;
12
- export declare function connectionPolicy(pageHostname: string, bridgeUrl: string, allowNonLocalhost: boolean, token: string | undefined,
13
- /** The page's protocol. Desktop webviews (file:, app:, tauri:) are local despite a non-loopback host. */
14
- pageProtocol?: string): {
6
+ export declare function connectionPolicy(pageHostname: string, bridgeUrl: string, allowNonLocalhost: boolean, token: string | undefined, pageProtocol?: string): {
15
7
  allowed: boolean;
16
8
  reason?: string;
17
9
  };
18
- /**
19
- * Resolve the session label. An absent label or the `auto` sentinel yields a fresh per-tab id (via
20
- * the injected generator) so multi-tab / new-tab routes never collide; any other label is used
21
- * verbatim so tabs can intentionally share a session. `gen` is injected to keep this clock-free.
22
- */
23
10
  export declare function resolveSessionLabel(option: string | undefined, gen: () => string): string;
24
11
  export { RETICLE_URL_PARAM };
25
- /**
26
- * Extract Reticle identity overrides from a `location.search` string. Pure (takes the string, not the
27
- * window) so it's testable without a DOM. Explicit connect options still win over these.
28
- */
29
12
  export declare function reticleParamsFromSearch(search: string): {
30
13
  session?: string;
31
14
  projectId?: string;
32
15
  };
33
- /**
34
- * Resolve the session + project identity for a connection: an explicit, non-`auto` option wins;
35
- * otherwise a launcher-stamped URL param is used; otherwise undefined (caller generates a per-tab id).
36
- * Crucially `auto` is treated like "unset" so an app that passes the auto sentinel still lets a pooled
37
- * launcher correlate the lease via __reticle_session. Pure for testability.
38
- */
39
16
  export declare function resolveConnectIdentity(options: {
40
17
  session?: string;
41
18
  projectId?: string;
@@ -43,11 +20,6 @@ export declare function resolveConnectIdentity(options: {
43
20
  session: string | undefined;
44
21
  projectId: string | undefined;
45
22
  };
46
- /**
47
- * Assemble an event envelope. Pure: `seq` (monotonic per session) and `t` (elapsed clock) are
48
- * injected, never read here - so it is unit-testable and honors the clock-injection rule. The
49
- * causing action's id is attributed server-side by the settle window, so it is not stamped here.
50
- */
51
23
  export declare function buildEvent(args: {
52
24
  seq: number;
53
25
  t: number;
@@ -58,38 +30,15 @@ export declare function buildEvent(args: {
58
30
  documentId?: string | undefined;
59
31
  editEpoch?: number | undefined;
60
32
  }): ReticleEvent;
61
- /**
62
- * The browser-side orchestrator. Wires observers -> events -> bridge, and bridge
63
- * commands -> handlers. Embedded in the host app (dev only).
64
- */
65
33
  export declare class Reticle {
66
34
  #private;
67
35
  connect(options?: ReticleConnectOptions): void;
68
- /** Whether the in-page SDK is connected to the bridge (read by createReticleEmitter, P5a). */
69
36
  get connected(): boolean;
70
- /** Surface an arbitrary app-domain observation the DOM can't express. */
71
37
  signal(name: string, data?: Record<string, unknown>): void;
72
- /** Report a framework/store state change the agent can observe and assert on. */
73
38
  state(name: string, value: unknown): void;
74
- /**
75
- * Report an aggregated count of React commits (the @reticlehq/react render meter calls this on a
76
- * throttle). Emits a single RENDER_COMMIT event per window so commit storms are observable without a
77
- * per-render flood. Dev-only, like the whole SDK.
78
- */
79
39
  renderCommit(commits: number): void;
80
- /** Advertise the app's testable surface so the agent learns it without reading source. */
81
40
  describe(input: CapabilitiesInput): void;
82
- /** Live-control: end the session programmatically from the host app (drives the panel to ended). */
83
41
  endSession(): void;
84
- /**
85
- * Hand the SDK the page's hot-update channel, so a stale ref can say the code changed underneath it.
86
- *
87
- * `unknown` on purpose: the only caller that has one is the build integration, the shape it passes
88
- * is Vite's `import.meta.hot`, and the SDK must not depend on Vite — it ships to Next, Electron,
89
- * Tauri and plain pages. Anything that is not a subscribable channel is ignored, which is the
90
- * normal case: with no channel the epoch stays at `NO_EDITS_OBSERVED`, meaning "no edits OBSERVED",
91
- * never "no edits happened".
92
- */
93
42
  observeHotUpdates(hot: unknown): void;
94
43
  disconnect(): void;
95
44
  }
package/dist/reticle.js CHANGED
@@ -1,34 +1,27 @@
1
1
  import { setPresenterVisible } from './dom/dom-ignore.js';
2
- import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, TRANSPORT_LIMITS, buildRedactionPolicy, isLoopbackHostname, isLocalPage, setActiveRedactionPolicy, wireRedactionKeys, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, CONTRACT_FINGERPRINT, newDocumentId, NO_EDITS_OBSERVED, } from '@reticlehq/core';
2
+ import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, TRANSPORT_LIMITS, buildRedactionPolicy, isLoopbackHostname, isLocalPage, setActiveRedactionPolicy, wireRedactionKeys, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, CONTRACT_FINGERPRINT, CONTRACT_PARTS, newDocumentId, NO_EDITS_OBSERVED, PresenterMode, } from '@reticlehq/core';
3
3
  import { rememberSessionLabel } from './session-continuity.js';
4
4
  import { editEpoch } from './edit-epoch.js';
5
5
  import { createCommandRegistry, RELOAD_CACHE_BUST_PARAM, } from './commands/commands.js';
6
6
  import { Transport } from './transport/transport.js';
7
7
  import { unreachableMessage } from './transport/unreachable-message.js';
8
- import { adapterNames } from './registry/adapters.js';
8
+ import { adapterNames } from './registry/stores/adapters.js';
9
+ import { declaredChannels } from './registry/declared-channels.js';
10
+ import { declaredCommands, declaredPlatform } from './registry/declared-platform.js';
9
11
  import { registerCapabilities, setCapabilitiesListener, hasCapabilities, } from './registry/capabilities.js';
10
- import { installAllObservers } from './observers/install-all.js';
11
- import { installOverlay } from './presenter/overlay.js';
12
- import { Presenter, LOG_KIND, LOG_RESULT, } from './presenter/presenter.js';
13
- import { getPresenterSettings } from './presenter/presenter-settings.js';
14
- import { actionVerb } from './presenter/presenter-verbs.js';
12
+ import { installAllObservers, runTeardowns } from './observers/install-all.js';
13
+ import { installOverlay } from './presenter/chrome/overlay.js';
14
+ import { LOG_KIND, LOG_RESULT } from './presenter/chrome/log-kinds.js';
15
+ import { actionVerb } from './presenter/chrome/presenter-verbs.js';
15
16
  import { str, refLabel, modeForCommand, presentStatus } from './reticle-presenter-helpers.js';
16
17
  import { resetClock } from './timers/clock.js';
17
- import { nativeWarn } from './timers/native-console.js';
18
+ import { nativeWarn } from './timers/console/native-console.js';
18
19
  import { installRecorder } from './recorder/recorder.js';
19
20
  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
21
  export function shouldBlockProduction(nodeEnv, allowInProduction) {
27
22
  return 'production' === nodeEnv && !allowInProduction;
28
23
  }
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:') {
24
+ export function connectionPolicy(pageHostname, bridgeUrl, allowNonLocalhost, token, pageProtocol = 'http:') {
32
25
  let bridge;
33
26
  try {
34
27
  bridge = new URL(bridgeUrl);
@@ -63,19 +56,11 @@ pageProtocol = 'http:') {
63
56
  }
64
57
  return { allowed: true };
65
58
  }
66
- /** HUD summary when the SDK self-ends a session because the bridge (server/agent) became unreachable. */
67
59
  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
60
  export function resolveSessionLabel(option, gen) {
74
61
  return option === undefined || option === SESSION_AUTO ? gen() : option;
75
62
  }
76
- // Re-exported from the protocol (the wire contract) so callers/tests can import it from the SDK too.
77
63
  export { RETICLE_URL_PARAM };
78
- /** Remove the `_reticle_reload` cache-buster a hard REFRESH left behind, via a native replaceState. */
79
64
  function stripReloadCacheBustParam() {
80
65
  try {
81
66
  const current = new URL(window.location.href);
@@ -85,13 +70,8 @@ function stripReloadCacheBustParam() {
85
70
  window.history.replaceState(window.history.state, '', current.toString());
86
71
  }
87
72
  catch {
88
- /* best-effort URL hygiene - never block connect() on it */
89
73
  }
90
74
  }
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
75
  export function reticleParamsFromSearch(search) {
96
76
  const params = new URLSearchParams(search);
97
77
  const out = {};
@@ -103,12 +83,6 @@ export function reticleParamsFromSearch(search) {
103
83
  out.projectId = projectId;
104
84
  return out;
105
85
  }
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
86
  export function resolveConnectIdentity(options, search) {
113
87
  const url = reticleParamsFromSearch(search);
114
88
  const explicitSession = options.session !== undefined && options.session !== SESSION_AUTO ? options.session : undefined;
@@ -118,11 +92,6 @@ export function resolveConnectIdentity(options, search) {
118
92
  projectId: projectId !== undefined && projectId.length > 0 ? projectId : undefined,
119
93
  };
120
94
  }
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
95
  export function buildEvent(args) {
127
96
  return {
128
97
  t: args.t,
@@ -130,55 +99,40 @@ export function buildEvent(args) {
130
99
  type: args.type,
131
100
  sessionId: args.sessionId,
132
101
  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
102
  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
103
  editEpoch: NO_EDITS_OBSERVED === args.editEpoch ? undefined : args.editEpoch,
142
104
  data: args.data,
143
105
  };
144
106
  }
145
- /**
146
- * The browser-side orchestrator. Wires observers -> events -> bridge, and bridge
147
- * commands -> handlers. Embedded in the host app (dev only).
148
- */
149
107
  export class Reticle {
150
108
  #transport;
151
109
  #registry = new Map();
152
110
  #teardowns = [];
153
111
  #connected = false;
154
112
  #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
113
  #documentId = newDocumentId(Math.random);
162
114
  #start = 0;
163
115
  #overlay;
164
116
  #presenter;
117
+ #pendingPushes = new Map();
118
+ #readPanelSettings;
119
+ #panelRequested = false;
120
+ #bridgeConnected = false;
165
121
  #recorder;
166
122
  #annotator;
167
123
  #eventCount = 0;
168
124
  #token;
169
125
  #sdkVersion;
126
+ #captureBodies = false;
127
+ #sourceMapping = undefined;
170
128
  #projectId;
171
- /** App-declared extra redaction keys, announced in hello so the driven path honours them too. */
172
129
  #redactKeys = [];
173
- /** Act-row log handle for the in-flight act/act_sequence, so its outcome stamps the right row. */
174
130
  #actHandle;
175
131
  connect(options = {}) {
176
132
  if (this.#connected)
177
133
  return;
178
134
  if ('undefined' === typeof window || 'undefined' === typeof document)
179
135
  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
136
  const proc = globalThis.process;
183
137
  const nodeEnv = proc?.env?.['NODE_ENV'];
184
138
  if (shouldBlockProduction(nodeEnv, true === options.allowInProduction)) {
@@ -186,9 +140,6 @@ export class Reticle {
186
140
  'import.meta.env.DEV, or pass allowInProduction:true to override.');
187
141
  return;
188
142
  }
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
143
  stripReloadCacheBustParam();
193
144
  const url = options.url ?? bridgeWsUrl(RETICLE_DEFAULT_PORT);
194
145
  const policy = connectionPolicy(window.location.hostname, url, true === options.allowNonLocalhost, options.token, window.location.protocol);
@@ -196,24 +147,15 @@ export class Reticle {
196
147
  globalThis.console.warn(`[Reticle] ${policy.reason ?? 'connection blocked'}`);
197
148
  return;
198
149
  }
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
150
  if (options.root !== undefined && options.root.length > 0) {
202
151
  globalThis[RETICLE_ROOT_GLOBAL] = options.root;
203
152
  }
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
153
  const declaredVersion = options.sdkVersion ?? globalThis[RETICLE_SDK_VERSION_GLOBAL];
207
154
  this.#sdkVersion =
208
155
  'string' === typeof declaredVersion && declaredVersion.length > 0
209
156
  ? declaredVersion
210
157
  : 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
158
  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
159
  const explicitSession = resolveSessionLabel(identity.session, () => '');
218
160
  this.#session = rememberSessionLabel(explicitSession.length > 0 ? explicitSession : undefined, 'undefined' === typeof globalThis.sessionStorage ? undefined : globalThis.sessionStorage, () => 'function' === typeof globalThis.crypto?.randomUUID
219
161
  ? `s${globalThis.crypto.randomUUID()}`
@@ -221,9 +163,6 @@ export class Reticle {
221
163
  this.#token =
222
164
  options.token !== undefined && options.token.length > 0 ? options.token : undefined;
223
165
  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
166
  this.#applyRedaction(options.redact);
228
167
  this.#start = performance.now();
229
168
  this.#registry = createCommandRegistry();
@@ -231,85 +170,49 @@ export class Reticle {
231
170
  url,
232
171
  hello: () => this.#hello(),
233
172
  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
- 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.
173
+ onConnected: () => {
174
+ this.#bridgeConnected = true;
175
+ this.#presenter?.sessionStart();
176
+ },
240
177
  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
178
  resetClock();
247
179
  if (true === this.#presenter?.sessionActive) {
248
180
  this.#presenter.setState(SessionState.ENDED, BRIDGE_LOST_SUMMARY);
249
181
  }
250
182
  },
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
183
  onUnreachable: ({ url: tried, attempts }) => {
254
184
  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
185
  this.#presenter?.showUnreachable(tried, attempts);
259
186
  },
260
187
  });
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
188
  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
189
  setPresenterVisible(true === options.exposePresenter);
267
190
  const emit = this.#emit;
191
+ this.#captureBodies = true === options.captureNetworkBodies;
192
+ this.#sourceMapping =
193
+ 'boolean' === typeof options.sourceMapping ? options.sourceMapping : undefined;
268
194
  this.#teardowns = installAllObservers(emit, {
269
- captureBodies: true === options.captureNetworkBodies,
195
+ captureBodies: this.#captureBodies,
196
+ ...(options.captureErrorBodies === undefined
197
+ ? {}
198
+ : { captureErrorBodies: options.captureErrorBodies }),
270
199
  });
271
200
  if (true === options.overlay) {
272
201
  this.#overlay = installOverlay();
273
202
  this.#overlay.update({ connected: true, events: 0 });
274
203
  }
275
- if (options.present !== false) {
276
- const presenterOptions = {};
277
- if (options.pace !== undefined)
278
- presenterOptions.paceMs = options.pace;
279
- if (options.narrationDwellMs !== undefined) {
280
- presenterOptions.narrationDwellMs = options.narrationDwellMs;
281
- }
282
- if (options.border !== undefined)
283
- presenterOptions.border = options.border;
284
- if (options.logMax !== undefined)
285
- presenterOptions.logMax = options.logMax;
286
- if (options.endedFadeMs !== undefined)
287
- presenterOptions.endedFadeMs = options.endedFadeMs;
288
- if (options.idleEndMs !== undefined)
289
- presenterOptions.idleEndMs = options.idleEndMs;
290
- 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
- presenterOptions.onControl = (intent) => this.#emit(EventType.HUMAN_CONTROL, intent.text !== undefined
294
- ? { kind: intent.kind, text: intent.text }
295
- : { kind: intent.kind });
296
- 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
- this.#presenter.mount();
300
- }
204
+ if (options.present !== false)
205
+ this.#fetchAndShowPanel(options);
301
206
  if (true === options.recorder) {
302
207
  this.#recorder = installRecorder({ emit, now: () => Date.now() });
303
208
  this.#recorder.mount();
304
209
  }
305
- // The page annotator rides with the presenter (the human surface) unless explicitly off.
306
210
  if (options.annotate ?? options.present !== false) {
307
- const presenter = this.#presenter;
308
211
  this.#annotator = new Annotator({
309
212
  emit,
310
213
  now: () => Date.now(),
311
- onMark: (mark) => presenter?.log(LOG_KIND.HUMAN, `🚩 #${String(mark.index)} ${mark.anchor}${mark.source !== undefined ? ` · ${mark.source}` : ''} — ${mark.note}`),
312
- shouldBlock: () => getPresenterSettings().blockPageInteractions,
214
+ onMark: (mark) => this.#presenter?.log(LOG_KIND.HUMAN, `🚩 #${String(mark.index)} ${mark.anchor}${mark.source !== undefined ? ` · ${mark.source}` : ''} — ${mark.note}`),
215
+ shouldBlock: () => this.#readPanelSettings?.().blockPageInteractions ?? true,
313
216
  });
314
217
  this.#annotator.mount();
315
218
  this.#presenter?.bindAnnotator(this.#annotator);
@@ -317,65 +220,106 @@ export class Reticle {
317
220
  this.#transport.connect();
318
221
  this.#connected = true;
319
222
  }
320
- /** Whether the in-page SDK is connected to the bridge (read by createReticleEmitter, P5a). */
223
+ #fetchAndShowPanel(options) {
224
+ if (this.#panelRequested)
225
+ return;
226
+ this.#panelRequested = true;
227
+ void import('./presenter/presenter.js')
228
+ .then(({ Presenter }) => import('./presenter/presenter-settings.js').then((settings) => ({ Presenter, settings })))
229
+ .then(({ Presenter, settings }) => {
230
+ if (!this.#connected)
231
+ return;
232
+ this.#readPanelSettings = settings.getPresenterSettings;
233
+ const panelOptions = {};
234
+ if (options.pace !== undefined)
235
+ panelOptions.paceMs = options.pace;
236
+ if (options.narrationDwellMs !== undefined) {
237
+ panelOptions.narrationDwellMs = options.narrationDwellMs;
238
+ }
239
+ if (options.border !== undefined)
240
+ panelOptions.border = options.border;
241
+ if (options.logMax !== undefined)
242
+ panelOptions.logMax = options.logMax;
243
+ if (options.endedFadeMs !== undefined)
244
+ panelOptions.endedFadeMs = options.endedFadeMs;
245
+ if (options.idleEndMs !== undefined)
246
+ panelOptions.idleEndMs = options.idleEndMs;
247
+ panelOptions.sessionId = this.#session;
248
+ panelOptions.onControl = (intent) => this.#emit(EventType.HUMAN_CONTROL, intent.text !== undefined
249
+ ? { kind: intent.kind, text: intent.text }
250
+ : { kind: intent.kind });
251
+ const panel = new Presenter(panelOptions);
252
+ this.#presenter = panel;
253
+ panel.mount();
254
+ for (const buffered of this.#pendingPushes.values())
255
+ panel.handlePush(buffered);
256
+ this.#pendingPushes.clear();
257
+ void import('./presenter/tour/tour.js').then((tour) => {
258
+ tour.mountTour({
259
+ document,
260
+ storage: tour.safeLocalStorage(),
261
+ projectId: reticleParamsFromSearch(window.location.search).projectId ?? window.location.origin,
262
+ isDriving: () => PresenterMode.IDLE !== panel.mode,
263
+ search: window.location.search,
264
+ copy: (text) => navigator.clipboard?.writeText(text).then(() => true, () => false) ?? false,
265
+ select: (element) => {
266
+ const selection = window.getSelection();
267
+ if (null === selection)
268
+ return;
269
+ const range = document.createRange();
270
+ range.selectNodeContents(element);
271
+ selection.removeAllRanges();
272
+ selection.addRange(range);
273
+ },
274
+ });
275
+ });
276
+ if (this.#bridgeConnected)
277
+ panel.sessionStart();
278
+ if (this.#annotator !== undefined)
279
+ panel.bindAnnotator(this.#annotator);
280
+ })
281
+ .catch(() => {
282
+ });
283
+ }
321
284
  get connected() {
322
285
  return this.#connected;
323
286
  }
324
- /** Surface an arbitrary app-domain observation the DOM can't express. */
325
287
  signal(name, data = {}) {
326
288
  this.#emit(EventType.SIGNAL, { name, data });
327
289
  }
328
- /** Report a framework/store state change the agent can observe and assert on. */
329
290
  state(name, value) {
330
291
  this.#emit(EventType.STATE_CHANGE, { name, value });
331
292
  }
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
293
  renderCommit(commits) {
338
294
  if (commits > 0)
339
295
  this.#emit(EventType.RENDER_COMMIT, { commits });
340
296
  }
341
- /** Advertise the app's testable surface so the agent learns it without reading source. */
342
297
  describe(input) {
343
- registerCapabilities(input); // the registry notifies the transport - see setCapabilitiesListener
298
+ registerCapabilities(input);
344
299
  }
345
- /** Live-control: end the session programmatically from the host app (drives the panel to ended). */
346
300
  endSession() {
347
301
  this.#presenter?.setState(SessionState.ENDED);
348
302
  }
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
303
  observeHotUpdates(hot) {
359
304
  editEpoch.observe(hot);
360
305
  }
361
306
  disconnect() {
362
307
  if (!this.#connected)
363
308
  return;
364
- for (const teardown of this.#teardowns)
365
- teardown();
309
+ runTeardowns(this.#emit, this.#teardowns);
366
310
  this.#teardowns = [];
367
311
  this.#transport?.close();
368
312
  this.#transport = undefined;
369
313
  this.#overlay?.destroy();
370
314
  this.#overlay = undefined;
371
- this.#presenter?.sessionEnd(); // fade the border out before tearing the overlay down
315
+ this.#presenter?.sessionEnd();
372
316
  this.#presenter?.destroy();
373
317
  this.#presenter = undefined;
374
318
  this.#recorder?.destroy();
375
319
  this.#recorder = undefined;
376
320
  this.#annotator?.destroy();
377
321
  this.#annotator = undefined;
378
- resetClock(); // restore any frozen timers
322
+ resetClock();
379
323
  this.#connected = false;
380
324
  }
381
325
  #emit = (type, data, ref) => {
@@ -389,29 +333,16 @@ export class Reticle {
389
333
  data,
390
334
  ref,
391
335
  });
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
336
  try {
398
337
  this.#transport?.sendEvent(event);
399
338
  this.#eventCount += 1;
400
339
  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
340
  if (type === EventType.ROUTE_CHANGE)
403
341
  this.#presenter?.refilterFlows();
404
342
  }
405
343
  catch {
406
- /* observation is best-effort; the host app's control flow is not */
407
344
  }
408
345
  };
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
346
  #applyRedaction(config) {
416
347
  setActiveRedactionPolicy(buildRedactionPolicy(config, nativeWarn));
417
348
  this.#redactKeys = wireRedactionKeys(config);
@@ -427,42 +358,48 @@ export class Reticle {
427
358
  adapters: adapterNames(),
428
359
  ...(this.#token === undefined ? {} : { token: this.#token }),
429
360
  hasCapabilities: hasCapabilities(),
430
- // Absent when no build plugin supplied one - "unknown", never "matching".
361
+ channels: declaredChannels(),
362
+ platform: declaredPlatform(),
363
+ commands: declaredCommands(),
364
+ captureBodies: this.#captureBodies,
365
+ ...(this.#sourceMapping === undefined ? {} : { sourceMapping: this.#sourceMapping }),
431
366
  ...(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
367
  contract: CONTRACT_FINGERPRINT,
368
+ contractParts: {
369
+ commands: [...CONTRACT_PARTS.commands],
370
+ events: [...CONTRACT_PARTS.events],
371
+ actions: [...CONTRACT_PARTS.actions],
372
+ },
435
373
  ...(0 === this.#redactKeys.length ? {} : { redactKeys: this.#redactKeys }),
436
374
  };
437
375
  }
438
376
  async #handleCommand(command) {
439
- // NARRATE: the agent tells the human what it's about to do / decide (presenter HUD).
440
377
  if (command.name === ReticleCommand.NARRATE) {
441
- this.#presenter?.sessionStart(); // first agent activity → reveal the glow + panel
378
+ this.#presenter?.sessionStart();
442
379
  this.#presenter?.narrate(str(command.args['text']), str(command.args['level'], 'info'));
443
380
  return { ok: true, result: { shown: this.#presenter !== undefined } };
444
381
  }
445
- // SESSION_CONFIG: the agent tunes the session for the app (currently the idle-end window).
446
382
  if (command.name === ReticleCommand.SESSION_CONFIG) {
447
383
  const idleEndMs = command.args['idleEndMs'];
448
384
  if ('number' === typeof idleEndMs)
449
385
  this.#presenter?.setIdleEndMs(idleEndMs);
450
386
  return { ok: true, result: { applied: this.#presenter !== undefined, idleEndMs } };
451
387
  }
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
388
  if (command.name === ReticleCommand.PRESENTER ||
456
389
  command.name === ReticleCommand.FLOWS ||
457
390
  command.name === ReticleCommand.IMPACT) {
458
- this.#presenter?.handlePush(command);
459
- return { ok: true, result: { applied: this.#presenter !== undefined } };
391
+ if (this.#presenter === undefined) {
392
+ this.#pendingPushes.set(command.name, command);
393
+ return { ok: true, result: { applied: false, buffered: true } };
394
+ }
395
+ this.#presenter.handlePush(command);
396
+ return { ok: true, result: { applied: true } };
460
397
  }
461
398
  const handler = this.#registry.get(command.name);
462
399
  if (handler === undefined) {
463
400
  return { ok: false, error: `unknown command '${command.name}'` };
464
401
  }
465
- this.#presenter?.sessionStart(); // first agent command → reveal the glow + panel
402
+ this.#presenter?.sessionStart();
466
403
  await this.#presentBefore(command);
467
404
  try {
468
405
  const result = await handler(command.args);
@@ -478,12 +415,11 @@ export class Reticle {
478
415
  this.#presenter?.scheduleIdle();
479
416
  }
480
417
  }
481
- /** Drive the presenter (cursor/effects/status) before the real action runs. */
482
418
  async #presentBefore(command) {
483
419
  const p = this.#presenter;
484
420
  if (p === undefined)
485
421
  return;
486
- p.setMode(modeForCommand(command.name)); // paint reading vs acting intent first
422
+ p.setMode(modeForCommand(command.name));
487
423
  this.#actHandle = undefined;
488
424
  if (command.name === ReticleCommand.ACT) {
489
425
  const ref = str(command.args['ref']);
@@ -497,7 +433,6 @@ export class Reticle {
497
433
  const s = step;
498
434
  const ref = str(s.ref);
499
435
  const label = refLabel(ref);
500
- // one log row per step; the last handle carries the sequence outcome glyph
501
436
  this.#actHandle = p.log(LOG_KIND.ACT, `${actionVerb(str(s.action))} ${label}`);
502
437
  await p.beforeAct(ref, str(s.action), label);
503
438
  }