@reticlehq/browser 2.3.0 → 2.5.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 (79) hide show
  1. package/dist/actions/actions-dom.js +10 -10
  2. package/dist/actions/actions.d.ts +13 -0
  3. package/dist/actions/actions.js +45 -15
  4. package/dist/actions/scroll.js +1 -1
  5. package/dist/commands/commands.js +39 -22
  6. package/dist/connect-options.d.ts +103 -0
  7. package/dist/connect-options.js +1 -0
  8. package/dist/dom/a11y.js +20 -20
  9. package/dist/dom/auto-anchor.js +1 -1
  10. package/dist/dom/blind-spots.js +1 -1
  11. package/dist/dom/chart.js +5 -5
  12. package/dist/dom/desktop-capture.d.ts +2 -1
  13. package/dist/dom/desktop-capture.js +4 -4
  14. package/dist/dom/occlusion.js +2 -2
  15. package/dist/dom/query.js +28 -18
  16. package/dist/dom/realm.js +13 -13
  17. package/dist/dom/refs.d.ts +14 -0
  18. package/dist/dom/refs.js +20 -1
  19. package/dist/dom/shadow-registry.js +2 -2
  20. package/dist/dom/snapshot.js +7 -7
  21. package/dist/dom/source.js +3 -3
  22. package/dist/dom/theme.js +6 -6
  23. package/dist/dom/virtualized.js +1 -1
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +2 -2
  26. package/dist/observers/blind-spots.js +2 -2
  27. package/dist/observers/console.js +3 -3
  28. package/dist/observers/dom.js +5 -5
  29. package/dist/observers/download.d.ts +2 -1
  30. package/dist/observers/download.js +2 -2
  31. package/dist/observers/focus.js +1 -1
  32. package/dist/observers/frames.js +1 -1
  33. package/dist/observers/health.js +70 -2
  34. package/dist/observers/install-all.d.ts +2 -1
  35. package/dist/observers/ipc.d.ts +2 -7
  36. package/dist/observers/ipc.js +7 -7
  37. package/dist/observers/network-body.js +2 -2
  38. package/dist/observers/network-redact.js +5 -5
  39. package/dist/observers/network-stream.js +2 -2
  40. package/dist/observers/network.d.ts +3 -3
  41. package/dist/observers/network.js +9 -9
  42. package/dist/observers/perf.js +1 -1
  43. package/dist/observers/scroll.js +1 -1
  44. package/dist/observers/state.js +1 -1
  45. package/dist/observers/storage.js +7 -7
  46. package/dist/presenter/presenter-brand.d.ts +19 -0
  47. package/dist/presenter/presenter-brand.js +21 -0
  48. package/dist/presenter/presenter-config.d.ts +17 -0
  49. package/dist/presenter/presenter-config.js +21 -0
  50. package/dist/presenter/presenter-controls.d.ts +1 -11
  51. package/dist/presenter/presenter-controls.js +6 -7
  52. package/dist/presenter/presenter-log.js +2 -2
  53. package/dist/presenter/presenter-run-state.js +1 -1
  54. package/dist/presenter/presenter-styles.d.ts +1 -1
  55. package/dist/presenter/presenter-styles.js +20 -9
  56. package/dist/presenter/presenter-tally.js +3 -3
  57. package/dist/presenter/presenter-test-helpers.d.ts +2 -1
  58. package/dist/presenter/presenter-test-helpers.js +2 -2
  59. package/dist/presenter/presenter-verbs.js +16 -11
  60. package/dist/presenter/presenter.js +13 -10
  61. package/dist/recorder/recorder.js +10 -9
  62. package/dist/registry/capabilities.d.ts +2 -0
  63. package/dist/registry/capabilities.js +16 -0
  64. package/dist/registry/store-adapters.d.ts +127 -1
  65. package/dist/registry/store-adapters.js +178 -0
  66. package/dist/registry/stores.js +3 -3
  67. package/dist/reticle-presenter-helpers.js +3 -3
  68. package/dist/reticle.d.ts +2 -57
  69. package/dist/reticle.js +57 -14
  70. package/dist/review/annotator.js +7 -7
  71. package/dist/security/serialization.js +9 -9
  72. package/dist/session-continuity.d.ts +17 -0
  73. package/dist/session-continuity.js +42 -0
  74. package/dist/timers/clock.js +4 -4
  75. package/dist/timers/native-console.js +1 -1
  76. package/dist/timers/native-timers.js +4 -4
  77. package/dist/transport/transport.d.ts +10 -0
  78. package/dist/transport/transport.js +19 -5
  79. package/package.json +11 -4
@@ -1,4 +1,4 @@
1
- import { EventType, HealthReason, RETICLE_IPC_GLOBAL, SESSION_HEALTH } from '@reticlehq/core';
1
+ import { BrowserBrand, EventType, HealthReason, RETICLE_IPC_GLOBAL, SESSION_HEALTH, } from '@reticlehq/core';
2
2
  import { nativeSetInterval } from '../timers/native-timers.js';
3
3
  /**
4
4
  * Which shell this page runs in. Reported with health because it is what makes a timeout
@@ -27,12 +27,80 @@ function detectEngine() {
27
27
  return 'gecko';
28
28
  return 'webkit';
29
29
  }
30
+ /**
31
+ * Brand names as the Chromium family and the UA string spell them, mapped to our closed vocabulary.
32
+ * Matched on the WHOLE lowercased name, never a substring: brand strings are attacker-influenced
33
+ * free text, and a substring rule is how "Search Browser" becomes `arc`.
34
+ */
35
+ const BRAND_BY_NAME = {
36
+ 'google chrome': BrowserBrand.CHROME,
37
+ chrome: BrowserBrand.CHROME,
38
+ 'microsoft edge': BrowserBrand.EDGE,
39
+ edge: BrowserBrand.EDGE,
40
+ brave: BrowserBrand.BRAVE,
41
+ opera: BrowserBrand.OPERA,
42
+ arc: BrowserBrand.ARC,
43
+ dia: BrowserBrand.DIA,
44
+ firefox: BrowserBrand.FIREFOX,
45
+ safari: BrowserBrand.SAFARI,
46
+ };
47
+ /** Chromium's own filler entries: the greased `Not_A Brand` (spelt differently every release) and the generic engine name. */
48
+ function isFiller(name) {
49
+ return 'chromium' === name || (name.includes('not') && name.includes('brand'));
50
+ }
51
+ /**
52
+ * The brand, from `navigator.userAgentData.brands` where it exists and the UA string where it does
53
+ * not (Firefox and Safari expose no userAgentData at all). Every fork claims `Google Chrome`
54
+ * alongside its own entry, so a non-Chrome match always wins; a Chromium that names nothing but
55
+ * filler is `other`, because calling it Chrome would be a guess.
56
+ *
57
+ * Never throws — a desktop webview may expose a `userAgentData` of any shape, and health reporting
58
+ * must survive it.
59
+ */
60
+ function detectBrand() {
61
+ try {
62
+ const nav = navigator;
63
+ const brands = nav.userAgentData?.brands;
64
+ if (Array.isArray(brands)) {
65
+ let chrome;
66
+ for (const entry of brands) {
67
+ const name = 'string' === typeof entry?.brand ? entry.brand.toLowerCase() : '';
68
+ if ('' === name || isFiller(name))
69
+ continue;
70
+ const known = BRAND_BY_NAME[name];
71
+ if (known === undefined)
72
+ continue;
73
+ if (BrowserBrand.CHROME === known)
74
+ chrome = known;
75
+ else
76
+ return known;
77
+ }
78
+ return chrome ?? BrowserBrand.OTHER;
79
+ }
80
+ }
81
+ catch {
82
+ /* a health report must never depend on an exotic userAgentData */
83
+ }
84
+ const ua = navigator.userAgent;
85
+ if (/Edg\//.test(ua))
86
+ return BrowserBrand.EDGE;
87
+ if (/OPR\/|Opera/.test(ua))
88
+ return BrowserBrand.OPERA;
89
+ if (/Firefox/.test(ua))
90
+ return BrowserBrand.FIREFOX;
91
+ if (/Chrome|Chromium/.test(ua))
92
+ return BrowserBrand.CHROME;
93
+ if (/Safari/.test(ua))
94
+ return BrowserBrand.SAFARI;
95
+ return BrowserBrand.OTHER;
96
+ }
30
97
  function snapshotHealth() {
31
98
  return {
32
- hidden: document.visibilityState === 'hidden',
99
+ hidden: 'hidden' === document.visibilityState,
33
100
  focused: document.hasFocus(),
34
101
  runtime: detectRuntime(),
35
102
  engine: detectEngine(),
103
+ brand: detectBrand(),
36
104
  };
37
105
  }
38
106
  /**
@@ -1,5 +1,6 @@
1
1
  import type { Emit, Teardown } from './types.js';
2
- export interface InstallOptions {
2
+ interface InstallOptions {
3
3
  captureBodies: boolean;
4
4
  }
5
5
  export declare function installAllObservers(emit: Emit, options: InstallOptions): Teardown[];
6
+ export {};
@@ -1,10 +1,4 @@
1
1
  import type { Emit, Teardown } from './types.js';
2
- /**
3
- * How Tauri v2 reports a command's verdict. The transport answers HTTP 200 whether the Rust command
4
- * returned Ok or Err — the verdict is in this response header, which Tauri explicitly CORS-exposes so
5
- * JS may read it. Without translating it, every failed command is recorded as a successful request.
6
- */
7
- export declare const TAURI_RESPONSE_HEADER_NAME = "Tauri-Response";
8
2
  /**
9
3
  * Fields to override on a NET_REQUEST that turned out to be a Tauri IPC call, or null when the
10
4
  * request was ordinary HTTP. Pure — the network observer calls this with the response header it
@@ -15,8 +9,9 @@ export declare function ipcNetOverrides(url: string, header: (name: string) => s
15
9
  * Subscribe to the Electron preload shim, if this app installed it. Inert on Tauri (whose calls the
16
10
  * network observer handles) and on a plain web page.
17
11
  */
18
- export interface IpcOptions {
12
+ interface IpcOptions {
19
13
  /** Forward the IPC payloads the preload recorded. Off by default, like the HTTP body capture. */
20
14
  captureBodies?: boolean;
21
15
  }
22
16
  export declare function installIpc(emit: Emit, options?: IpcOptions): Teardown;
17
+ export {};
@@ -5,7 +5,7 @@ import { observeSafely } from './types.js';
5
5
  * returned Ok or Err — the verdict is in this response header, which Tauri explicitly CORS-exposes so
6
6
  * JS may read it. Without translating it, every failed command is recorded as a successful request.
7
7
  */
8
- export const TAURI_RESPONSE_HEADER_NAME = 'Tauri-Response';
8
+ const TAURI_RESPONSE_HEADER_NAME = 'Tauri-Response';
9
9
  const TAURI_RESPONSE_ERROR = 'error';
10
10
  /** Rust/IPC vocabulary rather than HTTP's, so the text never claims a transport status it lacks. */
11
11
  const IPC_STATUS_TEXT = { OK: 'Ok', ERROR: 'Err' };
@@ -70,7 +70,7 @@ export function installIpc(emit, options = {}) {
70
70
  const token = channel.subscribe((record) => {
71
71
  observeSafely(() => {
72
72
  const url = `${IPC_URL_SCHEME}${record.channel}`;
73
- if (record.phase === 'start') {
73
+ if ('start' === record.phase) {
74
74
  emit(EventType.NET_PENDING, {
75
75
  id: record.id,
76
76
  method: NetInitiator.IPC,
@@ -83,18 +83,18 @@ export function installIpc(emit, options = {}) {
83
83
  // main process handled it. Emitting `ok/status` anyway would manufacture the success nobody
84
84
  // reported, so both are omitted and `oneWay` says why. `reticle_network { ok }` then matches
85
85
  // such a record in neither direction, which is the honest answer to "did it work".
86
- const verdictless = record.oneWay === true && record.ok === undefined;
86
+ const verdictless = true === record.oneWay && record.ok === undefined;
87
87
  // Declared, not inferred: the server cannot tell "no verdict" from "not yet settled" without
88
88
  // being told, and a send with nothing to observe must not read as a clean green.
89
89
  if (verdictless)
90
90
  emit(EventType.BLIND_SPOT, { kind: BlindSpotKind.VERDICTLESS_SEND, count: 1 });
91
- const ok = record.ok === true;
91
+ const ok = true === record.ok;
92
92
  emit(EventType.NET_REQUEST, {
93
93
  id: record.id,
94
94
  method: NetInitiator.IPC,
95
95
  url,
96
96
  ...(verdictless ? {} : { ok, status: ok ? IpcStatus.OK : IpcStatus.ERROR }),
97
- ...(record.oneWay === true ? { oneWay: true } : {}),
97
+ ...(true === record.oneWay ? { oneWay: true } : {}),
98
98
  durationMs: record.durationMs ?? 0,
99
99
  initiator: NetInitiator.IPC,
100
100
  ...(record.error === undefined ? {} : { error: record.error }),
@@ -102,11 +102,11 @@ export function installIpc(emit, options = {}) {
102
102
  // write returned ok and its payload went unread" from the size alone, and it cannot infer that
103
103
  // from an absent field — an unread payload and an empty one would look identical.
104
104
  ...(record.responseSize === undefined ? {} : { responseSize: record.responseSize }),
105
- ...(options.captureBodies === true
105
+ ...(true === options.captureBodies
106
106
  ? {
107
107
  ...(record.requestBody === undefined ? {} : { requestBody: record.requestBody }),
108
108
  ...(record.responseBody === undefined ? {} : { responseBody: record.responseBody }),
109
- ...(record.responseBodyTruncated === true ? { responseBodyTruncated: true } : {}),
109
+ ...(true === record.responseBodyTruncated ? { responseBodyTruncated: true } : {}),
110
110
  }
111
111
  : {}),
112
112
  });
@@ -45,7 +45,7 @@ const ENDLESS_BY_DESIGN = /event-stream/i;
45
45
  export function isStreamingBody(contentType, contentLength) {
46
46
  if (contentType !== null && ENDLESS_BY_DESIGN.test(contentType))
47
47
  return false;
48
- return contentLength === null;
48
+ return null === contentLength;
49
49
  }
50
50
  /**
51
51
  * Longest the SDK will wait on a response-body clone before emitting without it.
@@ -84,7 +84,7 @@ export async function withBodyDeadline(read) {
84
84
  }
85
85
  }
86
86
  export function isCapturableType(contentType) {
87
- if (contentType === null)
87
+ if (null === contentType)
88
88
  return false;
89
89
  if (STREAMING_CONTENT.test(contentType))
90
90
  return false;
@@ -12,11 +12,11 @@ const PATH_TOKEN_MIN_LENGTH = 12;
12
12
  */
13
13
  export function redactUrl(raw) {
14
14
  const hashStart = raw.indexOf('#');
15
- const hash = hashStart === -1 ? '' : raw.slice(hashStart);
16
- const beforeHash = hashStart === -1 ? raw : raw.slice(0, hashStart);
15
+ const hash = -1 === hashStart ? '' : raw.slice(hashStart);
16
+ const beforeHash = -1 === hashStart ? raw : raw.slice(0, hashStart);
17
17
  const queryStart = beforeHash.indexOf('?');
18
- const pathPart = queryStart === -1 ? beforeHash : beforeHash.slice(0, queryStart);
19
- const query = queryStart === -1 ? '' : beforeHash.slice(queryStart + 1);
18
+ const pathPart = -1 === queryStart ? beforeHash : beforeHash.slice(0, queryStart);
19
+ const query = -1 === queryStart ? '' : beforeHash.slice(queryStart + 1);
20
20
  let changed = false;
21
21
  // Credentials in the authority (`scheme://user:pass@host`) never belong in a transcript.
22
22
  // Match to the LAST `@` before the path (`[^/]*@`, greedy), not the first — a password containing
@@ -64,6 +64,6 @@ export function redactUrl(raw) {
64
64
  }
65
65
  if (!changed)
66
66
  return raw;
67
- const queryOut = queryStart === -1 ? '' : `?${newQuery}`;
67
+ const queryOut = -1 === queryStart ? '' : `?${newQuery}`;
68
68
  return `${segments.join('/')}${queryOut}${newHash}`;
69
69
  }
@@ -53,7 +53,7 @@ export function watchStreamedBody(emit, res, id, url, contentType, contentLength
53
53
  return; // already consumed or locked — the app owns it; observing it would take it away
54
54
  }
55
55
  const body = clone.body;
56
- if (body === null)
56
+ if (null === body)
57
57
  return;
58
58
  emit(EventType.NET_STREAM, {
59
59
  transport: StreamTransport.FETCH,
@@ -75,7 +75,7 @@ export function watchStreamedBody(emit, res, id, url, contentType, contentLength
75
75
  try {
76
76
  for (;;) {
77
77
  const next = await Promise.race([reader.read(), expired]);
78
- if (next === 'expired') {
78
+ if ('expired' === next) {
79
79
  gaveUp = true;
80
80
  void reader.cancel().catch(() => undefined);
81
81
  break;
@@ -10,8 +10,8 @@ export { redactUrl };
10
10
  * succeeded means nothing to a network observer — so the knowledge lives in the IPC observer and is
11
11
  * passed IN, rather than this module importing it. Returns undefined to leave the record untouched.
12
12
  */
13
- export type NetResponseReinterpreter = (url: string, header: (name: string) => string | null) => Record<string, unknown> | undefined;
14
- export interface NetworkOptions {
13
+ type NetResponseReinterpreter = (url: string, header: (name: string) => string | null) => Record<string, unknown> | undefined;
14
+ interface NetworkOptions {
15
15
  /** Capture request/response bodies (text-like content only, redacted, per-body capped). */
16
16
  captureBodies?: boolean;
17
17
  /** Optional hook that reinterprets a completed request — see NetResponseReinterpreter. */
@@ -20,7 +20,7 @@ export interface NetworkOptions {
20
20
  /** Pure: the first real app-code frame in a stack string, capped. Exported for unit testing. */
21
21
  export declare function firstAppFrame(stack: string | undefined): string | undefined;
22
22
  /** The timing fields we lift from a resource entry — TTFB is the perf signal a duration alone hides. */
23
- export interface NetTiming {
23
+ interface NetTiming {
24
24
  ttfbMs?: number;
25
25
  transferSize?: number;
26
26
  }
@@ -44,7 +44,7 @@ function projectRequestBody(body, captureBodies) {
44
44
  return {};
45
45
  let text;
46
46
  let contentType = 'application/json';
47
- if (typeof body === 'string') {
47
+ if ('string' === typeof body) {
48
48
  text = body;
49
49
  }
50
50
  else if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
@@ -55,7 +55,7 @@ function projectRequestBody(body, captureBodies) {
55
55
  const shape = body.constructor?.name ?? typeof body;
56
56
  return { requestBodyType: shape };
57
57
  }
58
- if (text === undefined || text.length === 0)
58
+ if (text === undefined || 0 === text.length)
59
59
  return {};
60
60
  const { body: out, truncated } = projectBody(text, contentType);
61
61
  return truncated ? { requestBody: out, requestBodyTruncated: true } : { requestBody: out };
@@ -78,7 +78,7 @@ function netResponseMeta(statusText, contentType, contentLength) {
78
78
  return out;
79
79
  }
80
80
  function urlOf(input) {
81
- if (typeof input === 'string')
81
+ if ('string' === typeof input)
82
82
  return input;
83
83
  if (input instanceof URL)
84
84
  return input.href;
@@ -112,7 +112,7 @@ export function firstAppFrame(stack) {
112
112
  if (NON_APP_FRAME.test(line))
113
113
  continue;
114
114
  const trimmed = line.trim();
115
- if (trimmed.length === 0)
115
+ if (0 === trimmed.length)
116
116
  continue;
117
117
  return trimmed.slice(0, 300);
118
118
  }
@@ -191,7 +191,7 @@ function isNativeFetch(fn) {
191
191
  /** Wrappers this module installed, so a re-install does not report itself as a foreign wrapper. */
192
192
  const OURS = new WeakSet();
193
193
  export function installNetwork(emit, opts = {}) {
194
- const captureBodies = opts.captureBodies === true;
194
+ const captureBodies = true === opts.captureBodies;
195
195
  const reinterpret = opts.reinterpret;
196
196
  // Keep the true original for teardown identity, plus a window-bound copy to invoke
197
197
  // (fetch throws "Illegal invocation" if called with the wrong `this`).
@@ -365,7 +365,7 @@ export function installNetwork(emit, opts = {}) {
365
365
  const xhrContentType = this.getResponseHeader('content-type');
366
366
  let responseBodyFields = {};
367
367
  // responseText throws unless responseType is '' or 'text' — guard before reading.
368
- const textReadable = this.responseType === '' || this.responseType === 'text';
368
+ const textReadable = '' === this.responseType || 'text' === this.responseType;
369
369
  if (captureBodies && textReadable && isCapturableType(xhrContentType)) {
370
370
  try {
371
371
  const { body: rb, truncated } = projectBody(this.responseText, xhrContentType);
@@ -403,7 +403,7 @@ export function installNetwork(emit, opts = {}) {
403
403
  const origWebSocket = window.WebSocket;
404
404
  let patchedEventSource;
405
405
  let patchedWebSocket;
406
- if (captureBodies && typeof origEventSource === 'function') {
406
+ if (captureBodies && 'function' === typeof origEventSource) {
407
407
  window.EventSource = class extends origEventSource {
408
408
  constructor(u, init) {
409
409
  super(u, init);
@@ -425,7 +425,7 @@ export function installNetwork(emit, opts = {}) {
425
425
  };
426
426
  patchedEventSource = window.EventSource;
427
427
  }
428
- if (captureBodies && typeof origWebSocket === 'function') {
428
+ if (captureBodies && 'function' === typeof origWebSocket) {
429
429
  window.WebSocket = class extends origWebSocket {
430
430
  /** Reticle's own bridge socket is never observed — see isBridgeSocket. */
431
431
  #isBridge;
@@ -470,7 +470,7 @@ export function installNetwork(emit, opts = {}) {
470
470
  // getPrototypeOf, robust whether or not the Navigator global exists); the descriptor read avoids an
471
471
  // unbound-method access; the send is synchronous so we emit one completed NET_REQUEST with its result.
472
472
  const navProto = (typeof navigator !== 'undefined' ? Object.getPrototypeOf(navigator) : null);
473
- const origBeacon = (navProto === null ? undefined : Object.getOwnPropertyDescriptor(navProto, 'sendBeacon')?.value);
473
+ const origBeacon = (null === navProto ? undefined : Object.getOwnPropertyDescriptor(navProto, 'sendBeacon')?.value);
474
474
  let patchedBeacon;
475
475
  if (navProto !== null && origBeacon !== undefined) {
476
476
  patchedBeacon = function (url, data) {
@@ -42,7 +42,7 @@ export function installPerf(emit) {
42
42
  observe('layout-shift', (e) => {
43
43
  const ls = e;
44
44
  // Shifts within 500ms of a user input are expected (not CLS) — the spec's hadRecentInput flag.
45
- if (ls.hadRecentInput === true)
45
+ if (true === ls.hadRecentInput)
46
46
  return;
47
47
  cls += ls.value ?? 0;
48
48
  emit(EventType.PERF, { metric: PerfMetric.CLS, value: cls, at: Math.round(e.startTime) });
@@ -41,7 +41,7 @@ export function installScroll(emit) {
41
41
  };
42
42
  window.addEventListener('scroll', onScroll, { passive: true });
43
43
  let io;
44
- if (typeof IntersectionObserver === 'function') {
44
+ if ('function' === typeof IntersectionObserver) {
45
45
  io = new IntersectionObserver((entries) => {
46
46
  for (const entry of entries) {
47
47
  if (entry.isIntersecting) {
@@ -2,7 +2,7 @@ import { EventType, REDACTED_VALUE } from '@reticlehq/core';
2
2
  import { subscribableStores, onStoreRegistered, } from '../registry/stores.js';
3
3
  import { isSensitiveKey, sanitizeForTransport } from '../security/serialization.js';
4
4
  function isRecord(value) {
5
- return typeof value === 'object' && value !== null && !Array.isArray(value);
5
+ return 'object' === typeof value && value !== null && !Array.isArray(value);
6
6
  }
7
7
  /**
8
8
  * Shallow top-level diff of two store states — the keys whose value changed. Immutable updates
@@ -12,11 +12,11 @@ function safeArea(get) {
12
12
  }
13
13
  function readArea(storage) {
14
14
  const out = {};
15
- if (storage === null)
15
+ if (null === storage)
16
16
  return out;
17
17
  for (let i = 0; i < storage.length; i += 1) {
18
18
  const key = storage.key(i);
19
- if (key === null)
19
+ if (null === key)
20
20
  continue;
21
21
  // Redact credential-bearing keys (token/session/password/…) so auth state never leaks verbatim.
22
22
  out[key] = isSensitiveKey(key) ? REDACTED_VALUE : (storage.getItem(key) ?? '');
@@ -36,10 +36,10 @@ function readCookies() {
36
36
  }
37
37
  for (const part of raw.split(';')) {
38
38
  const eq = part.indexOf('=');
39
- if (eq === -1)
39
+ if (-1 === eq)
40
40
  continue;
41
41
  const key = part.slice(0, eq).trim();
42
- if (key === '')
42
+ if ('' === key)
43
43
  continue;
44
44
  if (isSensitiveKey(key)) {
45
45
  out[key] = REDACTED_VALUE;
@@ -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 (area === 'cookies')
68
+ if ('cookies' === area)
69
69
  return readCookies();
70
70
  return {
71
71
  local: readArea(safeArea(() => window.localStorage)),
@@ -75,7 +75,7 @@ export function readStorage(area) {
75
75
  }
76
76
  /** Redact a value when its key is credential-bearing — the same rule the pull path applies. */
77
77
  function redactFor(key, value) {
78
- if (value === null)
78
+ if (null === value)
79
79
  return undefined;
80
80
  return isSensitiveKey(key) ? REDACTED_VALUE : value;
81
81
  }
@@ -87,7 +87,7 @@ function redactFor(key, value) {
87
87
  * method access) and re-dispatched via `.call(this)`. Fully reversible; values redacted like the reads.
88
88
  */
89
89
  export function installStorage(emit) {
90
- if (typeof Storage === 'undefined')
90
+ if ('undefined' === typeof Storage)
91
91
  return () => undefined;
92
92
  const proto = Storage.prototype;
93
93
  const origSet = Object.getOwnPropertyDescriptor(proto, 'setItem')?.value;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Brand marks, inlined. The SDK is bundled into someone else's page: it cannot read
3
+ * `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the two
4
+ * artwork files live here as strings (same precedent as SEND_ICON in presenter-controls.ts).
5
+ * Both are minified — editor metadata, clip paths, masks and sub-0.01 precision stripped — and paint
6
+ * with `currentColor` so the mark tracks --reticle-fg instead of hardcoding the export's #FAFAFA.
7
+ */
8
+ /** Accessible name of the HUD — the wordmark is artwork, this is what a screen reader reads. */
9
+ export declare const BRAND_NAME = "Reticle";
10
+ /** Mark alone (shown when the HUD is collapsed to a pill). */
11
+ export declare const MARK_SVG = "<svg class=\"reticle-mark\" viewBox=\"0 0 96 94\" fill=\"currentColor\" aria-hidden=\"true\"><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\"/></svg>";
12
+ /** Mark + "reticle" lettering (shown when the HUD is expanded). */
13
+ export declare const WORDMARK_SVG = "<svg class=\"reticle-wordmark\" viewBox=\"0 0 347 96\" fill=\"currentColor\" aria-hidden=\"true\"><g transform=\"translate(1 1)\"><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\"/></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>";
14
+ /**
15
+ * Header brand block. Both marks are always in the DOM and CSS picks one off the collapsed
16
+ * attribute, so there is no JS state to keep in sync. The artwork is decorative (aria-hidden);
17
+ * the visually-hidden name is what keeps the HUD identifiable to a screen reader in BOTH states.
18
+ */
19
+ export declare const BRAND_HTML = "<span class=\"reticle-brand\"><svg class=\"reticle-mark\" viewBox=\"0 0 96 94\" fill=\"currentColor\" aria-hidden=\"true\"><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\"/></svg><svg class=\"reticle-wordmark\" viewBox=\"0 0 347 96\" fill=\"currentColor\" aria-hidden=\"true\"><g transform=\"translate(1 1)\"><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\"/></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><span class=\"reticle-brand-sr\">Reticle</span></span>";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Brand marks, inlined. The SDK is bundled into someone else's page: it cannot read
3
+ * `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the two
4
+ * artwork files live here as strings (same precedent as SEND_ICON in presenter-controls.ts).
5
+ * Both are minified — editor metadata, clip paths, masks and sub-0.01 precision stripped — and paint
6
+ * with `currentColor` so the mark tracks --reticle-fg instead of hardcoding the export's #FAFAFA.
7
+ */
8
+ /** Accessible name of the HUD — the wordmark is artwork, this is what a screen reader reads. */
9
+ export const BRAND_NAME = 'Reticle';
10
+ /** The two glyph paths of the mark, shared by both marks — the wordmark just offsets them by (1,1). */
11
+ 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"/>`;
12
+ /** Mark alone (shown when the HUD is collapsed to a pill). */
13
+ export const MARK_SVG = `<svg class="reticle-mark" viewBox="0 0 96 94" fill="currentColor" aria-hidden="true">${MARK_PATHS}</svg>`;
14
+ /** Mark + "reticle" lettering (shown when the HUD is expanded). */
15
+ 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>`;
16
+ /**
17
+ * Header brand block. Both marks are always in the DOM and CSS picks one off the collapsed
18
+ * attribute, so there is no JS state to keep in sync. The artwork is decorative (aria-hidden);
19
+ * the visually-hidden name is what keeps the HUD identifiable to a screen reader in BOTH states.
20
+ */
21
+ export const BRAND_HTML = `<span class="reticle-brand">${MARK_SVG}${WORDMARK_SVG}<span class="reticle-brand-sr">${BRAND_NAME}</span></span>`;
@@ -49,6 +49,23 @@ export interface PresenterOptions {
49
49
  endedFadeMs?: number;
50
50
  }
51
51
  export declare const DEFAULT_PACE = 450;
52
+ /**
53
+ * The pace to actually use: the default in a browser a human is watching, zero in an automated one.
54
+ *
55
+ * The HUD paces `beforeAct` so a person can see the cursor reach the element before it fires. In a
56
+ * headless browser that is 450ms of nothing, charged to the agent, on every action. Measured with
57
+ * RETICLE_TRACE across the e2e battery: 42 `act` round-trips, 40 of them in a 452–460ms band —
58
+ * a fixed cost, not app work — 19.7 SECONDS in total, 98.5% of all time the battery spent in the
59
+ * browser, while every other command ran in 1–4ms.
60
+ *
61
+ * `navigator.webdriver` is the discriminator because it is exactly the question being asked: true
62
+ * under automation (Playwright, CDP, the browser pool's leased tabs, CI), false in the dev browser
63
+ * a human has open — the case the HUD exists for, which keeps its pacing untouched.
64
+ *
65
+ * An EXPLICIT `paceMs` always wins: a recorded demo runs under automation and still wants the glide,
66
+ * and an option that quietly ignores what the caller asked for is worse than no option.
67
+ */
68
+ export declare function effectivePaceMs(requested: number | undefined, nav?: Navigator | undefined): number;
52
69
  /**
53
70
  * Glow state machine phases (exposed via glowPhase for tests). A burst of activity flips the
54
71
  * border IN once on the first activity, holds steady (the slow reticle-pulse breathing keeps running
@@ -15,6 +15,27 @@ export const DATA_BUSY = 'data-busy';
15
15
  export const BUSY_ON = '1';
16
16
  export const BUSY_OFF = '0';
17
17
  export const DEFAULT_PACE = 450;
18
+ /**
19
+ * The pace to actually use: the default in a browser a human is watching, zero in an automated one.
20
+ *
21
+ * The HUD paces `beforeAct` so a person can see the cursor reach the element before it fires. In a
22
+ * headless browser that is 450ms of nothing, charged to the agent, on every action. Measured with
23
+ * RETICLE_TRACE across the e2e battery: 42 `act` round-trips, 40 of them in a 452–460ms band —
24
+ * a fixed cost, not app work — 19.7 SECONDS in total, 98.5% of all time the battery spent in the
25
+ * browser, while every other command ran in 1–4ms.
26
+ *
27
+ * `navigator.webdriver` is the discriminator because it is exactly the question being asked: true
28
+ * under automation (Playwright, CDP, the browser pool's leased tabs, CI), false in the dev browser
29
+ * a human has open — the case the HUD exists for, which keeps its pacing untouched.
30
+ *
31
+ * An EXPLICIT `paceMs` always wins: a recorded demo runs under automation and still wants the glide,
32
+ * and an option that quietly ignores what the caller asked for is worse than no option.
33
+ */
34
+ export function effectivePaceMs(requested, nav = 'undefined' === typeof navigator ? undefined : navigator) {
35
+ if (requested !== undefined)
36
+ return requested;
37
+ return true === nav?.webdriver ? 0 : DEFAULT_PACE;
38
+ }
18
39
  /**
19
40
  * Glow state machine phases (exposed via glowPhase for tests). A burst of activity flips the
20
41
  * border IN once on the first activity, holds steady (the slow reticle-pulse breathing keeps running
@@ -1,16 +1,6 @@
1
1
  import { HumanControlKind, PresenterTone, SessionState } from '@reticlehq/core';
2
- /** data-reticle-state attribute on the overlay root; its value is always a SessionState. */
3
- declare const DATA_RETICLE_STATE = "data-reticle-state";
4
2
  /** Border fade-out delay after a session ends (native timer; presenter-only tunable). */
5
3
  export declare const ENDED_FADE_MS = 4000;
6
- /**
7
- * One replayable flow as pushed to the panel. `start` is the first step's testid anchor — a page hint
8
- * used to show a flow only where it can actually begin. Absent when the first step isn't testid-anchored.
9
- */
10
- export interface FlowChip {
11
- name: string;
12
- start?: string;
13
- }
14
4
  /** Payload the panel hands to its host when the human drives a control. */
15
5
  export interface ControlIntent {
16
6
  kind: HumanControlKind;
@@ -70,4 +60,4 @@ export declare class ControlPanel {
70
60
  */
71
61
  setState(state: SessionState, text?: string, tone?: PresenterTone): void;
72
62
  }
73
- export { DATA_RETICLE_STATE };
63
+ export {};
@@ -178,7 +178,7 @@ export class ControlPanel {
178
178
  this.#refs.sendBtn?.addEventListener('click', () => this.#onSend());
179
179
  this.#refs.input?.addEventListener('keydown', (e) => {
180
180
  // Enter sends; Shift+Enter inserts a newline (falls through to the textarea's default).
181
- if (e instanceof KeyboardEvent && e.key === 'Enter' && !e.shiftKey) {
181
+ if (e instanceof KeyboardEvent && 'Enter' === e.key && !e.shiftKey) {
182
182
  e.preventDefault();
183
183
  this.#onSend();
184
184
  }
@@ -248,7 +248,7 @@ export class ControlPanel {
248
248
  if (this.#state === SessionState.ENDED)
249
249
  return;
250
250
  const text = (this.#refs.input?.value ?? '').trim();
251
- if (text.length === 0)
251
+ if (0 === text.length)
252
252
  return;
253
253
  this.#host.emit(HumanControlKind.MESSAGE, text);
254
254
  this.#host.logHuman(text);
@@ -271,15 +271,15 @@ export class ControlPanel {
271
271
  const list = Array.isArray(flows) ? flows : [];
272
272
  this.#flowItems = list
273
273
  .map((f) => {
274
- if (typeof f === 'string')
274
+ if ('string' === typeof f)
275
275
  return f.length > 0 ? { name: f } : null;
276
- if (typeof f === 'object' && f !== null) {
276
+ if ('object' === typeof f && f !== null) {
277
277
  const rec = f;
278
278
  const name = rec['name'];
279
- if (typeof name !== 'string' || name.length === 0)
279
+ if (typeof name !== 'string' || 0 === name.length)
280
280
  return null;
281
281
  const start = rec['start'];
282
- return typeof start === 'string' && start.length > 0 ? { name, start } : { name };
282
+ return 'string' === typeof start && start.length > 0 ? { name, start } : { name };
283
283
  }
284
284
  return null;
285
285
  })
@@ -364,4 +364,3 @@ export class ControlPanel {
364
364
  }
365
365
  }
366
366
  }
367
- export { DATA_RETICLE_STATE };
@@ -74,9 +74,9 @@ export function humanDuration(ms) {
74
74
  return `${s}s`;
75
75
  const m = Math.floor(s / 60);
76
76
  if (m < 60)
77
- return s % 60 === 0 ? `${m}m` : `${m}m ${s % 60}s`;
77
+ return 0 === s % 60 ? `${m}m` : `${m}m ${s % 60}s`;
78
78
  const h = Math.floor(m / 60);
79
- return m % 60 === 0 ? `${h}h` : `${h}h ${m % 60}m`;
79
+ return 0 === m % 60 ? `${h}h` : `${h}h ${m % 60}m`;
80
80
  }
81
81
  /** Per-row timestamp: time since the session's first row, human-readable (e.g. "2m", not "+132.4s"). */
82
82
  export function formatElapsed(ms) {