@reticlehq/browser 2.8.0 → 2.9.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 (79) hide show
  1. package/NOTICE +7 -0
  2. package/dist/actions/actions-dom.js +23 -15
  3. package/dist/actions/actions.d.ts +17 -0
  4. package/dist/actions/actions.js +132 -29
  5. package/dist/actions/synthetic-input.d.ts +9 -0
  6. package/dist/actions/synthetic-input.js +40 -0
  7. package/dist/commands/commands.js +11 -1
  8. package/dist/connect-options.d.ts +9 -9
  9. package/dist/dom/a11y.d.ts +9 -0
  10. package/dist/dom/a11y.js +19 -0
  11. package/dist/dom/dom-ignore.d.ts +19 -6
  12. package/dist/dom/dom-ignore.js +20 -13
  13. package/dist/dom/query.d.ts +2 -2
  14. package/dist/dom/query.js +85 -45
  15. package/dist/dom/snapshot.d.ts +5 -0
  16. package/dist/dom/snapshot.js +34 -0
  17. package/dist/dom/source.d.ts +14 -0
  18. package/dist/dom/source.js +16 -0
  19. package/dist/observers/install-all.js +4 -1
  20. package/dist/observers/ipc.d.ts +16 -0
  21. package/dist/observers/ipc.js +19 -1
  22. package/dist/observers/network.d.ts +8 -0
  23. package/dist/observers/network.js +24 -3
  24. package/dist/observers/state.js +21 -2
  25. package/dist/presenter/presenter-brand.d.ts +24 -7
  26. package/dist/presenter/presenter-brand.js +23 -6
  27. package/dist/presenter/presenter-config.d.ts +102 -9
  28. package/dist/presenter/presenter-config.js +94 -8
  29. package/dist/presenter/presenter-controls.d.ts +10 -10
  30. package/dist/presenter/presenter-controls.js +175 -85
  31. package/dist/presenter/presenter-dock-layout.d.ts +6 -0
  32. package/dist/presenter/presenter-dock-layout.js +170 -0
  33. package/dist/presenter/presenter-drag.d.ts +48 -0
  34. package/dist/presenter/presenter-drag.js +198 -0
  35. package/dist/presenter/presenter-effects.d.ts +1 -1
  36. package/dist/presenter/presenter-effects.js +1 -1
  37. package/dist/presenter/presenter-glow.d.ts +5 -5
  38. package/dist/presenter/presenter-glow.js +7 -7
  39. package/dist/presenter/presenter-heroicons-data.d.ts +41 -0
  40. package/dist/presenter/presenter-heroicons-data.js +38 -0
  41. package/dist/presenter/presenter-hud-chrome.d.ts +17 -0
  42. package/dist/presenter/presenter-hud-chrome.js +41 -0
  43. package/dist/presenter/presenter-icons.d.ts +44 -0
  44. package/dist/presenter/presenter-icons.js +100 -0
  45. package/dist/presenter/presenter-log.d.ts +27 -5
  46. package/dist/presenter/presenter-log.js +114 -28
  47. package/dist/presenter/presenter-report-copy.d.ts +83 -0
  48. package/dist/presenter/presenter-report-copy.js +127 -0
  49. package/dist/presenter/presenter-report-styles.d.ts +7 -0
  50. package/dist/presenter/presenter-report-styles.js +102 -0
  51. package/dist/presenter/presenter-report.d.ts +23 -0
  52. package/dist/presenter/presenter-report.js +217 -0
  53. package/dist/presenter/presenter-run-state.d.ts +1 -1
  54. package/dist/presenter/presenter-settings-styles.d.ts +2 -0
  55. package/dist/presenter/presenter-settings-styles.js +153 -0
  56. package/dist/presenter/presenter-settings.d.ts +82 -0
  57. package/dist/presenter/presenter-settings.js +494 -0
  58. package/dist/presenter/presenter-shell-styles.d.ts +1 -0
  59. package/dist/presenter/presenter-shell-styles.js +375 -0
  60. package/dist/presenter/presenter-shell.d.ts +48 -0
  61. package/dist/presenter/presenter-shell.js +408 -0
  62. package/dist/presenter/presenter-styles.d.ts +9 -4
  63. package/dist/presenter/presenter-styles.js +61 -125
  64. package/dist/presenter/presenter-tally.d.ts +8 -11
  65. package/dist/presenter/presenter-tally.js +48 -23
  66. package/dist/presenter/presenter-test-helpers.d.ts +1 -1
  67. package/dist/presenter/presenter-test-helpers.js +1 -1
  68. package/dist/presenter/presenter-verbs.js +2 -2
  69. package/dist/presenter/presenter-workspace.d.ts +15 -0
  70. package/dist/presenter/presenter-workspace.js +180 -0
  71. package/dist/presenter/presenter.d.ts +15 -10
  72. package/dist/presenter/presenter.js +194 -46
  73. package/dist/reticle.d.ts +2 -2
  74. package/dist/reticle.js +23 -19
  75. package/dist/review/annotator-styles.d.ts +6 -0
  76. package/dist/review/annotator-styles.js +81 -0
  77. package/dist/review/annotator.d.ts +29 -17
  78. package/dist/review/annotator.js +477 -146
  79. package/package.json +7 -4
@@ -2,18 +2,22 @@ import { ActionType, ReticleCommand, PresenterMode, SessionState, isPresenterTon
2
2
  import { refs } from '../dom/refs.js';
3
3
  import { actionVerb } from './presenter-verbs.js';
4
4
  import { nativeSetTimeout, nativeClearTimeout, nativeNow } from '../timers/native-timers.js';
5
- import { LOG_KIND, CHIP_LABEL, DATA_RETICLE_LOG, HUMAN_ROW_PREFIX, clampLogMax, formatElapsed, humanDuration, appendLogRow, } from './presenter-log.js';
5
+ import { LOG_KIND, CHIP_LABEL, DATA_RETICLE_LOG, clampLogMax, formatElapsed, humanDuration, appendLogRow, } from './presenter-log.js';
6
6
  import { PRESENTER_CSS } from './presenter-styles.js';
7
- import { BRAND_HTML } from './presenter-brand.js';
8
- import { BorderMode, DEFAULT_BORDER_MODE, DATA_BUSY, BUSY_OFF, effectivePaceMs, GlowPhase, IDLE_AFTER_MS, HEARTBEAT_MS, IDLE_NOTICE_MS, IDLE_END_MS, IDLE_END_MIN_MS, GLOW_FADE_MS, GLOW_ON, GLOW_OFF, DATA_ON, MIN_ATTR, THROTTLED_ATTR, } from './presenter-config.js';
7
+ import { HudShell } from './presenter-shell.js';
8
+ import { parseImpactSnapshot } from './presenter-report-copy.js';
9
+ import { BorderMode, DEFAULT_BORDER_MODE, DATA_BUSY, BUSY_OFF, effectivePaceMs, IDLE_AFTER_MS, HEARTBEAT_MS, IDLE_NOTICE_MS, IDLE_END_MS, IDLE_END_MIN_MS, ACT_STRIP, GLOW_FADE_MS, GLOW_ON, GLOW_OFF, DATA_ON, THROTTLED_ATTR, LIVENESS_ATTR, MARKERS_BTN_ATTR, CLEAR_MARKS_ATTR, MARK_COUNT_ATTR, } from './presenter-config.js';
9
10
  import { buildRunState } from './presenter-run-state.js';
10
11
  import { moveCursor, ringAround, spawnRipple, pace } from './presenter-effects.js';
11
12
  import { GlowController } from './presenter-glow.js';
13
+ import { PresenterIcon, PRESENTER_ICON_SIZE, hiIcon } from './presenter-icons.js';
12
14
  import { renderTally } from './presenter-tally.js';
13
- import { CONTROLS_HEAD_HTML, CONTROLS_BANNER_HTML, CONTROLS_FLOWS_HTML, CONTROLS_FOOT_HTML, ENDED_FADE_MS, ControlPanel, } from './presenter-controls.js';
15
+ import { CONTROLS_BANNER_HTML, CONTROLS_FLOWS_HTML, CONTROLS_FOOT_HTML, ENDED_FADE_MS, ControlPanel, } from './presenter-controls.js';
16
+ import { statusTheme, blockerHtml, getPresenterSettings, OutputDetail, syncPageBlocker, } from './presenter-settings.js';
17
+ import { Annotator } from '../review/annotator.js';
14
18
  // Re-export the config surface so the public import path (`./presenter.js`) is unchanged.
15
19
  export { GlowPhase } from './presenter-config.js';
16
- export { LOG_KIND, LOG_RESULT, } from './presenter-log.js';
20
+ export { LOG_KIND, LOG_RESULT } from './presenter-log.js';
17
21
  // Presenter / transparency layer: a human watches the agent work. Glowing border while
18
22
  // active, a synthetic cursor that flies to targets, click/hover/type effects, and a HUD that
19
23
  // shows the current action + the agent's narrated intent. All nodes carry data-reticle-* attrs
@@ -26,11 +30,15 @@ export class Presenter {
26
30
  #ring;
27
31
  #hud;
28
32
  #actLine;
33
+ #actStrip;
34
+ /** Label inside the minimised-chat capsule; mirrors the act strip. */
35
+ #chatPillText;
36
+ /** Age of the last action, shown in the capsule's own slot. */
37
+ #chatPillTime;
29
38
  #chip;
30
- /** Live verdict tally (✓N ✗M) in the header — the running testing score the human watches. */
39
+ /** Live verdict tally (✓N ✗M) in the header - the running testing score the human watches. */
31
40
  #tally;
32
41
  #tallied = { passes: 0, fails: 0 };
33
- #liveLine;
34
42
  #mode = PresenterMode.IDLE;
35
43
  #now;
36
44
  #heartbeatMs;
@@ -57,6 +65,8 @@ export class Presenter {
57
65
  // Live-control panel: the two-way control surface (Pause/Resume + End + message Send).
58
66
  #onControl;
59
67
  #panel;
68
+ #shell;
69
+ #annotator;
60
70
  constructor(options = {}) {
61
71
  this.#paceMs = effectivePaceMs(options.paceMs);
62
72
  this.#now = options.now ?? nativeNow;
@@ -77,10 +87,26 @@ export class Presenter {
77
87
  this.#panel = new ControlPanel({
78
88
  emit: (kind, text) => this.#onControl?.(text !== undefined ? { kind, text } : { kind }),
79
89
  logHuman: (text) => {
80
- this.log(LOG_KIND.HUMAN, HUMAN_ROW_PREFIX + text);
90
+ this.log(LOG_KIND.HUMAN, text);
81
91
  },
82
92
  endedFadeMs: options.endedFadeMs ?? ENDED_FADE_MS,
83
93
  runState: () => this.runState(),
94
+ clearRunLog: () => this.#clearRunLog(),
95
+ onStateChange: () => this.#syncAnnotator(),
96
+ });
97
+ this.#shell = new HudShell({
98
+ onChatOpen: () => this.#shell.pulseFab(false),
99
+ onExpand: () => this.#syncAnnotator(),
100
+ onAnnotateToggle: () => this.#syncAnnotator(),
101
+ onCollapse: () => this.#syncAnnotator(),
102
+ settings: {
103
+ onBeforeOpen: () => {
104
+ if (this.#shell.isCollapsed())
105
+ this.#shell.expand();
106
+ },
107
+ onHideUntilRestart: () => this.#applyHideUntilRestart(),
108
+ onSettingsChange: (s) => this.#onSettingsChange(s),
109
+ },
84
110
  });
85
111
  }
86
112
  /** Setter so reticle.ts can wire the control callback after construction. */
@@ -110,6 +136,12 @@ export class Presenter {
110
136
  const a = command.args;
111
137
  if (command.name === ReticleCommand.FLOWS)
112
138
  return void this.#panel.setFlows(a['flows']);
139
+ if (command.name === ReticleCommand.IMPACT) {
140
+ const snapshot = parseImpactSnapshot(a['snapshot']);
141
+ if (snapshot !== undefined)
142
+ this.#shell.report.setSnapshot(snapshot);
143
+ return;
144
+ }
113
145
  const state = a['state'];
114
146
  const tone = a['tone'];
115
147
  const text = 'string' === typeof a['text'] && a['text'].length > 0 ? a['text'] : undefined;
@@ -133,18 +165,13 @@ export class Presenter {
133
165
  document.head.appendChild(style);
134
166
  const root = document.createElement('div');
135
167
  root.setAttribute('data-reticle-overlay', '');
168
+ const actStrip = `<div class="reticle-act-strip" data-liveness="idle"><span class="reticle-act-dot" aria-hidden="true"></span><span class="reticle-act">${ACT_STRIP.READY}</span></div>`;
136
169
  root.innerHTML = `
170
+ ${blockerHtml()}
137
171
  <div data-reticle-glow></div>
138
172
  <div data-reticle-cursor></div>
139
173
  <div data-reticle-ring></div>
140
- <div data-reticle-hud>
141
- <div class="reticle-hud-head">${BRAND_HTML}<span class="reticle-chip" data-reticle-chip></span><span class="reticle-tally" data-reticle-tally hidden></span><span class="reticle-live"></span><span class="reticle-head-sp"></span><button type="button" data-reticle-min-btn title="Minimise" aria-label="Minimise the panel">⌄</button>${CONTROLS_HEAD_HTML}<span class="reticle-maxhint" aria-hidden="true">⌃</span></div>
142
- <div class="reticle-act-strip"><span class="reticle-act">idle</span></div>
143
- ${CONTROLS_BANNER_HTML}
144
- <div ${DATA_RETICLE_LOG}></div>
145
- ${CONTROLS_FLOWS_HTML}
146
- ${CONTROLS_FOOT_HTML}
147
- </div>`;
174
+ ${HudShell.dockHtml(actStrip, CONTROLS_BANNER_HTML, DATA_RETICLE_LOG, CONTROLS_FLOWS_HTML, CONTROLS_FOOT_HTML)}`;
148
175
  document.body.appendChild(root);
149
176
  this.#root = root;
150
177
  this.#glow = root.querySelector('[data-reticle-glow]') ?? undefined;
@@ -152,30 +179,60 @@ export class Presenter {
152
179
  this.#ring = root.querySelector('[data-reticle-ring]') ?? undefined;
153
180
  this.#hud = root.querySelector('[data-reticle-hud]') ?? undefined;
154
181
  this.#actLine = root.querySelector('.reticle-act') ?? undefined;
182
+ this.#actStrip = root.querySelector('.reticle-act-strip') ?? undefined;
183
+ this.#chatPillText =
184
+ root.querySelector('[data-reticle-chat-pill-text]') ?? undefined;
185
+ this.#chatPillTime =
186
+ root.querySelector('[data-reticle-chat-pill-time]') ?? undefined;
155
187
  this.#log = root.querySelector(`[${DATA_RETICLE_LOG}]`) ?? undefined;
156
188
  this.#chip = root.querySelector('[data-reticle-chip]') ?? undefined;
157
189
  this.#tally = root.querySelector('[data-reticle-tally]') ?? undefined;
158
- this.#liveLine = root.querySelector('.reticle-live') ?? undefined;
159
- // Minimise → collapse the panel to a bar (only the live line streams). Click the bar to restore.
160
- const setMin = (on) => root.setAttribute(MIN_ATTR, on ? '1' : '0');
161
- root.querySelector('[data-reticle-min-btn]')?.addEventListener('click', (e) => {
162
- e.stopPropagation(); // don't let the head's maximise handler immediately re-open it
163
- setMin(true);
164
- });
165
- root.querySelector('.reticle-hud-head')?.addEventListener('click', () => {
166
- if ('1' === root.getAttribute(MIN_ATTR))
167
- setMin(false); // clicking the minimised bar restores
168
- });
190
+ this.#shell.mount(root);
191
+ syncPageBlocker(root, getPresenterSettings(), false);
169
192
  this.#glowCtl.setElements(this.#glow, this.#cursor);
170
193
  // The panel queries its refs, binds listeners, and paints the initial active state.
171
194
  this.#panel.mount(root, this.#glow);
172
195
  this.setMode(this.#mode);
196
+ this.#renderTally();
197
+ }
198
+ /** Wire annotation chrome; expanding the HUD enters annotate mode. */
199
+ bindAnnotator(annotator) {
200
+ this.#annotator = annotator;
201
+ const root = this.#root;
202
+ if (root === undefined)
203
+ return;
204
+ const markers = root.querySelector(`[${MARKERS_BTN_ATTR}]`);
205
+ const clear = root.querySelector(`[${CLEAR_MARKS_ATTR}]`);
206
+ const count = root.querySelector(`[${MARK_COUNT_ATTR}]`);
207
+ const chrome = {};
208
+ if (markers instanceof HTMLElement)
209
+ chrome.markersBtn = markers;
210
+ if (clear instanceof HTMLElement)
211
+ chrome.clearBtn = clear;
212
+ if (count instanceof HTMLElement)
213
+ chrome.countEl = count;
214
+ annotator.attachChrome(chrome);
215
+ annotator.setAccent(statusTheme(getPresenterSettings().statusThemeId).active);
216
+ this.#syncAnnotator();
217
+ }
218
+ /** Annotate whenever the HUD is open and the person asked for it - agent or no agent. */
219
+ #syncAnnotator() {
220
+ // Two things have to agree, and both are the user's: the HUD is open, and annotate is switched
221
+ // on. Session state used to be a third, so the mode was refused the moment the agent
222
+ // disconnected - which is precisely when someone opens the HUD to record what they just saw.
223
+ // The button still lit up, so the refusal was invisible: no outline, no composer, no reason.
224
+ const live = !this.#shell.isCollapsed() && this.#shell.isAnnotateOn();
225
+ this.#annotator?.toggle(live);
226
+ if (this.#root !== undefined) {
227
+ syncPageBlocker(this.#root, getPresenterSettings(), live);
228
+ }
173
229
  }
174
230
  destroy() {
175
231
  this.#glowCtl.teardown();
176
232
  if (this.#heartbeatTimer !== undefined)
177
233
  nativeClearTimeout(this.#heartbeatTimer);
178
234
  this.#heartbeatTimer = undefined;
235
+ this.#shell.teardown();
179
236
  this.#panel.teardown();
180
237
  this.#sessionActive = false;
181
238
  this.#logBaseMs = undefined;
@@ -202,10 +259,14 @@ export class Presenter {
202
259
  this.#showSession();
203
260
  this.#glowCtl.resetActivity(this.#now());
204
261
  this.#startHeartbeat();
262
+ if (getPresenterSettings().autoOpenChat) {
263
+ this.#shell.openChat();
264
+ }
205
265
  }
206
- /** Turn the base border (session mode) + the HUD/log on — the visible "session is live" state. */
266
+ /** Turn the base border (session mode) + the HUD/log on - the visible "session is live" state. */
207
267
  #showSession() {
208
- // The activity log/HUD persists the WHOLE session, like the border — it never fades on idle.
268
+ const dock = this.#root?.querySelector('[data-reticle-dock]');
269
+ dock?.setAttribute(DATA_ON, GLOW_ON);
209
270
  this.#hud?.setAttribute(DATA_ON, GLOW_ON);
210
271
  // Base border persists in 'session' mode; 'busy' mode leaves it to the busy machine.
211
272
  if (this.#borderMode === BorderMode.SESSION)
@@ -231,14 +292,17 @@ export class Presenter {
231
292
  nativeClearTimeout(this.#heartbeatTimer);
232
293
  this.#heartbeatTimer = undefined;
233
294
  }
295
+ const dock = this.#root?.querySelector('[data-reticle-dock]');
296
+ dock?.setAttribute(DATA_ON, GLOW_OFF);
234
297
  this.#hud?.setAttribute(DATA_ON, GLOW_OFF);
298
+ this.#shell.collapse();
235
299
  if (this.#borderMode === BorderMode.SESSION) {
236
300
  this.#glow?.setAttribute(DATA_ON, GLOW_OFF);
237
301
  this.#glow?.setAttribute(DATA_BUSY, BUSY_OFF);
238
302
  }
239
303
  }
240
304
  /**
241
- * Record agent activity. Idempotent while busy — only the first activity from idle/fading flips
305
+ * Record agent activity. Idempotent while busy - only the first activity from idle/fading flips
242
306
  * the glow on, so a burst never restarts the reticle-pulse animation (no strobe). Subsequent calls
243
307
  * just refresh the last-activity timestamp and re-arm the idle check.
244
308
  */
@@ -265,23 +329,63 @@ export class Presenter {
265
329
  this.#mode = mode;
266
330
  this.#root?.setAttribute('data-reticle-mode', mode);
267
331
  if (this.#chip !== undefined) {
268
- this.#chip.textContent = CHIP_LABEL[mode];
332
+ const label = CHIP_LABEL[mode];
269
333
  this.#chip.setAttribute('data-mode', mode);
334
+ this.#chip.replaceChildren();
335
+ if (label.length > 0) {
336
+ const icon = mode === PresenterMode.READING
337
+ ? PresenterIcon.VIEW
338
+ : mode === PresenterMode.ACTING
339
+ ? PresenterIcon.POINTER
340
+ : undefined;
341
+ if (icon !== undefined)
342
+ this.#chip.appendChild(hiIcon(icon, PRESENTER_ICON_SIZE.CHIP));
343
+ const labelEl = document.createElement('span');
344
+ labelEl.className = 'reticle-chip-label';
345
+ labelEl.textContent = label;
346
+ this.#chip.appendChild(labelEl);
347
+ }
270
348
  }
271
- // READING has no real pointer to show (synthetic-hover pointer is native-only) — hide the cursor.
349
+ // READING has no real pointer to show (synthetic-hover pointer is native-only) - hide the cursor.
272
350
  if (mode === PresenterMode.READING)
273
351
  this.#cursor?.setAttribute(DATA_ON, GLOW_OFF);
274
352
  }
275
353
  status(text) {
276
354
  this.markActivity();
355
+ if (this.#chatPillTime !== undefined)
356
+ this.#chatPillTime.textContent = ACT_STRIP.NOW;
277
357
  this.#lastActionText = text;
358
+ this.#paintActStrip(text, false);
359
+ }
360
+ /**
361
+ * Sync act-strip text + the live/idle state.
362
+ *
363
+ * The liveness is mirrored onto the overlay root as well as the strip, because that is what the
364
+ * status COLOUR resolves against: the page glow, the collapsed FAB's halo and the minimised
365
+ * capsule all live outside the strip and still have to say whether the agent is working.
366
+ */
367
+ #paintActStrip(text, idle) {
278
368
  if (this.#actLine !== undefined)
279
369
  this.#actLine.textContent = text;
370
+ const liveness = idle ? 'idle' : 'active';
371
+ if (this.#actStrip !== undefined)
372
+ this.#actStrip.setAttribute('data-liveness', liveness);
373
+ this.#root?.setAttribute(LIVENESS_ATTR, liveness);
374
+ if (this.#chatPillText !== undefined) {
375
+ // The capsule reads "logo | what the agent did | how long ago | expand", so it takes the
376
+ // ACTION, never the composed "idle · 12s since last action" line the strip shows: the age
377
+ // lives in its own slot next to it.
378
+ this.#chatPillText.textContent = idle
379
+ ? this.#lastActionText !== ''
380
+ ? this.#lastActionText
381
+ : ACT_STRIP.READY
382
+ : text;
383
+ }
280
384
  }
281
385
  /**
282
- * Liveness heartbeat (native 1s timer — never rAF, so it ticks in a foreground tab regardless of
386
+ * Liveness heartbeat (native 1s timer - never rAF, so it ticks in a foreground tab regardless of
283
387
  * agent activity). Once the agent has been quiet for IDLE_NOTICE_MS, the act strip shows a LIVE,
284
- * growing "◌ idle · {duration} since last action" — the signal that was missing when a stopped
388
+ * growing "◌ idle · {duration} since last action" - the signal that was missing when a stopped
285
389
  * agent left the panel frozen and indistinguishable from one still thinking.
286
390
  */
287
391
  #startHeartbeat() {
@@ -297,16 +401,18 @@ export class Presenter {
297
401
  if (!this.#sessionActive || this.#actLine === undefined)
298
402
  return;
299
403
  if (this.state === SessionState.ENDED)
300
- return; // already ended — leave the summary
404
+ return; // already ended - leave the summary
301
405
  const idleMs = this.#now() - this.#glowCtl.lastActivityMs();
302
406
  if (idleMs >= this.#idleEndMs) {
303
407
  this.#endIdle(idleMs); // crossed the idle-end window → auto-end (glow off, panel kept)
304
408
  return;
305
409
  }
306
410
  if (idleMs < this.#idleNoticeMs)
307
- return; // still active (or a brief think) — keep the action text
308
- const since = this.#lastActionText !== '' ? ` since last action` : '';
309
- this.#actLine.textContent = `◌ idle · ${humanDuration(idleMs)}${since}`;
411
+ return; // still active (or a brief think) - keep the action text
412
+ const since = this.#lastActionText !== '' ? ACT_STRIP.SINCE_LAST : '';
413
+ this.#paintActStrip(`${ACT_STRIP.IDLE_PREFIX}${humanDuration(idleMs)}${since}`, true);
414
+ if (this.#chatPillTime !== undefined)
415
+ this.#chatPillTime.textContent = humanDuration(idleMs);
310
416
  }
311
417
  /** Auto-end after the idle window: stamp the end, drive the panel to ENDED, stop the heartbeat. */
312
418
  #endIdle(idleMs) {
@@ -324,12 +430,12 @@ export class Presenter {
324
430
  this.#idleEndMs = Math.max(IDLE_END_MIN_MS, Math.floor(ms));
325
431
  }
326
432
  /**
327
- * The exported "run state" for the Copy/Export buttons — everything the page holds about this
433
+ * The exported "run state" for the Copy/Export buttons - everything the page holds about this
328
434
  * run: session id, url, duration, capability surface, per-kind counts, and the full activity log.
329
435
  * (The full network/console ring-buffer lives server-side; this is the in-page run summary.)
330
436
  */
331
437
  runState() {
332
- return buildRunState({
438
+ const base = buildRunState({
333
439
  sessionId: this.#sessionId,
334
440
  state: this.state,
335
441
  startMs: this.#startMs,
@@ -337,6 +443,45 @@ export class Presenter {
337
443
  now: this.#now(),
338
444
  runLog: this.#runLog,
339
445
  });
446
+ const settings = getPresenterSettings();
447
+ if (settings.outputDetail === OutputDetail.MINIMAL) {
448
+ return {
449
+ session: base.session,
450
+ url: base.url,
451
+ state: base.state,
452
+ startedMs: base.startedMs,
453
+ durationMs: base.durationMs,
454
+ counts: base.counts,
455
+ };
456
+ }
457
+ if (settings.outputDetail === OutputDetail.VERBOSE && settings.reactComponents) {
458
+ return { ...base, includeReactComponents: true };
459
+ }
460
+ return base;
461
+ }
462
+ #clearRunLog() {
463
+ this.#runLog = [];
464
+ this.#tallied = { passes: 0, fails: 0 };
465
+ if (this.#log !== undefined)
466
+ this.#log.replaceChildren();
467
+ this.#renderTally();
468
+ }
469
+ #applyHideUntilRestart() {
470
+ this.#root?.setAttribute('data-reticle-hidden', '1');
471
+ this.#shell.collapse();
472
+ }
473
+ #onSettingsChange(settings) {
474
+ this.#annotator?.setAccent(statusTheme(settings.statusThemeId).active);
475
+ if (!settings.showTally) {
476
+ this.#tally?.setAttribute('hidden', '');
477
+ }
478
+ else {
479
+ this.#renderTally();
480
+ }
481
+ if (this.#root !== undefined) {
482
+ const live = SessionState.ACTIVE === this.#panel.state && !this.#shell.isCollapsed();
483
+ syncPageBlocker(this.#root, settings, live);
484
+ }
340
485
  }
341
486
  /**
342
487
  * Append an activity-log row. Accumulates (never overwrites): each call adds a timestamped row
@@ -363,10 +508,9 @@ export class Presenter {
363
508
  const handle = appendLogRow(this.#log, kind, trimmed, ts, this.#logMax);
364
509
  if (result !== undefined)
365
510
  handle.result(result);
366
- // Feed the minimised-bar live line so the latest activity always shows when collapsed.
367
- if (this.#liveLine !== undefined)
368
- this.#liveLine.textContent = trimmed;
369
- this.#renderTally(); // a row that landed with a verdict updates the header score immediately
511
+ if (this.#shell.isCollapsed())
512
+ this.#shell.pulseFab(true);
513
+ this.#renderTally();
370
514
  // Wrap the handle so a later outcome stamp updates BOTH the DOM glyph and the run-log entry.
371
515
  return {
372
516
  result: (r) => {
@@ -378,6 +522,10 @@ export class Presenter {
378
522
  }
379
523
  /** Repaint the header verdict tally from the run log; the side that grew gets a one-shot pop. */
380
524
  #renderTally() {
525
+ if (!getPresenterSettings().showTally) {
526
+ this.#tally?.setAttribute('hidden', '');
527
+ return;
528
+ }
381
529
  this.#tallied = renderTally(this.#tally, this.#runLog, this.#tallied);
382
530
  }
383
531
  /** Back-compat: narration appends to the live log (append-only, never overwrites). */
@@ -394,14 +542,14 @@ export class Presenter {
394
542
  }
395
543
  /**
396
544
  * Mirror the server's session.throttled state onto the HUD border. When throttled (tab
397
- * backgrounded or stale), the border turns amber so the developer knows actions are no-oping —
545
+ * backgrounded or stale), the border turns amber so the developer knows actions are no-oping -
398
546
  * the same signal the agent already reads from result.session.throttled.
399
547
  */
400
548
  setThrottled(throttled) {
401
549
  this.#root?.setAttribute(THROTTLED_ATTR, throttled ? '1' : '0');
402
550
  if (throttled && this.#actLine !== undefined) {
403
551
  this.#actLine.textContent =
404
- 'Tab backgrounded — actions throttled. Bring tab to front or use `reticle drive`.';
552
+ 'Tab backgrounded - actions throttled. Bring tab to front or use `reticle drive`.';
405
553
  }
406
554
  }
407
555
  /** Fly the cursor to an element, play the action's effect, then pace for the human. */
package/dist/reticle.d.ts CHANGED
@@ -5,7 +5,7 @@ export type { ReticleConnectOptions };
5
5
  /**
6
6
  * Runtime backstop for the dev-only SDK: block connecting when the build reports production, unless
7
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
8
+ * in a raw browser). This is defense-in-depth - the primary guard is the consumer gating the import
9
9
  * behind `import.meta.env.DEV` so the SDK is dead-code-eliminated from prod bundles entirely.
10
10
  */
11
11
  export declare function shouldBlockProduction(nodeEnv: string | undefined, allowInProduction: boolean): boolean;
@@ -45,7 +45,7 @@ export declare function resolveConnectIdentity(options: {
45
45
  };
46
46
  /**
47
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
48
+ * injected, never read here - so it is unit-testable and honors the clock-injection rule. The
49
49
  * causing action's id is attributed server-side by the settle window, so it is not stamped here.
50
50
  */
51
51
  export declare function buildEvent(args: {
package/dist/reticle.js CHANGED
@@ -8,6 +8,7 @@ import { registerCapabilities, setCapabilitiesListener, hasCapabilities, } from
8
8
  import { installAllObservers } from './observers/install-all.js';
9
9
  import { installOverlay } from './presenter/overlay.js';
10
10
  import { Presenter, LOG_KIND, LOG_RESULT, } from './presenter/presenter.js';
11
+ import { getPresenterSettings } from './presenter/presenter-settings.js';
11
12
  import { actionVerb } from './presenter/presenter-verbs.js';
12
13
  import { str, refLabel, modeForCommand, presentStatus } from './reticle-presenter-helpers.js';
13
14
  import { resetClock } from './timers/clock.js';
@@ -17,7 +18,7 @@ import { Annotator } from './review/annotator.js';
17
18
  /**
18
19
  * Runtime backstop for the dev-only SDK: block connecting when the build reports production, unless
19
20
  * explicitly overridden. Pure so it's testable; connect reads NODE_ENV safely (process may be absent
20
- * in a raw browser). This is defense-in-depth — the primary guard is the consumer gating the import
21
+ * in a raw browser). This is defense-in-depth - the primary guard is the consumer gating the import
21
22
  * behind `import.meta.env.DEV` so the SDK is dead-code-eliminated from prod bundles entirely.
22
23
  */
23
24
  export function shouldBlockProduction(nodeEnv, allowInProduction) {
@@ -61,7 +62,7 @@ pageProtocol = 'http:') {
61
62
  return { allowed: true };
62
63
  }
63
64
  /** HUD summary when the SDK self-ends a session because the bridge (server/agent) became unreachable. */
64
- const BRIDGE_LOST_SUMMARY = 'Session ended — lost connection to Reticle (the agent is no longer running).';
65
+ const BRIDGE_LOST_SUMMARY = 'Session ended - lost connection to Reticle (the agent is no longer running).';
65
66
  /**
66
67
  * Resolve the session label. An absent label or the `auto` sentinel yields a fresh per-tab id (via
67
68
  * the injected generator) so multi-tab / new-tab routes never collide; any other label is used
@@ -82,7 +83,7 @@ function stripReloadCacheBustParam() {
82
83
  window.history.replaceState(window.history.state, '', current.toString());
83
84
  }
84
85
  catch {
85
- /* best-effort URL hygiene — never block connect() on it */
86
+ /* best-effort URL hygiene - never block connect() on it */
86
87
  }
87
88
  }
88
89
  /**
@@ -117,7 +118,7 @@ export function resolveConnectIdentity(options, search) {
117
118
  }
118
119
  /**
119
120
  * Assemble an event envelope. Pure: `seq` (monotonic per session) and `t` (elapsed clock) are
120
- * injected, never read here — so it is unit-testable and honors the clock-injection rule. The
121
+ * injected, never read here - so it is unit-testable and honors the clock-injection rule. The
121
122
  * causing action's id is attributed server-side by the settle window, so it is not stamped here.
122
123
  */
123
124
  export function buildEvent(args) {
@@ -168,7 +169,7 @@ export class Reticle {
168
169
  return;
169
170
  }
170
171
  // A `hard` REFRESH left a cache-busting `_reticle_reload=<nonce>` in the address bar. Strip it now
171
- // (before the route observer installs, so it emits no spurious ROUTE_CHANGE) — otherwise every hard
172
+ // (before the route observer installs, so it emits no spurious ROUTE_CHANGE) - otherwise every hard
172
173
  // reload permanently pollutes the URL the app and the agent see.
173
174
  stripReloadCacheBustParam();
174
175
  const url = options.url ?? bridgeWsUrl(RETICLE_DEFAULT_PORT);
@@ -178,7 +179,7 @@ export class Reticle {
178
179
  return;
179
180
  }
180
181
  // Publish the project root for the adapters' source relativisation. Done HERE, in library code,
181
- // so the app-side connect stays plain JavaScript — see ReticleConnectOptions.root.
182
+ // so the app-side connect stays plain JavaScript - see ReticleConnectOptions.root.
182
183
  if (options.root !== undefined && options.root.length > 0) {
183
184
  globalThis[RETICLE_ROOT_GLOBAL] = options.root;
184
185
  }
@@ -193,7 +194,7 @@ export class Reticle {
193
194
  // options win, but the `auto` sentinel defers to the URL param so leases correlate.
194
195
  const identity = resolveConnectIdentity(options, window.location.search);
195
196
  // Remembered per TAB, so a reload rejoins the same session instead of appearing as a new one and
196
- // stranding the agent's handle — see session-continuity. An explicit id (connect option, or the
197
+ // stranding the agent's handle - see session-continuity. An explicit id (connect option, or the
197
198
  // lease's URL param) still wins.
198
199
  const explicitSession = resolveSessionLabel(identity.session, () => '');
199
200
  this.#session = rememberSessionLabel(explicitSession.length > 0 ? explicitSession : undefined, 'undefined' === typeof globalThis.sessionStorage ? undefined : globalThis.sessionStorage, () => 'function' === typeof globalThis.crypto?.randomUUID
@@ -212,18 +213,18 @@ export class Reticle {
212
213
  url,
213
214
  hello: () => this.#hello(),
214
215
  handleCommand: (command) => this.#handleCommand(command),
215
- // Show the presenter HUD as soon as the agent bridge connects — the user immediately sees
216
+ // Show the presenter HUD as soon as the agent bridge connects - the user immediately sees
216
217
  // the glow border and narration panel, even before the first tool call lands.
217
218
  onConnected: () => this.#presenter?.sessionStart(),
218
219
  // Liveness fallback: if the bridge stays unreachable (the agent killed the server process),
219
- // no server-pushed end can arrive — so end the run we're presenting ourselves. A returning
220
+ // no server-pushed end can arrive - so end the run we're presenting ourselves. A returning
220
221
  // agent revives it via the normal sessionStart path on its next command.
221
222
  onConnectionLost: () => {
222
223
  // Restore real timers. A frozen clock is driven by the agent through the bridge; once the
223
224
  // bridge is gone nothing can ever advance it, so leaving it installed pins Date.now and
224
225
  // queues every setTimeout into a scheduler that will never run. Concretely that kills every
225
226
  // lodash debounce/throttle in the app (now - lastCall stays 0), so search boxes, autosave
226
- // and resize handlers stop firing until a reload — with nothing on screen explaining why.
227
+ // and resize handlers stop firing until a reload - with nothing on screen explaining why.
227
228
  resetClock();
228
229
  if (true === this.#presenter?.sessionActive) {
229
230
  this.#presenter.setState(SessionState.ENDED, BRIDGE_LOST_SUMMARY);
@@ -269,23 +270,24 @@ export class Reticle {
269
270
  : { kind: intent.kind });
270
271
  this.#presenter = new Presenter(presenterOptions);
271
272
  // Mount the overlay. The session (glow + HUD) activates on bridge connect via onConnected,
272
- // so the presenter is visible as soon as the agent is reachable — not just on first command.
273
+ // so the presenter is visible as soon as the agent is reachable - not just on first command.
273
274
  this.#presenter.mount();
274
275
  }
275
276
  if (true === options.recorder) {
276
277
  this.#recorder = installRecorder({ emit, now: () => Date.now() });
277
278
  this.#recorder.mount();
278
279
  }
279
- // The "Flag a bug" annotator rides with the presenter (the human surface) unless explicitly off.
280
+ // The page annotator rides with the presenter (the human surface) unless explicitly off.
280
281
  if (options.annotate ?? options.present !== false) {
281
282
  const presenter = this.#presenter;
282
283
  this.#annotator = new Annotator({
283
284
  emit,
284
285
  now: () => Date.now(),
285
- // Echo the flag into the live panel so the human watches their bug report land in the log.
286
- onMark: (note, label) => presenter?.log(LOG_KIND.HUMAN, `🚩 ${label}: ${note}`),
286
+ onMark: (mark) => presenter?.log(LOG_KIND.HUMAN, `🚩 #${String(mark.index)} ${mark.anchor}${mark.source !== undefined ? ` · ${mark.source}` : ''} — ${mark.note}`),
287
+ shouldBlock: () => getPresenterSettings().blockPageInteractions,
287
288
  });
288
289
  this.#annotator.mount();
290
+ this.#presenter?.bindAnnotator(this.#annotator);
289
291
  }
290
292
  this.#transport.connect();
291
293
  this.#connected = true;
@@ -313,7 +315,7 @@ export class Reticle {
313
315
  }
314
316
  /** Advertise the app's testable surface so the agent learns it without reading source. */
315
317
  describe(input) {
316
- registerCapabilities(input); // the registry notifies the transport — see setCapabilitiesListener
318
+ registerCapabilities(input); // the registry notifies the transport - see setCapabilitiesListener
317
319
  }
318
320
  /** Live-control: end the session programmatically from the host app (drives the panel to ended). */
319
321
  endSession() {
@@ -349,7 +351,7 @@ export class Reticle {
349
351
  ref,
350
352
  });
351
353
  // Guarded because #emit runs INLINE IN THE APP'S CALL STACK: every monkey-patch calls it from
352
- // inside the function it replaced. An exception here does not surface as an SDK error — it
354
+ // inside the function it replaced. An exception here does not surface as an SDK error - it
353
355
  // propagates out of history.pushState (crashing a router's navigate), out of localStorage.setItem
354
356
  // after the write already succeeded, or out of console.log before the message reaches the console.
355
357
  // A dev-only observability SDK must never be able to break the app it is observing.
@@ -367,7 +369,7 @@ export class Reticle {
367
369
  };
368
370
  /**
369
371
  * Resolve the app's redaction config into the ambient policy, and remember the part of it that
370
- * travels. A config of `undefined` is not "no policy" — it is the DEFAULT policy, installed
372
+ * travels. A config of `undefined` is not "no policy" - it is the DEFAULT policy, installed
371
373
  * explicitly so a second connect() in the same page (HMR, a re-mount) cannot inherit a rule the
372
374
  * previous one set.
373
375
  */
@@ -386,7 +388,7 @@ export class Reticle {
386
388
  adapters: adapterNames(),
387
389
  ...(this.#token === undefined ? {} : { token: this.#token }),
388
390
  hasCapabilities: hasCapabilities(),
389
- // Absent when no build plugin supplied one — "unknown", never "matching".
391
+ // Absent when no build plugin supplied one - "unknown", never "matching".
390
392
  ...(this.#sdkVersion === undefined ? {} : { sdkVersion: this.#sdkVersion }),
391
393
  // Always present: derived from THIS build's core, so it needs no build plugin to supply it.
392
394
  // It is the half of the skew check that works on a hand-wired connect.
@@ -411,7 +413,9 @@ export class Reticle {
411
413
  // Bridge → browser presenter pushes (PRESENTER state echo / FLOWS replay list). The presenter owns
412
414
  // the parsing; here we only report whether a panel was mounted to apply it. setState-only, so a
413
415
  // PRESENTER echo of a HUMAN_CONTROL can't loop back into a re-emit.
414
- if (command.name === ReticleCommand.PRESENTER || command.name === ReticleCommand.FLOWS) {
416
+ if (command.name === ReticleCommand.PRESENTER ||
417
+ command.name === ReticleCommand.FLOWS ||
418
+ command.name === ReticleCommand.IMPACT) {
415
419
  this.#presenter?.handlePush(command);
416
420
  return { ok: true, result: { applied: this.#presenter !== undefined } };
417
421
  }
@@ -0,0 +1,6 @@
1
+ export declare const MARK_PLACEHOLDER = "What should change?";
2
+ export declare const MARK_SUBMIT = "Add";
3
+ export declare const MARK_CANCEL = "Cancel";
4
+ export declare const MARK_PENDING_GLYPH = "+";
5
+ export declare const ANNOTATOR_CSS: string;
6
+ export declare const ANNOTATOR_ROOT_HTML = "<div data-reticle-mark=\"hi\"><span data-reticle-mark=\"hilabel\"></span></div><div data-reticle-mark=\"sel\" hidden></div>";