@reticlehq/browser 3.6.0 → 3.7.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 (83) hide show
  1. package/dist/actions/action-report.d.ts +54 -0
  2. package/dist/actions/action-report.js +60 -0
  3. package/dist/actions/actions-dom.d.ts +1 -0
  4. package/dist/actions/actions-dom.js +12 -0
  5. package/dist/actions/actions-press.d.ts +1 -0
  6. package/dist/actions/actions-press.js +25 -1
  7. package/dist/actions/actions.d.ts +1 -37
  8. package/dist/actions/actions.js +24 -66
  9. package/dist/actions/appeared-text.js +3 -0
  10. package/dist/actions/danger-context.d.ts +2 -0
  11. package/dist/actions/danger-context.js +28 -2
  12. package/dist/actions/value-input.d.ts +1 -0
  13. package/dist/actions/value-input.js +11 -0
  14. package/dist/commands/commands.js +4 -0
  15. package/dist/dom/a11y.js +251 -10
  16. package/dist/dom/addressing/coverage-markers.d.ts +6 -0
  17. package/dist/dom/addressing/coverage-markers.js +13 -0
  18. package/dist/dom/dom-ignore.d.ts +2 -0
  19. package/dist/dom/dom-ignore.js +15 -2
  20. package/dist/dom/query.d.ts +1 -1
  21. package/dist/dom/query.js +78 -16
  22. package/dist/dom/render-page.d.ts +12 -0
  23. package/dist/dom/render-page.js +135 -0
  24. package/dist/dom/snapshot.js +55 -7
  25. package/dist/observers/dom.js +32 -16
  26. package/dist/observers/field.js +3 -21
  27. package/dist/observers/network.js +56 -7
  28. package/dist/observers/perf.js +53 -1
  29. package/dist/observers/route.js +6 -5
  30. package/dist/presenter/carousel/carousel.js +2 -7
  31. package/dist/presenter/carousel/panel-slides.d.ts +2 -4
  32. package/dist/presenter/carousel/panel-slides.js +17 -15
  33. package/dist/presenter/chrome/log-memory.d.ts +1 -0
  34. package/dist/presenter/chrome/presenter-log.d.ts +7 -0
  35. package/dist/presenter/chrome/presenter-log.js +48 -8
  36. package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
  37. package/dist/presenter/chrome/presenter-morph.js +34 -0
  38. package/dist/presenter/chrome/presenter-report-copy.d.ts +17 -4
  39. package/dist/presenter/chrome/presenter-report-copy.js +26 -4
  40. package/dist/presenter/hud-telemetry.js +55 -4
  41. package/dist/presenter/presenter-account-styles.js +3 -3
  42. package/dist/presenter/presenter-account.d.ts +27 -5
  43. package/dist/presenter/presenter-account.js +83 -6
  44. package/dist/presenter/presenter-agent-link.d.ts +9 -0
  45. package/dist/presenter/presenter-agent-link.js +62 -0
  46. package/dist/presenter/presenter-chat-views.d.ts +27 -5
  47. package/dist/presenter/presenter-chat-views.js +312 -72
  48. package/dist/presenter/presenter-config.d.ts +2 -0
  49. package/dist/presenter/presenter-config.js +1 -0
  50. package/dist/presenter/presenter-controls.d.ts +0 -1
  51. package/dist/presenter/presenter-controls.js +13 -91
  52. package/dist/presenter/presenter-design-tokens.js +1 -1
  53. package/dist/presenter/presenter-frame.d.ts +1 -1
  54. package/dist/presenter/presenter-frame.js +8 -10
  55. package/dist/presenter/presenter-harness-row.d.ts +67 -0
  56. package/dist/presenter/presenter-harness-row.js +209 -0
  57. package/dist/presenter/presenter-personas.d.ts +35 -0
  58. package/dist/presenter/presenter-personas.js +95 -0
  59. package/dist/presenter/presenter-plan.js +25 -24
  60. package/dist/presenter/presenter-report-styles.js +40 -22
  61. package/dist/presenter/presenter-report.d.ts +2 -1
  62. package/dist/presenter/presenter-report.js +83 -17
  63. package/dist/presenter/presenter-settings-styles.js +8 -4
  64. package/dist/presenter/presenter-settings.d.ts +7 -6
  65. package/dist/presenter/presenter-settings.js +75 -34
  66. package/dist/presenter/presenter-shell-styles.js +12 -3
  67. package/dist/presenter/presenter-shell.d.ts +10 -4
  68. package/dist/presenter/presenter-shell.js +31 -23
  69. package/dist/presenter/presenter-styles.js +0 -2
  70. package/dist/presenter/presenter-workspace.js +10 -3
  71. package/dist/presenter/presenter.d.ts +3 -2
  72. package/dist/presenter/presenter.js +81 -20
  73. package/dist/registry/declared-platform.js +2 -0
  74. package/dist/reticle-inject.js +777 -700
  75. package/dist/reticle.js +36 -2
  76. package/dist/storage-keys.d.ts +3 -2
  77. package/dist/storage-keys.js +7 -3
  78. package/dist/transport/transport.js +5 -3
  79. package/dist/transport/unreachable-message.d.ts +1 -1
  80. package/dist/transport/unreachable-message.js +3 -5
  81. package/package.json +2 -2
  82. package/dist/presenter/carousel/offer-card.d.ts +0 -21
  83. package/dist/presenter/carousel/offer-card.js +0 -56
@@ -6,7 +6,6 @@ import { PresenterIcon, PRESENTER_ICON_SIZE, hiIcon, hiIconHtml } from './icons/
6
6
  import { getPresenterSettings } from './presenter-settings.js';
7
7
  import { mountWorkspaceSelector, workspaceRowHtml } from './presenter-workspace.js';
8
8
  import { CHAT_VIEWS_HTML } from './presenter-chat-views.js';
9
- import { ReticleStorageKey } from '../storage-keys.js';
10
9
  const DATA_RETICLE_STATE = 'data-reticle-state';
11
10
  const DATA_RETICLE_TONE = 'data-reticle-tone';
12
11
  const DATA_ON = 'data-on';
@@ -21,9 +20,6 @@ const PAUSED_BADGE_TEXT = 'PAUSED';
21
20
  const ENDED_BANNER_TEXT = 'Session ended';
22
21
  const COPY_LABEL = 'Copy run';
23
22
  const EXPORT_LABEL = 'Export';
24
- const FLOWS_LABEL = 'Replay a flow';
25
- const FLOWS_ALL_LABEL = 'See all';
26
- const SEE_LOGS_LABEL = 'See the logs';
27
23
  const PROGRESS_LINGER_MS = 6000;
28
24
  const PROGRESS_GLYPH = {
29
25
  [FlowProgressStatus.PLAYING]: '⏵',
@@ -41,7 +37,6 @@ function parseFlowProgress(args) {
41
37
  const COPIED_TEXT = 'Copied ✓';
42
38
  const RUN_FILENAME = 'reticle-run.json';
43
39
  export const ENDED_FADE_MS = 4000;
44
- const RECENT_PLAYS_KEY = ReticleStorageKey.RECENT_PLAYS;
45
40
  const FLOW_TEXT = {
46
41
  REPLAYABLE: 'Replay without an agent',
47
42
  ELSEWHERE: 'Starts on another page',
@@ -53,9 +48,9 @@ export function flowTitle(name) {
53
48
  return 0 === words.length ? name : words.charAt(0).toUpperCase() + words.slice(1);
54
49
  }
55
50
  export const CONTROLS_CSS = `
56
- [data-reticle-chat-panel] [data-reticle-foot]{flex:none;padding:8px 10px 10px;border-top:1px solid rgba(255,255,255,.07);
51
+ [data-reticle-chat-panel] [data-reticle-foot]{flex:none;padding:6px 10px 8px;border-top:1px solid rgba(255,255,255,.07);
57
52
  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;}
53
+ [data-reticle-chat-panel] .reticle-foot-workspace-row{display:flex;flex-wrap:wrap;align-items:center;min-width:0;}
59
54
  [data-reticle-chat-panel] .reticle-hud-log-well{margin:0 0 4px;}
60
55
  [data-reticle-chat-panel] .reticle-workspace-wrap{position:relative;align-self:flex-start;max-width:100%;}
61
56
  [data-reticle-chat-panel] .reticle-workspace-wrap[hidden]{display:none;}
@@ -64,6 +59,7 @@ export const CONTROLS_CSS = `
64
59
  border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.04);color:rgba(255,255,255,.78);
65
60
  font:inherit;font-size:11px;font-weight:500;line-height:1.2;
66
61
  box-shadow:inset 0 1px 0 rgba(255,255,255,.05);transition:background .15s,border-color .15s,color .15s;}
62
+ [data-reticle-chat-panel] .reticle-workspace-k{color:var(--reticle-hud-text-faint);font-weight:400;}
67
63
  [data-reticle-chat-panel] .reticle-workspace:hover{background:rgba(255,255,255,.07);border-color:rgba(255,255,255,.14);color:#fff;}
68
64
  [data-reticle-chat-panel] .reticle-workspace[aria-expanded="true"]{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.16);color:#fff;}
69
65
  [data-reticle-chat-panel] .reticle-workspace-icon,
@@ -87,7 +83,7 @@ export const CONTROLS_CSS = `
87
83
  [data-reticle-chat-panel] .reticle-workspace-menu-head{
88
84
  display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
89
85
  [data-reticle-chat-panel] .reticle-workspace-menu-title{
90
- color:rgba(255,255,255,.42);font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;}
86
+ color:rgba(255,255,255,.42);font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;}
91
87
  [data-reticle-chat-panel] .reticle-workspace-copy{
92
88
  display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:none;border-radius:6px;
93
89
  background:rgba(255,255,255,.06);color:rgba(255,255,255,.7);cursor:pointer;line-height:0;transition:background .12s,color .12s;}
@@ -123,13 +119,6 @@ export const CONTROLS_CSS = `
123
119
  [data-reticle-overlay][data-reticle-tone="ask"] [data-reticle-glow][data-on="1"],
124
120
  [data-reticle-overlay][data-reticle-tone="warn"] [data-reticle-glow][data-on="1"]{
125
121
  box-shadow:inset 0 0 0 2px rgba(255,255,255,.22);}
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
122
  /* A replay the person started: the chip becomes a player, its bar filling step by step. */
134
123
  [data-reticle-chat-panel] :is(.reticle-flow,.reticle-flow-row)[data-state]{position:relative;overflow:hidden;
135
124
  border-color:color-mix(in srgb,var(--reticle-c-active) 55%,transparent);color:var(--reticle-fg);}
@@ -139,10 +128,6 @@ export const CONTROLS_CSS = `
139
128
  @keyframes reticle-flow-pulse{50%{opacity:.55}}
140
129
  [data-reticle-chat-panel] :is(.reticle-flow,.reticle-flow-row)[data-state="passed"]::after{background:#4ade80;}
141
130
  [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
131
  [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
132
  [data-reticle-chat-panel] .reticle-all-flows .reticle-flow{width:100%;height:auto;min-height:32px;text-align:left;justify-content:flex-start;}
148
133
  [data-reticle-chat-panel] .reticle-all-flows .reticle-flow:disabled{opacity:.45;cursor:not-allowed;}
@@ -180,8 +165,7 @@ const TB = PRESENTER_ICON_SIZE.TOOLBAR;
180
165
  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>`;
181
166
  export const CONTROLS_BANNER_HTML = `<div data-reticle-banner class="reticle-banner">${ENDED_BANNER_TEXT}</div>`;
182
167
  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>`;
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>`;
168
+ export const CONTROLS_FOOT_HTML = `<div data-reticle-foot><div class="reticle-foot-stack">${CHAT_VIEWS_HTML}<div class="reticle-foot-workspace-row">${workspaceRowHtml()}</div></div></div>`;
185
169
  function queryControlRefs(root) {
186
170
  return {
187
171
  pauseBtn: root.querySelector('[data-reticle-pause]') ?? undefined,
@@ -190,22 +174,10 @@ function queryControlRefs(root) {
190
174
  copyBtn: root.querySelector('[data-reticle-copy]') ?? undefined,
191
175
  exportBtn: root.querySelector('[data-reticle-export]') ?? undefined,
192
176
  exportMsg: root.querySelector('[data-reticle-export-msg]') ?? undefined,
193
- flows: root.querySelector('[data-reticle-flows]') ?? undefined,
194
177
  allFlows: root.querySelector('[data-reticle-all-flows]') ?? undefined,
195
178
  };
196
179
  }
197
180
  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
- })();
209
181
  #refs = {
210
182
  pauseBtn: undefined,
211
183
  endBtn: undefined,
@@ -213,7 +185,6 @@ export class ControlPanel {
213
185
  copyBtn: undefined,
214
186
  exportBtn: undefined,
215
187
  exportMsg: undefined,
216
- flows: undefined,
217
188
  allFlows: undefined,
218
189
  };
219
190
  #state = SessionState.ACTIVE;
@@ -253,31 +224,10 @@ export class ControlPanel {
253
224
  return;
254
225
  const name = button.getAttribute('data-reticle-replay');
255
226
  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();
263
227
  this.#host.emit(HumanControlKind.REPLAY, name);
264
228
  }
265
229
  };
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 });
230
+ this.#refs.allFlows?.addEventListener('click', onReplay, { signal });
281
231
  this.#refs.copyBtn?.addEventListener('click', () => this.#onCopy(), { signal });
282
232
  this.#refs.exportBtn?.addEventListener('click', () => this.#onExport(), { signal });
283
233
  this.#workspaceTeardown = mountWorkspaceSelector(root);
@@ -353,15 +303,9 @@ export class ControlPanel {
353
303
  this.#paintProgress();
354
304
  }
355
305
  #paintProgress() {
356
- const root = this.#refs.flows?.ownerDocument;
357
- if (root === undefined)
358
- return;
359
306
  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]') ?? []) {
307
+ {
308
+ for (const btn of this.#refs.allFlows?.querySelectorAll('[data-reticle-replay]') ?? []) {
365
309
  const name = btn.getAttribute('data-reticle-replay') ?? '';
366
310
  const mine = progress !== undefined && progress.name === name;
367
311
  if (!mine) {
@@ -414,31 +358,13 @@ export class ControlPanel {
414
358
  this.#renderFlows();
415
359
  }
416
360
  #renderFlows() {
417
- const el = this.#refs.flows;
418
- if (el === undefined)
419
- return;
420
361
  const all = this.#refs.allFlows;
421
- const doc = el.ownerDocument;
362
+ if (all === undefined)
363
+ return;
364
+ const doc = all.ownerDocument;
422
365
  const testids = new Set(Array.from(doc.querySelectorAll(testIdSelector())).map((n) => readTestId(n)));
423
366
  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) => {
432
- const btn = doc.createElement('button');
433
- btn.type = 'button';
434
- btn.className = 'reticle-flow';
435
- btn.setAttribute('data-reticle-replay', flow.name);
436
- btn.textContent = `▶ ${flow.name}`;
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
- };
367
+ all.replaceChildren();
442
368
  const addRow = (container, flow, enabled) => {
443
369
  const btn = doc.createElement('button');
444
370
  btn.type = 'button';
@@ -462,11 +388,7 @@ export class ControlPanel {
462
388
  btn.append(play, text);
463
389
  container.appendChild(btn);
464
390
  };
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) {
391
+ {
470
392
  if (0 === this.#flowItems.length) {
471
393
  const empty = doc.createElement('div');
472
394
  empty.className = 'reticle-flows-empty';
@@ -9,7 +9,7 @@ export const PRESENTER_DESIGN_TOKENS_CSS = `
9
9
  --reticle-hud-radius-md:10px;
10
10
  --reticle-hud-radius-lg:14px;
11
11
  --reticle-hud-radius-pill:9999px;
12
- --reticle-hud-size-xs:10px;
12
+ --reticle-hud-size-xs:11px;
13
13
  --reticle-hud-size-sm:12px;
14
14
  --reticle-hud-size-base:13px;
15
15
  --reticle-hud-icon-size:18px;
@@ -4,7 +4,7 @@ export declare const RAIL_SIGNIN_ATTR = "data-reticle-rail-signin";
4
4
  export declare const RAIL_PROMO_ATTR = "data-reticle-rail-promo";
5
5
  export declare const RAIL_TEXT: {
6
6
  readonly TITLE: "Sign in to try Reticle Harness";
7
- readonly DETAIL: "Free credits every month for autonomous end-to-end checks";
7
+ readonly DETAIL: "Free Harness credits to start.";
8
8
  readonly CTA: "Sign in";
9
9
  };
10
10
  export declare const RAIL_HTML: string;
@@ -9,7 +9,7 @@ const PAGE_PANEL_ATTR = 'data-reticle-page-panel';
9
9
  const OVERLAY = 'data-reticle-overlay';
10
10
  export const RAIL_TEXT = {
11
11
  TITLE: 'Sign in to try Reticle Harness',
12
- DETAIL: 'Free credits every month for autonomous end-to-end checks',
12
+ DETAIL: 'Free Harness credits to start.',
13
13
  CTA: 'Sign in',
14
14
  };
15
15
  export const RAIL_HTML = `<div ${RAIL_ATTR} class="reticle-rail" role="complementary" aria-label="Reticle">
@@ -53,7 +53,7 @@ export const FRAME_CSS = `
53
53
  /*
54
54
  * Two rows above the log in every state: the header and the status row. "Session ended" (or the
55
55
  * handoff notice) takes the status text's place instead of arriving as a row of its own, and the
56
- * Harness control sits in the footer beside the project capsule.
56
+ * Harness block sits in the footer above the project capsule.
57
57
  */
58
58
  [${OVERLAY}] [${CHAT_PANEL_ATTR}] .reticle-act-strip .reticle-banner{display:none;flex:1;min-width:0;min-height:0;padding:0;border:0;background:none;
59
59
  font-size:11px;font-weight:600;line-height:1.3;color:var(--reticle-hud-text);
@@ -61,11 +61,9 @@ export const FRAME_CSS = `
61
61
  [${OVERLAY}][data-reticle-state="ended"] [${CHAT_PANEL_ATTR}] .reticle-act-strip .reticle-banner{display:-webkit-box;}
62
62
  [${OVERLAY}][data-reticle-state="ended"] [${CHAT_PANEL_ATTR}] .reticle-act-strip .reticle-act{display:none;}
63
63
  [${CHAT_PANEL_ATTR}] .reticle-foot-workspace-row{gap:var(--reticle-hud-space-2);}
64
- [${CHAT_PANEL_ATTR}] [data-reticle-foot] .reticle-harness-spot{flex:none;margin-left:auto;padding:0;border:0;}
65
- [${CHAT_PANEL_ATTR}] [data-reticle-foot] .reticle-harness-row{min-height:0;gap:var(--reticle-hud-space-2);}
66
- [${CHAT_PANEL_ATTR}] [data-reticle-foot] .reticle-harness-copy strong{font-size:var(--reticle-hud-size-xs);color:var(--reticle-hud-text-muted);white-space:nowrap;}
67
- /* The explanation stays for screen readers; sighted users get it from the link it sits beside. */
68
- [${CHAT_PANEL_ATTR}] [data-reticle-foot] .reticle-harness-copy span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
64
+ /* The Harness block sits above the project capsule, full width: one clear thing to press. */
65
+ [${CHAT_PANEL_ATTR}] [data-reticle-foot] .reticle-foot-stack{display:flex;flex-direction:column;gap:var(--reticle-hud-space-2);}
66
+ [${CHAT_PANEL_ATTR}] [data-reticle-foot] .reticle-foot-workspace-row:has(> .reticle-workspace-wrap[hidden]){display:none;}
69
67
  ${BELOW} ${PAGES}{bottom:auto;top:calc(100% + 8px + var(--reticle-rail-h) + var(--reticle-frame-gap));}
70
68
  [${DOCK_ATTR}][${DOCK_ALIGN_ATTR}="start"] :is(${PAGES},[${RAIL_ATTR}]){right:auto;left:0;}
71
69
 
@@ -107,7 +105,7 @@ ${BELOW} ${PAGES}{bottom:auto;top:calc(100% + 8px + var(--reticle-rail-h) + var(
107
105
  [${PAGE_PANEL_ATTR}="flows"] .reticle-flow-row:focus-visible{outline:2px solid var(--reticle-hud-brand);outline-offset:1px;}
108
106
  [${PAGE_PANEL_ATTR}="flows"] .reticle-flow-row:disabled{cursor:not-allowed;opacity:.5;}
109
107
  [${PAGE_PANEL_ATTR}="flows"] .reticle-flow-play{flex:none;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;
110
- border-radius:50%;background:var(--reticle-hud-brand-soft);color:var(--reticle-hud-brand);font-size:10px;}
108
+ border-radius:50%;background:var(--reticle-hud-brand-soft);color:var(--reticle-hud-brand);font-size:11px;}
111
109
  [${PAGE_PANEL_ATTR}="flows"] .reticle-flow-row:disabled .reticle-flow-play{background:var(--reticle-hud-inset);color:var(--reticle-hud-text-faint);}
112
110
  [${PAGE_PANEL_ATTR}="flows"] .reticle-flow-text{display:flex;flex:1;min-width:0;flex-direction:column;gap:1px;}
113
111
  [${PAGE_PANEL_ATTR}="flows"] .reticle-flow-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--reticle-hud-size-sm);font-weight:600;}
@@ -143,11 +141,11 @@ ${BELOW} ${PAGES}{bottom:auto;top:calc(100% + 8px + var(--reticle-rail-h) + var(
143
141
  [${SETTINGS_PANEL_ATTR}] .reticle-settings-reset{background:none;text-align:left;padding-left:14px;font-weight:500;}
144
142
 
145
143
  /* ── Toolbar: Settings and Exit are tabs like the four beside them, so they light the same way. ── */
146
- [data-reticle-hud] .reticle-toolbar-chrome .reticle-tb-btn{width:var(--reticle-hud-control-size);height:var(--reticle-hud-control-size);
144
+ [data-reticle-hud] .reticle-toolbar-chrome .reticle-tb-btn{width:100%;height:auto;flex-direction:column;gap:2px;min-height:42px;padding:4px 0 3px;
147
145
  border-radius:var(--reticle-hud-radius-md);background:transparent;color:var(--reticle-hud-text-muted);}
148
146
  [data-reticle-hud] .reticle-toolbar-chrome .reticle-tb-btn:hover{background:var(--reticle-hud-hover);color:var(--reticle-hud-text);}
149
147
  [data-reticle-hud] .reticle-toolbar-chrome .reticle-tb-btn[data-active="1"],
150
- [data-reticle-hud] .reticle-toolbar-chrome .reticle-tb-btn[data-active="1"]:hover{background:var(--reticle-hud-inset);color:var(--reticle-hud-text);}
148
+ [data-reticle-hud] .reticle-toolbar-chrome .reticle-tb-btn[data-active="1"]:hover{background:transparent;color:var(--reticle-hud-text);}
151
149
 
152
150
  [${RAIL_ATTR}]{
153
151
  display:none;position:absolute;right:0;left:auto;bottom:calc(100% + 8px);z-index:7;
@@ -0,0 +1,67 @@
1
+ import { type AccountState, type HarnessConfig, type HarnessDrive } from '@reticlehq/core';
2
+ import { type Credits } from '@reticlehq/core/hud';
3
+ export declare const HARNESS_ROW_TEXT: {
4
+ readonly RUN: "Run Harness";
5
+ readonly STARTING: "Starting…";
6
+ readonly STOP: "Stop";
7
+ readonly SIGNED_OUT: string;
8
+ readonly SIGN_IN: "Sign in";
9
+ readonly UNLINKED: "This project is not on your dashboard yet.";
10
+ readonly LINK: "Link this project";
11
+ readonly LINKING: "Linking…";
12
+ readonly NOT_ENTITLED: "Harness is not on your plan";
13
+ readonly NO_CREDITS: "No Harness credits left";
14
+ readonly NO_PROVIDER: "No model provider set";
15
+ readonly OFF: "Harness is off";
16
+ readonly canSwitchOn: (percent: number) => string;
17
+ readonly TURN_ON: "Switch on";
18
+ readonly COPY_PROMPT: "Copy setup prompt";
19
+ readonly COPIED: "Copied";
20
+ readonly SET_UP: "Set up ↗";
21
+ readonly PLANS: "See plans ↗";
22
+ readonly PLAN: "Plan ↗";
23
+ readonly MORE: "Get more ↗";
24
+ readonly driving: (as: string | undefined, steps: number) => string;
25
+ readonly NAME: "Harness";
26
+ readonly TOGGLE: "Reticle Harness on this project";
27
+ readonly creditsLeft: (left: number, days: number | undefined) => string;
28
+ readonly FOOT_LINK: "Link project";
29
+ readonly FOOT_NO_CREDITS: "No credits";
30
+ readonly FOOT_NOT_ENTITLED: "Not on your plan";
31
+ readonly FOOT_NO_PROVIDER: "No model provider";
32
+ readonly footSetup: (percent: number) => string;
33
+ readonly PAGE_TITLE: "Reticle Harness";
34
+ readonly LOG_LABEL: "What the Harness did";
35
+ readonly LOG_EMPTY: "Runs show here, step by step.";
36
+ readonly PAGE_SUBTITLE: "Tests your app by itself";
37
+ };
38
+ export declare const HARNESS_SWITCH_ON_ATTR = "data-reticle-harness-switch";
39
+ export declare const HARNESS_LINK_ATTR = "data-reticle-harness-link";
40
+ export declare const HARNESS_OPEN_ATTR = "data-reticle-harness-open";
41
+ export declare const HARNESS_LOG_ATTR = "data-reticle-harness-log";
42
+ export declare const HARNESS_TOGGLE_ATTR = "data-reticle-harness-toggle";
43
+ export declare const HARNESS_COPY_PROMPT_ATTR = "data-reticle-harness-copy-prompt";
44
+ export interface HarnessGateView {
45
+ percent: number;
46
+ unlocked: boolean;
47
+ reason?: string;
48
+ prompt?: string;
49
+ }
50
+ export declare function harnessGateIn(instrumentation: Readonly<Record<string, unknown>> | undefined, config?: HarnessConfig): HarnessGateView | undefined;
51
+ export interface HarnessRowState {
52
+ account: AccountState | undefined;
53
+ config: HarnessConfig | undefined;
54
+ gate?: HarnessGateView | undefined;
55
+ drive: HarnessDrive | undefined;
56
+ canDrive: boolean;
57
+ pick: string;
58
+ startedAs: string | undefined;
59
+ setupUrl: string;
60
+ planUrl: string;
61
+ linking?: boolean;
62
+ starting?: boolean;
63
+ }
64
+ export declare function creditsShort(credits: Credits | undefined): string;
65
+ export declare function harnessFootHtml(s: HarnessRowState): string;
66
+ export declare function harnessRowHtml(s: HarnessRowState): string;
67
+ export declare const HARNESS_ROW_CSS = "\n[data-reticle-chat-panel] .reticle-harness-spot{flex:none;display:flex;flex-direction:column;gap:4px;}\n[data-reticle-chat-panel] .reticle-harness-spot[hidden],[data-reticle-chat-panel] .reticle-harness-spot:empty{display:none;}\n[data-reticle-chat-panel] .reticle-harness-row{display:flex;align-items:center;gap:6px;min-height:30px;min-width:0;}\n[data-reticle-chat-panel] .reticle-harness-lead{flex:1;min-width:0;display:flex;align-items:center;gap:6px;}\n[data-reticle-chat-panel] .reticle-harness-said{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--reticle-hud-size-sm);color:var(--reticle-hud-text);}\n[data-reticle-chat-panel] .reticle-harness-row-wrap{flex-wrap:wrap;row-gap:2px;padding:4px 0;}\n[data-reticle-chat-panel] .reticle-harness-row-wrap .reticle-harness-said{flex:1 1 100%;white-space:normal;}\n[data-reticle-chat-panel] .reticle-harness-said code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;}\n[data-reticle-chat-panel] .reticle-harness-as{flex:none;font-size:var(--reticle-hud-size-xs);color:var(--reticle-hud-text-muted);}\n[data-reticle-chat-panel] .reticle-harness-persona{box-sizing:border-box;flex:1;min-width:0;width:100%;height:30px;font:inherit;font-size:var(--reticle-hud-size-sm);color:var(--reticle-hud-text);\n background:var(--reticle-hud-ground);border:1px solid var(--reticle-hud-border-strong);border-radius:var(--reticle-hud-radius-sm);padding:0 var(--reticle-hud-space-2);}\n[data-reticle-chat-panel] .reticle-harness-spot > .reticle-harness-persona{flex:none;}\n[data-reticle-chat-panel] select.reticle-harness-persona{padding-right:2px;text-overflow:ellipsis;}\n[data-reticle-chat-panel] .reticle-harness-persona::placeholder{color:var(--reticle-hud-text-faint);}\n[data-reticle-chat-panel] .reticle-harness-persona:focus-visible{outline:2px solid var(--reticle-hud-brand);outline-offset:0;}\n[data-reticle-chat-panel] .reticle-harness-run{flex:none;box-sizing:border-box;height:30px;padding:0 12px;border:0;border-radius:var(--reticle-hud-radius-sm);cursor:pointer;white-space:nowrap;\n background:var(--reticle-hud-brand);color:#160b02;font:inherit;font-size:var(--reticle-hud-size-sm);font-weight:700;}\n[data-reticle-chat-panel] .reticle-harness-run:hover{filter:brightness(1.08);}\n[data-reticle-chat-panel] .reticle-harness-run:focus-visible{outline:2px solid var(--reticle-hud-text);outline-offset:2px;}\n[data-reticle-chat-panel] .reticle-harness-stop{background:transparent;color:var(--reticle-hud-text);border:1px solid var(--reticle-hud-border-strong);}\n[data-reticle-chat-panel] .reticle-harness-link{flex:none;border:0;padding:0;background:none;cursor:pointer;font:inherit;color:var(--reticle-hud-brand);font-size:var(--reticle-hud-size-sm);font-weight:600;text-decoration:none;white-space:nowrap;}\n[data-reticle-chat-panel] .reticle-harness-link:hover{text-decoration:underline;}\n[data-reticle-chat-panel] .reticle-harness-open{flex:1;min-width:0;height:30px;display:flex;align-items:center;gap:8px;padding:0 var(--reticle-hud-space-2);border:0;border-radius:var(--reticle-hud-radius-sm);background:transparent;cursor:pointer;\n text-align:left;white-space:nowrap;font:inherit;font-size:var(--reticle-hud-size-sm);color:var(--reticle-hud-text);}\n[data-reticle-chat-panel] .reticle-harness-name{flex:none;font-weight:600;}\n[data-reticle-chat-panel] .reticle-harness-state{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;color:var(--reticle-hud-text-muted);font-size:11px;font-variant-numeric:tabular-nums;}\n[data-reticle-chat-panel] .reticle-harness-chev{flex:none;color:var(--reticle-hud-text-faint);}\n[data-reticle-chat-panel] .reticle-harness-toggle{flex:none;position:relative;width:30px;height:18px;padding:0;border:0;border-radius:9px;cursor:pointer;background:var(--reticle-hud-border-strong);transition:background .15s;}\n[data-reticle-chat-panel] .reticle-harness-toggle[aria-checked=\"true\"]{background:var(--reticle-hud-brand);}\n[data-reticle-chat-panel] .reticle-harness-knob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:transform .15s;}\n[data-reticle-chat-panel] .reticle-harness-toggle[aria-checked=\"true\"] .reticle-harness-knob{transform:translateX(12px);}\n[data-reticle-chat-panel] .reticle-harness-toggle:focus-visible{outline:2px solid var(--reticle-hud-text);outline-offset:2px;}\n[data-reticle-chat-panel] .reticle-harness-open:hover{background:var(--reticle-hud-hover);}\n[data-reticle-chat-panel] .reticle-harness-open:focus-visible{outline:2px solid var(--reticle-hud-brand);outline-offset:0;}\n[data-reticle-chat-panel] .reticle-harness-run:disabled,[data-reticle-chat-panel] .reticle-harness-persona:disabled{opacity:.6;cursor:default;}\n[data-reticle-chat-panel] .reticle-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}\n[data-reticle-chat-panel] .reticle-foot-meta{margin:0;font-size:11px;line-height:1.3;color:var(--reticle-hud-text-muted);overflow-wrap:anywhere;}\n[data-reticle-chat-panel] .reticle-foot-meta:empty{display:none;}\n";
@@ -0,0 +1,209 @@
1
+ import { CreditKind, } from '@reticlehq/core';
2
+ import { SIGN_UP_PITCH, creditsSpent, creditsUsageText, creditsUsedText, } from '@reticlehq/core/hud';
3
+ import { esc } from './chrome/presenter-safe-html.js';
4
+ import { ACCOUNT_SIGNIN_ATTR } from './presenter-account.js';
5
+ import { personaPickHtml, personaCustomHtml, personaLabel } from './presenter-personas.js';
6
+ export const HARNESS_ROW_TEXT = {
7
+ RUN: 'Run Harness',
8
+ STARTING: 'Starting…',
9
+ STOP: 'Stop',
10
+ SIGNED_OUT: SIGN_UP_PITCH,
11
+ SIGN_IN: 'Sign in',
12
+ UNLINKED: 'This project is not on your dashboard yet.',
13
+ LINK: 'Link this project',
14
+ LINKING: 'Linking…',
15
+ NOT_ENTITLED: 'Harness is not on your plan',
16
+ NO_CREDITS: 'No Harness credits left',
17
+ NO_PROVIDER: 'No model provider set',
18
+ OFF: 'Harness is off',
19
+ canSwitchOn: (percent) => `Setup score ${String(percent)}%. Harness is off.`,
20
+ TURN_ON: 'Switch on',
21
+ COPY_PROMPT: 'Copy setup prompt',
22
+ COPIED: 'Copied',
23
+ SET_UP: 'Set up ↗',
24
+ PLANS: 'See plans ↗',
25
+ PLAN: 'Plan ↗',
26
+ MORE: 'Get more ↗',
27
+ driving: (as, steps) => `Driving${as === undefined ? '' : ` as ${as}`} · ${String(steps)} step${1 === steps ? '' : 's'}`,
28
+ NAME: 'Harness',
29
+ TOGGLE: 'Reticle Harness on this project',
30
+ creditsLeft: (left, days) => `${left.toLocaleString('en-US')} credit${1 === left ? '' : 's'}${days === undefined ? '' : ` · ${String(days)} day${1 === days ? '' : 's'}`}`,
31
+ FOOT_LINK: 'Link project',
32
+ FOOT_NO_CREDITS: 'No credits',
33
+ FOOT_NOT_ENTITLED: 'Not on your plan',
34
+ FOOT_NO_PROVIDER: 'No model provider',
35
+ footSetup: (percent) => `Setup ${String(percent)}% · needs 80%`,
36
+ PAGE_TITLE: 'Reticle Harness',
37
+ LOG_LABEL: 'What the Harness did',
38
+ LOG_EMPTY: 'Runs show here, step by step.',
39
+ PAGE_SUBTITLE: 'Tests your app by itself',
40
+ };
41
+ export const HARNESS_SWITCH_ON_ATTR = 'data-reticle-harness-switch';
42
+ export const HARNESS_LINK_ATTR = 'data-reticle-harness-link';
43
+ export const HARNESS_OPEN_ATTR = 'data-reticle-harness-open';
44
+ export const HARNESS_LOG_ATTR = 'data-reticle-harness-log';
45
+ export const HARNESS_TOGGLE_ATTR = 'data-reticle-harness-toggle';
46
+ export const HARNESS_COPY_PROMPT_ATTR = 'data-reticle-harness-copy-prompt';
47
+ export function harnessGateIn(instrumentation, config) {
48
+ const local = localGateIn(instrumentation);
49
+ const platform = config?.gate;
50
+ if (platform === undefined)
51
+ return local;
52
+ const prompt = platform.prompt ?? local?.prompt;
53
+ const reason = platform.unlocked ? undefined : (platform.reason ?? local?.reason);
54
+ return {
55
+ percent: platform.percent ?? local?.percent ?? 0,
56
+ unlocked: platform.unlocked,
57
+ ...(reason === undefined ? {} : { reason }),
58
+ ...(prompt === undefined ? {} : { prompt }),
59
+ };
60
+ }
61
+ function localGateIn(instrumentation) {
62
+ const raw = instrumentation?.['harnessGate'];
63
+ if ('object' !== typeof raw || null === raw)
64
+ return undefined;
65
+ const gate = raw;
66
+ const percent = gate['percent'];
67
+ const unlocked = gate['unlocked'];
68
+ if ('number' !== typeof percent || 'boolean' !== typeof unlocked)
69
+ return undefined;
70
+ const reason = gate['reason'];
71
+ const prompt = instrumentation?.['prompt'];
72
+ return {
73
+ percent: Math.round(percent),
74
+ unlocked,
75
+ ...('string' === typeof reason ? { reason } : {}),
76
+ ...('string' === typeof prompt ? { prompt } : {}),
77
+ };
78
+ }
79
+ const row = (inner) => `<div class="reticle-harness-row">${inner}</div>`;
80
+ const wrapRow = (inner) => `<div class="reticle-harness-row reticle-harness-row-wrap">${inner}</div>`;
81
+ const said = (text) => `<span class="reticle-harness-said">${text}</span>`;
82
+ const runRow = (lead, button) => row(`<span class="reticle-harness-lead">${lead}</span>${button}`);
83
+ const STOP_BUTTON = `<button type="button" data-reticle-harness-stop class="reticle-harness-run reticle-harness-stop">${HARNESS_ROW_TEXT.STOP}</button>`;
84
+ const link = (href, label) => `<a class="reticle-harness-link" href="${href}" target="_blank" rel="noopener noreferrer">${label}</a>`;
85
+ export function creditsShort(credits) {
86
+ return credits === undefined || creditsSpent(credits) ? '' : creditsUsageText(credits);
87
+ }
88
+ function creditsUsedRow(credits, planUrl) {
89
+ if (credits.kind === undefined)
90
+ return row(`${said(HARNESS_ROW_TEXT.NO_CREDITS)}${link(planUrl, HARNESS_ROW_TEXT.MORE)}`);
91
+ const text = creditsUsedText(credits);
92
+ const action = text.action !== undefined
93
+ ? link(planUrl, `${text.action} ↗`)
94
+ : CreditKind.TRIAL === credits.kind
95
+ ? link(planUrl, HARNESS_ROW_TEXT.PLAN)
96
+ : '';
97
+ return wrapRow(`${said(text.said)}${action}`);
98
+ }
99
+ function footState(s) {
100
+ if (true === s.starting)
101
+ return HARNESS_ROW_TEXT.STARTING;
102
+ const gate = s.gate;
103
+ if (gate !== undefined && !gate.unlocked && gate.reason !== undefined)
104
+ return HARNESS_ROW_TEXT.footSetup(gate.percent);
105
+ const config = s.config;
106
+ if (config === undefined)
107
+ return HARNESS_ROW_TEXT.FOOT_LINK;
108
+ if (config.credits !== undefined && creditsSpent(config.credits))
109
+ return HARNESS_ROW_TEXT.FOOT_NO_CREDITS;
110
+ if (!config.harnessEntitled)
111
+ return HARNESS_ROW_TEXT.FOOT_NOT_ENTITLED;
112
+ if (false === config.providerReady)
113
+ return HARNESS_ROW_TEXT.FOOT_NO_PROVIDER;
114
+ const credits = config.credits;
115
+ return credits === undefined
116
+ ? ''
117
+ : HARNESS_ROW_TEXT.creditsLeft(Math.max(0, credits.limit - credits.used), credits.daysLeft);
118
+ }
119
+ function switchable(s) {
120
+ const config = s.config;
121
+ if (config === undefined)
122
+ return false;
123
+ if (s.gate !== undefined && !s.gate.unlocked && s.gate.reason !== undefined)
124
+ return false;
125
+ if (config.credits !== undefined && creditsSpent(config.credits))
126
+ return false;
127
+ return config.harnessEntitled && false !== config.providerReady;
128
+ }
129
+ export function harnessFootHtml(s) {
130
+ if (s.account === undefined || !s.account.signedIn)
131
+ return '';
132
+ if (s.drive !== undefined && s.canDrive)
133
+ return row(`<button type="button" ${HARNESS_OPEN_ATTR} class="reticle-harness-open"><span class="reticle-harness-name">${HARNESS_ROW_TEXT.NAME}</span><span class="reticle-harness-state">${esc(HARNESS_ROW_TEXT.driving(s.startedAs === undefined ? undefined : personaLabel(s.startedAs), s.drive.steps))}</span><span class="reticle-harness-chev" aria-hidden="true">›</span></button>${STOP_BUTTON}`);
134
+ const on = true === s.config?.harnessEnabled;
135
+ const credits = s.config?.credits;
136
+ const title = credits === undefined ? '' : ` title="${esc(creditsUsageText(credits))}"`;
137
+ const toggle = switchable(s)
138
+ ? `<button type="button" role="switch" ${HARNESS_TOGGLE_ATTR} class="reticle-harness-toggle" aria-checked="${String(on)}" aria-label="${HARNESS_ROW_TEXT.TOGGLE}"><span class="reticle-harness-knob"></span></button>`
139
+ : '';
140
+ return row(`<button type="button" ${HARNESS_OPEN_ATTR} class="reticle-harness-open"${title}><span class="reticle-harness-name">${HARNESS_ROW_TEXT.NAME}</span><span class="reticle-harness-state">${esc(footState(s))}</span><span class="reticle-harness-chev" aria-hidden="true">›</span></button>${toggle}`);
141
+ }
142
+ export function harnessRowHtml(s) {
143
+ if (s.account === undefined)
144
+ return '';
145
+ if (!s.account.signedIn)
146
+ return wrapRow(`${said(HARNESS_ROW_TEXT.SIGNED_OUT)}<button type="button" ${ACCOUNT_SIGNIN_ATTR} class="reticle-harness-link">${HARNESS_ROW_TEXT.SIGN_IN}</button>`);
147
+ if (s.drive !== undefined && s.canDrive)
148
+ return (runRow(said(HARNESS_ROW_TEXT.driving(s.startedAs === undefined ? undefined : personaLabel(s.startedAs), s.drive.steps)), STOP_BUTTON) + personaCustomHtml(s.pick, true));
149
+ const gate = s.gate;
150
+ if (gate !== undefined && !gate.unlocked && gate.reason !== undefined)
151
+ return wrapRow(`${said(esc(gate.reason))}${gate.prompt === undefined
152
+ ? ''
153
+ : `<button type="button" ${HARNESS_COPY_PROMPT_ATTR} class="reticle-harness-link">${HARNESS_ROW_TEXT.COPY_PROMPT}</button>`}`);
154
+ const config = s.config;
155
+ if (config === undefined)
156
+ return wrapRow(`${said(HARNESS_ROW_TEXT.UNLINKED)}<button type="button" ${HARNESS_LINK_ATTR} class="reticle-harness-run"${true === s.linking ? ' disabled' : ''}>${true === s.linking ? HARNESS_ROW_TEXT.LINKING : HARNESS_ROW_TEXT.LINK}</button>`);
157
+ if (config.credits !== undefined && creditsSpent(config.credits))
158
+ return creditsUsedRow(config.credits, s.planUrl);
159
+ if (!config.harnessEntitled)
160
+ return row(`${said(HARNESS_ROW_TEXT.NOT_ENTITLED)}${link(s.planUrl, HARNESS_ROW_TEXT.PLANS)}`);
161
+ if (false === config.providerReady)
162
+ return row(`${said(HARNESS_ROW_TEXT.NO_PROVIDER)}${link(s.setupUrl, HARNESS_ROW_TEXT.SET_UP)}`);
163
+ if (!config.harnessEnabled)
164
+ return row(`${said(gate === undefined ? HARNESS_ROW_TEXT.OFF : HARNESS_ROW_TEXT.canSwitchOn(gate.percent))}<button type="button" ${HARNESS_SWITCH_ON_ATTR} class="reticle-harness-run">${HARNESS_ROW_TEXT.TURN_ON}</button>`);
165
+ if (!s.canDrive)
166
+ return '';
167
+ const starting = true === s.starting;
168
+ return (runRow(personaPickHtml(s.pick, starting), `<button type="button" data-reticle-harness-run class="reticle-harness-run"${starting ? ' disabled' : ''}>${starting ? HARNESS_ROW_TEXT.STARTING : HARNESS_ROW_TEXT.RUN}</button>`) + personaCustomHtml(s.pick, starting));
169
+ }
170
+ export const HARNESS_ROW_CSS = `
171
+ [data-reticle-chat-panel] .reticle-harness-spot{flex:none;display:flex;flex-direction:column;gap:4px;}
172
+ [data-reticle-chat-panel] .reticle-harness-spot[hidden],[data-reticle-chat-panel] .reticle-harness-spot:empty{display:none;}
173
+ [data-reticle-chat-panel] .reticle-harness-row{display:flex;align-items:center;gap:6px;min-height:30px;min-width:0;}
174
+ [data-reticle-chat-panel] .reticle-harness-lead{flex:1;min-width:0;display:flex;align-items:center;gap:6px;}
175
+ [data-reticle-chat-panel] .reticle-harness-said{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--reticle-hud-size-sm);color:var(--reticle-hud-text);}
176
+ [data-reticle-chat-panel] .reticle-harness-row-wrap{flex-wrap:wrap;row-gap:2px;padding:4px 0;}
177
+ [data-reticle-chat-panel] .reticle-harness-row-wrap .reticle-harness-said{flex:1 1 100%;white-space:normal;}
178
+ [data-reticle-chat-panel] .reticle-harness-said code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;}
179
+ [data-reticle-chat-panel] .reticle-harness-as{flex:none;font-size:var(--reticle-hud-size-xs);color:var(--reticle-hud-text-muted);}
180
+ [data-reticle-chat-panel] .reticle-harness-persona{box-sizing:border-box;flex:1;min-width:0;width:100%;height:30px;font:inherit;font-size:var(--reticle-hud-size-sm);color:var(--reticle-hud-text);
181
+ background:var(--reticle-hud-ground);border:1px solid var(--reticle-hud-border-strong);border-radius:var(--reticle-hud-radius-sm);padding:0 var(--reticle-hud-space-2);}
182
+ [data-reticle-chat-panel] .reticle-harness-spot > .reticle-harness-persona{flex:none;}
183
+ [data-reticle-chat-panel] select.reticle-harness-persona{padding-right:2px;text-overflow:ellipsis;}
184
+ [data-reticle-chat-panel] .reticle-harness-persona::placeholder{color:var(--reticle-hud-text-faint);}
185
+ [data-reticle-chat-panel] .reticle-harness-persona:focus-visible{outline:2px solid var(--reticle-hud-brand);outline-offset:0;}
186
+ [data-reticle-chat-panel] .reticle-harness-run{flex:none;box-sizing:border-box;height:30px;padding:0 12px;border:0;border-radius:var(--reticle-hud-radius-sm);cursor:pointer;white-space:nowrap;
187
+ background:var(--reticle-hud-brand);color:#160b02;font:inherit;font-size:var(--reticle-hud-size-sm);font-weight:700;}
188
+ [data-reticle-chat-panel] .reticle-harness-run:hover{filter:brightness(1.08);}
189
+ [data-reticle-chat-panel] .reticle-harness-run:focus-visible{outline:2px solid var(--reticle-hud-text);outline-offset:2px;}
190
+ [data-reticle-chat-panel] .reticle-harness-stop{background:transparent;color:var(--reticle-hud-text);border:1px solid var(--reticle-hud-border-strong);}
191
+ [data-reticle-chat-panel] .reticle-harness-link{flex:none;border:0;padding:0;background:none;cursor:pointer;font:inherit;color:var(--reticle-hud-brand);font-size:var(--reticle-hud-size-sm);font-weight:600;text-decoration:none;white-space:nowrap;}
192
+ [data-reticle-chat-panel] .reticle-harness-link:hover{text-decoration:underline;}
193
+ [data-reticle-chat-panel] .reticle-harness-open{flex:1;min-width:0;height:30px;display:flex;align-items:center;gap:8px;padding:0 var(--reticle-hud-space-2);border:0;border-radius:var(--reticle-hud-radius-sm);background:transparent;cursor:pointer;
194
+ text-align:left;white-space:nowrap;font:inherit;font-size:var(--reticle-hud-size-sm);color:var(--reticle-hud-text);}
195
+ [data-reticle-chat-panel] .reticle-harness-name{flex:none;font-weight:600;}
196
+ [data-reticle-chat-panel] .reticle-harness-state{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;color:var(--reticle-hud-text-muted);font-size:11px;font-variant-numeric:tabular-nums;}
197
+ [data-reticle-chat-panel] .reticle-harness-chev{flex:none;color:var(--reticle-hud-text-faint);}
198
+ [data-reticle-chat-panel] .reticle-harness-toggle{flex:none;position:relative;width:30px;height:18px;padding:0;border:0;border-radius:9px;cursor:pointer;background:var(--reticle-hud-border-strong);transition:background .15s;}
199
+ [data-reticle-chat-panel] .reticle-harness-toggle[aria-checked="true"]{background:var(--reticle-hud-brand);}
200
+ [data-reticle-chat-panel] .reticle-harness-knob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:transform .15s;}
201
+ [data-reticle-chat-panel] .reticle-harness-toggle[aria-checked="true"] .reticle-harness-knob{transform:translateX(12px);}
202
+ [data-reticle-chat-panel] .reticle-harness-toggle:focus-visible{outline:2px solid var(--reticle-hud-text);outline-offset:2px;}
203
+ [data-reticle-chat-panel] .reticle-harness-open:hover{background:var(--reticle-hud-hover);}
204
+ [data-reticle-chat-panel] .reticle-harness-open:focus-visible{outline:2px solid var(--reticle-hud-brand);outline-offset:0;}
205
+ [data-reticle-chat-panel] .reticle-harness-run:disabled,[data-reticle-chat-panel] .reticle-harness-persona:disabled{opacity:.6;cursor:default;}
206
+ [data-reticle-chat-panel] .reticle-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
207
+ [data-reticle-chat-panel] .reticle-foot-meta{margin:0;font-size:11px;line-height:1.3;color:var(--reticle-hud-text-muted);overflow-wrap:anywhere;}
208
+ [data-reticle-chat-panel] .reticle-foot-meta:empty{display:none;}
209
+ `;
@@ -0,0 +1,35 @@
1
+ export declare const CUSTOM_PERSONA = "custom";
2
+ export declare const PERSONAS: readonly [{
3
+ readonly id: "first-time";
4
+ readonly label: "First-time visitor";
5
+ readonly hint: "starts from the first page, reads the labels and follows the obvious path";
6
+ }, {
7
+ readonly id: "power-user";
8
+ readonly label: "Returning power user";
9
+ readonly hint: "goes straight to the main task, takes shortcuts and repeats it quickly";
10
+ }, {
11
+ readonly id: "phone";
12
+ readonly label: "On a phone, slow connection";
13
+ readonly hint: "small-screen habits: taps, waits on loading, retries when nothing seems to happen";
14
+ }, {
15
+ readonly id: "keyboard";
16
+ readonly label: "Keyboard only";
17
+ readonly hint: "Tab, Enter and Escape only: focus order, traps and controls it cannot reach";
18
+ }, {
19
+ readonly id: "careless";
20
+ readonly label: "Careless user (bad and unexpected input)";
21
+ readonly hint: "empty fields, wrong formats, very long text, double submits, going back mid-flow";
22
+ }, {
23
+ readonly id: "admin";
24
+ readonly label: "Admin / settings";
25
+ readonly hint: "changes settings and permissions, then checks they stick after a reload";
26
+ }];
27
+ export declare const DEFAULT_PERSONA: "first-time";
28
+ export declare const PERSONA_PICK_ID = "reticle-harness-persona-pick";
29
+ export declare function personaText(pick: string, custom: string): string | undefined;
30
+ export declare function personaLabel(pick: string): string;
31
+ export declare function personaPickHtml(pick: string, disabled?: boolean): string;
32
+ export declare function personaCustomHtml(pick: string, disabled?: boolean): string;
33
+ export declare function knownPick(value: string | null | undefined): string | undefined;
34
+ export declare function readPick(projectId: string | undefined): string;
35
+ export declare function savePick(projectId: string | undefined, pick: string): void;