@reticlehq/browser 3.5.0 → 3.6.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 (87) hide show
  1. package/dist/actions/actions-dom.js +44 -43
  2. package/dist/actions/actions-press.d.ts +10 -4
  3. package/dist/actions/actions-press.js +108 -17
  4. package/dist/actions/actions.d.ts +2 -0
  5. package/dist/actions/actions.js +25 -16
  6. package/dist/actions/danger-context.js +1 -1
  7. package/dist/connect-options.d.ts +1 -0
  8. package/dist/dom/a11y.js +13 -4
  9. package/dist/dom/addressing/refs.js +2 -1
  10. package/dist/dom/addressing/testid-attr.d.ts +4 -0
  11. package/dist/dom/addressing/testid-attr.js +14 -0
  12. package/dist/dom/query.d.ts +1 -0
  13. package/dist/dom/query.js +38 -11
  14. package/dist/dom/snapshot.js +1 -0
  15. package/dist/observers/departure.js +51 -9
  16. package/dist/observers/dom.js +3 -2
  17. package/dist/observers/field.js +5 -3
  18. package/dist/observers/health.js +63 -2
  19. package/dist/observers/network-stream.d.ts +1 -1
  20. package/dist/observers/network-stream.js +3 -3
  21. package/dist/observers/network.js +1 -1
  22. package/dist/observers/storage.js +6 -1
  23. package/dist/presenter/carousel/carousel.d.ts +5 -2
  24. package/dist/presenter/carousel/carousel.js +34 -7
  25. package/dist/presenter/carousel/offer-card.d.ts +2 -2
  26. package/dist/presenter/carousel/offer-card.js +2 -1
  27. package/dist/presenter/carousel/panel-slides.d.ts +4 -3
  28. package/dist/presenter/carousel/panel-slides.js +43 -6
  29. package/dist/presenter/carousel/talk-card.d.ts +1 -1
  30. package/dist/presenter/chrome/log-kinds.d.ts +5 -0
  31. package/dist/presenter/chrome/log-kinds.js +1 -0
  32. package/dist/presenter/chrome/log-memory.d.ts +12 -0
  33. package/dist/presenter/chrome/log-memory.js +39 -0
  34. package/dist/presenter/chrome/presenter-hud-chrome.d.ts +1 -0
  35. package/dist/presenter/chrome/presenter-hud-chrome.js +7 -1
  36. package/dist/presenter/chrome/presenter-log.d.ts +5 -4
  37. package/dist/presenter/chrome/presenter-log.js +55 -5
  38. package/dist/presenter/chrome/presenter-report-copy.d.ts +11 -1
  39. package/dist/presenter/chrome/presenter-report-copy.js +14 -1
  40. package/dist/presenter/hud-telemetry.js +31 -2
  41. package/dist/presenter/icons/presenter-heroicons-data.d.ts +1 -0
  42. package/dist/presenter/icons/presenter-heroicons-data.js +4 -3
  43. package/dist/presenter/icons/presenter-icons.d.ts +2 -0
  44. package/dist/presenter/icons/presenter-icons.js +2 -0
  45. package/dist/presenter/presenter-account.d.ts +5 -4
  46. package/dist/presenter/presenter-account.js +23 -10
  47. package/dist/presenter/presenter-chat-views.d.ts +21 -0
  48. package/dist/presenter/presenter-chat-views.js +382 -0
  49. package/dist/presenter/presenter-config.d.ts +3 -2
  50. package/dist/presenter/presenter-config.js +4 -3
  51. package/dist/presenter/presenter-controls.d.ts +4 -2
  52. package/dist/presenter/presenter-controls.js +231 -51
  53. package/dist/presenter/presenter-design-tokens.d.ts +1 -0
  54. package/dist/presenter/presenter-design-tokens.js +34 -0
  55. package/dist/presenter/presenter-dock-layout.js +1 -1
  56. package/dist/presenter/presenter-frame.d.ts +12 -0
  57. package/dist/presenter/presenter-frame.js +189 -0
  58. package/dist/presenter/presenter-plan.d.ts +10 -0
  59. package/dist/presenter/presenter-plan.js +185 -0
  60. package/dist/presenter/presenter-report-styles.js +7 -0
  61. package/dist/presenter/presenter-report.d.ts +1 -1
  62. package/dist/presenter/presenter-report.js +53 -16
  63. package/dist/presenter/presenter-settings-styles.js +10 -0
  64. package/dist/presenter/presenter-settings.d.ts +5 -0
  65. package/dist/presenter/presenter-settings.js +71 -11
  66. package/dist/presenter/presenter-shell-styles.js +39 -42
  67. package/dist/presenter/presenter-shell.d.ts +8 -1
  68. package/dist/presenter/presenter-shell.js +124 -72
  69. package/dist/presenter/presenter-styles.js +9 -1
  70. package/dist/presenter/presenter.d.ts +4 -3
  71. package/dist/presenter/presenter.js +101 -13
  72. package/dist/presenter/tour/tour.d.ts +4 -1
  73. package/dist/presenter/tour/tour.js +6 -4
  74. package/dist/recorder/recorder.js +2 -2
  75. package/dist/registry/auto-testids.js +3 -4
  76. package/dist/registry/declared-platform.js +2 -0
  77. package/dist/reticle-inject.js +880 -513
  78. package/dist/reticle.d.ts +10 -1
  79. package/dist/reticle.js +73 -9
  80. package/dist/review/annotator-styles.d.ts +1 -1
  81. package/dist/review/annotator.d.ts +7 -0
  82. package/dist/review/annotator.js +20 -2
  83. package/dist/review/mark-anchor.js +2 -2
  84. package/dist/session-continuity.js +2 -1
  85. package/dist/storage-keys.d.ts +14 -0
  86. package/dist/storage-keys.js +17 -0
  87. package/package.json +3 -2
@@ -1,9 +1,12 @@
1
- import { HumanControlKind, PresenterTone, SessionState } from '@reticlehq/core';
1
+ import { readTestId, testIdSelector } from '../dom/addressing/testid-attr.js';
2
+ import { FlowProgressStatus, HumanControlKind, PresenterTone, SessionState, } from '@reticlehq/core';
2
3
  import { nativeSetTimeout, nativeClearTimeout } from '../timers/native/native-timers.js';
3
- import { CHAT_TOGGLE_ATTR, CLEAR_MARKS_ATTR, COPY_MARKS_ATTR, MARKERS_BTN_ATTR, MARKS_ROW_ATTR, } from './presenter-config.js';
4
- import { PresenterIcon, PRESENTER_ICON_SIZE, hiIcon, hiIconHtml, hiToggleIconHtml, } from './icons/presenter-icons.js';
4
+ import { COPY_MARKS_ATTR, MARKS_ROW_ATTR } from './presenter-config.js';
5
+ import { PresenterIcon, PRESENTER_ICON_SIZE, hiIcon, hiIconHtml } from './icons/presenter-icons.js';
5
6
  import { getPresenterSettings } from './presenter-settings.js';
6
7
  import { mountWorkspaceSelector, workspaceRowHtml } from './presenter-workspace.js';
8
+ import { CHAT_VIEWS_HTML } from './presenter-chat-views.js';
9
+ import { ReticleStorageKey } from '../storage-keys.js';
7
10
  const DATA_RETICLE_STATE = 'data-reticle-state';
8
11
  const DATA_RETICLE_TONE = 'data-reticle-tone';
9
12
  const DATA_ON = 'data-on';
@@ -19,14 +22,43 @@ const ENDED_BANNER_TEXT = 'Session ended';
19
22
  const COPY_LABEL = 'Copy run';
20
23
  const EXPORT_LABEL = 'Export';
21
24
  const FLOWS_LABEL = 'Replay a flow';
25
+ const FLOWS_ALL_LABEL = 'See all';
26
+ const SEE_LOGS_LABEL = 'See the logs';
27
+ const PROGRESS_LINGER_MS = 6000;
28
+ const PROGRESS_GLYPH = {
29
+ [FlowProgressStatus.PLAYING]: '⏵',
30
+ [FlowProgressStatus.PASSED]: '✓',
31
+ [FlowProgressStatus.FAILED]: '✗',
32
+ };
33
+ function parseFlowProgress(args) {
34
+ const { name, done, total, status } = args;
35
+ if ('string' !== typeof name || 'number' !== typeof done || 'number' !== typeof total)
36
+ return undefined;
37
+ if (!Object.values(FlowProgressStatus).includes(status))
38
+ return undefined;
39
+ return { name, done, total, status: status };
40
+ }
22
41
  const COPIED_TEXT = 'Copied ✓';
23
42
  const RUN_FILENAME = 'reticle-run.json';
24
43
  export const ENDED_FADE_MS = 4000;
44
+ const RECENT_PLAYS_KEY = ReticleStorageKey.RECENT_PLAYS;
45
+ const FLOW_TEXT = {
46
+ REPLAYABLE: 'Replay without an agent',
47
+ ELSEWHERE: 'Starts on another page',
48
+ EMPTY_TITLE: 'No saved flows yet',
49
+ EMPTY_HINT: 'Every journey your agent verifies is saved here, ready to replay in one click.',
50
+ };
51
+ export function flowTitle(name) {
52
+ const words = name.replace(/[-_]+/g, ' ').trim();
53
+ return 0 === words.length ? name : words.charAt(0).toUpperCase() + words.slice(1);
54
+ }
25
55
  export const CONTROLS_CSS = `
26
56
  [data-reticle-chat-panel] [data-reticle-foot]{flex:none;padding:8px 10px 10px;border-top:1px solid rgba(255,255,255,.07);
27
57
  background:linear-gradient(180deg,rgba(0,0,0,.35) 0%,rgba(0,0,0,.55) 100%);pointer-events:auto;}
58
+ [data-reticle-chat-panel] .reticle-foot-workspace-row{display:flex;align-items:center;min-width:0;}
28
59
  [data-reticle-chat-panel] .reticle-hud-log-well{margin:0 0 4px;}
29
60
  [data-reticle-chat-panel] .reticle-workspace-wrap{position:relative;align-self:flex-start;max-width:100%;}
61
+ [data-reticle-chat-panel] .reticle-workspace-wrap[hidden]{display:none;}
30
62
  [data-reticle-chat-panel] .reticle-workspace{
31
63
  display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:3px 8px 3px 6px;border-radius:999px;cursor:pointer;
32
64
  border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.04);color:rgba(255,255,255,.78);
@@ -37,6 +69,10 @@ export const CONTROLS_CSS = `
37
69
  [data-reticle-chat-panel] .reticle-workspace-icon,
38
70
  [data-reticle-chat-panel] .reticle-workspace-caret{display:inline-flex;align-items:center;opacity:.72;flex:none;}
39
71
  [data-reticle-chat-panel] .reticle-workspace-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:min(200px,calc(100vw - 120px));}
72
+ @media (max-width:480px){
73
+ [data-reticle-chat-panel] .reticle-workspace-wrap{flex:none;}
74
+ [data-reticle-chat-panel] .reticle-workspace-name{max-width:88px;}
75
+ }
40
76
  [data-reticle-chat-panel] .reticle-workspace[aria-expanded="true"] .reticle-workspace-caret{transform:rotate(180deg);}
41
77
  [data-reticle-chat-panel] .reticle-workspace-caret{transition:transform .18s ease;}
42
78
  [data-reticle-chat-panel] .reticle-workspace-menu{
@@ -65,9 +101,9 @@ export const CONTROLS_CSS = `
65
101
  [data-reticle-chat-panel] .reticle-workspace-menu-row{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:4px 0;}
66
102
  [data-reticle-chat-panel] .reticle-workspace-menu-k{flex:none;color:rgba(255,255,255,.42);}
67
103
  [data-reticle-chat-panel] .reticle-workspace-menu-v{min-width:0;text-align:right;color:rgba(255,255,255,.9);font-weight:500;word-break:break-all;}
68
- [data-reticle-chat-panel] .reticle-banner{display:none;flex:none;align-items:center;gap:8px;padding:10px 14px;color:var(--reticle-fg);
69
- font-size:12px;font-weight:500;border-bottom:1px solid var(--reticle-line2);background:var(--reticle-surface);}
70
- [data-reticle-overlay][data-reticle-state="ended"] [data-reticle-chat-panel] .reticle-banner{display:block;}
104
+ [data-reticle-chat-panel] .reticle-banner{display:none;flex:none;align-items:center;gap:8px;min-height:38px;box-sizing:border-box;padding:8px 14px;
105
+ color:var(--reticle-hud-text);font-size:12px;font-weight:600;border-bottom:1px solid var(--reticle-hud-border);background:var(--reticle-hud-inset);line-height:1.35;}
106
+ [data-reticle-overlay][data-reticle-state="ended"] [data-reticle-chat-panel] .reticle-banner{display:flex;}
71
107
  [data-reticle-overlay][data-reticle-state="paused"] [data-reticle-glow][data-on="1"]{
72
108
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.2);}
73
109
  [data-reticle-overlay][data-reticle-state="ended"] [data-reticle-glow][data-on="1"]{
@@ -87,13 +123,30 @@ export const CONTROLS_CSS = `
87
123
  [data-reticle-overlay][data-reticle-tone="ask"] [data-reticle-glow][data-on="1"],
88
124
  [data-reticle-overlay][data-reticle-tone="warn"] [data-reticle-glow][data-on="1"]{
89
125
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.22);}
90
- [data-reticle-chat-panel] .reticle-flows{display:none;flex:none;flex-wrap:wrap;align-content:flex-start;gap:6px;
91
- padding:9px 12px;border-top:1px solid var(--reticle-line2);max-height:88px;overflow-y:auto;overscroll-behavior:contain;
92
- pointer-events:auto;touch-action:pan-y;}
93
- [data-reticle-chat-panel] .reticle-flows[data-has="1"]{display:flex;}
94
- [data-reticle-chat-panel] .reticle-flows::-webkit-scrollbar{width:9px;}
95
- [data-reticle-chat-panel] .reticle-flows::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:9px;border:2px solid transparent;background-clip:content-box;}
96
- [data-reticle-chat-panel] .reticle-flows-cap{flex:0 0 100%;margin-bottom:1px;color:var(--reticle-faint);font-size:9.5px;letter-spacing:.09em;text-transform:uppercase;}
126
+ [data-reticle-chat-panel] .reticle-flows{display:none;flex:none;min-width:0;padding:8px 12px;border-top:1px solid var(--reticle-line2);pointer-events:auto;}
127
+ [data-reticle-chat-panel] .reticle-flows[data-has="1"]{display:block;}
128
+ [data-reticle-chat-panel] .reticle-flows-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-bottom:5px;}
129
+ [data-reticle-chat-panel] .reticle-flows-all{border:0;padding:0;background:none;cursor:pointer;color:var(--reticle-c-active);font:inherit;font-size:10.5px;font-weight:500;}
130
+ [data-reticle-chat-panel] .reticle-flows-all:hover{text-decoration:underline;}
131
+ [data-reticle-chat-panel] .reticle-flows-links{display:inline-flex;gap:10px;}
132
+ [data-reticle-chat-panel] .reticle-flows-all[hidden]{display:none;}
133
+ /* A replay the person started: the chip becomes a player, its bar filling step by step. */
134
+ [data-reticle-chat-panel] :is(.reticle-flow,.reticle-flow-row)[data-state]{position:relative;overflow:hidden;
135
+ border-color:color-mix(in srgb,var(--reticle-c-active) 55%,transparent);color:var(--reticle-fg);}
136
+ [data-reticle-chat-panel] :is(.reticle-flow,.reticle-flow-row)[data-state]::after{content:"";position:absolute;left:0;bottom:0;height:2px;
137
+ width:var(--reticle-flow-progress,4%);background:var(--reticle-c-active);transition:width .25s var(--reticle-hud-ease,ease);}
138
+ [data-reticle-chat-panel] :is(.reticle-flow,.reticle-flow-row)[data-state="playing"]::after{animation:reticle-flow-pulse 1.2s ease-in-out infinite;}
139
+ @keyframes reticle-flow-pulse{50%{opacity:.55}}
140
+ [data-reticle-chat-panel] :is(.reticle-flow,.reticle-flow-row)[data-state="passed"]::after{background:#4ade80;}
141
+ [data-reticle-chat-panel] :is(.reticle-flow,.reticle-flow-row)[data-state="failed"]::after{background:#f87171;}
142
+ [data-reticle-chat-panel] .reticle-flows-cap{display:block;color:var(--reticle-faint);font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;}
143
+ [data-reticle-chat-panel] .reticle-flow-strip{display:flex;gap:6px;min-width:0;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;overscroll-behavior-inline:contain;pointer-events:auto;}
144
+ [data-reticle-chat-panel] .reticle-flow-strip::-webkit-scrollbar{display:none;}
145
+ [data-reticle-chat-panel] .reticle-flow-strip .reticle-flow{flex:none;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
146
+ [data-reticle-chat-panel] .reticle-all-flows{flex:1;min-height:0;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column;align-items:stretch;gap:5px;}
147
+ [data-reticle-chat-panel] .reticle-all-flows .reticle-flow{width:100%;height:auto;min-height:32px;text-align:left;justify-content:flex-start;}
148
+ [data-reticle-chat-panel] .reticle-all-flows .reticle-flow:disabled{opacity:.45;cursor:not-allowed;}
149
+ [data-reticle-chat-panel] .reticle-flows-empty{padding:25px 8px;color:var(--reticle-faint);font-size:11px;text-align:center;}
97
150
  [data-reticle-chat-panel] .reticle-flow{pointer-events:auto;cursor:pointer;display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 10px;
98
151
  border-radius:7px;border:1px solid var(--reticle-line);background:rgba(255,255,255,.04);color:var(--reticle-muted);
99
152
  font-family:var(--reticle-font);font-size:11px;font-weight:500;transition:background .15s,color .15s,border-color .15s,transform .1s;}
@@ -122,35 +175,13 @@ function paintPauseBtn(btn, paused) {
122
175
  btn.classList.toggle('reticle-tb-btn--primary', paused);
123
176
  btn.setAttribute('data-active', paused ? '1' : '0');
124
177
  }
125
- const CHAT_LABEL = 'Agent chat';
126
- const MARKERS_LABEL = 'Hide markers';
127
- const CLEAR_MARKS_LABEL = 'Clear all';
128
178
  const COPY_MARKS_LABEL = 'Copy for your agent';
129
179
  const TB = PRESENTER_ICON_SIZE.TOOLBAR;
130
- function tbWrap(btn, tip, kbd) {
131
- const shortcut = kbd === undefined ? '' : `<span class="reticle-tb-kbd">${kbd}</span>`;
132
- return `<div class="reticle-tb-wrap">${btn}<span class="reticle-tb-tip">${tip}${shortcut}</span></div>`;
133
- }
134
- function pauseToolbarHtml() {
135
- const btn = `<button type="button" data-reticle-pause class="reticle-tb-btn" title="${CONTROL_LABEL.PAUSE}" aria-label="${CONTROL_LABEL.PAUSE}">${hiIconHtml(PresenterIcon.PAUSE, TB)}</button>`;
136
- const badge = `<span data-reticle-badge class="reticle-pause-badge">${PAUSED_BADGE_TEXT}</span>`;
137
- const tip = `<span class="reticle-tb-tip">${CONTROL_LABEL.PAUSE}</span>`;
138
- return `<div class="reticle-tb-wrap reticle-tb-wrap--pause">${btn}${badge}${tip}</div>`;
139
- }
140
- export const CONTROLS_TOOLBAR_HTML = [
141
- pauseToolbarHtml(),
142
- tbWrap(`<button type="button" ${CHAT_TOGGLE_ATTR} class="reticle-tb-btn reticle-tb-btn--toggle" title="${CHAT_LABEL}" aria-label="${CHAT_LABEL}" aria-pressed="false" data-active="0">${hiToggleIconHtml(PresenterIcon.MESSAGE, TB)}</button>`, CHAT_LABEL),
143
- tbWrap(`<button type="button" ${MARKERS_BTN_ATTR} class="reticle-tb-btn reticle-tb-btn--toggle" title="${MARKERS_LABEL}" aria-label="${MARKERS_LABEL}" aria-pressed="false" data-active="0" disabled>${hiToggleIconHtml(PresenterIcon.VIEW, TB)}</button>`, MARKERS_LABEL, 'H'),
144
- tbWrap(`<button type="button" data-reticle-end class="reticle-tb-btn" title="${CONTROL_LABEL.END}" aria-label="${CONTROL_LABEL.END}">${hiIconHtml(PresenterIcon.STOP, TB)}</button>`, CONTROL_LABEL.END),
145
- tbWrap(`<button type="button" ${CLEAR_MARKS_ATTR} class="reticle-tb-btn" title="${CLEAR_MARKS_LABEL}" aria-label="${CLEAR_MARKS_LABEL}" data-danger disabled>${hiIconHtml(PresenterIcon.TRASH, TB)}</button>`, CLEAR_MARKS_LABEL, 'X'),
146
- `<button type="button" data-reticle-copy class="reticle-tb-btn reticle-tb-btn--export" title="${COPY_LABEL}" aria-label="${COPY_LABEL}" hidden>${hiIconHtml(PresenterIcon.COPY, TB)}</button>`,
147
- `<button type="button" data-reticle-export class="reticle-tb-btn reticle-tb-btn--export" title="${EXPORT_LABEL}" aria-label="${EXPORT_LABEL}" hidden>${hiIconHtml(PresenterIcon.DOWNLOAD, TB)}</button>`,
148
- '<span data-reticle-export-msg class="reticle-export-msg" aria-live="polite"></span>',
149
- ].join('');
180
+ export const CONTROLS_STATUS_HTML = `<button type="button" data-reticle-pause class="reticle-status-btn" title="${CONTROL_LABEL.PAUSE}" aria-label="${CONTROL_LABEL.PAUSE}">${hiIconHtml(PresenterIcon.PAUSE, TB)}</button><span data-reticle-badge class="reticle-pause-badge">${PAUSED_BADGE_TEXT}</span><button type="button" data-reticle-end class="reticle-status-btn reticle-status-stop" title="${CONTROL_LABEL.END}" aria-label="${CONTROL_LABEL.END}">${hiIconHtml(PresenterIcon.STOP, TB)}</button><button type="button" data-reticle-copy class="reticle-status-btn" title="${COPY_LABEL}" aria-label="${COPY_LABEL}" hidden>${hiIconHtml(PresenterIcon.COPY, TB)}</button><button type="button" data-reticle-export class="reticle-status-btn" title="${EXPORT_LABEL}" aria-label="${EXPORT_LABEL}" hidden>${hiIconHtml(PresenterIcon.DOWNLOAD, TB)}</button><span data-reticle-export-msg class="reticle-export-msg" aria-live="polite"></span>`;
150
181
  export const CONTROLS_BANNER_HTML = `<div data-reticle-banner class="reticle-banner">${ENDED_BANNER_TEXT}</div>`;
151
182
  export const CONTROLS_MARKS_HTML = `<div ${MARKS_ROW_ATTR} class="reticle-marks-row" hidden><span data-reticle-marks-text class="reticle-marks-text"></span><button type="button" ${COPY_MARKS_ATTR} class="reticle-marks-copy" title="${COPY_MARKS_LABEL}">${hiIconHtml(PresenterIcon.COPY, PRESENTER_ICON_SIZE.HELP)}<span>${COPY_MARKS_LABEL}</span></button></div>`;
152
- export const CONTROLS_FLOWS_HTML = `<div data-reticle-flows class="reticle-flows"><span class="reticle-flows-cap">${FLOWS_LABEL}</span></div>`;
153
- export const CONTROLS_FOOT_HTML = `<div data-reticle-foot><div class="reticle-foot-stack">${workspaceRowHtml()}</div></div>`;
183
+ export const CONTROLS_FLOWS_HTML = `<div data-reticle-flows class="reticle-flows"><div class="reticle-flows-head"><span class="reticle-flows-cap">${FLOWS_LABEL}</span><span class="reticle-flows-links"><button type="button" data-reticle-see-logs class="reticle-flows-all" hidden>${SEE_LOGS_LABEL} →</button><button type="button" data-reticle-flows-all class="reticle-flows-all">${FLOWS_ALL_LABEL} →</button></span></div><div data-reticle-flow-strip class="reticle-flow-strip"></div></div>`;
184
+ export const CONTROLS_FOOT_HTML = `<div data-reticle-foot><div class="reticle-foot-stack"><div class="reticle-foot-workspace-row">${workspaceRowHtml()}${CHAT_VIEWS_HTML}</div></div></div>`;
154
185
  function queryControlRefs(root) {
155
186
  return {
156
187
  pauseBtn: root.querySelector('[data-reticle-pause]') ?? undefined,
@@ -160,9 +191,21 @@ function queryControlRefs(root) {
160
191
  exportBtn: root.querySelector('[data-reticle-export]') ?? undefined,
161
192
  exportMsg: root.querySelector('[data-reticle-export-msg]') ?? undefined,
162
193
  flows: root.querySelector('[data-reticle-flows]') ?? undefined,
194
+ allFlows: root.querySelector('[data-reticle-all-flows]') ?? undefined,
163
195
  };
164
196
  }
165
197
  export class ControlPanel {
198
+ #recentPlays = (() => {
199
+ try {
200
+ const raw = JSON.parse(localStorage.getItem(RECENT_PLAYS_KEY) ?? '{}');
201
+ return 'object' === typeof raw && raw !== null && !Array.isArray(raw)
202
+ ? raw
203
+ : {};
204
+ }
205
+ catch {
206
+ return {};
207
+ }
208
+ })();
166
209
  #refs = {
167
210
  pauseBtn: undefined,
168
211
  endBtn: undefined,
@@ -171,12 +214,15 @@ export class ControlPanel {
171
214
  exportBtn: undefined,
172
215
  exportMsg: undefined,
173
216
  flows: undefined,
217
+ allFlows: undefined,
174
218
  };
175
219
  #state = SessionState.ACTIVE;
176
220
  #fadeTimer;
177
221
  #root;
178
222
  #glow;
179
223
  #flowItems = [];
224
+ #progress;
225
+ #progressTimer;
180
226
  #workspaceTeardown;
181
227
  #listeners;
182
228
  #host;
@@ -198,18 +244,44 @@ export class ControlPanel {
198
244
  }
199
245
  this.#refs.pauseBtn?.addEventListener('click', () => this.#onPauseToggle(), { signal });
200
246
  this.#refs.endBtn?.addEventListener('click', () => this.#onEnd(), { signal });
201
- this.#refs.flows?.addEventListener('click', (e) => {
247
+ const onReplay = (e) => {
202
248
  const target = e.target;
203
249
  if (!(target instanceof HTMLElement))
204
250
  return;
205
- const name = target.closest('[data-reticle-replay]')?.getAttribute('data-reticle-replay');
206
- if (name !== null && name !== undefined && name.length > 0) {
251
+ const button = target.closest('[data-reticle-replay]');
252
+ if (null === button || button.disabled)
253
+ return;
254
+ const name = button.getAttribute('data-reticle-replay');
255
+ if (name !== null && name.length > 0) {
256
+ this.#recentPlays[name] = Date.now();
257
+ try {
258
+ localStorage.setItem(RECENT_PLAYS_KEY, JSON.stringify(this.#recentPlays));
259
+ }
260
+ catch {
261
+ }
262
+ this.#renderFlows();
207
263
  this.#host.emit(HumanControlKind.REPLAY, name);
208
264
  }
209
- }, { signal });
265
+ };
266
+ for (const container of [this.#refs.flows, this.#refs.allFlows])
267
+ container?.addEventListener('click', onReplay, { signal });
268
+ this.#refs.flows?.querySelector('[data-reticle-flow-strip]')?.addEventListener('wheel', (e) => {
269
+ const strip = e.currentTarget;
270
+ if (0 !== e.deltaX || strip.scrollWidth <= strip.clientWidth)
271
+ return;
272
+ strip.scrollLeft += e.deltaY;
273
+ e.preventDefault();
274
+ }, { signal, passive: false });
275
+ this.#refs.flows
276
+ ?.querySelector('[data-reticle-see-logs]')
277
+ ?.addEventListener('click', () => root.querySelector('[data-reticle-chat-view-btn="activity"]')?.click(), { signal });
278
+ this.#refs.flows
279
+ ?.querySelector('[data-reticle-flows-all]')
280
+ ?.addEventListener('click', () => root.querySelector('[data-reticle-chat-view-btn="flows"]')?.click(), { signal });
210
281
  this.#refs.copyBtn?.addEventListener('click', () => this.#onCopy(), { signal });
211
282
  this.#refs.exportBtn?.addEventListener('click', () => this.#onExport(), { signal });
212
283
  this.#workspaceTeardown = mountWorkspaceSelector(root);
284
+ this.#renderFlows();
213
285
  this.setState(SessionState.ACTIVE);
214
286
  }
215
287
  #runJson() {
@@ -264,6 +336,56 @@ export class ControlPanel {
264
336
  this.#host.emit(HumanControlKind.END);
265
337
  this.setState(SessionState.ENDED);
266
338
  }
339
+ setFlowProgress(args) {
340
+ const progress = parseFlowProgress(args);
341
+ if (progress === undefined)
342
+ return;
343
+ this.#progress = progress;
344
+ if (this.#progressTimer !== undefined)
345
+ nativeClearTimeout(this.#progressTimer);
346
+ this.#progressTimer =
347
+ FlowProgressStatus.PLAYING === progress.status
348
+ ? undefined
349
+ : nativeSetTimeout(() => {
350
+ this.#progress = undefined;
351
+ this.#paintProgress();
352
+ }, PROGRESS_LINGER_MS);
353
+ this.#paintProgress();
354
+ }
355
+ #paintProgress() {
356
+ const root = this.#refs.flows?.ownerDocument;
357
+ if (root === undefined)
358
+ return;
359
+ const progress = this.#progress;
360
+ const seeLogs = this.#refs.flows?.querySelector('[data-reticle-see-logs]');
361
+ if (seeLogs !== null && seeLogs !== undefined)
362
+ seeLogs.hidden = progress === undefined;
363
+ for (const container of [this.#refs.flows, this.#refs.allFlows]) {
364
+ for (const btn of container?.querySelectorAll('[data-reticle-replay]') ?? []) {
365
+ const name = btn.getAttribute('data-reticle-replay') ?? '';
366
+ const mine = progress !== undefined && progress.name === name;
367
+ if (!mine) {
368
+ btn.removeAttribute('data-state');
369
+ btn.style.removeProperty('--reticle-flow-progress');
370
+ if (btn.classList.contains('reticle-flow'))
371
+ btn.textContent = `▶ ${name}`;
372
+ continue;
373
+ }
374
+ const share = 0 < progress.total ? Math.round((100 * progress.done) / progress.total) : 4;
375
+ btn.setAttribute('data-state', progress.status);
376
+ btn.style.setProperty('--reticle-flow-progress', `${String(Math.max(4, share))}%`);
377
+ const glyph = PROGRESS_GLYPH[progress.status];
378
+ if (btn.classList.contains('reticle-flow'))
379
+ btn.textContent = `${glyph} ${name}`;
380
+ const play = btn.querySelector('.reticle-flow-play');
381
+ if (play !== null)
382
+ play.textContent = glyph;
383
+ const meta = btn.querySelector('.reticle-flow-meta');
384
+ if (meta !== null && 0 < progress.total)
385
+ meta.textContent = `Step ${String(progress.done)} of ${String(progress.total)}`;
386
+ }
387
+ }
388
+ }
267
389
  setFlows(flows) {
268
390
  const list = Array.isArray(flows) ? flows : [];
269
391
  this.#flowItems = list
@@ -276,7 +398,12 @@ export class ControlPanel {
276
398
  if (typeof name !== 'string' || 0 === name.length)
277
399
  return null;
278
400
  const start = rec['start'];
279
- return 'string' === typeof start && start.length > 0 ? { name, start } : { name };
401
+ const createdAt = rec['createdAt'];
402
+ return {
403
+ name,
404
+ ...('string' === typeof start && start.length > 0 ? { start } : {}),
405
+ ...('number' === typeof createdAt && Number.isFinite(createdAt) ? { createdAt } : {}),
406
+ };
280
407
  }
281
408
  return null;
282
409
  })
@@ -290,19 +417,72 @@ export class ControlPanel {
290
417
  const el = this.#refs.flows;
291
418
  if (el === undefined)
292
419
  return;
420
+ const all = this.#refs.allFlows;
293
421
  const doc = el.ownerDocument;
294
- const testids = new Set(Array.from(doc.querySelectorAll('[data-testid]')).map((n) => n.getAttribute('data-testid')));
295
- const visible = this.#flowItems.filter((f) => f.start === undefined || testids.has(f.start));
296
- el.querySelectorAll('[data-reticle-replay]').forEach((b) => b.remove());
297
- for (const flow of visible) {
422
+ const testids = new Set(Array.from(doc.querySelectorAll(testIdSelector())).map((n) => readTestId(n)));
423
+ const playable = (f) => f.start === undefined || testids.has(f.start);
424
+ const recent = [...this.#flowItems]
425
+ .filter(playable)
426
+ .sort((a, b) => (this.#recentPlays[b.name] ?? b.createdAt ?? 0) -
427
+ (this.#recentPlays[a.name] ?? a.createdAt ?? 0));
428
+ const strip = el.querySelector('[data-reticle-flow-strip]');
429
+ strip?.replaceChildren();
430
+ all?.replaceChildren();
431
+ const addButton = (container, flow, enabled) => {
298
432
  const btn = doc.createElement('button');
299
433
  btn.type = 'button';
300
434
  btn.className = 'reticle-flow';
301
435
  btn.setAttribute('data-reticle-replay', flow.name);
302
436
  btn.textContent = `▶ ${flow.name}`;
303
- el.appendChild(btn);
437
+ btn.disabled = !enabled;
438
+ if (!enabled)
439
+ btn.title = 'Open the page where this flow starts to replay it';
440
+ container.appendChild(btn);
441
+ };
442
+ const addRow = (container, flow, enabled) => {
443
+ const btn = doc.createElement('button');
444
+ btn.type = 'button';
445
+ btn.className = 'reticle-flow-row';
446
+ btn.setAttribute('data-reticle-replay', flow.name);
447
+ btn.title = flow.name;
448
+ btn.disabled = !enabled;
449
+ const play = doc.createElement('span');
450
+ play.className = 'reticle-flow-play';
451
+ play.setAttribute('aria-hidden', 'true');
452
+ play.textContent = '▶';
453
+ const text = doc.createElement('span');
454
+ text.className = 'reticle-flow-text';
455
+ const name = doc.createElement('span');
456
+ name.className = 'reticle-flow-name';
457
+ name.textContent = flowTitle(flow.name);
458
+ const meta = doc.createElement('span');
459
+ meta.className = 'reticle-flow-meta';
460
+ meta.textContent = enabled ? FLOW_TEXT.REPLAYABLE : FLOW_TEXT.ELSEWHERE;
461
+ text.append(name, meta);
462
+ btn.append(play, text);
463
+ container.appendChild(btn);
464
+ };
465
+ for (const flow of recent.slice(0, 3))
466
+ if (strip !== null)
467
+ addButton(strip, flow, true);
468
+ el.setAttribute('data-has', recent.length > 0 ? '1' : '0');
469
+ if (all !== undefined) {
470
+ if (0 === this.#flowItems.length) {
471
+ const empty = doc.createElement('div');
472
+ empty.className = 'reticle-flows-empty';
473
+ const title = doc.createElement('strong');
474
+ title.textContent = FLOW_TEXT.EMPTY_TITLE;
475
+ const hint = doc.createElement('span');
476
+ hint.textContent = FLOW_TEXT.EMPTY_HINT;
477
+ empty.append(title, hint);
478
+ all.appendChild(empty);
479
+ }
480
+ else {
481
+ for (const flow of [...this.#flowItems].sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0)))
482
+ addRow(all, flow, playable(flow));
483
+ }
304
484
  }
305
- el.setAttribute('data-has', visible.length > 0 ? '1' : '0');
485
+ this.#paintProgress();
306
486
  }
307
487
  setState(state, text, tone) {
308
488
  this.#state = state;
@@ -320,10 +500,10 @@ export class ControlPanel {
320
500
  const ended = state === SessionState.ENDED;
321
501
  if (refs.pauseBtn !== undefined) {
322
502
  paintPauseBtn(refs.pauseBtn, state === SessionState.PAUSED);
323
- refs.pauseBtn.disabled = ended;
503
+ refs.pauseBtn.hidden = ended;
324
504
  }
325
505
  if (refs.endBtn !== undefined)
326
- refs.endBtn.disabled = ended;
506
+ refs.endBtn.hidden = ended;
327
507
  if (refs.copyBtn !== undefined) {
328
508
  if (ended)
329
509
  refs.copyBtn.removeAttribute('hidden');
@@ -0,0 +1 @@
1
+ export declare const PRESENTER_DESIGN_TOKENS_CSS: string;
@@ -0,0 +1,34 @@
1
+ export const PRESENTER_DESIGN_TOKENS_CSS = `
2
+ [data-reticle-overlay]{
3
+ --reticle-hud-space-0:0px;
4
+ --reticle-hud-space-1:4px;
5
+ --reticle-hud-space-2:8px;
6
+ --reticle-hud-space-3:12px;
7
+ --reticle-hud-space-4:16px;
8
+ --reticle-hud-radius-sm:6px;
9
+ --reticle-hud-radius-md:10px;
10
+ --reticle-hud-radius-lg:14px;
11
+ --reticle-hud-radius-pill:9999px;
12
+ --reticle-hud-size-xs:10px;
13
+ --reticle-hud-size-sm:12px;
14
+ --reticle-hud-size-base:13px;
15
+ --reticle-hud-icon-size:18px;
16
+ --reticle-hud-control-size:36px;
17
+ --reticle-hud-control-gap:4px;
18
+ /* Compact: the HUD sits over the user's own app. 400×560 covered too much of it. */
19
+ --reticle-hud-dock-width:320px;
20
+ --reticle-hud-panel-height:440px;
21
+ --reticle-hud-ground:#0b0b0c;
22
+ --reticle-hud-surface:#151417;
23
+ --reticle-hud-inset:#232227;
24
+ --reticle-hud-hover:#1e1d22;
25
+ --reticle-hud-border:#29282d;
26
+ --reticle-hud-border-strong:#3a3941;
27
+ --reticle-hud-text:#f5f3ef;
28
+ --reticle-hud-text-muted:#a6a29a;
29
+ --reticle-hud-text-faint:#8b867d;
30
+ --reticle-hud-brand:#f0872e;
31
+ --reticle-hud-brand-soft:#2a1b0d;
32
+ --reticle-hud-ease:cubic-bezier(.4,0,.2,1);
33
+ }
34
+ `;
@@ -5,7 +5,7 @@ const DOCK_PANEL_GAP_PX = 8;
5
5
  const DOCK_SETTINGS_GAP_PX = 12;
6
6
  const HUD_BAR_HEIGHT_PX = 44;
7
7
  const CHAT_MIN_HEIGHT_PX = 160;
8
- const CHAT_DEFAULT_MAX_HEIGHT_PX = 420;
8
+ const CHAT_DEFAULT_MAX_HEIGHT_PX = 440;
9
9
  const SETTINGS_DEFAULT_MAX_HEIGHT_PX = 520;
10
10
  const SETTINGS_MIN_HEIGHT_PX = 200;
11
11
  function unionRects(rects) {
@@ -0,0 +1,12 @@
1
+ import type { AccountState } from '@reticlehq/core';
2
+ export declare const RAIL_ATTR = "data-reticle-rail";
3
+ export declare const RAIL_SIGNIN_ATTR = "data-reticle-rail-signin";
4
+ export declare const RAIL_PROMO_ATTR = "data-reticle-rail-promo";
5
+ export declare const RAIL_TEXT: {
6
+ readonly TITLE: "Sign in to try Reticle Harness";
7
+ readonly DETAIL: "Free credits every month for autonomous end-to-end checks";
8
+ readonly CTA: "Sign in";
9
+ };
10
+ export declare const RAIL_HTML: string;
11
+ export declare function paintRail(root: HTMLElement, account: AccountState | undefined): void;
12
+ export declare const FRAME_CSS: string;