@reticlehq/browser 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/actions-dom.js +41 -3
- package/dist/actions/actions.js +43 -11
- package/dist/commands/commands.js +6 -2
- package/dist/connect-options.d.ts +18 -1
- package/dist/dom/a11y.js +92 -9
- package/dist/dom/dom-ignore.d.ts +9 -0
- package/dist/dom/dom-ignore.js +32 -3
- package/dist/dom/query.js +168 -97
- package/dist/dom/refs.d.ts +11 -0
- package/dist/dom/refs.js +30 -1
- package/dist/dom/snapshot.d.ts +37 -0
- package/dist/dom/snapshot.js +141 -37
- package/dist/edit-epoch.d.ts +32 -0
- package/dist/edit-epoch.js +108 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/dist/observers/animation.js +6 -8
- package/dist/observers/context-open.d.ts +12 -0
- package/dist/observers/context-open.js +25 -0
- package/dist/observers/focus.js +5 -6
- package/dist/observers/health.js +6 -9
- package/dist/observers/install-all.js +2 -0
- package/dist/observers/network-redact.d.ts +13 -0
- package/dist/observers/network-redact.js +12 -1
- package/dist/observers/network.d.ts +2 -2
- package/dist/observers/network.js +24 -17
- package/dist/observers/scroll.js +4 -2
- package/dist/observers/storage.js +1 -1
- package/dist/presenter/presenter-brand.d.ts +4 -18
- package/dist/presenter/presenter-brand.js +6 -18
- package/dist/presenter/presenter-config.d.ts +11 -8
- package/dist/presenter/presenter-config.js +9 -3
- package/dist/presenter/presenter-controls.d.ts +0 -3
- package/dist/presenter/presenter-controls.js +0 -3
- package/dist/presenter/presenter-hud-chrome.d.ts +1 -1
- package/dist/presenter/presenter-report-copy.d.ts +30 -1
- package/dist/presenter/presenter-report-copy.js +31 -2
- package/dist/presenter/presenter-report-styles.js +24 -0
- package/dist/presenter/presenter-report.d.ts +2 -2
- package/dist/presenter/presenter-report.js +95 -3
- package/dist/presenter/presenter-settings.js +10 -2
- package/dist/presenter/presenter-shell-styles.js +20 -1
- package/dist/presenter/presenter-shell.d.ts +0 -2
- package/dist/presenter/presenter-shell.js +1 -6
- package/dist/presenter/presenter.d.ts +13 -0
- package/dist/presenter/presenter.js +37 -2
- package/dist/registry/auto-testids.d.ts +21 -0
- package/dist/registry/auto-testids.js +48 -0
- package/dist/registry/capabilities.d.ts +28 -1
- package/dist/registry/capabilities.js +59 -7
- package/dist/registry/store-adapters.d.ts +1 -1
- package/dist/registry/store-adapters.js +6 -1
- package/dist/registry/stores.d.ts +4 -0
- package/dist/registry/stores.js +35 -0
- package/dist/reticle.d.ts +12 -0
- package/dist/reticle.js +40 -1
- package/dist/transport/unreachable-message.d.ts +7 -0
- package/dist/transport/unreachable-message.js +11 -1
- package/package.json +4 -7
- package/dist/dom/blind-spots.d.ts +0 -13
- package/dist/dom/blind-spots.js +0 -29
- package/dist/observers/subresources.d.ts +0 -10
- package/dist/observers/subresources.js +0 -63
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { BlindSpotKind, EventType, RETICLE_WS_PATH, StreamTransport, StreamDirection, } from '@reticlehq/core';
|
|
2
2
|
import { captureMethod } from '../patching/capture-method.js';
|
|
3
3
|
import { isCapturableType, projectBody, withBodyDeadline } from './network-body.js';
|
|
4
|
-
import { redactUrl } from './network-redact.js';
|
|
4
|
+
import { redactUrl, netUrlFields } from './network-redact.js';
|
|
5
5
|
import { watchStreamedBody } from './network-stream.js';
|
|
6
6
|
import { requireCapturedMethod } from '../util/captured-method.js';
|
|
7
7
|
// Redaction moved to its own cohesive module (network.ts is at its line cap); re-exported so callers
|
|
8
8
|
// and the existing test suite keep importing it from here.
|
|
9
|
-
export { redactUrl };
|
|
9
|
+
export { redactUrl, netUrlFields };
|
|
10
10
|
/** The byte size of a binary frame (ArrayBuffer / Blob / typed-array view), or undefined if unknown. */
|
|
11
11
|
function binaryFrameBytes(data) {
|
|
12
12
|
if (data instanceof ArrayBuffer)
|
|
@@ -244,10 +244,17 @@ export function installNetwork(emit, opts = {}) {
|
|
|
244
244
|
const id = nextId();
|
|
245
245
|
const start = performance.now();
|
|
246
246
|
const method = methodOf(input, init);
|
|
247
|
-
const
|
|
247
|
+
const urlFields = netUrlFields(rawUrl);
|
|
248
|
+
const url = urlFields.url;
|
|
248
249
|
const initiatorStack = initiatorFrame();
|
|
249
250
|
const initiatorFields = initiatorStack === undefined ? {} : { initiatorStack };
|
|
250
|
-
emit(EventType.NET_PENDING, {
|
|
251
|
+
emit(EventType.NET_PENDING, {
|
|
252
|
+
id,
|
|
253
|
+
method,
|
|
254
|
+
...urlFields,
|
|
255
|
+
initiator: 'fetch',
|
|
256
|
+
...initiatorFields,
|
|
257
|
+
});
|
|
251
258
|
try {
|
|
252
259
|
const res = await callFetch(input, init);
|
|
253
260
|
// The app's fetch resolves HERE — at headers — like a native fetch. durationMs is measured to
|
|
@@ -261,7 +268,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
261
268
|
emit(EventType.NET_REQUEST, {
|
|
262
269
|
id,
|
|
263
270
|
method,
|
|
264
|
-
|
|
271
|
+
...urlFields,
|
|
265
272
|
status: res.status,
|
|
266
273
|
ok: statusIsOk(res.status),
|
|
267
274
|
durationMs: Math.round(headersAt - start),
|
|
@@ -376,7 +383,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
376
383
|
emit(EventType.NET_PENDING, {
|
|
377
384
|
id: m.id,
|
|
378
385
|
method: m.method,
|
|
379
|
-
|
|
386
|
+
...netUrlFields(m.rawUrl),
|
|
380
387
|
initiator: 'xhr',
|
|
381
388
|
...initiatorFields,
|
|
382
389
|
});
|
|
@@ -405,7 +412,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
405
412
|
emit(EventType.NET_REQUEST, {
|
|
406
413
|
id: cur.id,
|
|
407
414
|
method: cur.method,
|
|
408
|
-
|
|
415
|
+
...netUrlFields(cur.rawUrl),
|
|
409
416
|
status: this.status,
|
|
410
417
|
ok: statusIsOk(this.status),
|
|
411
418
|
durationMs: Math.round(performance.now() - cur.start),
|
|
@@ -432,17 +439,17 @@ export function installNetwork(emit, opts = {}) {
|
|
|
432
439
|
window.EventSource = class extends origEventSource {
|
|
433
440
|
constructor(u, init) {
|
|
434
441
|
super(u, init);
|
|
435
|
-
const
|
|
442
|
+
const urlFields = netUrlFields(String(u));
|
|
436
443
|
emit(EventType.NET_STREAM, {
|
|
437
444
|
transport: StreamTransport.SSE,
|
|
438
445
|
direction: StreamDirection.OPEN,
|
|
439
|
-
|
|
446
|
+
...urlFields,
|
|
440
447
|
});
|
|
441
448
|
this.addEventListener('message', (ev) => {
|
|
442
449
|
emit(EventType.NET_STREAM, {
|
|
443
450
|
transport: StreamTransport.SSE,
|
|
444
451
|
direction: StreamDirection.IN,
|
|
445
|
-
|
|
452
|
+
...urlFields,
|
|
446
453
|
...frameFields(ev.data, captureBodies),
|
|
447
454
|
});
|
|
448
455
|
});
|
|
@@ -459,17 +466,17 @@ export function installNetwork(emit, opts = {}) {
|
|
|
459
466
|
this.#isBridge = isBridgeSocket(String(u));
|
|
460
467
|
if (this.#isBridge)
|
|
461
468
|
return;
|
|
462
|
-
const
|
|
469
|
+
const urlFields = netUrlFields(String(u));
|
|
463
470
|
emit(EventType.NET_STREAM, {
|
|
464
471
|
transport: StreamTransport.WS,
|
|
465
472
|
direction: StreamDirection.OPEN,
|
|
466
|
-
|
|
473
|
+
...urlFields,
|
|
467
474
|
});
|
|
468
475
|
this.addEventListener('message', (ev) => {
|
|
469
476
|
emit(EventType.NET_STREAM, {
|
|
470
477
|
transport: StreamTransport.WS,
|
|
471
478
|
direction: StreamDirection.IN,
|
|
472
|
-
|
|
479
|
+
...urlFields,
|
|
473
480
|
...frameFields(ev.data, captureBodies),
|
|
474
481
|
});
|
|
475
482
|
});
|
|
@@ -486,7 +493,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
486
493
|
emit(EventType.NET_STREAM, {
|
|
487
494
|
transport: StreamTransport.WS,
|
|
488
495
|
direction: StreamDirection.OUT,
|
|
489
|
-
|
|
496
|
+
...netUrlFields(this.url),
|
|
490
497
|
...frameFields(data, captureBodies),
|
|
491
498
|
});
|
|
492
499
|
super.send(data);
|
|
@@ -504,7 +511,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
504
511
|
if (navProto !== null && origBeacon !== undefined) {
|
|
505
512
|
patchedBeacon = function (url, data) {
|
|
506
513
|
const id = nextId();
|
|
507
|
-
const
|
|
514
|
+
const urlFields = netUrlFields(String(url));
|
|
508
515
|
const initiatorStack = initiatorFrame();
|
|
509
516
|
const sent = origBeacon.call(this, url, data);
|
|
510
517
|
// sendBeacon returns whether the payload was QUEUED, not an HTTP result — the response never
|
|
@@ -513,7 +520,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
513
520
|
emit(EventType.NET_REQUEST, {
|
|
514
521
|
id,
|
|
515
522
|
method: 'POST',
|
|
516
|
-
|
|
523
|
+
...urlFields,
|
|
517
524
|
status: 0,
|
|
518
525
|
ok: sent,
|
|
519
526
|
queued: sent,
|
|
@@ -558,7 +565,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
558
565
|
emit(EventType.NET_REQUEST, {
|
|
559
566
|
id: nextId(),
|
|
560
567
|
method: 'GET',
|
|
561
|
-
|
|
568
|
+
...netUrlFields(rawUrl),
|
|
562
569
|
durationMs: Math.round(entry.duration),
|
|
563
570
|
initiator: type,
|
|
564
571
|
...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
|
package/dist/observers/scroll.js
CHANGED
|
@@ -5,6 +5,8 @@ const THROTTLE_MS = 100;
|
|
|
5
5
|
const REVEAL_SELECTOR = '[data-reticle-reveal], [data-reveal], section';
|
|
6
6
|
/** Observe scroll position + reveal-on-scroll for modern scroll-reactive UIs. */
|
|
7
7
|
export function installScroll(emit) {
|
|
8
|
+
const ac = new AbortController();
|
|
9
|
+
const { signal } = ac;
|
|
8
10
|
let lastEmit = 0;
|
|
9
11
|
let lastY = 0;
|
|
10
12
|
let trailingTimer;
|
|
@@ -39,7 +41,7 @@ export function installScroll(emit) {
|
|
|
39
41
|
}, THROTTLE_MS - elapsed);
|
|
40
42
|
}
|
|
41
43
|
};
|
|
42
|
-
window.addEventListener('scroll', onScroll, { passive: true });
|
|
44
|
+
window.addEventListener('scroll', onScroll, { passive: true, signal });
|
|
43
45
|
let io;
|
|
44
46
|
if ('function' === typeof IntersectionObserver) {
|
|
45
47
|
io = new IntersectionObserver((entries) => {
|
|
@@ -60,7 +62,7 @@ export function installScroll(emit) {
|
|
|
60
62
|
observer.observe(el);
|
|
61
63
|
}
|
|
62
64
|
return () => {
|
|
63
|
-
|
|
65
|
+
ac.abort();
|
|
64
66
|
if (trailingTimer !== undefined)
|
|
65
67
|
nativeClearTimeout(trailingTimer);
|
|
66
68
|
io?.disconnect();
|
|
@@ -65,7 +65,7 @@ export function readStorage(area) {
|
|
|
65
65
|
return readArea(safeArea(() => window.localStorage));
|
|
66
66
|
if (area === StorageArea.SESSION)
|
|
67
67
|
return readArea(safeArea(() => window.sessionStorage));
|
|
68
|
-
if (
|
|
68
|
+
if (StorageArea.COOKIE === area)
|
|
69
69
|
return readCookies();
|
|
70
70
|
return {
|
|
71
71
|
local: readArea(safeArea(() => window.localStorage)),
|
|
@@ -9,28 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* Brand marks, inlined. The SDK is bundled into someone else's page: it cannot read
|
|
12
|
-
* `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the mark
|
|
13
|
+
* lives here as a string (same precedent as SEND_ICON in presenter-controls.ts).
|
|
14
|
+
* Minified - editor metadata, clip paths, masks and sub-0.01 precision stripped - and painted
|
|
15
15
|
* with `currentColor` so the mark tracks --reticle-fg instead of hardcoding the export's #FAFAFA.
|
|
16
16
|
*/
|
|
17
|
-
/** Accessible name of the HUD
|
|
17
|
+
/** Accessible name of the HUD. */
|
|
18
18
|
export declare const BRAND_NAME = "Reticle";
|
|
19
19
|
/** Mark alone (shown in the FAB and compact toolbar). */
|
|
20
20
|
export declare const MARK_SVG: string;
|
|
21
|
-
/** FAB-sized mark for the collapsed circular control. */
|
|
22
|
-
export declare const FAB_MARK_SVG: string;
|
|
23
|
-
/** Mark + "reticle" lettering (legacy wordmark artwork). */
|
|
24
|
-
export declare const WORDMARK_SVG: string;
|
|
25
|
-
/** Compact text label beside the mark in the expanded toolbar. */
|
|
26
|
-
export declare const BRAND_MINI_CLASS = "reticle-brand-mini";
|
|
27
|
-
/** Toolbar brand: mark + Reticle word (always shown together when expanded). */
|
|
28
|
-
export declare const TOOLBAR_BRAND_HTML: string;
|
|
29
|
-
/**
|
|
30
|
-
* Header brand block. Both marks are always in the DOM and CSS picks one off the collapsed
|
|
31
|
-
* attribute, so there is no JS state to keep in sync. The artwork is decorative (aria-hidden);
|
|
32
|
-
* the visually-hidden name is what keeps the HUD identifiable to a screen reader in BOTH states.
|
|
33
|
-
*/
|
|
34
|
-
export declare const BRAND_HTML: string;
|
|
35
21
|
/** Collapsed FAB control - Reticle mark only. */
|
|
36
22
|
export declare const FAB_TOGGLE_HTML: string;
|
|
@@ -9,30 +9,18 @@
|
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* Brand marks, inlined. The SDK is bundled into someone else's page: it cannot read
|
|
12
|
-
* `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the mark
|
|
13
|
+
* lives here as a string (same precedent as SEND_ICON in presenter-controls.ts).
|
|
14
|
+
* Minified - editor metadata, clip paths, masks and sub-0.01 precision stripped - and painted
|
|
15
15
|
* with `currentColor` so the mark tracks --reticle-fg instead of hardcoding the export's #FAFAFA.
|
|
16
16
|
*/
|
|
17
|
-
/** Accessible name of the HUD
|
|
17
|
+
/** Accessible name of the HUD. */
|
|
18
18
|
export const BRAND_NAME = 'Reticle';
|
|
19
|
-
/** The two glyph paths of the mark
|
|
19
|
+
/** The two glyph paths of the mark. */
|
|
20
20
|
const MARK_PATHS = `<path d="M92.82 92.16C93.07 93.4 91.77 94.4 90.63 93.83C89.62 93.33 88.62 92.82 87.63 92.3C71.14 83.78 54.79 72.27 39.61 57.96C39.09 57.49 38.57 56.98 38.06 56.49C22.45 41.52 9.78 25.14 .33 8.52C0.29 8.45 .25 8.38 .2 8.31C-0.44 7.2 .51 5.84 1.79 6.05L24.07 9.71C43.06 12.83 60.63 11.75 75.55 6.53L93.95 .09C95.15-0.33 96.31 .78 95.93 1.98L90.1 20.49C85.37 35.48 84.9 52.97 88.69 71.73L92.83 92.16H92.82Z"/><path d="M58.72 89.23L6.15 92.8C5.24 92.86 4.48 92.13 4.51 91.22L6.56 30.39C6.61 28.91 8.51 28.36 9.35 29.57C16.23 39.43 24.98 52.35 33.01 61.26C41.05 70.17 50.6 78.72 59.62 86.56C60.65 87.45 60.08 89.13 58.72 89.23Z"/>`;
|
|
21
21
|
/** Mark alone (shown in the FAB and compact toolbar). */
|
|
22
22
|
export const MARK_SVG = `<svg class="reticle-mark" viewBox="0 0 96 94" fill="currentColor" aria-hidden="true">${MARK_PATHS}</svg>`;
|
|
23
23
|
/** FAB-sized mark for the collapsed circular control. */
|
|
24
|
-
|
|
25
|
-
/** Mark + "reticle" lettering (legacy wordmark artwork). */
|
|
26
|
-
export const WORDMARK_SVG = `<svg class="reticle-wordmark" viewBox="0 0 347 96" fill="currentColor" aria-hidden="true"><g transform="translate(1 1)">${MARK_PATHS}</g><path d="M116.43 78V27.09H136.51C140.35 27.09 143.64 27.78 146.35 29.15C149.09 30.51 151.17 32.44 152.59 34.95C154.04 37.43 154.76 40.36 154.76 43.72C154.76 47.1 154.03 50.01 152.57 52.45C151.11 54.87 149 56.72 146.23 58.01C143.48 59.31 140.15 59.95 136.24 59.95H122.79V51.3H134.5C136.55 51.3 138.26 51.02 139.62 50.46C140.98 49.89 141.99 49.05 142.65 47.92C143.33 46.8 143.67 45.39 143.67 43.72C143.67 42.03 143.33 40.61 142.65 39.45C141.99 38.29 140.97 37.41 139.59 36.81C138.23 36.2 136.52 35.89 134.45 35.89H127.19V78H116.43ZM143.92 54.83L156.57 78H144.69L132.31 54.83H143.92ZM178.95 78.75C175.02 78.75 171.64 77.95 168.81 76.36C165.99 74.75 163.82 72.48 162.29 69.55C160.77 66.6 160.01 63.11 160.01 59.08C160.01 55.16 160.77 51.71 162.29 48.74C163.82 45.78 165.97 43.46 168.73 41.81C171.52 40.15 174.78 39.32 178.53 39.32C181.05 39.32 183.39 39.73 185.56 40.54C187.75 41.33 189.65 42.54 191.28 44.14C192.92 45.75 194.2 47.77 195.11 50.21C196.02 52.63 196.47 55.46 196.47 58.71V61.62H164.23V55.06H186.51C186.51 53.53 186.17 52.18 185.51 51C184.85 49.83 183.93 48.91 182.75 48.25C181.59 47.57 180.24 47.23 178.7 47.23C177.09 47.23 175.67 47.6 174.43 48.34C173.2 49.07 172.24 50.06 171.54 51.3C170.85 52.53 170.49 53.9 170.47 55.4V61.64C170.47 63.53 170.82 65.17 171.52 66.54C172.23 67.92 173.23 68.98 174.52 69.72C175.82 70.47 177.35 70.84 179.12 70.84C180.3 70.84 181.38 70.68 182.35 70.34C183.33 70.01 184.17 69.52 184.87 68.85C185.56 68.19 186.09 67.38 186.46 66.42L196.25 67.06C195.75 69.42 194.73 71.47 193.19 73.23C191.67 74.97 189.7 76.33 187.28 77.3C184.87 78.27 182.1 78.75 178.95 78.75ZM223.69 39.82V47.77H200.7V39.82H223.69ZM205.92 30.67H216.51V66.27C216.51 67.24 216.66 68.01 216.96 68.55C217.26 69.08 217.67 69.46 218.2 69.67C218.75 69.89 219.38 70 220.09 70C220.59 70 221.08 69.95 221.58 69.87C222.08 69.77 222.46 69.7 222.72 69.65L224.39 77.53C223.86 77.69 223.11 77.88 222.15 78.1C221.19 78.33 220.02 78.47 218.65 78.52C216.1 78.62 213.86 78.28 211.94 77.5C210.03 76.72 208.55 75.51 207.49 73.87C206.43 72.23 205.9 70.16 205.92 67.66V30.67ZM230.59 78V39.82H241.18V78H230.59ZM235.91 34.9C234.34 34.9 232.99 34.37 231.86 33.33C230.75 32.27 230.19 31 230.19 29.53C230.19 28.07 230.75 26.82 231.86 25.77C232.99 24.71 234.34 24.18 235.91 24.18C237.49 24.18 238.83 24.71 239.94 25.77C241.07 26.82 241.63 28.07 241.63 29.53C241.63 31 241.07 32.27 239.94 33.33C238.83 34.37 237.49 34.9 235.91 34.9ZM266.92 78.75C263 78.75 259.64 77.92 256.82 76.26C254.02 74.59 251.87 72.27 250.36 69.3C248.87 66.33 248.12 62.92 248.12 59.06C248.12 55.15 248.88 51.72 250.38 48.77C251.91 45.8 254.07 43.49 256.87 41.83C259.67 40.16 263 39.32 266.87 39.32C270.2 39.32 273.11 39.93 275.62 41.14C278.12 42.35 280.1 44.04 281.56 46.23C283.01 48.42 283.82 50.99 283.97 53.94H273.97C273.69 52.03 272.95 50.5 271.74 49.34C270.54 48.16 268.98 47.57 267.04 47.57C265.4 47.57 263.96 48.02 262.74 48.92C261.53 49.79 260.58 51.08 259.9 52.77C259.23 54.46 258.89 56.51 258.89 58.91C258.89 61.35 259.22 63.42 259.88 65.12C260.56 66.83 261.51 68.13 262.74 69.03C263.96 69.92 265.4 70.37 267.04 70.37C268.25 70.37 269.33 70.12 270.3 69.62C271.27 69.13 272.08 68.4 272.71 67.46C273.35 66.5 273.78 65.35 273.97 64.01H283.97C283.8 66.92 283.01 69.49 281.58 71.71C280.17 73.92 278.23 75.64 275.74 76.88C273.25 78.12 270.31 78.75 266.92 78.75ZM301.34 27.09V78H290.75V27.09H301.34ZM327.22 78.75C323.29 78.75 319.91 77.95 317.08 76.36C314.26 74.75 312.09 72.48 310.57 69.55C309.04 66.6 308.28 63.11 308.28 59.08C308.28 55.16 309.04 51.71 310.57 48.74C312.09 45.78 314.24 43.46 317 41.81C319.79 40.15 323.05 39.32 326.8 39.32C329.32 39.32 331.66 39.73 333.83 40.54C336.02 41.33 337.93 42.54 339.55 44.14C341.19 45.75 342.47 47.77 343.38 50.21C344.29 52.63 344.75 55.46 344.75 58.71V61.62H312.5V55.06H334.78C334.78 53.53 334.45 52.18 333.78 51C333.12 49.83 332.2 48.91 331.02 48.25C329.86 47.57 328.51 47.23 326.97 47.23C325.36 47.23 323.94 47.6 322.7 48.34C321.47 49.07 320.51 50.06 319.81 51.3C319.12 52.53 318.76 53.9 318.74 55.4V61.64C318.74 63.53 319.09 65.17 319.79 66.54C320.5 67.92 321.5 68.98 322.8 69.72C324.09 70.47 325.62 70.84 327.39 70.84C328.57 70.84 329.65 70.68 330.63 70.34C331.6 70.01 332.44 69.52 333.14 68.85C333.83 68.19 334.36 67.38 334.73 66.42L344.52 67.06C344.02 69.42 343.01 71.47 341.46 73.23C339.94 74.97 337.97 76.33 335.55 77.3C333.15 78.27 330.37 78.75 327.22 78.75Z"/></svg>`;
|
|
27
|
-
/** Compact text label beside the mark in the expanded toolbar. */
|
|
28
|
-
export const BRAND_MINI_CLASS = 'reticle-brand-mini';
|
|
29
|
-
/** Toolbar brand: mark + Reticle word (always shown together when expanded). */
|
|
30
|
-
export const TOOLBAR_BRAND_HTML = `<span class="reticle-brand reticle-brand--toolbar">${MARK_SVG}<span class="${BRAND_MINI_CLASS}">${BRAND_NAME}</span><span class="reticle-brand-sr">${BRAND_NAME}</span></span>`;
|
|
31
|
-
/**
|
|
32
|
-
* Header brand block. Both marks are always in the DOM and CSS picks one off the collapsed
|
|
33
|
-
* attribute, so there is no JS state to keep in sync. The artwork is decorative (aria-hidden);
|
|
34
|
-
* the visually-hidden name is what keeps the HUD identifiable to a screen reader in BOTH states.
|
|
35
|
-
*/
|
|
36
|
-
export const BRAND_HTML = `<span class="reticle-brand">${MARK_SVG}${WORDMARK_SVG}<span class="${BRAND_MINI_CLASS}">${BRAND_NAME}</span><span class="reticle-brand-sr">${BRAND_NAME}</span></span>`;
|
|
24
|
+
const FAB_MARK_SVG = `<svg class="reticle-fab-mark" viewBox="0 0 96 94" fill="currentColor" aria-hidden="true">${MARK_PATHS}</svg>`;
|
|
37
25
|
/** Collapsed FAB control - Reticle mark only. */
|
|
38
26
|
export const FAB_TOGGLE_HTML = `<button type="button" class="reticle-fab" data-reticle-fab aria-label="Start ${BRAND_NAME}" aria-expanded="false">${FAB_MARK_SVG}<span class="reticle-fab-pulse" aria-hidden="true"></span><span data-reticle-mark-count class="reticle-fab-badge" hidden>0</span></button>`;
|
|
@@ -104,6 +104,15 @@ export declare const GLOW_FADE_MS = 250;
|
|
|
104
104
|
export declare const GLOW_ON = "1";
|
|
105
105
|
export declare const GLOW_OFF = "0";
|
|
106
106
|
export declare const DATA_ON = "data-on";
|
|
107
|
+
/** The overlay root's state attribute, which the shell styles resolve colour against. */
|
|
108
|
+
export declare const STATE_ATTR = "data-reticle-state";
|
|
109
|
+
/**
|
|
110
|
+
* Not a SessionState: there is no session. It is the absence of one, said out loud.
|
|
111
|
+
*
|
|
112
|
+
* Kept off `SessionState` deliberately — that enum crosses the wire, and this state exists only
|
|
113
|
+
* because nothing crossed the wire.
|
|
114
|
+
*/
|
|
115
|
+
export declare const UNREACHABLE_STATE = "unreachable";
|
|
107
116
|
/** Overlay-root attribute toggled when the HUD is collapsed to the FAB. */
|
|
108
117
|
export declare const MIN_ATTR = "data-reticle-min";
|
|
109
118
|
/** Overlay-root attribute toggled when the agent chat panel is open. */
|
|
@@ -126,10 +135,6 @@ export declare const Placement: {
|
|
|
126
135
|
readonly ABOVE: "above";
|
|
127
136
|
readonly BELOW: "below";
|
|
128
137
|
};
|
|
129
|
-
export declare const DockAlign: {
|
|
130
|
-
readonly START: "start";
|
|
131
|
-
readonly END: "end";
|
|
132
|
-
};
|
|
133
138
|
/** Show/hide annotation markers. */
|
|
134
139
|
export declare const MARKERS_BTN_ATTR = "data-reticle-markers-btn";
|
|
135
140
|
/** Clear every annotation on the page. */
|
|
@@ -177,10 +182,8 @@ export declare const LOG_TIMESTAMPS_ATTR = "data-reticle-log-ts";
|
|
|
177
182
|
/** When 1, the chat panel uses a narrower width. */
|
|
178
183
|
/** When 1, HUD motion is reduced for accessibility. */
|
|
179
184
|
export declare const REDUCE_MOTION_ATTR = "data-reticle-reduce-motion";
|
|
180
|
-
/** Presenter settings card version label (browser package). */
|
|
181
|
-
export declare const PRESENTER_UI_VERSION = "2.7.0";
|
|
182
185
|
/** Docs opened from the settings footer. */
|
|
183
|
-
export declare const MCP_DOCS_URL
|
|
186
|
+
export declare const MCP_DOCS_URL: string;
|
|
184
187
|
/** HUD has been dragged off the default dock - positioned with explicit left/top. */
|
|
185
188
|
export declare const HUD_DRAGGED_ATTR = "data-dragged";
|
|
186
189
|
/** CSS custom properties written when the HUD is dragged. */
|
|
@@ -191,5 +194,5 @@ export declare const HUD_DRAG_THRESHOLD_PX = 4;
|
|
|
191
194
|
/** Minimum gap between a dragged HUD and the viewport edge. */
|
|
192
195
|
export declare const HUD_DOCK_MARGIN_PX = 8;
|
|
193
196
|
/** Interactive nodes inside the drag handle that must not start a drag (not the FAB itself). */
|
|
194
|
-
export declare const HUD_DRAG_IGNORE_SEL
|
|
197
|
+
export declare const HUD_DRAG_IGNORE_SEL: string;
|
|
195
198
|
export declare const THROTTLED_ATTR = "data-reticle-throttled";
|
|
@@ -72,6 +72,15 @@ export const GLOW_FADE_MS = 250;
|
|
|
72
72
|
export const GLOW_ON = '1';
|
|
73
73
|
export const GLOW_OFF = '0';
|
|
74
74
|
export const DATA_ON = 'data-on';
|
|
75
|
+
/** The overlay root's state attribute, which the shell styles resolve colour against. */
|
|
76
|
+
export const STATE_ATTR = 'data-reticle-state';
|
|
77
|
+
/**
|
|
78
|
+
* Not a SessionState: there is no session. It is the absence of one, said out loud.
|
|
79
|
+
*
|
|
80
|
+
* Kept off `SessionState` deliberately — that enum crosses the wire, and this state exists only
|
|
81
|
+
* because nothing crossed the wire.
|
|
82
|
+
*/
|
|
83
|
+
export const UNREACHABLE_STATE = 'unreachable';
|
|
75
84
|
/** Overlay-root attribute toggled when the HUD is collapsed to the FAB. */
|
|
76
85
|
export const MIN_ATTR = 'data-reticle-min';
|
|
77
86
|
/** Overlay-root attribute toggled when the agent chat panel is open. */
|
|
@@ -91,7 +100,6 @@ export const SETTINGS_PLACEMENT_ATTR = 'data-reticle-settings-placement';
|
|
|
91
100
|
/** Horizontal alignment of dock-anchored panels (start = left, end = right). */
|
|
92
101
|
export const DOCK_ALIGN_ATTR = 'data-reticle-dock-align';
|
|
93
102
|
export const Placement = { ABOVE: 'above', BELOW: 'below' };
|
|
94
|
-
export const DockAlign = { START: 'start', END: 'end' };
|
|
95
103
|
/** Show/hide annotation markers. */
|
|
96
104
|
export const MARKERS_BTN_ATTR = 'data-reticle-markers-btn';
|
|
97
105
|
/** Clear every annotation on the page. */
|
|
@@ -139,8 +147,6 @@ export const LOG_TIMESTAMPS_ATTR = 'data-reticle-log-ts';
|
|
|
139
147
|
/** When 1, the chat panel uses a narrower width. */
|
|
140
148
|
/** When 1, HUD motion is reduced for accessibility. */
|
|
141
149
|
export const REDUCE_MOTION_ATTR = 'data-reticle-reduce-motion';
|
|
142
|
-
/** Presenter settings card version label (browser package). */
|
|
143
|
-
export const PRESENTER_UI_VERSION = '2.7.0';
|
|
144
150
|
/** Docs opened from the settings footer. */
|
|
145
151
|
export const MCP_DOCS_URL = 'https://github.com/reticlehq/reticle/blob/main/docs/getting-started.md';
|
|
146
152
|
/** HUD has been dragged off the default dock - positioned with explicit left/top. */
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { HumanControlKind, PresenterTone, SessionState } from '@reticlehq/core';
|
|
2
|
-
export declare const PAUSED_BADGE_LABEL = "PAUSED";
|
|
3
2
|
/** Border fade-out delay after a session ends (native timer; presenter-only tunable). */
|
|
4
3
|
export declare const ENDED_FADE_MS = 4000;
|
|
5
4
|
/** Payload the panel hands to its host when the human drives a control. */
|
|
@@ -12,8 +11,6 @@ export type ControlHandler = (intent: ControlIntent) => void;
|
|
|
12
11
|
export declare const CONTROLS_CSS: string;
|
|
13
12
|
/** Icon toolbar: pause, chat, markers, end, clear - copy/export appear after a run ends. */
|
|
14
13
|
export declare const CONTROLS_TOOLBAR_HTML: string;
|
|
15
|
-
/** @deprecated Legacy head markup - replaced by CONTROLS_TOOLBAR_HTML in the morphing shell. */
|
|
16
|
-
export declare const CONTROLS_HEAD_HTML: string;
|
|
17
14
|
/** Banner markup (between head and log, hidden unless ended). */
|
|
18
15
|
export declare const CONTROLS_BANNER_HTML = "<div data-reticle-banner class=\"reticle-banner\">Session ended</div>";
|
|
19
16
|
/** Replay-a-flow row (between log and footer); buttons are filled in by setFlows once flows arrive. */
|
|
@@ -26,7 +26,6 @@ const INPUT_PLACEHOLDER = 'Tell the agent something…';
|
|
|
26
26
|
/** Accessible name for the composer (a placeholder is not an accessible name). */
|
|
27
27
|
const INPUT_ARIA_LABEL = 'Message to the agent';
|
|
28
28
|
const PAUSED_BADGE_TEXT = 'PAUSED';
|
|
29
|
-
export const PAUSED_BADGE_LABEL = PAUSED_BADGE_TEXT;
|
|
30
29
|
const ENDED_BANNER_TEXT = 'Session ended';
|
|
31
30
|
const COPY_LABEL = 'Copy run';
|
|
32
31
|
const EXPORT_LABEL = 'Export';
|
|
@@ -177,8 +176,6 @@ export const CONTROLS_TOOLBAR_HTML = [
|
|
|
177
176
|
`<button type="button" data-reticle-export class="reticle-tb-btn reticle-tb-btn--export" title="${EXPORT_LABEL}" aria-label="${EXPORT_LABEL}" hidden>${hiIconHtml(PresenterIcon.DOWNLOAD, TB)}</button>`,
|
|
178
177
|
'<span data-reticle-export-msg class="reticle-export-msg" aria-live="polite"></span>',
|
|
179
178
|
].join('');
|
|
180
|
-
/** @deprecated Legacy head markup - replaced by CONTROLS_TOOLBAR_HTML in the morphing shell. */
|
|
181
|
-
export const CONTROLS_HEAD_HTML = CONTROLS_TOOLBAR_HTML;
|
|
182
179
|
/** Banner markup (between head and log, hidden unless ended). */
|
|
183
180
|
export const CONTROLS_BANNER_HTML = `<div data-reticle-banner class="reticle-banner">${ENDED_BANNER_TEXT}</div>`;
|
|
184
181
|
/** Replay-a-flow row (between log and footer); buttons are filled in by setFlows once flows arrive. */
|
|
@@ -14,4 +14,4 @@ export declare const HUD_LOG_WELL_CLASS = "reticle-hud-log-well";
|
|
|
14
14
|
* the panel read as a container rather than as one card. It also grew and shrank with its content,
|
|
15
15
|
* so the panel resized on every row; the height is fixed now and the feed scrolls inside it.
|
|
16
16
|
*/
|
|
17
|
-
export declare const HUD_LOG_WELL_CSS
|
|
17
|
+
export declare const HUD_LOG_WELL_CSS: string;
|
|
@@ -32,7 +32,19 @@ export declare const REPORT_TEXT: {
|
|
|
32
32
|
readonly TITLE: "Impact";
|
|
33
33
|
readonly PROJECT: "This project";
|
|
34
34
|
readonly GLOBAL: "Everything on this machine";
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* What `counts.failed` actually is: a verdict whose declared consequence did not hold.
|
|
37
|
+
*
|
|
38
|
+
* It used to read "defects caught before you saw them", which overclaims in the one direction a
|
|
39
|
+
* verification tool must never overclaim. A failed verdict is not proof of a defect in the app —
|
|
40
|
+
* it is equally the shape of an assertion that was wrong. Measured in the field: an agent
|
|
41
|
+
* asserted a clean console on an app with ordinary dev-mode logging, the verdict went red, and
|
|
42
|
+
* the panel reported a defect nobody had found.
|
|
43
|
+
*
|
|
44
|
+
* The honest word is what Reticle DID: it refused to pass them. That is true of the assertion
|
|
45
|
+
* error and the real bug alike, and it still reads as the tool having done its job.
|
|
46
|
+
*/
|
|
47
|
+
readonly HERO_DEFECTS: "checks Reticle refused to pass";
|
|
36
48
|
readonly VERDICTS: "Verdicts";
|
|
37
49
|
readonly PASSED: "passed";
|
|
38
50
|
readonly FAILED: "failed";
|
|
@@ -48,6 +60,23 @@ export declare const REPORT_TEXT: {
|
|
|
48
60
|
readonly SAVED_MINUTES: "Time saved";
|
|
49
61
|
readonly ESTIMATE_TAG: "estimate";
|
|
50
62
|
readonly CHART: "Verdicts, last 30 days";
|
|
63
|
+
readonly DEFECTS: "What broke";
|
|
64
|
+
readonly DEFECTS_MORE: "Manage all of them on the dashboard";
|
|
65
|
+
/**
|
|
66
|
+
* What an UNLINKED user is told, and the only place the product tells them.
|
|
67
|
+
*
|
|
68
|
+
* The dashboard link renders only when `dashboardUrl` exists, which means only once a repo is
|
|
69
|
+
* already linked — so the person most likely to want one, watching this record climb on their own
|
|
70
|
+
* machine, was never told it existed. The single mention anywhere else is a daemon log line.
|
|
71
|
+
*
|
|
72
|
+
* Stated as a FACT about where the record lives, not as a pitch. It sits at the foot of a panel
|
|
73
|
+
* somebody opened on purpose, so it informs rather than interrupts, and it appears only once
|
|
74
|
+
* there is a verdict worth keeping — an offer to preserve nothing is just an advert.
|
|
75
|
+
*/
|
|
76
|
+
readonly LOCAL_ONLY: "This record stops at this machine.";
|
|
77
|
+
readonly LOCAL_ONLY_ACTION: "reticle login";
|
|
78
|
+
readonly LOCAL_ONLY_TAIL: "keeps it, and lets a team see it.";
|
|
79
|
+
readonly DEFECTS_NONE: "Nothing has failed a declared consequence yet.";
|
|
51
80
|
readonly EMPTY: "Nothing recorded yet. Drive the app once and this fills in.";
|
|
52
81
|
readonly SHARE: "Share";
|
|
53
82
|
readonly COPY: "Copy";
|
|
@@ -32,7 +32,19 @@ export const REPORT_TEXT = {
|
|
|
32
32
|
TITLE: 'Impact',
|
|
33
33
|
PROJECT: 'This project',
|
|
34
34
|
GLOBAL: 'Everything on this machine',
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* What `counts.failed` actually is: a verdict whose declared consequence did not hold.
|
|
37
|
+
*
|
|
38
|
+
* It used to read "defects caught before you saw them", which overclaims in the one direction a
|
|
39
|
+
* verification tool must never overclaim. A failed verdict is not proof of a defect in the app —
|
|
40
|
+
* it is equally the shape of an assertion that was wrong. Measured in the field: an agent
|
|
41
|
+
* asserted a clean console on an app with ordinary dev-mode logging, the verdict went red, and
|
|
42
|
+
* the panel reported a defect nobody had found.
|
|
43
|
+
*
|
|
44
|
+
* The honest word is what Reticle DID: it refused to pass them. That is true of the assertion
|
|
45
|
+
* error and the real bug alike, and it still reads as the tool having done its job.
|
|
46
|
+
*/
|
|
47
|
+
HERO_DEFECTS: 'checks Reticle refused to pass',
|
|
36
48
|
VERDICTS: 'Verdicts',
|
|
37
49
|
PASSED: 'passed',
|
|
38
50
|
FAILED: 'failed',
|
|
@@ -48,6 +60,23 @@ export const REPORT_TEXT = {
|
|
|
48
60
|
SAVED_MINUTES: 'Time saved',
|
|
49
61
|
ESTIMATE_TAG: 'estimate',
|
|
50
62
|
CHART: 'Verdicts, last 30 days',
|
|
63
|
+
DEFECTS: 'What broke',
|
|
64
|
+
DEFECTS_MORE: 'Manage all of them on the dashboard',
|
|
65
|
+
/**
|
|
66
|
+
* What an UNLINKED user is told, and the only place the product tells them.
|
|
67
|
+
*
|
|
68
|
+
* The dashboard link renders only when `dashboardUrl` exists, which means only once a repo is
|
|
69
|
+
* already linked — so the person most likely to want one, watching this record climb on their own
|
|
70
|
+
* machine, was never told it existed. The single mention anywhere else is a daemon log line.
|
|
71
|
+
*
|
|
72
|
+
* Stated as a FACT about where the record lives, not as a pitch. It sits at the foot of a panel
|
|
73
|
+
* somebody opened on purpose, so it informs rather than interrupts, and it appears only once
|
|
74
|
+
* there is a verdict worth keeping — an offer to preserve nothing is just an advert.
|
|
75
|
+
*/
|
|
76
|
+
LOCAL_ONLY: 'This record stops at this machine.',
|
|
77
|
+
LOCAL_ONLY_ACTION: 'reticle login',
|
|
78
|
+
LOCAL_ONLY_TAIL: 'keeps it, and lets a team see it.',
|
|
79
|
+
DEFECTS_NONE: 'Nothing has failed a declared consequence yet.',
|
|
51
80
|
EMPTY: 'Nothing recorded yet. Drive the app once and this fills in.',
|
|
52
81
|
SHARE: 'Share',
|
|
53
82
|
COPY: 'Copy',
|
|
@@ -88,7 +117,7 @@ export function buildShareText(scope, projectName) {
|
|
|
88
117
|
const where = projectName !== undefined && projectName.length > 0 ? ` on ${projectName}` : '';
|
|
89
118
|
const lines = [
|
|
90
119
|
`My agent verified its own work ${String(c.verdicts)} times${where}.`,
|
|
91
|
-
`${String(c.failed)}
|
|
120
|
+
`${String(c.failed)} checks it refused to pass before I looked at any of them.`,
|
|
92
121
|
];
|
|
93
122
|
if (c.unknown > 0) {
|
|
94
123
|
lines.push(`${String(c.unknown)} came back "unknown" - it drove the app and could not tell. That is the number I watch.`);
|
|
@@ -73,6 +73,30 @@ export const REPORT_CSS = `
|
|
|
73
73
|
[${REPORT_PANEL_ATTR}] .reticle-report-basis{
|
|
74
74
|
display:block;margin-top:2px;color:var(--reticle-c-active);opacity:.8;
|
|
75
75
|
font-size:8.5px;letter-spacing:.06em;text-transform:uppercase;cursor:help;}
|
|
76
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-defects-wrap{margin-top:14px;}
|
|
77
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-defects{
|
|
78
|
+
display:flex;flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;}
|
|
79
|
+
/* A left rule rather than a card per row: ten cards in a 320px panel is a wall, and the rule keeps
|
|
80
|
+
the list scannable as a list — which is what a person is doing when they open this. */
|
|
81
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-defect{
|
|
82
|
+
display:flex;flex-direction:column;gap:1px;padding:4px 0 4px 8px;
|
|
83
|
+
border-left:2px solid color-mix(in srgb,#f87171 70%,transparent);}
|
|
84
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-defect-title{
|
|
85
|
+
font-size:11px;line-height:1.35;overflow-wrap:anywhere;}
|
|
86
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-defect-detail{
|
|
87
|
+
color:var(--reticle-faint);font-size:10px;line-height:1.35;overflow-wrap:anywhere;}
|
|
88
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-defect-source{
|
|
89
|
+
color:var(--reticle-faint);font-size:9.5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
90
|
+
overflow-wrap:anywhere;}
|
|
91
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-defects-more{
|
|
92
|
+
display:inline-block;margin-top:8px;font-size:10px;color:var(--reticle-c-active);
|
|
93
|
+
text-decoration:none;}
|
|
94
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-defects-more:hover{text-decoration:underline;}
|
|
95
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-local-only{
|
|
96
|
+
margin:14px 0 0;padding-top:10px;border-top:1px solid var(--reticle-line);
|
|
97
|
+
color:var(--reticle-faint);font-size:10px;line-height:1.45;}
|
|
98
|
+
[${REPORT_PANEL_ATTR}] .reticle-report-local-only code{
|
|
99
|
+
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--reticle-c-active);}
|
|
76
100
|
[${REPORT_PANEL_ATTR}] .reticle-report-chart-wrap{margin-top:12px;}
|
|
77
101
|
[${REPORT_PANEL_ATTR}] .reticle-report-section{
|
|
78
102
|
display:block;margin-bottom:6px;color:var(--reticle-faint);font-size:9.5px;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ImpactScope, type ImpactSnapshot } from '@reticlehq/core';
|
|
2
2
|
export declare function reportPanelHtml(): string;
|
|
3
|
-
export declare function reportBodyHtml(scope: ImpactScope): string;
|
|
3
|
+
export declare function reportBodyHtml(scope: ImpactScope, dashboardUrl?: string): string;
|
|
4
4
|
export interface ReportHost {
|
|
5
5
|
/** Opened from the toolbar and from the chat, so the shell decides what else must close. */
|
|
6
6
|
onBeforeOpen?: () => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IMPACT_DEFECT_LIMIT } from '@reticlehq/core';
|
|
1
2
|
import { PresenterIcon, PRESENTER_ICON_SIZE, hiIconHtml } from './presenter-icons.js';
|
|
2
3
|
import { HUD_SURFACE_CLASS } from './presenter-hud-chrome.js';
|
|
3
4
|
import { REPORT_PANEL_ATTR, REPORT_ATTR, REPORT_CLOSE_ATTR } from './presenter-config.js';
|
|
@@ -67,7 +68,80 @@ function chart(scope) {
|
|
|
67
68
|
.join('');
|
|
68
69
|
return `<div class="reticle-report-chart-wrap"><span class="reticle-report-section">${REPORT_TEXT.CHART}</span><div class="reticle-report-chart">${bars}</div></div>`;
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Escape text that came from the app under test.
|
|
73
|
+
*
|
|
74
|
+
* A defect title is an element's accessible name or a verdict's failure reason — both of which are
|
|
75
|
+
* ultimately the CONTENT of somebody else's page, and this panel builds its DOM from an HTML string.
|
|
76
|
+
* Everything else the report renders is a number or a date; this is the first app-derived text to
|
|
77
|
+
* reach it, so the escaping arrives with it.
|
|
78
|
+
*/
|
|
79
|
+
function esc(value) {
|
|
80
|
+
return value
|
|
81
|
+
.replace(/&/g, '&')
|
|
82
|
+
.replace(/</g, '<')
|
|
83
|
+
.replace(/>/g, '>')
|
|
84
|
+
.replace(/"/g, '"');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The short list of what is currently broken.
|
|
88
|
+
*
|
|
89
|
+
* The hero number already says HOW MANY defects Reticle caught; this says WHICH ONES, which is the
|
|
90
|
+
* difference between a statistic and something a person can act on. Deliberately short: a panel in
|
|
91
|
+
* the corner of somebody's app is not a triage queue, and the link at the bottom is where the queue
|
|
92
|
+
* lives. When the project is not linked to a workspace there is no link and no nagging — the free
|
|
93
|
+
* tool is complete on its own.
|
|
94
|
+
*/
|
|
95
|
+
/**
|
|
96
|
+
* Whether a dashboard link is safe to render as a clickable href.
|
|
97
|
+
*
|
|
98
|
+
* The url arrives from `.reticle/cloud.json` — a file in somebody's repository, which means it is
|
|
99
|
+
* INPUT. Escaping the quotes stops it breaking out of the attribute but does nothing about the
|
|
100
|
+
* SCHEME, so `javascript:...` produced a link that ran code inside the developer's own application,
|
|
101
|
+
* from a panel Reticle injected there. Only the two schemes a dashboard can actually live on are
|
|
102
|
+
* allowed; anything else renders no link at all, which is the same state as an unlinked project and
|
|
103
|
+
* therefore already a supported one.
|
|
104
|
+
*/
|
|
105
|
+
function isSafeDashboardUrl(raw) {
|
|
106
|
+
try {
|
|
107
|
+
const scheme = new URL(raw).protocol;
|
|
108
|
+
return 'https:' === scheme || 'http:' === scheme;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Not a url at all. A relative path cannot address a dashboard on another origin, so there is
|
|
112
|
+
// nothing to render and nothing lost by refusing it.
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function defects(scope, dashboardUrl) {
|
|
117
|
+
/*
|
|
118
|
+
* Defaulted here as well as in the schema. Zod's default applies when a record is PARSED, and the
|
|
119
|
+
* snapshot reaching this panel is pushed straight from the daemon rather than round-tripped
|
|
120
|
+
* through the schema — so a record written by an older build arrives with no `defects` field at
|
|
121
|
+
* all and used to throw, taking the whole report down with it.
|
|
122
|
+
*/
|
|
123
|
+
const list = (scope.defects ?? []).slice(0, IMPACT_DEFECT_LIMIT);
|
|
124
|
+
if (0 === list.length)
|
|
125
|
+
return '';
|
|
126
|
+
const rows = list
|
|
127
|
+
.map((d) => {
|
|
128
|
+
const detail = d.detail === undefined
|
|
129
|
+
? ''
|
|
130
|
+
: `<span class="reticle-report-defect-detail">${esc(d.detail)}</span>`;
|
|
131
|
+
const source = d.source === undefined
|
|
132
|
+
? ''
|
|
133
|
+
: `<span class="reticle-report-defect-source">${esc(d.source)}</span>`;
|
|
134
|
+
return `<li class="reticle-report-defect"><span class="reticle-report-defect-title">${esc(d.title)}</span>${detail}${source}</li>`;
|
|
135
|
+
})
|
|
136
|
+
.join('');
|
|
137
|
+
// Only claim there are more when there actually are — `counts.failed` is every defect ever, and
|
|
138
|
+
// this list is the recent tail of it.
|
|
139
|
+
const more = dashboardUrl === undefined || !isSafeDashboardUrl(dashboardUrl)
|
|
140
|
+
? ''
|
|
141
|
+
: `<a class="reticle-report-defects-more" href="${esc(dashboardUrl)}" target="_blank" rel="noreferrer noopener">${REPORT_TEXT.DEFECTS_MORE}${scope.counts.failed > list.length ? ` (${String(scope.counts.failed)})` : ''}</a>`;
|
|
142
|
+
return `<div class="reticle-report-defects-wrap"><span class="reticle-report-section">${REPORT_TEXT.DEFECTS}</span><ul class="reticle-report-defects">${rows}</ul>${more}</div>`;
|
|
143
|
+
}
|
|
144
|
+
export function reportBodyHtml(scope, dashboardUrl) {
|
|
71
145
|
const c = scope.counts;
|
|
72
146
|
if (0 === c.calls)
|
|
73
147
|
return `<p class="reticle-report-empty">${REPORT_TEXT.EMPTY}</p>`;
|
|
@@ -90,7 +164,25 @@ export function reportBodyHtml(scope) {
|
|
|
90
164
|
card(compactNumber(scope.savings.tokens.value), REPORT_TEXT.SAVED_TOKENS, scope.savings.tokens.basis),
|
|
91
165
|
card(compactDuration(scope.savings.minutes.value * 60_000), REPORT_TEXT.SAVED_MINUTES, scope.savings.minutes.basis),
|
|
92
166
|
].join('');
|
|
93
|
-
return `${streak}${hero}${verdicts}<div class="reticle-report-grid">${cards}</div>${chart(scope)}`;
|
|
167
|
+
return `${streak}${hero}${verdicts}<div class="reticle-report-grid">${cards}</div>${defects(scope, dashboardUrl)}${chart(scope)}${localOnly(scope, dashboardUrl)}`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The one line an UNLINKED user is shown about the dashboard.
|
|
171
|
+
*
|
|
172
|
+
* Absent `dashboardUrl` IS the unlinked signal — it is only ever set from a repo's cloud.json — so
|
|
173
|
+
* this needs no new plumbing and cannot be wrong about the state it describes.
|
|
174
|
+
*
|
|
175
|
+
* Gated on a VERDICT, not on tool calls. Somebody who has driven the app but proved nothing has not
|
|
176
|
+
* yet received the thing this offers to preserve, and offering to keep nothing is an advert. Past
|
|
177
|
+
* that bar it is a fact about where their record lives, at the foot of a panel they opened on
|
|
178
|
+
* purpose — which is why it does not need to be dismissible.
|
|
179
|
+
*/
|
|
180
|
+
function localOnly(scope, dashboardUrl) {
|
|
181
|
+
if (dashboardUrl !== undefined)
|
|
182
|
+
return '';
|
|
183
|
+
if (scope.counts.verdicts <= 0)
|
|
184
|
+
return '';
|
|
185
|
+
return `<p class="reticle-report-local-only">${REPORT_TEXT.LOCAL_ONLY} <code>${REPORT_TEXT.LOCAL_ONLY_ACTION}</code> ${REPORT_TEXT.LOCAL_ONLY_TAIL}</p>`;
|
|
94
186
|
}
|
|
95
187
|
/** The report panel controller: scope toggle, live repaint, share + referral actions. */
|
|
96
188
|
export class PresenterReport {
|
|
@@ -194,7 +286,7 @@ export class PresenterReport {
|
|
|
194
286
|
this.#body.innerHTML =
|
|
195
287
|
scope === undefined
|
|
196
288
|
? `<p class="reticle-report-empty">${REPORT_TEXT.EMPTY}</p>`
|
|
197
|
-
: reportBodyHtml(scope);
|
|
289
|
+
: reportBodyHtml(scope, this.#snapshot?.dashboardUrl);
|
|
198
290
|
}
|
|
199
291
|
#openShare(url) {
|
|
200
292
|
window.open(url, '_blank', 'noopener,noreferrer');
|