@reticlehq/browser 3.7.0 → 3.8.1

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 (104) hide show
  1. package/dist/actions/action-report.d.ts +10 -0
  2. package/dist/actions/action-report.js +59 -14
  3. package/dist/actions/actions-dom.d.ts +4 -0
  4. package/dist/actions/actions-dom.js +20 -4
  5. package/dist/actions/actions.d.ts +1 -0
  6. package/dist/actions/actions.js +94 -46
  7. package/dist/actions/appeared-text.js +2 -2
  8. package/dist/actions/click-target.d.ts +1 -0
  9. package/dist/actions/click-target.js +14 -0
  10. package/dist/actions/danger-context.d.ts +2 -1
  11. package/dist/actions/danger-context.js +43 -17
  12. package/dist/actions/scroll.d.ts +2 -0
  13. package/dist/actions/scroll.js +10 -2
  14. package/dist/activate.d.ts +14 -0
  15. package/dist/activate.js +43 -0
  16. package/dist/busy.d.ts +3 -0
  17. package/dist/busy.js +42 -0
  18. package/dist/commands/commands.js +45 -17
  19. package/dist/connect-options.d.ts +12 -1
  20. package/dist/connect-options.js +18 -1
  21. package/dist/dom/a11y.d.ts +0 -2
  22. package/dist/dom/a11y.js +103 -363
  23. package/dist/dom/chart.d.ts +3 -2
  24. package/dist/dom/chart.js +3 -2
  25. package/dist/dom/dom-ignore.d.ts +6 -0
  26. package/dist/dom/dom-ignore.js +60 -5
  27. package/dist/dom/layout-audit.d.ts +15 -0
  28. package/dist/dom/layout-audit.js +208 -0
  29. package/dist/dom/paint-context.d.ts +15 -0
  30. package/dist/dom/paint-context.js +78 -0
  31. package/dist/dom/query-finders.d.ts +6 -0
  32. package/dist/dom/query-finders.js +216 -0
  33. package/dist/dom/query.d.ts +1 -1
  34. package/dist/dom/query.js +154 -227
  35. package/dist/dom/shadow-registry.d.ts +1 -0
  36. package/dist/dom/shadow-registry.js +27 -0
  37. package/dist/dom/snapshot.d.ts +5 -0
  38. package/dist/dom/snapshot.js +87 -35
  39. package/dist/dom/stable-selector.d.ts +1 -0
  40. package/dist/dom/stable-selector.js +19 -0
  41. package/dist/dom/text-match.d.ts +12 -0
  42. package/dist/dom/text-match.js +115 -0
  43. package/dist/dom/theme.d.ts +1 -0
  44. package/dist/dom/theme.js +21 -9
  45. package/dist/dom/virtualized.js +5 -1
  46. package/dist/dom/visibility.d.ts +7 -0
  47. package/dist/dom/visibility.js +322 -0
  48. package/dist/index.d.ts +4 -2
  49. package/dist/index.js +7 -1
  50. package/dist/observers/blind-spots.js +3 -2
  51. package/dist/observers/canvas.d.ts +18 -0
  52. package/dist/observers/canvas.js +76 -0
  53. package/dist/observers/dom.js +68 -19
  54. package/dist/observers/field.d.ts +11 -0
  55. package/dist/observers/field.js +117 -13
  56. package/dist/observers/install-all.js +4 -0
  57. package/dist/observers/media.d.ts +2 -0
  58. package/dist/observers/media.js +32 -0
  59. package/dist/observers/net-detail/network-body.d.ts +2 -0
  60. package/dist/observers/net-detail/network-body.js +36 -9
  61. package/dist/observers/net-detail/network-event-stream.d.ts +5 -0
  62. package/dist/observers/net-detail/network-event-stream.js +55 -0
  63. package/dist/observers/net-detail/network-frames.d.ts +2 -0
  64. package/dist/observers/net-detail/network-frames.js +39 -0
  65. package/dist/observers/net-detail/network-response-body.d.ts +3 -0
  66. package/dist/observers/net-detail/network-response-body.js +97 -0
  67. package/dist/observers/network.js +111 -101
  68. package/dist/observers/route.d.ts +2 -0
  69. package/dist/observers/route.js +33 -4
  70. package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -5
  71. package/dist/presenter/chrome/presenter-report-copy.js +0 -14
  72. package/dist/presenter/hud-telemetry.js +1 -1
  73. package/dist/presenter/presenter-chat-views.d.ts +3 -4
  74. package/dist/presenter/presenter-chat-views.js +47 -20
  75. package/dist/presenter/presenter-config.d.ts +1 -0
  76. package/dist/presenter/presenter-config.js +1 -0
  77. package/dist/presenter/presenter-frame.d.ts +3 -2
  78. package/dist/presenter/presenter-frame.js +24 -15
  79. package/dist/presenter/presenter-harness-row.d.ts +4 -15
  80. package/dist/presenter/presenter-harness-row.js +11 -50
  81. package/dist/presenter/presenter-report-styles.js +0 -17
  82. package/dist/presenter/presenter-report.d.ts +1 -2
  83. package/dist/presenter/presenter-report.js +4 -54
  84. package/dist/presenter/presenter-run-state.js +2 -1
  85. package/dist/presenter/presenter-settings.d.ts +2 -3
  86. package/dist/presenter/presenter-settings.js +14 -17
  87. package/dist/presenter/presenter-shell.d.ts +6 -4
  88. package/dist/presenter/presenter-shell.js +37 -7
  89. package/dist/presenter/presenter.js +32 -11
  90. package/dist/registry/store-adapters.d.ts +5 -2
  91. package/dist/registry/store-adapters.js +40 -10
  92. package/dist/reticle-inject.js +700 -645
  93. package/dist/reticle.d.ts +4 -2
  94. package/dist/reticle.js +36 -6
  95. package/dist/security/page-location.d.ts +2 -0
  96. package/dist/security/page-location.js +7 -0
  97. package/dist/security/serialization.js +1 -1
  98. package/dist/storage-keys.d.ts +1 -0
  99. package/dist/storage-keys.js +1 -0
  100. package/dist/timers/native/native-timers.d.ts +1 -0
  101. package/dist/timers/native/native-timers.js +5 -0
  102. package/dist/transport/transport.d.ts +1 -0
  103. package/dist/transport/transport.js +18 -0
  104. package/package.json +6 -2
@@ -1,42 +1,21 @@
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, 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, } from './net-detail/network-body.js';
5
+ import { captureResponseBody, isEventStream } from './net-detail/network-response-body.js';
5
6
  import { redactUrl, netUrlFields } from './net-detail/network-redact.js';
6
7
  import { watchStreamedBody } from './network-stream.js';
8
+ import { frameFields, isBridgeSocket } from './net-detail/network-frames.js';
7
9
  import { requireCapturedMethod } from '../util/captured-method.js';
8
10
  export { redactUrl, netUrlFields };
9
- function binaryFrameBytes(data) {
10
- if (data instanceof ArrayBuffer)
11
- return data.byteLength;
12
- if (typeof Blob !== 'undefined' && data instanceof Blob)
13
- return data.size;
14
- if (ArrayBuffer.isView(data))
15
- return data.byteLength;
16
- return undefined;
17
- }
18
- function frameFields(data, captureBodies) {
19
- if (typeof data !== 'string') {
20
- const bytes = binaryFrameBytes(data);
21
- return bytes === undefined
22
- ? { frameType: typeof data }
23
- : { frameType: 'binary', frameBytes: bytes };
24
- }
25
- const out = { frameBytes: data.length };
26
- if (captureBodies) {
27
- const { body, truncated } = projectBody(data, 'application/json');
28
- out['frame'] = body;
29
- if (truncated)
30
- out['frameTruncated'] = true;
31
- }
32
- return out;
33
- }
34
11
  const ShapeToken = { NULL: '0', STRING: 's', NUMBER: 'n', BOOLEAN: 'b', OTHER: '?' };
35
12
  function shapeOf(value) {
36
- if (null === value)
13
+ if (null === value) {
37
14
  return ShapeToken.NULL;
38
- if (Array.isArray(value))
15
+ }
16
+ if (Array.isArray(value)) {
39
17
  return `[${value.map(shapeOf).join(',')}]`;
18
+ }
40
19
  if ('object' === typeof value) {
41
20
  const fields = value;
42
21
  return `{${Object.keys(fields)
@@ -44,12 +23,15 @@ function shapeOf(value) {
44
23
  .map((key) => `${key}:${shapeOf(fields[key])}`)
45
24
  .join(',')}}`;
46
25
  }
47
- if ('string' === typeof value)
26
+ if ('string' === typeof value) {
48
27
  return ShapeToken.STRING;
49
- if ('number' === typeof value)
28
+ }
29
+ if ('number' === typeof value) {
50
30
  return ShapeToken.NUMBER;
51
- if ('boolean' === typeof value)
31
+ }
32
+ if ('boolean' === typeof value) {
52
33
  return ShapeToken.BOOLEAN;
34
+ }
53
35
  return ShapeToken.OTHER;
54
36
  }
55
37
  function fingerprintBody(shape, length) {
@@ -72,8 +54,9 @@ const DELETE = 0x7f;
72
54
  function hasControlCharacter(text) {
73
55
  for (let i = 0; i < text.length; i += 1) {
74
56
  const code = text.charCodeAt(i);
75
- if (code === DELETE)
57
+ if (code === DELETE) {
76
58
  return true;
59
+ }
77
60
  if (code < FIRST_PRINTABLE && code !== TAB && code !== LINE_FEED && code !== CARRIAGE_RETURN) {
78
61
  return true;
79
62
  }
@@ -81,10 +64,12 @@ function hasControlCharacter(text) {
81
64
  return false;
82
65
  }
83
66
  function textOfBytes(body) {
84
- if (!(body instanceof ArrayBuffer || ArrayBuffer.isView(body)))
67
+ if (!(body instanceof ArrayBuffer || ArrayBuffer.isView(body))) {
85
68
  return undefined;
86
- if (body.byteLength > MAX_DECODED_BODY_BYTES)
69
+ }
70
+ if (body.byteLength > MAX_DECODED_BODY_BYTES) {
87
71
  return undefined;
72
+ }
88
73
  try {
89
74
  const text = new TextDecoder('utf-8', { fatal: true }).decode(body);
90
75
  return hasControlCharacter(text) ? undefined : text;
@@ -97,8 +82,9 @@ function formShape(form) {
97
82
  const fields = [];
98
83
  let length = 0;
99
84
  form.forEach((value, name) => {
100
- if ('string' === typeof value)
85
+ if ('string' === typeof value) {
101
86
  length += value.length;
87
+ }
102
88
  fields.push(JSON.stringify([name, 'string' === typeof value ? 0 : [value.type, value.size]]));
103
89
  });
104
90
  return 0 === fields.length ? undefined : fingerprintBody(fields.sort().join(), length);
@@ -111,8 +97,9 @@ function projectRequestBody(body, captureBodies) {
111
97
  const typeOf = () => body.constructor?.name ?? typeof body;
112
98
  if ('string' === typeof body || bytesText !== undefined) {
113
99
  text = bytesText ?? body;
114
- if (text.length > 0)
100
+ if (text.length > 0) {
115
101
  shape = fingerprintBody(jsonShape(text), text.length);
102
+ }
116
103
  }
117
104
  else if (typeof FormData !== 'undefined' && body instanceof FormData) {
118
105
  const form = formShape(body);
@@ -133,8 +120,9 @@ function projectRequestBody(body, captureBodies) {
133
120
  return captureBodies ? { requestBodyType: typeOf() } : {};
134
121
  }
135
122
  const fields = { [REQUEST_SHAPE_FIELD]: shape };
136
- if (!captureBodies || text === undefined || 0 === text.length)
123
+ if (!captureBodies || text === undefined || 0 === text.length) {
137
124
  return fields;
125
+ }
138
126
  const { body: out, truncated } = projectBody(text, contentType);
139
127
  return truncated
140
128
  ? { ...fields, requestBody: out, requestBodyTruncated: true }
@@ -145,27 +133,34 @@ function statusIsOk(status) {
145
133
  }
146
134
  function netResponseMeta(statusText, contentType, contentLength) {
147
135
  const out = {};
148
- if (statusText !== '')
136
+ if (statusText !== '') {
149
137
  out['statusText'] = statusText;
150
- if (contentType !== null && contentType !== '')
138
+ }
139
+ if (contentType !== null && contentType !== '') {
151
140
  out['contentType'] = contentType;
141
+ }
152
142
  const size = contentLength !== null ? Number.parseInt(contentLength, 10) : Number.NaN;
153
- if (Number.isFinite(size))
143
+ if (Number.isFinite(size)) {
154
144
  out['responseSize'] = size;
145
+ }
155
146
  return out;
156
147
  }
157
148
  function urlOf(input) {
158
- if ('string' === typeof input)
149
+ if ('string' === typeof input) {
159
150
  return input;
160
- if (input instanceof URL)
151
+ }
152
+ if (input instanceof URL) {
161
153
  return input.href;
154
+ }
162
155
  return input.url;
163
156
  }
164
157
  function methodOf(input, init) {
165
- if (init?.method !== undefined)
158
+ if (init?.method !== undefined) {
166
159
  return init.method.toUpperCase();
167
- if (input instanceof Request)
160
+ }
161
+ if (input instanceof Request) {
168
162
  return input.method.toUpperCase();
163
+ }
169
164
  return 'GET';
170
165
  }
171
166
  function nextActionOf(input, init) {
@@ -180,8 +175,9 @@ function nextActionOf(input, init) {
180
175
  const NON_APP_FRAME = /@reticlehq|reticle\.ts|network\.ts|transport\.ts|<anonymous>|new Promise|node:internal/i;
181
176
  const DEPENDENCY_FRAME = /node_modules|\/\.vite\/deps\//i;
182
177
  export function firstAppFrame(stack) {
183
- if (stack === undefined)
178
+ if (stack === undefined) {
184
179
  return undefined;
180
+ }
185
181
  const frames = stack
186
182
  .split('\n')
187
183
  .slice(1)
@@ -194,14 +190,16 @@ function initiatorFrame() {
194
190
  return firstAppFrame(new Error().stack);
195
191
  }
196
192
  export function extractTiming(entry) {
197
- if (entry === undefined)
193
+ if (entry === undefined) {
198
194
  return {};
195
+ }
199
196
  const timing = {};
200
197
  if (entry.responseStart > 0 && entry.requestStart > 0) {
201
198
  timing.ttfbMs = Math.round(entry.responseStart - entry.requestStart);
202
199
  }
203
- if (entry.transferSize > 0)
200
+ if (entry.transferSize > 0) {
204
201
  timing.transferSize = entry.transferSize;
202
+ }
205
203
  return timing;
206
204
  }
207
205
  function resourceTiming(rawUrl) {
@@ -213,14 +211,6 @@ function resourceTiming(rawUrl) {
213
211
  return {};
214
212
  }
215
213
  }
216
- function isBridgeSocket(url) {
217
- try {
218
- return new URL(url, location.href).pathname === RETICLE_WS_PATH;
219
- }
220
- catch {
221
- return url.includes(RETICLE_WS_PATH);
222
- }
223
- }
224
214
  function isNativeFetch(fn) {
225
215
  try {
226
216
  return Function.prototype.toString.call(fn).includes('native code');
@@ -230,8 +220,16 @@ function isNativeFetch(fn) {
230
220
  }
231
221
  }
232
222
  const OURS = new WeakSet();
223
+ const PAGE_CAPTURE = Symbol.for('reticle.network');
224
+ const pageSlots = () => window;
233
225
  export function installNetwork(emit, opts = {}) {
226
+ if (undefined !== pageSlots()[PAGE_CAPTURE]) {
227
+ return () => undefined;
228
+ }
229
+ const owner = {};
230
+ pageSlots()[PAGE_CAPTURE] = owner;
234
231
  const captureBodies = true === opts.captureBodies;
232
+ const bodyCapture = new AbortController();
235
233
  const captureErrorBodies = false !== opts.captureErrorBodies;
236
234
  const reinterpret = opts.reinterpret;
237
235
  const origFetch = requireCapturedMethod(window, 'fetch');
@@ -243,8 +241,9 @@ export function installNetwork(emit, opts = {}) {
243
241
  const nextId = () => `n${++seq}`;
244
242
  window.fetch = async (input, init) => {
245
243
  const rawUrl = urlOf(input);
246
- if (true === opts.ignore?.(rawUrl))
244
+ if (true === opts.ignore?.(rawUrl)) {
247
245
  return callFetch(input, init);
246
+ }
248
247
  const id = nextId();
249
248
  const start = performance.now();
250
249
  const method = methodOf(input, init);
@@ -293,41 +292,30 @@ export function installNetwork(emit, opts = {}) {
293
292
  });
294
293
  };
295
294
  const wantErrorBody = shouldCaptureErrorBody(res.status, captureErrorBodies);
296
- if ((captureBodies || wantErrorBody) && isCapturableType(contentType)) {
297
- let clone;
298
- try {
299
- clone = res.clone();
300
- }
301
- catch {
302
- }
303
- void (async () => {
304
- let responseBodyFields = {};
305
- if (clone !== undefined) {
306
- try {
307
- const text = await withBodyDeadline(clone.text());
308
- if (text !== undefined) {
309
- const { body, truncated } = captureBodies
310
- ? projectBody(text, contentType)
311
- : projectErrorBody(text, contentType);
312
- responseBodyFields = {
313
- responseBody: body,
314
- ...(truncated ? { responseBodyTruncated: true } : {}),
315
- ...(captureBodies ? {} : { responseBodyReason: 'error-status' }),
316
- };
317
- }
318
- }
319
- catch {
320
- }
321
- }
322
- try {
323
- emitRequest(responseBodyFields);
324
- }
325
- catch {
295
+ if (captureBodies && isEventStream(contentType)) {
296
+ emitRequest({ responseBodyReason: ResponseBodyReason.UNREADABLE });
297
+ void captureResponseBody(res, contentType, true, bodyCapture.signal)
298
+ .then((fields) => {
299
+ if (typeof fields['responseBody'] !== 'string') {
300
+ return;
326
301
  }
327
- })().catch(() => undefined);
302
+ observeSafely(() => emit(EventType.NET_RESPONSE_BODY, {
303
+ id,
304
+ method,
305
+ ...urlFields,
306
+ ...fields,
307
+ }));
308
+ })
309
+ .catch(() => undefined);
310
+ }
311
+ else if ((captureBodies || wantErrorBody) && isCapturableType(contentType)) {
312
+ void captureResponseBody(res, contentType, captureBodies, bodyCapture.signal)
313
+ .then(emitRequest)
314
+ .catch(() => undefined);
328
315
  }
329
316
  else {
330
- emitRequest({});
317
+ const skipped = captureBodies ? skippedBodyReason(contentType) : undefined;
318
+ emitRequest(skipped === undefined ? {} : { responseBodyReason: skipped });
331
319
  }
332
320
  return res;
333
321
  }
@@ -390,16 +378,25 @@ export function installNetwork(emit, opts = {}) {
390
378
  this.addEventListener('loadend', () => {
391
379
  observeSafely(() => {
392
380
  const cur = meta.get(this);
393
- if (cur === undefined)
381
+ if (cur === undefined) {
394
382
  return;
383
+ }
395
384
  reportedNetUrls.add(cur.rawUrl);
396
385
  const xhrContentType = this.getResponseHeader('content-type');
397
386
  let responseBodyFields = {};
398
387
  const textReadable = '' === this.responseType || 'text' === this.responseType;
399
388
  const wantXhrErrorBody = shouldCaptureErrorBody(this.status, captureErrorBodies);
400
- if ((captureBodies || wantXhrErrorBody) &&
401
- textReadable &&
402
- isCapturableType(xhrContentType)) {
389
+ const capturable = isCapturableType(xhrContentType) ||
390
+ (captureBodies && isEventStream(xhrContentType));
391
+ if (captureBodies) {
392
+ const skipped = capturable
393
+ ? ResponseBodyReason.UNREADABLE
394
+ : skippedBodyReason(xhrContentType);
395
+ if (skipped !== undefined) {
396
+ responseBodyFields = { responseBodyReason: skipped };
397
+ }
398
+ }
399
+ if ((captureBodies || wantXhrErrorBody) && textReadable && capturable) {
403
400
  try {
404
401
  const { body: rb, truncated } = captureBodies
405
402
  ? projectBody(this.responseText, xhrContentType)
@@ -407,7 +404,9 @@ export function installNetwork(emit, opts = {}) {
407
404
  responseBodyFields = {
408
405
  responseBody: rb,
409
406
  ...(truncated ? { responseBodyTruncated: true } : {}),
410
- ...(captureBodies ? {} : { responseBodyReason: 'error-status' }),
407
+ ...(captureBodies
408
+ ? {}
409
+ : { responseBodyReason: ResponseBodyReason.ERROR_STATUS }),
411
410
  };
412
411
  }
413
412
  catch {
@@ -471,8 +470,9 @@ export function installNetwork(emit, opts = {}) {
471
470
  constructor(u, protocols) {
472
471
  super(u, protocols);
473
472
  this.#isBridge = isBridgeSocket(String(u));
474
- if (this.#isBridge)
473
+ if (this.#isBridge) {
475
474
  return;
475
+ }
476
476
  observeSafely(() => {
477
477
  const urlFields = netUrlFields(String(u));
478
478
  emit(EventType.NET_STREAM, {
@@ -546,14 +546,17 @@ export function installNetwork(emit, opts = {}) {
546
546
  const observer = new PerformanceObserver((list) => {
547
547
  for (const raw of list.getEntries()) {
548
548
  const entry = raw;
549
- if (subresourceEvents >= SUBRESOURCE_CAP)
549
+ if (subresourceEvents >= SUBRESOURCE_CAP) {
550
550
  return;
551
+ }
551
552
  const type = entry.initiatorType || 'other';
552
- if ('fetch' === type || 'xmlhttprequest' === type)
553
+ if ('fetch' === type || 'xmlhttprequest' === type) {
553
554
  continue;
555
+ }
554
556
  const rawUrl = entry.name;
555
- if (reportedNetUrls.has(rawUrl))
557
+ if (reportedNetUrls.has(rawUrl)) {
556
558
  continue;
559
+ }
557
560
  reportedNetUrls.add(rawUrl);
558
561
  subresourceEvents += 1;
559
562
  emit(EventType.NET_REQUEST, {
@@ -579,13 +582,20 @@ export function installNetwork(emit, opts = {}) {
579
582
  }
580
583
  }
581
584
  return () => {
585
+ if (pageSlots()[PAGE_CAPTURE] === owner) {
586
+ delete pageSlots()[PAGE_CAPTURE];
587
+ }
588
+ bodyCapture.abort();
582
589
  subresourceObserver?.disconnect();
583
- if (window.fetch === patchedFetch)
590
+ if (window.fetch === patchedFetch) {
584
591
  window.fetch = origFetch;
585
- if (captureMethod(proto, 'open') === patchedOpen)
592
+ }
593
+ if (captureMethod(proto, 'open') === patchedOpen) {
586
594
  proto.open = origOpen;
587
- if (captureMethod(proto, 'send') === patchedSend)
595
+ }
596
+ if (captureMethod(proto, 'send') === patchedSend) {
588
597
  proto.send = origSend;
598
+ }
589
599
  if (patchedEventSource !== undefined && window.EventSource === patchedEventSource) {
590
600
  window.EventSource = origEventSource;
591
601
  }
@@ -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
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');
@@ -22,12 +49,14 @@ export function installRoute(emit) {
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),
31
60
  [ROUTE_CHANGE_HOW_FIELD]: how,
32
61
  });
33
62
  });
@@ -49,17 +49,12 @@ export declare const REPORT_TEXT: {
49
49
  readonly KEY_REFUSED: "Key refused";
50
50
  readonly NOT_SENT: "not synced";
51
51
  readonly NOT_SENT_HELP: "Runs saved on this machine but not on your dashboard yet. Run reticle connect in this project to send them.";
52
- readonly INSTRUMENTATION: "What Reticle can see";
53
- readonly INSTRUMENTATION_HELP: "What Reticle can see in this app, from its handshake and the gaps its verdicts recorded";
54
- readonly covered: (covered: number, total: number) => string;
55
- readonly NOT_SEEN_YET: "not checked yet";
56
52
  readonly COPY_PROMPT: "Copy prompt for your coding agent";
57
53
  readonly SHARE_LABEL: "Share";
58
54
  readonly POST_X: "X";
59
55
  readonly POST_IN: "LinkedIn";
60
56
  readonly REFER: "Invite a friend";
61
57
  };
62
- export declare const CAPABILITY_WORDS: Readonly<Record<string, string>>;
63
58
  export declare function compactNumber(n: number): string;
64
59
  export declare function compactDuration(ms: number): string;
65
60
  export declare function buildShareText(scope: ImpactScope, projectName?: string): string;
@@ -50,26 +50,12 @@ export const REPORT_TEXT = {
50
50
  KEY_REFUSED: 'Key refused',
51
51
  NOT_SENT: 'not synced',
52
52
  NOT_SENT_HELP: 'Runs saved on this machine but not on your dashboard yet. Run reticle connect in this project to send them.',
53
- INSTRUMENTATION: 'What Reticle can see',
54
- INSTRUMENTATION_HELP: 'What Reticle can see in this app, from its handshake and the gaps its verdicts recorded',
55
- covered: (covered, total) => `${String(covered)} of ${String(total)}`,
56
- NOT_SEEN_YET: 'not checked yet',
57
53
  COPY_PROMPT: 'Copy prompt for your coding agent',
58
54
  SHARE_LABEL: 'Share',
59
55
  POST_X: 'X',
60
56
  POST_IN: 'LinkedIn',
61
57
  REFER: 'Invite a friend',
62
58
  };
63
- export const CAPABILITY_WORDS = {
64
- 'page connected': 'Page connected',
65
- 'DOM actions': 'Clicks and typing',
66
- network: 'Network requests',
67
- console: 'Console errors',
68
- 'app state (store registered)': 'App state',
69
- 'signals on mutation': 'Success signals from your code',
70
- 'file:line source mapping': 'Source file and line',
71
- 'stable test ids': 'Stable test ids',
72
- };
73
59
  export function compactNumber(n) {
74
60
  if (n < 1000)
75
61
  return String(Math.round(n));
@@ -64,7 +64,7 @@ const PROMPTS = [
64
64
  {
65
65
  surface: HudPromptSurface.RAIL,
66
66
  selector: `[${RAIL_SIGNIN_ATTR}]`,
67
- shown: (at) => HudView.EXPANDED === at.view,
67
+ shown: (at) => HudPanel.CHAT === at.panel,
68
68
  },
69
69
  {
70
70
  surface: HudPromptSurface.HARNESS,
@@ -1,6 +1,5 @@
1
- import { type AccountState, type HarnessConfig, type AgentLink, type HarnessDrive } from '@reticlehq/core';
1
+ import { type AccountState, type HarnessConfig, type AgentLink, type HarnessDrive, type LastDrive } from '@reticlehq/core';
2
2
  import type { AnnotationItem } from '../review/annotator.js';
3
- import { type HarnessGateView } from './presenter-harness-row.js';
4
3
  export type ChatView = 'activity' | 'flows' | 'annotations' | 'harness';
5
4
  export declare function platformBase(config: HarnessConfig | undefined, account: AccountState): string;
6
5
  export interface HudDriveHost {
@@ -35,8 +34,8 @@ export declare class ChatViews {
35
34
  setProjectId(projectId: string | undefined): void;
36
35
  paintAgent(link: AgentLink | undefined): void;
37
36
  paintAccount(account: AccountState | undefined): void;
38
- paintDrive(drive: HarnessDrive | undefined): void;
39
- paintHarness(config: HarnessConfig | undefined, gate?: HarnessGateView): void;
37
+ paintDrive(drive: HarnessDrive | undefined, last?: LastDrive): void;
38
+ paintHarness(config: HarnessConfig | undefined): void;
40
39
  paintImpact(verdicts: number): void;
41
40
  setImpactOpen(open: boolean): void;
42
41
  paintAnnotations(items: readonly AnnotationItem[]): void;