@reticlehq/browser 3.6.0 → 3.8.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 (129) hide show
  1. package/dist/actions/action-report.d.ts +64 -0
  2. package/dist/actions/action-report.js +87 -0
  3. package/dist/actions/actions-dom.d.ts +5 -0
  4. package/dist/actions/actions-dom.js +32 -4
  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 +2 -37
  8. package/dist/actions/actions.js +56 -80
  9. package/dist/actions/appeared-text.js +5 -2
  10. package/dist/actions/click-target.d.ts +1 -0
  11. package/dist/actions/click-target.js +14 -0
  12. package/dist/actions/danger-context.d.ts +4 -1
  13. package/dist/actions/danger-context.js +45 -4
  14. package/dist/actions/value-input.d.ts +1 -0
  15. package/dist/actions/value-input.js +11 -0
  16. package/dist/activate.d.ts +14 -0
  17. package/dist/activate.js +43 -0
  18. package/dist/busy.d.ts +3 -0
  19. package/dist/busy.js +42 -0
  20. package/dist/commands/commands.js +49 -17
  21. package/dist/connect-options.d.ts +12 -1
  22. package/dist/connect-options.js +18 -1
  23. package/dist/dom/a11y.d.ts +2 -0
  24. package/dist/dom/a11y.js +353 -80
  25. package/dist/dom/addressing/coverage-markers.d.ts +6 -0
  26. package/dist/dom/addressing/coverage-markers.js +13 -0
  27. package/dist/dom/chart.d.ts +3 -2
  28. package/dist/dom/chart.js +3 -2
  29. package/dist/dom/dom-ignore.d.ts +8 -0
  30. package/dist/dom/dom-ignore.js +57 -2
  31. package/dist/dom/layout-audit.d.ts +15 -0
  32. package/dist/dom/layout-audit.js +208 -0
  33. package/dist/dom/paint-context.d.ts +15 -0
  34. package/dist/dom/paint-context.js +78 -0
  35. package/dist/dom/query.d.ts +2 -1
  36. package/dist/dom/query.js +163 -38
  37. package/dist/dom/render-page.d.ts +12 -0
  38. package/dist/dom/render-page.js +135 -0
  39. package/dist/dom/shadow-registry.d.ts +1 -0
  40. package/dist/dom/shadow-registry.js +27 -0
  41. package/dist/dom/snapshot.d.ts +5 -0
  42. package/dist/dom/snapshot.js +135 -36
  43. package/dist/dom/stable-selector.d.ts +1 -0
  44. package/dist/dom/stable-selector.js +19 -0
  45. package/dist/dom/text-match.d.ts +12 -0
  46. package/dist/dom/text-match.js +115 -0
  47. package/dist/dom/theme.d.ts +1 -0
  48. package/dist/dom/theme.js +21 -9
  49. package/dist/dom/virtualized.js +5 -1
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.js +5 -0
  52. package/dist/observers/blind-spots.js +3 -2
  53. package/dist/observers/canvas.d.ts +18 -0
  54. package/dist/observers/canvas.js +76 -0
  55. package/dist/observers/dom.js +96 -32
  56. package/dist/observers/field.d.ts +11 -0
  57. package/dist/observers/field.js +116 -30
  58. package/dist/observers/install-all.js +4 -0
  59. package/dist/observers/media.d.ts +2 -0
  60. package/dist/observers/media.js +32 -0
  61. package/dist/observers/net-detail/network-body.d.ts +2 -0
  62. package/dist/observers/net-detail/network-body.js +8 -1
  63. package/dist/observers/network.js +82 -13
  64. package/dist/observers/perf.js +53 -1
  65. package/dist/observers/route.d.ts +2 -0
  66. package/dist/observers/route.js +39 -9
  67. package/dist/presenter/carousel/carousel.js +2 -7
  68. package/dist/presenter/carousel/panel-slides.d.ts +2 -4
  69. package/dist/presenter/carousel/panel-slides.js +17 -15
  70. package/dist/presenter/chrome/log-memory.d.ts +1 -0
  71. package/dist/presenter/chrome/presenter-log.d.ts +7 -0
  72. package/dist/presenter/chrome/presenter-log.js +48 -8
  73. package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
  74. package/dist/presenter/chrome/presenter-morph.js +34 -0
  75. package/dist/presenter/chrome/presenter-report-copy.d.ts +12 -4
  76. package/dist/presenter/chrome/presenter-report-copy.js +12 -4
  77. package/dist/presenter/hud-telemetry.js +55 -4
  78. package/dist/presenter/presenter-account-styles.js +3 -3
  79. package/dist/presenter/presenter-account.d.ts +27 -5
  80. package/dist/presenter/presenter-account.js +83 -6
  81. package/dist/presenter/presenter-agent-link.d.ts +9 -0
  82. package/dist/presenter/presenter-agent-link.js +62 -0
  83. package/dist/presenter/presenter-chat-views.d.ts +25 -4
  84. package/dist/presenter/presenter-chat-views.js +339 -72
  85. package/dist/presenter/presenter-config.d.ts +3 -0
  86. package/dist/presenter/presenter-config.js +2 -0
  87. package/dist/presenter/presenter-controls.d.ts +0 -1
  88. package/dist/presenter/presenter-controls.js +13 -91
  89. package/dist/presenter/presenter-design-tokens.js +1 -1
  90. package/dist/presenter/presenter-frame.d.ts +3 -2
  91. package/dist/presenter/presenter-frame.js +31 -24
  92. package/dist/presenter/presenter-harness-row.d.ts +56 -0
  93. package/dist/presenter/presenter-harness-row.js +170 -0
  94. package/dist/presenter/presenter-personas.d.ts +35 -0
  95. package/dist/presenter/presenter-personas.js +95 -0
  96. package/dist/presenter/presenter-plan.js +25 -24
  97. package/dist/presenter/presenter-report-styles.js +23 -22
  98. package/dist/presenter/presenter-report.js +29 -13
  99. package/dist/presenter/presenter-run-state.js +2 -1
  100. package/dist/presenter/presenter-settings-styles.js +8 -4
  101. package/dist/presenter/presenter-settings.d.ts +4 -4
  102. package/dist/presenter/presenter-settings.js +66 -28
  103. package/dist/presenter/presenter-shell-styles.js +12 -3
  104. package/dist/presenter/presenter-shell.d.ts +11 -3
  105. package/dist/presenter/presenter-shell.js +60 -22
  106. package/dist/presenter/presenter-styles.js +0 -2
  107. package/dist/presenter/presenter-workspace.js +10 -3
  108. package/dist/presenter/presenter.d.ts +3 -2
  109. package/dist/presenter/presenter.js +107 -25
  110. package/dist/registry/declared-platform.js +2 -0
  111. package/dist/registry/store-adapters.d.ts +5 -2
  112. package/dist/registry/store-adapters.js +40 -10
  113. package/dist/reticle-inject.js +771 -703
  114. package/dist/reticle.d.ts +4 -2
  115. package/dist/reticle.js +72 -8
  116. package/dist/security/page-location.d.ts +2 -0
  117. package/dist/security/page-location.js +7 -0
  118. package/dist/security/serialization.js +1 -1
  119. package/dist/storage-keys.d.ts +4 -2
  120. package/dist/storage-keys.js +8 -3
  121. package/dist/timers/native/native-timers.d.ts +1 -0
  122. package/dist/timers/native/native-timers.js +5 -0
  123. package/dist/transport/transport.d.ts +1 -0
  124. package/dist/transport/transport.js +23 -3
  125. package/dist/transport/unreachable-message.d.ts +1 -1
  126. package/dist/transport/unreachable-message.js +3 -5
  127. package/package.json +6 -2
  128. package/dist/presenter/carousel/offer-card.d.ts +0 -21
  129. package/dist/presenter/carousel/offer-card.js +0 -56
@@ -1,7 +1,7 @@
1
- import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
1
+ import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, ResponseBodyReason, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
2
2
  import { captureMethod } from '../patching/capture-method.js';
3
3
  import { observeSafely, observeValue } from './types.js';
4
- import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, withBodyDeadline, } from './net-detail/network-body.js';
4
+ import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, skippedBodyReason, withBodyDeadline, } from './net-detail/network-body.js';
5
5
  import { redactUrl, netUrlFields } from './net-detail/network-redact.js';
6
6
  import { watchStreamedBody } from './network-stream.js';
7
7
  import { requireCapturedMethod } from '../util/captured-method.js';
@@ -63,15 +63,64 @@ function jsonShape(text) {
63
63
  return ShapeToken.OTHER;
64
64
  }
65
65
  }
66
+ const MAX_DECODED_BODY_BYTES = 1 << 20;
67
+ const TAB = 0x09;
68
+ const LINE_FEED = 0x0a;
69
+ const CARRIAGE_RETURN = 0x0d;
70
+ const FIRST_PRINTABLE = 0x20;
71
+ const DELETE = 0x7f;
72
+ function hasControlCharacter(text) {
73
+ for (let i = 0; i < text.length; i += 1) {
74
+ const code = text.charCodeAt(i);
75
+ if (code === DELETE)
76
+ return true;
77
+ if (code < FIRST_PRINTABLE && code !== TAB && code !== LINE_FEED && code !== CARRIAGE_RETURN) {
78
+ return true;
79
+ }
80
+ }
81
+ return false;
82
+ }
83
+ function textOfBytes(body) {
84
+ if (!(body instanceof ArrayBuffer || ArrayBuffer.isView(body)))
85
+ return undefined;
86
+ if (body.byteLength > MAX_DECODED_BODY_BYTES)
87
+ return undefined;
88
+ try {
89
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(body);
90
+ return hasControlCharacter(text) ? undefined : text;
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
96
+ function formShape(form) {
97
+ const fields = [];
98
+ let length = 0;
99
+ form.forEach((value, name) => {
100
+ if ('string' === typeof value)
101
+ length += value.length;
102
+ fields.push(JSON.stringify([name, 'string' === typeof value ? 0 : [value.type, value.size]]));
103
+ });
104
+ return 0 === fields.length ? undefined : fingerprintBody(fields.sort().join(), length);
105
+ }
66
106
  function projectRequestBody(body, captureBodies) {
67
107
  let text;
68
108
  let contentType = 'application/json';
69
109
  let shape = REQUEST_SHAPE_NONE;
70
- if ('string' === typeof body) {
71
- text = body;
110
+ const bytesText = textOfBytes(body);
111
+ const typeOf = () => body.constructor?.name ?? typeof body;
112
+ if ('string' === typeof body || bytesText !== undefined) {
113
+ text = bytesText ?? body;
72
114
  if (text.length > 0)
73
115
  shape = fingerprintBody(jsonShape(text), text.length);
74
116
  }
117
+ else if (typeof FormData !== 'undefined' && body instanceof FormData) {
118
+ const form = formShape(body);
119
+ return {
120
+ ...(form === undefined ? {} : { [REQUEST_SHAPE_FIELD]: form }),
121
+ ...(captureBodies ? { requestBodyType: typeOf() } : {}),
122
+ };
123
+ }
75
124
  else if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
76
125
  text = body.toString();
77
126
  contentType = 'application/x-www-form-urlencoded';
@@ -81,8 +130,7 @@ function projectRequestBody(body, captureBodies) {
81
130
  }
82
131
  }
83
132
  else if (body !== undefined && body !== null) {
84
- const kind = body.constructor?.name ?? typeof body;
85
- return captureBodies ? { requestBodyType: kind } : {};
133
+ return captureBodies ? { requestBodyType: typeOf() } : {};
86
134
  }
87
135
  const fields = { [REQUEST_SHAPE_FIELD]: shape };
88
136
  if (!captureBodies || text === undefined || 0 === text.length)
@@ -182,7 +230,13 @@ function isNativeFetch(fn) {
182
230
  }
183
231
  }
184
232
  const OURS = new WeakSet();
233
+ const PAGE_CAPTURE = Symbol.for('reticle.network');
234
+ const pageSlots = () => window;
185
235
  export function installNetwork(emit, opts = {}) {
236
+ if (undefined !== pageSlots()[PAGE_CAPTURE])
237
+ return () => undefined;
238
+ const owner = {};
239
+ pageSlots()[PAGE_CAPTURE] = owner;
186
240
  const captureBodies = true === opts.captureBodies;
187
241
  const captureErrorBodies = false !== opts.captureErrorBodies;
188
242
  const reinterpret = opts.reinterpret;
@@ -202,6 +256,7 @@ export function installNetwork(emit, opts = {}) {
202
256
  const method = methodOf(input, init);
203
257
  const nextAction = nextActionOf(input, init);
204
258
  const nextActionFields = nextAction === undefined ? {} : { [NEXT_ACTION_FIELD]: nextAction };
259
+ const bodyFields = observeValue(() => projectRequestBody(init?.body, captureBodies)) ?? {};
205
260
  const urlFields = netUrlFields(rawUrl);
206
261
  const url = urlFields.url;
207
262
  const initiatorStack = observeValue(() => initiatorFrame());
@@ -236,7 +291,7 @@ export function installNetwork(emit, opts = {}) {
236
291
  ...initiatorFields,
237
292
  ...resourceTiming(rawUrl),
238
293
  ...netResponseMeta(res.statusText, contentType, res.headers.get('content-length')),
239
- ...projectRequestBody(init?.body, captureBodies),
294
+ ...bodyFields,
240
295
  ...nextActionFields,
241
296
  ...responseBodyFields,
242
297
  ...(reinterpret?.(url, (name) => res.headers.get(name)) ?? {}),
@@ -252,7 +307,9 @@ export function installNetwork(emit, opts = {}) {
252
307
  catch {
253
308
  }
254
309
  void (async () => {
255
- let responseBodyFields = {};
310
+ let responseBodyFields = captureBodies
311
+ ? { responseBodyReason: ResponseBodyReason.UNREADABLE }
312
+ : {};
256
313
  if (clone !== undefined) {
257
314
  try {
258
315
  const text = await withBodyDeadline(clone.text());
@@ -263,7 +320,7 @@ export function installNetwork(emit, opts = {}) {
263
320
  responseBodyFields = {
264
321
  responseBody: body,
265
322
  ...(truncated ? { responseBodyTruncated: true } : {}),
266
- ...(captureBodies ? {} : { responseBodyReason: 'error-status' }),
323
+ ...(captureBodies ? {} : { responseBodyReason: ResponseBodyReason.ERROR_STATUS }),
267
324
  };
268
325
  }
269
326
  }
@@ -278,7 +335,8 @@ export function installNetwork(emit, opts = {}) {
278
335
  })().catch(() => undefined);
279
336
  }
280
337
  else {
281
- emitRequest({});
338
+ const skipped = captureBodies ? skippedBodyReason(contentType) : undefined;
339
+ emitRequest(skipped === undefined ? {} : { responseBodyReason: skipped });
282
340
  }
283
341
  return res;
284
342
  }
@@ -326,7 +384,7 @@ export function installNetwork(emit, opts = {}) {
326
384
  const m = meta.get(this);
327
385
  if (m !== undefined) {
328
386
  m.start = performance.now();
329
- m.reqBody = body ?? null;
387
+ m.reqFields = observeValue(() => projectRequestBody(body ?? null, captureBodies)) ?? {};
330
388
  m.initiatorStack = initiatorFrame();
331
389
  const initiatorFields = m.initiatorStack === undefined ? {} : { initiatorStack: m.initiatorStack };
332
390
  emit(EventType.NET_PENDING, {
@@ -348,6 +406,13 @@ export function installNetwork(emit, opts = {}) {
348
406
  let responseBodyFields = {};
349
407
  const textReadable = '' === this.responseType || 'text' === this.responseType;
350
408
  const wantXhrErrorBody = shouldCaptureErrorBody(this.status, captureErrorBodies);
409
+ if (captureBodies) {
410
+ const skipped = isCapturableType(xhrContentType)
411
+ ? ResponseBodyReason.UNREADABLE
412
+ : skippedBodyReason(xhrContentType);
413
+ if (skipped !== undefined)
414
+ responseBodyFields = { responseBodyReason: skipped };
415
+ }
351
416
  if ((captureBodies || wantXhrErrorBody) &&
352
417
  textReadable &&
353
418
  isCapturableType(xhrContentType)) {
@@ -358,7 +423,9 @@ export function installNetwork(emit, opts = {}) {
358
423
  responseBodyFields = {
359
424
  responseBody: rb,
360
425
  ...(truncated ? { responseBodyTruncated: true } : {}),
361
- ...(captureBodies ? {} : { responseBodyReason: 'error-status' }),
426
+ ...(captureBodies
427
+ ? {}
428
+ : { responseBodyReason: ResponseBodyReason.ERROR_STATUS }),
362
429
  };
363
430
  }
364
431
  catch {
@@ -375,7 +442,7 @@ export function installNetwork(emit, opts = {}) {
375
442
  ...(cur.initiatorStack === undefined ? {} : { initiatorStack: cur.initiatorStack }),
376
443
  ...resourceTiming(cur.rawUrl),
377
444
  ...netResponseMeta(this.statusText, xhrContentType, this.getResponseHeader('content-length')),
378
- ...projectRequestBody(cur.reqBody, captureBodies),
445
+ ...cur.reqFields,
379
446
  ...responseBodyFields,
380
447
  });
381
448
  });
@@ -530,6 +597,8 @@ export function installNetwork(emit, opts = {}) {
530
597
  }
531
598
  }
532
599
  return () => {
600
+ if (pageSlots()[PAGE_CAPTURE] === owner)
601
+ delete pageSlots()[PAGE_CAPTURE];
533
602
  subresourceObserver?.disconnect();
534
603
  if (window.fetch === patchedFetch)
535
604
  window.fetch = origFetch;
@@ -1,4 +1,50 @@
1
1
  import { EventType, PerfMetric } from '@reticlehq/core';
2
+ import { refs } from '../dom/addressing/refs.js';
3
+ import { getTestIdAttr, readTestId } from '../dom/addressing/testid-attr.js';
4
+ import { isReticleOverlay } from '../dom/dom-ignore.js';
5
+ import { isElement } from '../dom/realm.js';
6
+ const areaOf = (r) => r === undefined ? 0 : Math.max(0, r.width) * Math.max(0, r.height);
7
+ function cssIdent(value) {
8
+ const css = globalThis.CSS;
9
+ if ('function' === typeof css?.escape)
10
+ return css.escape(value);
11
+ let out = '';
12
+ for (const [i, ch] of [...value].entries()) {
13
+ const startsWithDigit = /\d/.test(ch) && (0 === i || (1 === i && value.startsWith('-')));
14
+ if (startsWithDigit)
15
+ out += `\\${ch.charCodeAt(0).toString(16)} `;
16
+ else if (/[\w-]/.test(ch))
17
+ out += ch;
18
+ else
19
+ out += `\\${ch}`;
20
+ }
21
+ return out;
22
+ }
23
+ function shortSelector(el) {
24
+ const testid = readTestId(el);
25
+ if (null !== testid)
26
+ return `[${getTestIdAttr()}="${testid.replace(/["\\]/g, '\\$&')}"]`;
27
+ const tag = el.tagName.toLowerCase();
28
+ if (0 < el.id.length)
29
+ return `${tag}#${cssIdent(el.id)}`;
30
+ const cls = el.classList.item(0);
31
+ return null === cls ? tag : `${tag}.${cssIdent(cls)}`;
32
+ }
33
+ function largestShifted(sources) {
34
+ let best;
35
+ for (const source of sources ?? []) {
36
+ const node = source.node ?? null;
37
+ const el = null === node ? null : isElement(node) ? node : node.parentElement;
38
+ if (null === el || isReticleOverlay(el))
39
+ continue;
40
+ const area = Math.max(areaOf(source.previousRect), areaOf(source.currentRect));
41
+ if (best === undefined || area > best.area)
42
+ best = { el, area };
43
+ }
44
+ return best === undefined
45
+ ? undefined
46
+ : { ref: refs.refFor(best.el), selector: shortSelector(best.el) };
47
+ }
2
48
  export function installPerf(emit) {
3
49
  if (typeof PerformanceObserver !== 'function')
4
50
  return () => undefined;
@@ -29,7 +75,13 @@ export function installPerf(emit) {
29
75
  if (true === ls.hadRecentInput)
30
76
  return;
31
77
  cls += ls.value ?? 0;
32
- emit(EventType.PERF, { metric: PerfMetric.CLS, value: cls, at: Math.round(e.startTime) });
78
+ const shifted = largestShifted(ls.sources);
79
+ emit(EventType.PERF, {
80
+ metric: PerfMetric.CLS,
81
+ value: cls,
82
+ at: Math.round(e.startTime),
83
+ ...(shifted === undefined ? {} : { shifted }),
84
+ });
33
85
  });
34
86
  observe('longtask', (e) => {
35
87
  emit(EventType.PERF, {
@@ -1,2 +1,4 @@
1
1
  import { type Emit, type Teardown } from './types.js';
2
+ export declare function declareRoutes(patterns: readonly string[]): void;
3
+ export declare function patternOf(pathname: string): string | undefined;
2
4
  export declare function installRoute(emit: Emit): Teardown;
@@ -1,6 +1,7 @@
1
- import { EventType } from '@reticlehq/core';
1
+ import { EventType, ROUTE_CHANGE_HOW_FIELD, RouteChangeHow } from '@reticlehq/core';
2
2
  import { observeSafely } from './types.js';
3
3
  import { captureMethod } from '../patching/capture-method.js';
4
+ import { redactUrl } from '@reticlehq/core';
4
5
  function snapshotLocation() {
5
6
  return {
6
7
  pathname: location.pathname,
@@ -9,6 +10,32 @@ function snapshotLocation() {
9
10
  href: location.href,
10
11
  };
11
12
  }
13
+ let declared = [];
14
+ export function declareRoutes(patterns) {
15
+ declared = patterns.map((pattern) => ({ pattern, parts: pattern.split('/').filter(Boolean) }));
16
+ }
17
+ function nextPattern() {
18
+ const raw = globalThis.next?.router?.pathname;
19
+ if ('string' !== typeof raw || !raw.startsWith('/'))
20
+ return undefined;
21
+ return raw.replace(/\[\.\.\.[^\]]+\]/g, '*').replace(/\[([^\]]+)\]/g, ':$1');
22
+ }
23
+ export function patternOf(pathname) {
24
+ const segments = pathname.split('/').filter(Boolean);
25
+ let best;
26
+ for (const { pattern, parts } of declared) {
27
+ const rest = '*' === parts.at(-1);
28
+ const fixed = rest ? parts.slice(0, -1) : parts;
29
+ if (rest ? segments.length < fixed.length : segments.length !== fixed.length)
30
+ continue;
31
+ if (!fixed.every((part, i) => part.startsWith(':') || part === segments[i]))
32
+ continue;
33
+ const params = parts.filter((part) => part.startsWith(':') || '*' === part).length;
34
+ if (best === undefined || params < best.params)
35
+ best = { pattern, params };
36
+ }
37
+ return best?.pattern ?? nextPattern();
38
+ }
12
39
  export function installRoute(emit) {
13
40
  const ac = new AbortController();
14
41
  const origPush = captureMethod(history, 'pushState');
@@ -16,35 +43,38 @@ export function installRoute(emit) {
16
43
  const callPush = origPush.bind(history);
17
44
  const callReplace = origReplace.bind(history);
18
45
  let lastHref = location.href;
19
- const fire = (from) => {
46
+ const fire = (from, how) => {
20
47
  observeSafely(() => {
21
48
  const to = snapshotLocation();
22
49
  if (to.href === from)
23
50
  return;
24
51
  lastHref = to.href;
52
+ const pattern = patternOf(to.pathname);
25
53
  emit(EventType.ROUTE_CHANGE, {
26
- from,
27
- to: to.href,
54
+ from: redactUrl(from),
55
+ to: redactUrl(to.href),
28
56
  pathname: to.pathname,
29
- search: to.search,
30
- hash: to.hash,
57
+ ...(pattern === undefined ? {} : { pattern }),
58
+ search: redactUrl(to.search),
59
+ hash: redactUrl(to.hash),
60
+ [ROUTE_CHANGE_HOW_FIELD]: how,
31
61
  });
32
62
  });
33
63
  };
34
64
  const patchedPush = (data, unused, url) => {
35
65
  const from = location.href;
36
66
  callPush(data, unused, url ?? null);
37
- fire(from);
67
+ fire(from, RouteChangeHow.PUSH);
38
68
  };
39
69
  const patchedReplace = (data, unused, url) => {
40
70
  const from = location.href;
41
71
  callReplace(data, unused, url ?? null);
42
- fire(from);
72
+ fire(from, RouteChangeHow.REPLACE);
43
73
  };
44
74
  history.pushState = patchedPush;
45
75
  history.replaceState = patchedReplace;
46
76
  const onNav = () => {
47
- fire(lastHref);
77
+ fire(lastHref, RouteChangeHow.POP);
48
78
  };
49
79
  window.addEventListener('popstate', onNav, { signal: ac.signal });
50
80
  window.addEventListener('hashchange', onNav, { signal: ac.signal });
@@ -135,7 +135,7 @@ function startRotation(log, root, count) {
135
135
  export const CAROUSEL_CSS = `
136
136
  .reticle-carousel{position:relative;margin:8px 10px;padding:10px 12px 8px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}
137
137
  .reticle-carousel-track{height:100px;overflow:hidden;}
138
- .reticle-carousel-slide .reticle-offer,.reticle-carousel-slide .reticle-talk{margin:0;padding:0;border:0;background:none;}
138
+ .reticle-carousel-slide .reticle-talk{margin:0;padding:0;border:0;background:none;}
139
139
  .reticle-carousel-close{position:absolute;top:4px;right:6px;width:20px;height:20px;padding:0;border:0;background:none;color:inherit;opacity:.55;cursor:pointer;font-size:14px;line-height:20px;}
140
140
  .reticle-carousel-close:hover{opacity:1;}
141
141
  .reticle-carousel-dots{display:flex;justify-content:center;gap:6px;margin-top:6px;}
@@ -144,10 +144,5 @@ export const CAROUSEL_CSS = `
144
144
  .reticle-carousel-controls{display:flex;align-items:center;gap:8px;margin-top:6px;}
145
145
  .reticle-carousel-arrow{width:24px;height:24px;flex:none;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--reticle-line);border-radius:6px;background:rgba(255,255,255,.04);color:var(--reticle-fg);font:inherit;font-size:18px;line-height:1;cursor:pointer;}
146
146
  .reticle-carousel-arrow:hover{border-color:var(--reticle-accent);background:var(--reticle-accent-soft);}
147
- .reticle-carousel-position{margin-left:auto;color:var(--reticle-muted);font-size:10px;font-variant-numeric:tabular-nums;white-space:nowrap;}
148
- .reticle-carousel-slide .reticle-offer{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:3px 8px;margin:0;padding:0;border:0;background:none;}
149
- .reticle-carousel-slide .reticle-offer-title{grid-column:1/-1;overflow:hidden;padding:0;font-size:11px;text-overflow:ellipsis;white-space:nowrap;}
150
- .reticle-carousel-slide .reticle-offer-body{grid-column:1;grid-row:2;margin:0;overflow:hidden;font-size:10px;line-height:1.2;text-overflow:ellipsis;white-space:nowrap;}
151
- .reticle-carousel-slide .reticle-offer-claim{grid-column:2;grid-row:2;padding:3px 7px;font-size:10px;white-space:nowrap;}
152
- .reticle-carousel-slide .reticle-offer-claimed{overflow:hidden;margin:0;text-overflow:ellipsis;white-space:nowrap;}
147
+ .reticle-carousel-position{margin-left:auto;color:var(--reticle-muted);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap;}
153
148
  `;
@@ -1,8 +1,6 @@
1
- import { type OfferState } from './offer-card.js';
2
1
  import type { Slide } from './carousel.js';
3
2
  export declare const SLIDE_ID: {
4
- readonly HARNESS: "harness-journeys";
5
3
  readonly REPLAY: "replay-confidence";
6
- readonly OFFER: "harness-offer";
4
+ readonly NOTES: "notes-for-agent";
7
5
  };
8
- export declare function panelSlides(offer: OfferState | undefined, offerDeclined: boolean, notices?: readonly unknown[], base?: string): Slide[];
6
+ export declare function panelSlides(notices?: readonly unknown[], base?: string): Slide[];
@@ -1,23 +1,29 @@
1
- import { offerHtml } from './offer-card.js';
2
1
  import { HudNoticeSchema } from '@reticlehq/core/hud';
3
2
  import { esc } from '../chrome/presenter-safe-html.js';
4
3
  import { DEFAULT_PLATFORM_URL } from '@reticlehq/core';
5
4
  const HARNESS_PATH = '/settings?group=billing';
6
5
  export const SLIDE_ID = {
7
- HARNESS: 'harness-journeys',
8
6
  REPLAY: 'replay-confidence',
9
- OFFER: 'harness-offer',
7
+ NOTES: 'notes-for-agent',
8
+ };
9
+ const SLIDE_TEXT = {
10
+ REPLAY_TITLE: 'Replay what your agent verified',
11
+ REPLAY_DETAIL: 'Saved flows re-run after each edit.',
12
+ REPLAY_LINK: 'See how →',
13
+ NOTES_TITLE: 'Point your agent at anything',
14
+ NOTES_DETAIL: 'Open Notes, then click an element.',
10
15
  };
11
16
  const valueSlides = (base) => [
12
17
  {
13
- id: SLIDE_ID.HARNESS,
14
- html: `<div class="reticle-promo-copy"><span class="reticle-promo-kicker">RETICLE HARNESS</span><strong class="reticle-promo-title">Let Reticle drive your app for you</strong><span class="reticle-promo-detail">Describe a user; it drives the journey and saves it to replay.</span><a data-reticle-promo-link class="reticle-promo-link" href="${base}${HARNESS_PATH}" target="_blank" rel="noopener noreferrer">Explore →</a></div>`,
18
+ id: SLIDE_ID.REPLAY,
19
+ html: `<div class="reticle-promo-copy"><strong class="reticle-promo-title">${SLIDE_TEXT.REPLAY_TITLE}</strong><span class="reticle-promo-detail">${SLIDE_TEXT.REPLAY_DETAIL}</span><a data-reticle-promo-link class="reticle-promo-link" href="${base}${HARNESS_PATH}" target="_blank" rel="noopener noreferrer">${SLIDE_TEXT.REPLAY_LINK}</a></div>`,
15
20
  },
16
21
  {
17
- id: SLIDE_ID.REPLAY,
18
- html: `<div class="reticle-promo-copy"><span class="reticle-promo-kicker">FROM RUN TO REGRESSION</span><strong class="reticle-promo-title">Keep every journey replayable</strong><span class="reticle-promo-detail">Catch what changes before users do.</span><a data-reticle-promo-link class="reticle-promo-link" href="${base}${HARNESS_PATH}" target="_blank" rel="noopener noreferrer">See how →</a></div>`,
22
+ id: SLIDE_ID.NOTES,
23
+ html: `<div class="reticle-promo-copy"><strong class="reticle-promo-title">${SLIDE_TEXT.NOTES_TITLE}</strong><span class="reticle-promo-detail">${SLIDE_TEXT.NOTES_DETAIL}</span></div>`,
19
24
  },
20
25
  ];
26
+ const HARNESS_NOTICE_PREFIX = 'harness';
21
27
  const NOTICE_SLIDE_PREFIX = 'notice-';
22
28
  function noticeSlide(notice) {
23
29
  const kicker = notice.kicker === undefined
@@ -25,7 +31,7 @@ function noticeSlide(notice) {
25
31
  : `<span class="reticle-promo-kicker">${esc(notice.kicker)}</span>`;
26
32
  const detail = notice.detail === undefined
27
33
  ? ''
28
- : `<span class="reticle-promo-detail">${esc(notice.detail)}</span>`;
34
+ : `<span class="reticle-promo-detail" title="${esc(notice.detail)}">${esc(notice.detail)}</span>`;
29
35
  const link = notice.cta === undefined
30
36
  ? ''
31
37
  : `<a data-reticle-promo-link class="reticle-promo-link" href="${esc(notice.cta.url)}" target="_blank" rel="noopener noreferrer">${esc(notice.cta.label)} →</a>`;
@@ -34,14 +40,10 @@ function noticeSlide(notice) {
34
40
  html: `<div class="reticle-promo-copy">${kicker}<strong class="reticle-promo-title">${esc(notice.title)}</strong>${detail}${link}</div>`,
35
41
  };
36
42
  }
37
- export function panelSlides(offer, offerDeclined, notices = [], base = DEFAULT_PLATFORM_URL) {
38
- const harnessOffer = offerHtml(offer, offerDeclined);
43
+ export function panelSlides(notices = [], base = DEFAULT_PLATFORM_URL) {
39
44
  const valid = notices.flatMap((n) => {
40
45
  const parsed = HudNoticeSchema.safeParse(n);
41
- return parsed.success ? [parsed.data] : [];
46
+ return parsed.success && !parsed.data.id.startsWith(HARNESS_NOTICE_PREFIX) ? [parsed.data] : [];
42
47
  });
43
- return [
44
- ...('' === harnessOffer ? [] : [{ id: SLIDE_ID.OFFER, html: harnessOffer }]),
45
- ...(0 < valid.length ? valid.map(noticeSlide) : valueSlides(base)),
46
- ];
48
+ return 0 < valid.length ? valid.map(noticeSlide) : [...valueSlides(base)];
47
49
  }
@@ -5,6 +5,7 @@ export interface RememberedRow {
5
5
  ts: string;
6
6
  at: number;
7
7
  actor?: LogActor;
8
+ summary?: true;
8
9
  result?: LogResult;
9
10
  }
10
11
  export declare function readRememberedLog(): RememberedRow[];
@@ -3,8 +3,15 @@ import { LOG_ACTOR, LOG_KIND, LOG_RESULT, type LogActor, type LogKind, type LogR
3
3
  export { LOG_ACTOR, LOG_KIND, LOG_RESULT };
4
4
  export type { LogActor, LogKind, LogResult };
5
5
  export declare const CHIP_LABEL: Record<PresenterMode, string>;
6
+ export declare const LAST_ACTOR_ATTR = "data-reticle-log-actor";
6
7
  export declare const DATA_RETICLE_LOG = "data-reticle-log";
7
8
  export declare const LOG_TIME_ATTR = "data-reticle-log-time";
9
+ export declare const LOG_EMPTY_TEXT: {
10
+ readonly TITLE: "Reticle checks your app from the inside.";
11
+ readonly OFFLINE_TITLE: "Not connected yet.";
12
+ readonly OFFLINE_BODY: "Once Reticle is running, this page connects by itself and the steps show here.";
13
+ readonly BODY: "Ask your coding agent to verify a change with Reticle: each step shows here, live.\\AOr open Harness below: Reticle clicks through the app as the person you pick and reports what breaks.";
14
+ };
8
15
  export declare const LOG_CSS: string;
9
16
  export declare const LOG_SCROLLABLE_ATTR = "data-reticle-log-scrollable";
10
17
  export declare function settleLogAtLatest(container: HTMLElement): void;
@@ -25,7 +25,7 @@ const HANDOVER_TEXT = {
25
25
  agent: 'Your agent is driving',
26
26
  harness: 'Reticle Harness is driving',
27
27
  };
28
- const LAST_ACTOR_ATTR = 'data-reticle-log-actor';
28
+ export const LAST_ACTOR_ATTR = 'data-reticle-log-actor';
29
29
  const DATA_ACTOR = 'data-actor';
30
30
  const DATA_STATE = 'data-state';
31
31
  const HANDOVER_KIND = 'handover';
@@ -35,18 +35,29 @@ const DATA_RETICLE_LOG_ROW = 'data-reticle-log-row';
35
35
  export const LOG_TIME_ATTR = 'data-reticle-log-time';
36
36
  const DATA_KIND = 'data-kind';
37
37
  const LOG_TEXT_CLASS = 'reticle-log-text';
38
+ const REPEAT_ATTR = 'data-reticle-repeat';
39
+ const LOG_REPEAT_CLASS = 'reticle-log-repeat';
38
40
  const LOG_RES_CLASS = 'reticle-res';
39
41
  const LOG_CHIP_CLASS = 'reticle-chip';
40
- const LOG_EMPTY_HINT = 'Agent activity will appear here';
42
+ export const LOG_EMPTY_TEXT = {
43
+ TITLE: 'Reticle checks your app from the inside.',
44
+ OFFLINE_TITLE: 'Not connected yet.',
45
+ OFFLINE_BODY: 'Once Reticle is running, this page connects by itself and the steps show here.',
46
+ BODY: 'Ask your coding agent to verify a change with Reticle: each step shows here, live.\\AOr open Harness below: Reticle clicks through the app as the person you pick and reports what breaks.',
47
+ };
41
48
  const LOG_SETTLE_MS = 160;
42
49
  export const LOG_CSS = `
43
50
  [data-reticle-log]{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;pointer-events:auto;touch-action:pan-y;
44
51
  display:flex;flex-direction:column;
45
52
  gap:4px;padding:8px 10px 10px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.16) transparent;
46
53
  background:transparent;}
47
- [data-reticle-log]:empty{align-items:center;justify-content:center;}
48
- [data-reticle-log]:empty::after{content:"${LOG_EMPTY_HINT}";display:block;padding:24px 16px;text-align:center;
49
- color:var(--reticle-faint);font-size:11.5px;line-height:1.5;letter-spacing:.01em;}
54
+ [data-reticle-log]:empty{align-items:center;justify-content:center;gap:6px;padding:16px 18px;}
55
+ [data-reticle-log]:empty::before{content:"${LOG_EMPTY_TEXT.TITLE}";display:block;text-align:center;
56
+ color:var(--reticle-hud-text);font-size:var(--reticle-hud-size-base);font-weight:600;line-height:1.4;}
57
+ [data-reticle-log]:empty::after{content:"${LOG_EMPTY_TEXT.BODY}";display:block;max-width:260px;text-align:center;white-space:pre-line;
58
+ color:var(--reticle-hud-text-muted);font-size:var(--reticle-hud-size-sm);line-height:1.5;}
59
+ [data-reticle-state="unreachable"] [data-reticle-log]:empty::before{content:"${LOG_EMPTY_TEXT.OFFLINE_TITLE}";}
60
+ [data-reticle-state="unreachable"] [data-reticle-log]:empty::after{content:"${LOG_EMPTY_TEXT.OFFLINE_BODY}";}
50
61
  [data-reticle-log]::-webkit-scrollbar{width:8px;}
51
62
  [data-reticle-log]::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:8px;border:2px solid transparent;background-clip:content-box;}
52
63
  [data-reticle-log]::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.26);background-clip:content-box;}
@@ -69,8 +80,9 @@ export const LOG_CSS = `
69
80
  [data-reticle-log] [data-reticle-log-row] .reticle-chip-label{
70
81
  position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}
71
82
  [data-reticle-log] [data-reticle-log-row][data-kind="read"] .reticle-chip{opacity:.5;}
72
- [${LOG_TIME_ATTR}]{flex:none;color:var(--reticle-faint);font-size:9px;font-variant-numeric:tabular-nums;padding-top:2px;min-width:2em;opacity:.85;}
83
+ [${LOG_TIME_ATTR}]{flex:none;color:var(--reticle-faint);font-size:11px;font-variant-numeric:tabular-nums;padding-top:2px;min-width:2em;opacity:.85;}
73
84
  [data-reticle-log] .reticle-log-text{flex:1;min-width:0;color:var(--reticle-muted);overflow-wrap:anywhere;word-break:break-word;}
85
+ [data-reticle-log] .reticle-log-repeat{flex:none;color:var(--reticle-muted);font-variant-numeric:tabular-nums;opacity:.7;}
74
86
  [data-reticle-log] .reticle-res{flex:none;font-size:7.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--reticle-bad);opacity:.75;padding-top:2px;}
75
87
  [data-reticle-log] .reticle-res.reticle-pass{display:none;}
76
88
  [data-reticle-log-row][data-kind="human"]{align-self:flex-end;max-width:78%;margin:6px 0 2px;
@@ -85,14 +97,14 @@ export const LOG_CSS = `
85
97
  [data-reticle-log-row][data-state="running"] .reticle-res{display:inline-block;width:6px;height:6px;margin-top:5px;
86
98
  border-radius:50%;background:var(--reticle-c-active);opacity:1;animation:reticle-row-pulse 1s ease-in-out infinite;}
87
99
  @keyframes reticle-row-pulse{0%,100%{opacity:.35;transform:scale(.8)}50%{opacity:1;transform:scale(1)}}
88
- [data-reticle-log] .reticle-res.reticle-pass{display:inline;color:#4ade80;font-size:10px;opacity:.9;}
100
+ [data-reticle-log] .reticle-res.reticle-pass{display:inline;color:#4ade80;font-size:11px;opacity:.9;}
89
101
  [data-reticle-log] .reticle-res.reticle-fail{color:#fca5a5;background:rgba(239,68,68,.16);border-radius:999px;padding:1px 6px;font-size:8px;opacity:1;}
90
102
  /* The Harness's rows wear its own colour, so two drivers never read as one. */
91
103
  [data-reticle-log-row][data-actor="harness"]{--reticle-row-accent:#a78bfa;}
92
104
  [data-reticle-log-row][data-actor="harness"] .reticle-chip{color:#a78bfa;}
93
105
  [data-reticle-log-row][data-actor="harness"]:not([data-kind="handover"]){box-shadow:inset 2px 0 0 color-mix(in srgb,#a78bfa 55%,transparent);padding-left:8px;}
94
106
  [data-reticle-log-row][data-kind="handover"]{align-items:center;gap:8px;margin:8px 0 4px;padding:0;
95
- font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--reticle-row-accent,var(--reticle-c-active));}
107
+ font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--reticle-row-accent,var(--reticle-c-active));}
96
108
  [data-reticle-log-row][data-kind="handover"]::before,[data-reticle-log-row][data-kind="handover"]::after{
97
109
  content:"";flex:1;height:1px;background:color-mix(in srgb,var(--reticle-row-accent,var(--reticle-c-active)) 40%,transparent);}
98
110
  [data-reticle-log-row][data-kind="handover"] .reticle-log-text{flex:none;color:inherit;}
@@ -144,6 +156,23 @@ export function formatElapsed(ms) {
144
156
  return humanDuration(ms);
145
157
  }
146
158
  export function appendLogRow(container, kind, text, ts, logMax, actor = LOG_ACTOR.AGENT) {
159
+ const repeated = kind === LOG_KIND.READ ? sameReadAsLast(container, text, actor) : undefined;
160
+ if (repeated !== undefined) {
161
+ const times = Number(repeated.getAttribute(REPEAT_ATTR) ?? '1') + 1;
162
+ repeated.setAttribute(REPEAT_ATTR, String(times));
163
+ let count = repeated.querySelector(`.${LOG_REPEAT_CLASS}`);
164
+ if (null === count) {
165
+ count = document.createElement('span');
166
+ count.className = LOG_REPEAT_CLASS;
167
+ repeated.querySelector(`.${LOG_TEXT_CLASS}`)?.after(count);
168
+ }
169
+ count.textContent = `×${String(times)}`;
170
+ const time = repeated.querySelector(`[${LOG_TIME_ATTR}]`);
171
+ if (null !== time)
172
+ time.textContent = ts;
173
+ const res = repeated.querySelector(`.${LOG_RES_CLASS}`);
174
+ return null === res ? { result: () => undefined } : rowHandle(repeated, res);
175
+ }
147
176
  const driven = kind === LOG_KIND.ACT || kind === LOG_KIND.READ;
148
177
  if (driven && (container.getAttribute(LAST_ACTOR_ATTR) ?? LOG_ACTOR.AGENT) !== actor) {
149
178
  const divider = document.createElement('div');
@@ -200,6 +229,17 @@ export function appendLogRow(container, kind, text, ts, logMax, actor = LOG_ACTO
200
229
  requestAnimationFrame(() => {
201
230
  scrollLogToLatest(container);
202
231
  });
232
+ return rowHandle(row, resEl);
233
+ }
234
+ function sameReadAsLast(container, text, actor) {
235
+ const last = container.lastElementChild;
236
+ if (!(last instanceof HTMLElement) || !last.hasAttribute(DATA_RETICLE_LOG_ROW))
237
+ return undefined;
238
+ if (last.getAttribute(DATA_KIND) !== LOG_KIND.READ || last.getAttribute(DATA_ACTOR) !== actor)
239
+ return undefined;
240
+ return last.querySelector(`.${LOG_TEXT_CLASS}`)?.textContent === text ? last : undefined;
241
+ }
242
+ function rowHandle(row, resEl) {
203
243
  return {
204
244
  result: (r) => {
205
245
  row.setAttribute(DATA_STATE, r);
@@ -0,0 +1 @@
1
+ export declare function morphHtml(target: Element, html: string): void;
@@ -0,0 +1,34 @@
1
+ export function morphHtml(target, html) {
2
+ const next = document.createElement('template');
3
+ next.innerHTML = html;
4
+ morphChildren(target, next.content);
5
+ }
6
+ function morphChildren(from, to) {
7
+ const have = [...from.childNodes];
8
+ const want = [...to.childNodes];
9
+ want.forEach((node, i) => {
10
+ const old = have[i];
11
+ if (old === undefined)
12
+ from.appendChild(node);
13
+ else if (old.nodeType === node.nodeType && old.nodeName === node.nodeName)
14
+ morphNode(old, node);
15
+ else
16
+ from.replaceChild(node, old);
17
+ });
18
+ for (const extra of have.slice(want.length))
19
+ from.removeChild(extra);
20
+ }
21
+ function morphNode(old, node) {
22
+ if (!(old instanceof Element) || !(node instanceof Element)) {
23
+ if (old.nodeValue !== node.nodeValue)
24
+ old.nodeValue = node.nodeValue;
25
+ return;
26
+ }
27
+ for (const { name } of [...old.attributes])
28
+ if (!node.hasAttribute(name))
29
+ old.removeAttribute(name);
30
+ for (const { name, value } of [...node.attributes])
31
+ if (old.getAttribute(name) !== value)
32
+ old.setAttribute(name, value);
33
+ morphChildren(old, node);
34
+ }