@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.
- package/NOTICE +7 -0
- package/dist/actions/actions-dom.js +23 -15
- package/dist/actions/actions.d.ts +17 -0
- package/dist/actions/actions.js +132 -29
- package/dist/actions/synthetic-input.d.ts +9 -0
- package/dist/actions/synthetic-input.js +40 -0
- package/dist/commands/commands.js +11 -1
- package/dist/connect-options.d.ts +9 -9
- package/dist/dom/a11y.d.ts +9 -0
- package/dist/dom/a11y.js +19 -0
- package/dist/dom/dom-ignore.d.ts +19 -6
- package/dist/dom/dom-ignore.js +20 -13
- package/dist/dom/query.d.ts +2 -2
- package/dist/dom/query.js +85 -45
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +34 -0
- package/dist/dom/source.d.ts +14 -0
- package/dist/dom/source.js +16 -0
- package/dist/observers/install-all.js +4 -1
- package/dist/observers/ipc.d.ts +16 -0
- package/dist/observers/ipc.js +19 -1
- package/dist/observers/network.d.ts +8 -0
- package/dist/observers/network.js +24 -3
- package/dist/observers/state.js +21 -2
- package/dist/presenter/presenter-brand.d.ts +24 -7
- package/dist/presenter/presenter-brand.js +23 -6
- package/dist/presenter/presenter-config.d.ts +102 -9
- package/dist/presenter/presenter-config.js +94 -8
- package/dist/presenter/presenter-controls.d.ts +10 -10
- package/dist/presenter/presenter-controls.js +175 -85
- package/dist/presenter/presenter-dock-layout.d.ts +6 -0
- package/dist/presenter/presenter-dock-layout.js +170 -0
- package/dist/presenter/presenter-drag.d.ts +48 -0
- package/dist/presenter/presenter-drag.js +198 -0
- package/dist/presenter/presenter-effects.d.ts +1 -1
- package/dist/presenter/presenter-effects.js +1 -1
- package/dist/presenter/presenter-glow.d.ts +5 -5
- package/dist/presenter/presenter-glow.js +7 -7
- package/dist/presenter/presenter-heroicons-data.d.ts +41 -0
- package/dist/presenter/presenter-heroicons-data.js +38 -0
- package/dist/presenter/presenter-hud-chrome.d.ts +17 -0
- package/dist/presenter/presenter-hud-chrome.js +41 -0
- package/dist/presenter/presenter-icons.d.ts +44 -0
- package/dist/presenter/presenter-icons.js +100 -0
- package/dist/presenter/presenter-log.d.ts +27 -5
- package/dist/presenter/presenter-log.js +114 -28
- package/dist/presenter/presenter-report-copy.d.ts +83 -0
- package/dist/presenter/presenter-report-copy.js +127 -0
- package/dist/presenter/presenter-report-styles.d.ts +7 -0
- package/dist/presenter/presenter-report-styles.js +102 -0
- package/dist/presenter/presenter-report.d.ts +23 -0
- package/dist/presenter/presenter-report.js +217 -0
- package/dist/presenter/presenter-run-state.d.ts +1 -1
- package/dist/presenter/presenter-settings-styles.d.ts +2 -0
- package/dist/presenter/presenter-settings-styles.js +153 -0
- package/dist/presenter/presenter-settings.d.ts +82 -0
- package/dist/presenter/presenter-settings.js +494 -0
- package/dist/presenter/presenter-shell-styles.d.ts +1 -0
- package/dist/presenter/presenter-shell-styles.js +375 -0
- package/dist/presenter/presenter-shell.d.ts +48 -0
- package/dist/presenter/presenter-shell.js +408 -0
- package/dist/presenter/presenter-styles.d.ts +9 -4
- package/dist/presenter/presenter-styles.js +61 -125
- package/dist/presenter/presenter-tally.d.ts +8 -11
- package/dist/presenter/presenter-tally.js +48 -23
- package/dist/presenter/presenter-test-helpers.d.ts +1 -1
- package/dist/presenter/presenter-test-helpers.js +1 -1
- package/dist/presenter/presenter-verbs.js +2 -2
- package/dist/presenter/presenter-workspace.d.ts +15 -0
- package/dist/presenter/presenter-workspace.js +180 -0
- package/dist/presenter/presenter.d.ts +15 -10
- package/dist/presenter/presenter.js +194 -46
- package/dist/reticle.d.ts +2 -2
- package/dist/reticle.js +23 -19
- package/dist/review/annotator-styles.d.ts +6 -0
- package/dist/review/annotator-styles.js +81 -0
- package/dist/review/annotator.d.ts +29 -17
- package/dist/review/annotator.js +477 -146
- 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,
|
|
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 {
|
|
8
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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.#
|
|
159
|
-
|
|
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
|
|
266
|
+
/** Turn the base border (session mode) + the HUD/log on - the visible "session is live" state. */
|
|
207
267
|
#showSession() {
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
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"
|
|
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
|
|
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)
|
|
308
|
-
const since = this.#lastActionText !== '' ?
|
|
309
|
-
this.#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
286
|
-
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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 ||
|
|
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>";
|