@reticlehq/browser 2.7.0 → 2.9.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 (92) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +2 -2
  3. package/dist/actions/actions-dom.js +23 -15
  4. package/dist/actions/actions.d.ts +17 -0
  5. package/dist/actions/actions.js +132 -29
  6. package/dist/actions/synthetic-input.d.ts +9 -0
  7. package/dist/actions/synthetic-input.js +40 -0
  8. package/dist/commands/commands.js +18 -1
  9. package/dist/connect-options.d.ts +9 -9
  10. package/dist/dom/a11y.d.ts +9 -0
  11. package/dist/dom/a11y.js +19 -0
  12. package/dist/dom/desktop-capture.js +20 -3
  13. package/dist/dom/dom-ignore.d.ts +19 -6
  14. package/dist/dom/dom-ignore.js +20 -13
  15. package/dist/dom/query.d.ts +2 -2
  16. package/dist/dom/query.js +85 -45
  17. package/dist/dom/refs.d.ts +28 -0
  18. package/dist/dom/refs.js +87 -0
  19. package/dist/dom/snapshot.d.ts +5 -0
  20. package/dist/dom/snapshot.js +34 -0
  21. package/dist/dom/source.d.ts +14 -0
  22. package/dist/dom/source.js +16 -0
  23. package/dist/dom/theme.d.ts +33 -10
  24. package/dist/dom/theme.js +109 -50
  25. package/dist/observers/console.js +5 -2
  26. package/dist/observers/install-all.js +4 -1
  27. package/dist/observers/ipc.d.ts +16 -0
  28. package/dist/observers/ipc.js +19 -1
  29. package/dist/observers/network.d.ts +8 -0
  30. package/dist/observers/network.js +29 -6
  31. package/dist/observers/state.js +21 -2
  32. package/dist/presenter/presenter-brand.d.ts +24 -7
  33. package/dist/presenter/presenter-brand.js +23 -6
  34. package/dist/presenter/presenter-config.d.ts +102 -9
  35. package/dist/presenter/presenter-config.js +94 -8
  36. package/dist/presenter/presenter-controls.d.ts +10 -10
  37. package/dist/presenter/presenter-controls.js +175 -85
  38. package/dist/presenter/presenter-dock-layout.d.ts +6 -0
  39. package/dist/presenter/presenter-dock-layout.js +170 -0
  40. package/dist/presenter/presenter-drag.d.ts +48 -0
  41. package/dist/presenter/presenter-drag.js +198 -0
  42. package/dist/presenter/presenter-effects.d.ts +1 -1
  43. package/dist/presenter/presenter-effects.js +1 -1
  44. package/dist/presenter/presenter-glow.d.ts +5 -5
  45. package/dist/presenter/presenter-glow.js +7 -7
  46. package/dist/presenter/presenter-heroicons-data.d.ts +41 -0
  47. package/dist/presenter/presenter-heroicons-data.js +38 -0
  48. package/dist/presenter/presenter-hud-chrome.d.ts +17 -0
  49. package/dist/presenter/presenter-hud-chrome.js +41 -0
  50. package/dist/presenter/presenter-icons.d.ts +44 -0
  51. package/dist/presenter/presenter-icons.js +100 -0
  52. package/dist/presenter/presenter-log.d.ts +27 -5
  53. package/dist/presenter/presenter-log.js +114 -28
  54. package/dist/presenter/presenter-report-copy.d.ts +83 -0
  55. package/dist/presenter/presenter-report-copy.js +127 -0
  56. package/dist/presenter/presenter-report-styles.d.ts +7 -0
  57. package/dist/presenter/presenter-report-styles.js +102 -0
  58. package/dist/presenter/presenter-report.d.ts +23 -0
  59. package/dist/presenter/presenter-report.js +217 -0
  60. package/dist/presenter/presenter-run-state.d.ts +1 -1
  61. package/dist/presenter/presenter-settings-styles.d.ts +2 -0
  62. package/dist/presenter/presenter-settings-styles.js +153 -0
  63. package/dist/presenter/presenter-settings.d.ts +82 -0
  64. package/dist/presenter/presenter-settings.js +494 -0
  65. package/dist/presenter/presenter-shell-styles.d.ts +1 -0
  66. package/dist/presenter/presenter-shell-styles.js +375 -0
  67. package/dist/presenter/presenter-shell.d.ts +48 -0
  68. package/dist/presenter/presenter-shell.js +408 -0
  69. package/dist/presenter/presenter-styles.d.ts +9 -4
  70. package/dist/presenter/presenter-styles.js +61 -125
  71. package/dist/presenter/presenter-tally.d.ts +8 -11
  72. package/dist/presenter/presenter-tally.js +48 -23
  73. package/dist/presenter/presenter-test-helpers.d.ts +1 -1
  74. package/dist/presenter/presenter-test-helpers.js +1 -1
  75. package/dist/presenter/presenter-verbs.js +2 -2
  76. package/dist/presenter/presenter-workspace.d.ts +15 -0
  77. package/dist/presenter/presenter-workspace.js +180 -0
  78. package/dist/presenter/presenter.d.ts +15 -10
  79. package/dist/presenter/presenter.js +194 -46
  80. package/dist/registry/emitter.d.ts +2 -2
  81. package/dist/reticle.d.ts +2 -2
  82. package/dist/reticle.js +27 -25
  83. package/dist/review/annotator-styles.d.ts +6 -0
  84. package/dist/review/annotator-styles.js +81 -0
  85. package/dist/review/annotator.d.ts +29 -17
  86. package/dist/review/annotator.js +477 -146
  87. package/dist/timers/clock.js +8 -5
  88. package/dist/transport/unreachable-message.d.ts +12 -0
  89. package/dist/transport/unreachable-message.js +20 -0
  90. package/dist/util/captured-method.d.ts +37 -0
  91. package/dist/util/captured-method.js +45 -0
  92. package/package.json +9 -6
@@ -46,6 +46,22 @@ export function sourceFor(el, adapterSource) {
46
46
  return { file: adapterSource.file, line: adapterSource.line };
47
47
  return sourceFromDom(el);
48
48
  }
49
+ /**
50
+ * Does ANY element in this document carry a source stamp?
51
+ *
52
+ * The difference between "this element has no stamp" and "the stamping loader is not running at
53
+ * all" — two answers that look identical from a single missing `source` field, and that call for
54
+ * completely different next actions. The first is ordinary: not every element sits under a stamped
55
+ * host. The second means "fix at file:line", the single most-cited reason to reach for Reticle over
56
+ * a generic DOM tool, is silently unavailable for the whole session — and it has been discovered by
57
+ * reading the adapter's source rather than from anything Reticle said.
58
+ *
59
+ * One `querySelector`, which stops at the first hit, so this is cheap enough to answer on a
60
+ * single-element path like inspect. It is only consulted when a source is already missing.
61
+ */
62
+ export function documentHasSourceStamps(doc) {
63
+ return doc.querySelector(`[${SOURCE_ATTR}]`) !== null;
64
+ }
49
65
  /**
50
66
  * `file:line` — the form an agent can act on directly.
51
67
  *
@@ -1,25 +1,48 @@
1
1
  /**
2
2
  * Design-token (theme) awareness — the in-source answer to "is this color on-theme?". A DOM/a11y
3
3
  * tool sees a color rendered; it does not know the app's INTENDED palette. Reticle reads the design
4
- * tokens the app declares as `:root` CSS custom properties and reports, per element, whether its
5
- * color/background maps to a token or is an off-palette hardcoded value. That is a theme-compliance
6
- * signal no outside-the-page tool has without re-deriving the palette itself.
4
+ * tokens the app declares as CSS custom properties, resolves them AS THE PAGE CURRENTLY RENDERS
5
+ * THEM, and reports, per element, which tokens its color/background map to or whether the value is
6
+ * an off-palette hardcoded one. That is a theme-compliance signal no outside-the-page tool has
7
+ * without re-deriving the palette itself.
7
8
  *
8
9
  * Conservative by design: only a SET, opaque color with no matching token is flagged off-theme, and
9
- * the matched token name is returned when there is one — so the agent can judge, not just trust a flag.
10
+ * every matching token name is returned — so the agent can judge, not just trust a flag.
11
+ *
12
+ * Two defects made this field untrustworthy in BOTH directions before it was rewritten:
13
+ * - the reverse map was color → ONE name, first writer wins, so a button styled with a brand token
14
+ * was told its background was a focus-border token because the two resolve to the same hex. The
15
+ * name is the part an agent reasons about, and it was arbitrary among the candidates.
16
+ * - the palette was built from the RAW `:root` declaration text and cached for the page lifetime,
17
+ * on the reasoning that a theme rarely changes at runtime. A human toggling the theme between
18
+ * two inspect calls left every lookup keyed on the previous theme's colors, so a `<summary>` that
19
+ * had just been changed to a text token reported a surface token. The reporter read that as their
20
+ * edit not applying and went hunting for a theme-scope bug that did not exist.
21
+ * Hence: no cache, values resolved through `getComputedStyle` so the active theme wins, and the
22
+ * scope that was active at capture time reported alongside, so two results minutes apart are
23
+ * comparable at all.
10
24
  */
11
25
  interface ThemeReport {
12
- /** Matched design-token name for the text color, or null when the color is off-palette. */
26
+ /**
27
+ * The ONE design token matching the text color, or null. Null both when nothing matches and when
28
+ * several tokens share the color: the previous behaviour picked an arbitrary winner and the caller
29
+ * could not tell, which is the whole defect. Read `colorTokens` for the full answer; this field
30
+ * stays for callers that only ever wanted the unambiguous case.
31
+ */
13
32
  colorToken: string | null;
14
- /** Matched design-token name for the background color, or null. */
33
+ /** Every design token whose resolved value is the text color. Empty when the color is off-palette. */
34
+ colorTokens: string[];
35
+ /** The one design token matching the background color, or null (same abstain-on-tie rule). */
15
36
  backgroundToken: string | null;
16
- /** True when a set, opaque color uses no design token — a theme violation worth surfacing. */
37
+ /** Every design token whose resolved value is the background color. */
38
+ backgroundTokens: string[];
39
+ /** True when a set, opaque color matches NO design token — a theme violation worth surfacing. */
17
40
  offTheme: boolean;
18
- /** How many color tokens the app declared (0 ⇒ no palette found, so offTheme is never asserted). */
41
+ /** How many color tokens resolved under the active theme (0 ⇒ offTheme is never asserted). */
19
42
  tokenCount: number;
43
+ /** Theme scope active at capture time, e.g. `.dark`, so two results taken apart are comparable. */
44
+ themeScope: string | null;
20
45
  }
21
46
  /** Report an element's theme compliance from its computed style. */
22
47
  export declare function themeReport(cs: CSSStyleDeclaration): ThemeReport;
23
- /** Drop the cached palette (call if the app swaps themes at runtime). */
24
- export declare function resetThemeCache(): void;
25
48
  export {};
package/dist/dom/theme.js CHANGED
@@ -1,33 +1,32 @@
1
1
  /**
2
2
  * Design-token (theme) awareness — the in-source answer to "is this color on-theme?". A DOM/a11y
3
3
  * tool sees a color rendered; it does not know the app's INTENDED palette. Reticle reads the design
4
- * tokens the app declares as `:root` CSS custom properties and reports, per element, whether its
5
- * color/background maps to a token or is an off-palette hardcoded value. That is a theme-compliance
6
- * signal no outside-the-page tool has without re-deriving the palette itself.
4
+ * tokens the app declares as CSS custom properties, resolves them AS THE PAGE CURRENTLY RENDERS
5
+ * THEM, and reports, per element, which tokens its color/background map to or whether the value is
6
+ * an off-palette hardcoded one. That is a theme-compliance signal no outside-the-page tool has
7
+ * without re-deriving the palette itself.
7
8
  *
8
9
  * Conservative by design: only a SET, opaque color with no matching token is flagged off-theme, and
9
- * the matched token name is returned when there is one — so the agent can judge, not just trust a flag.
10
+ * every matching token name is returned — so the agent can judge, not just trust a flag.
11
+ *
12
+ * Two defects made this field untrustworthy in BOTH directions before it was rewritten:
13
+ * - the reverse map was color → ONE name, first writer wins, so a button styled with a brand token
14
+ * was told its background was a focus-border token because the two resolve to the same hex. The
15
+ * name is the part an agent reasons about, and it was arbitrary among the candidates.
16
+ * - the palette was built from the RAW `:root` declaration text and cached for the page lifetime,
17
+ * on the reasoning that a theme rarely changes at runtime. A human toggling the theme between
18
+ * two inspect calls left every lookup keyed on the previous theme's colors, so a `<summary>` that
19
+ * had just been changed to a text token reported a surface token. The reporter read that as their
20
+ * edit not applying and went hunting for a theme-scope bug that did not exist.
21
+ * Hence: no cache, values resolved through `getComputedStyle` so the active theme wins, and the
22
+ * scope that was active at capture time reported alongside, so two results minutes apart are
23
+ * comparable at all.
10
24
  */
11
- let cached = null;
12
- /** Resolve any CSS color string to canonical computed `rgb...)`/`rgba...)`, or null if not a color. */
13
- function toRgb(value) {
14
- if (0 === value.length)
15
- return null;
16
- const probe = document.createElement('span');
17
- probe.style.color = '';
18
- probe.style.color = value; // invalid colors leave it empty (the setter rejects them)
19
- if ('' === probe.style.color)
20
- return null;
21
- probe.style.position = 'absolute';
22
- probe.style.pointerEvents = 'none';
23
- document.body.appendChild(probe);
24
- const rgb = getComputedStyle(probe).color;
25
- probe.remove();
26
- return rgb;
27
- }
28
- /** Collect `--token: value` declarations from every:root/html rule across the app's stylesheets. */
29
- function collectTokens() {
30
- const out = {};
25
+ /** Attribute an app flips to switch themes, reported as part of the scope so results are comparable. */
26
+ const THEME_ATTRIBUTE = 'data-theme';
27
+ /** Collect every custom-property NAME the app declares, in any rule. */
28
+ function tokenNames() {
29
+ const names = new Set();
31
30
  for (const sheet of Array.from(document.styleSheets)) {
32
31
  let rules = null;
33
32
  try {
@@ -41,49 +40,109 @@ function collectTokens() {
41
40
  for (const rule of Array.from(rules)) {
42
41
  if (!(rule instanceof CSSStyleRule))
43
42
  continue;
44
- if (!/(^|,)\s*(:root|html)\b/.test(rule.selectorText))
45
- continue;
43
+ // Deliberately NOT filtered to `:root`/`html`: a theme block is usually `.dark` or
44
+ // `[data-theme="dark"]`, and an app that declares its palette only there had no palette at all
45
+ // under the old filter. Names are cheap; a name that is not in scope resolves to nothing below
46
+ // and is dropped, so widening collection cannot invent a token.
46
47
  for (const prop of Array.from(rule.style)) {
47
48
  if (prop.startsWith('--'))
48
- out[prop] = rule.style.getPropertyValue(prop).trim();
49
+ names.add(prop);
49
50
  }
50
51
  }
51
52
  }
52
- return out;
53
+ return names;
53
54
  }
54
- /** The app's palette (color tokens → rgb), built once and cached (theme rarely changes at runtime). */
55
+ /**
56
+ * The palette as the page renders it right now: canonical `rgb(...)` → every token with that color.
57
+ *
58
+ * Resolved against `document.body` rather than the raw declaration text, because custom properties
59
+ * inherit: this picks up a theme scoped to `<html>` or to `<body>`, and picks up the value the
60
+ * ACTIVE theme block won with rather than whichever block the stylesheet happened to list first.
61
+ */
55
62
  function palette() {
56
- if (cached !== null)
57
- return cached;
58
63
  const byColor = new Map();
59
- for (const [name, value] of Object.entries(collectTokens())) {
60
- const rgb = toRgb(value);
61
- if (rgb !== null && !byColor.has(rgb))
62
- byColor.set(rgb, name);
64
+ const root = document.body ?? document.documentElement;
65
+ if (null === root)
66
+ return byColor;
67
+ const rootStyle = getComputedStyle(root);
68
+ // One shared probe for the whole build. There is no cache any more, so this runs per inspect, and
69
+ // a span per token would be a few hundred appends on a real design system.
70
+ const probe = document.createElement('span');
71
+ probe.style.position = 'absolute';
72
+ probe.style.pointerEvents = 'none';
73
+ document.body.appendChild(probe);
74
+ try {
75
+ for (const name of tokenNames()) {
76
+ const rgb = toRgb(probe, rootStyle.getPropertyValue(name).trim());
77
+ if (null === rgb)
78
+ continue;
79
+ const named = byColor.get(rgb);
80
+ if (named === undefined)
81
+ byColor.set(rgb, [name]);
82
+ else
83
+ named.push(name);
84
+ }
85
+ }
86
+ finally {
87
+ probe.remove();
63
88
  }
64
- cached = { byColor };
65
- return cached;
89
+ // Sorted so the reported order is a property of the app's tokens, not of stylesheet iteration
90
+ // order — the same collision must read the same way on two consecutive inspects.
91
+ for (const named of byColor.values())
92
+ named.sort();
93
+ return byColor;
94
+ }
95
+ /** Resolve any CSS color string to canonical computed `rgb...)`/`rgba...)`, or null if not a color. */
96
+ function toRgb(probe, value) {
97
+ if (0 === value.length)
98
+ return null;
99
+ probe.style.color = '';
100
+ probe.style.color = value; // invalid colors leave it empty (the setter rejects them)
101
+ if ('' === probe.style.color)
102
+ return null;
103
+ return getComputedStyle(probe).color;
66
104
  }
67
105
  /** True for a color that carries no visual weight (fully transparent) — never flagged off-theme. */
68
106
  function isTransparent(rgb) {
69
107
  return 'rgba(0, 0, 0, 0)' === rgb || 'transparent' === rgb;
70
108
  }
109
+ /**
110
+ * The theme scope in force at capture time, as a selector-shaped string (`.dark[data-theme="dark"]`),
111
+ * or null when the root carries no theme marker at all.
112
+ */
113
+ function themeScope() {
114
+ const el = document.documentElement;
115
+ if (null === el)
116
+ return null;
117
+ const classes = Array.from(el.classList)
118
+ .map((c) => `.${c}`)
119
+ .join('');
120
+ const attr = el.getAttribute(THEME_ATTRIBUTE);
121
+ const scope = `${classes}${null === attr ? '' : `[${THEME_ATTRIBUTE}="${attr}"]`}`;
122
+ return 0 === scope.length ? null : scope;
123
+ }
124
+ /** The unambiguous match, or null when the palette cannot tell the candidates apart. */
125
+ function sole(names) {
126
+ return 1 === names.length ? (names[0] ?? null) : null;
127
+ }
71
128
  /** Report an element's theme compliance from its computed style. */
72
129
  export function themeReport(cs) {
73
- const p = palette();
74
- const colorToken = p.byColor.get(cs.color) ?? null;
75
- const backgroundToken = p.byColor.get(cs.backgroundColor) ?? null;
76
- const colorOff = !isTransparent(cs.color) && null === colorToken;
77
- const bgOff = !isTransparent(cs.backgroundColor) && null === backgroundToken;
130
+ const byColor = palette();
131
+ const colorTokens = byColor.get(cs.color) ?? [];
132
+ const backgroundTokens = byColor.get(cs.backgroundColor) ?? [];
133
+ const colorOff = !isTransparent(cs.color) && 0 === colorTokens.length;
134
+ const bgOff = !isTransparent(cs.backgroundColor) && 0 === backgroundTokens.length;
135
+ let tokenCount = 0;
136
+ for (const named of byColor.values())
137
+ tokenCount += named.length;
78
138
  return {
79
- colorToken,
80
- backgroundToken,
139
+ colorToken: sole(colorTokens),
140
+ colorTokens,
141
+ backgroundToken: sole(backgroundTokens),
142
+ backgroundTokens,
81
143
  // Only meaningful when a palette exists; an app with no tokens can't violate one.
82
- offTheme: p.byColor.size > 0 && (colorOff || bgOff),
83
- tokenCount: p.byColor.size,
144
+ offTheme: tokenCount > 0 && (colorOff || bgOff),
145
+ tokenCount,
146
+ themeScope: themeScope(),
84
147
  };
85
148
  }
86
- /** Drop the cached palette (call if the app swaps themes at runtime). */
87
- export function resetThemeCache() {
88
- cached = null;
89
- }
@@ -1,5 +1,6 @@
1
1
  import { EventType, TRANSPORT_LIMITS } from '@reticlehq/core';
2
2
  import { safeStringify } from '../security/serialization.js';
3
+ import { requireCapturedMethod } from '../util/captured-method.js';
3
4
  const METHOD_EVENT = {
4
5
  log: EventType.CONSOLE_LOG,
5
6
  warn: EventType.CONSOLE_WARN,
@@ -41,8 +42,10 @@ export function installConsole(emit) {
41
42
  const originals = new Map();
42
43
  const patched = new Map();
43
44
  for (const method of methods) {
44
- // Store the true original for teardown identity; call through a bound copy.
45
- const original = console[method];
45
+ // Store the true original for teardown identity; call through a bound copy. Read as a stored
46
+ // VALUE rather than a method reference — see capturedMethod for why that distinction is the
47
+ // honest way to say "I am detaching this deliberately and putting it back".
48
+ const original = requireCapturedMethod(console, method);
46
49
  originals.set(method, original);
47
50
  const callOriginal = original.bind(console);
48
51
  const wrapper = (...args) => {
@@ -18,7 +18,7 @@ import { installFocus } from './focus.js';
18
18
  import { installBlindSpots } from './blind-spots.js';
19
19
  import { installDownload } from './download.js';
20
20
  import { installNetwork } from './network.js';
21
- import { installIpc, ipcNetOverrides } from './ipc.js';
21
+ import { installIpc, ipcNetOverrides, isReticleOwnIpc } from './ipc.js';
22
22
  import { installPerf } from './perf.js';
23
23
  import { installRoute } from './route.js';
24
24
  import { installConsole } from './console.js';
@@ -43,6 +43,9 @@ export function installAllObservers(emit, options) {
43
43
  guard(emit, SdkSite.NETWORK_OBSERVER, () => installNetwork(emit, {
44
44
  captureBodies: options.captureBodies,
45
45
  reinterpret: ipcNetOverrides,
46
+ // The SDK's own Tauri screenshot is a fetch like any other — skip it, or the observer
47
+ // reports its own captures as the app's writes.
48
+ ignore: isReticleOwnIpc,
46
49
  })),
47
50
  // Desktop backends are reached over IPC, not HTTP — inert on a plain web page.
48
51
  guard(emit, SdkSite.IPC_OBSERVER, () => installIpc(emit, { captureBodies: options.captureBodies })),
@@ -5,6 +5,22 @@ import type { Emit, Teardown } from './types.js';
5
5
  * already holds, so IPC knowledge stays in this file.
6
6
  */
7
7
  export declare function ipcNetOverrides(url: string, header: (name: string) => string | null): Record<string, unknown> | undefined;
8
+ /**
9
+ * Is this fetch Reticle's OWN desktop call rather than the app's?
10
+ *
11
+ * Tauri has no preload stage, so `reticle_screenshot` reaches the shell the only way it can: an
12
+ * `invoke` — and an invoke IS a fetch to the `ipc://` protocol, straight through the patch the
13
+ * network observer installed. So the observer recorded its own screenshots as app IPC calls, and
14
+ * because `IPC` counts as a mutating method, three concurrent captures inside one action window came
15
+ * back to the agent as `duplicate-request`: "the same write fired 3 times", against an app that had
16
+ * never made the call. Accusing a correct app of a double submit is the most damaging thing a
17
+ * verification tool can do, and here the tool was the one submitting.
18
+ *
19
+ * Electron never had this problem — its preload captures through the UNPATCHED invoke for exactly
20
+ * this reason — and the bridge's own WebSocket is skipped on the same principle (`isBridgeSocket`).
21
+ * The capture command is the SDK's whole Tauri-side surface, so matching it is the whole rule.
22
+ */
23
+ export declare function isReticleOwnIpc(url: string): boolean;
8
24
  /**
9
25
  * Subscribe to the Electron preload shim, if this app installed it. Inert on Tauri (whose calls the
10
26
  * network observer handles) and on a plain web page.
@@ -1,4 +1,4 @@
1
- import { BlindSpotKind, EventType, IPC_URL_SCHEME, IpcStatus, NetInitiator, RETICLE_IPC_GLOBAL, } from '@reticlehq/core';
1
+ import { BlindSpotKind, EventType, IPC_URL_SCHEME, IpcStatus, NetInitiator, RETICLE_IPC_GLOBAL, RETICLE_TAURI_CAPTURE_COMMAND, } from '@reticlehq/core';
2
2
  import { observeSafely } from './types.js';
3
3
  /**
4
4
  * How Tauri v2 reports a command's verdict. The transport answers HTTP 200 whether the Rust command
@@ -48,6 +48,24 @@ export function ipcNetOverrides(url, header) {
48
48
  statusText: ok ? IPC_STATUS_TEXT.OK : IPC_STATUS_TEXT.ERROR,
49
49
  };
50
50
  }
51
+ /**
52
+ * Is this fetch Reticle's OWN desktop call rather than the app's?
53
+ *
54
+ * Tauri has no preload stage, so `reticle_screenshot` reaches the shell the only way it can: an
55
+ * `invoke` — and an invoke IS a fetch to the `ipc://` protocol, straight through the patch the
56
+ * network observer installed. So the observer recorded its own screenshots as app IPC calls, and
57
+ * because `IPC` counts as a mutating method, three concurrent captures inside one action window came
58
+ * back to the agent as `duplicate-request`: "the same write fired 3 times", against an app that had
59
+ * never made the call. Accusing a correct app of a double submit is the most damaging thing a
60
+ * verification tool can do, and here the tool was the one submitting.
61
+ *
62
+ * Electron never had this problem — its preload captures through the UNPATCHED invoke for exactly
63
+ * this reason — and the bridge's own WebSocket is skipped on the same principle (`isBridgeSocket`).
64
+ * The capture command is the SDK's whole Tauri-side surface, so matching it is the whole rule.
65
+ */
66
+ export function isReticleOwnIpc(url) {
67
+ return TAURI_IPC_URL.exec(url)?.[1] === RETICLE_TAURI_CAPTURE_COMMAND;
68
+ }
51
69
  /**
52
70
  * Is this page an Electron renderer? Read off the user agent, which Electron stamps and which is
53
71
  * available whether or not the preload ran — the whole point is to recognise a renderer that has
@@ -16,6 +16,14 @@ interface NetworkOptions {
16
16
  captureBodies?: boolean;
17
17
  /** Optional hook that reinterprets a completed request — see NetResponseReinterpreter. */
18
18
  reinterpret?: NetResponseReinterpreter;
19
+ /**
20
+ * Requests Reticle itself makes, which must never reach the app's evidence.
21
+ *
22
+ * Same seam and same reason as `reinterpret`: this observer knows nothing about desktop IPC, and
23
+ * the SDK's own Tauri screenshot travels as an ordinary fetch through this very patch. Passed in
24
+ * rather than imported, so the knowledge stays in the IPC observer.
25
+ */
26
+ ignore?: (url: string) => boolean;
19
27
  }
20
28
  /** Pure: the first real app-code frame in a stack string, capped. Exported for unit testing. */
21
29
  export declare function firstAppFrame(stack: string | undefined): string | undefined;
@@ -3,6 +3,7 @@ import { captureMethod } from '../patching/capture-method.js';
3
3
  import { isCapturableType, projectBody, withBodyDeadline } from './network-body.js';
4
4
  import { redactUrl } from './network-redact.js';
5
5
  import { watchStreamedBody } from './network-stream.js';
6
+ import { requireCapturedMethod } from '../util/captured-method.js';
6
7
  // Redaction moved to its own cohesive module (network.ts is at its line cap); re-exported so callers
7
8
  // and the existing test suite keep importing it from here.
8
9
  export { redactUrl };
@@ -60,6 +61,22 @@ function projectRequestBody(body, captureBodies) {
60
61
  const { body: out, truncated } = projectBody(text, contentType);
61
62
  return truncated ? { requestBody: out, requestBodyTruncated: true } : { requestBody: out };
62
63
  }
64
+ /**
65
+ * Did this response succeed? A REDIRECT DID.
66
+ *
67
+ * The one rule both transports must share. `Response.ok` is true only for 200-299, so reading it
68
+ * straight through stamped every 3xx as a failed request, while the XHR path computed the same field
69
+ * itself and called the identical status a success. Downstream, `ok` is authoritative when present —
70
+ * so a POST-redirect-GET login, the first flow anyone verifies in an app with auth, produced a
71
+ * `ui-advanced-request-failed` contradiction citing its own success path, and the agent had to
72
+ * overrule the verdict by hand. A verdict that gets manually overruled is not deciding anything.
73
+ *
74
+ * 3xx is normal successful navigation. An opaque (no-cors) response reports status 0 and is correctly
75
+ * excluded here, exactly as `Response.ok` excluded it.
76
+ */
77
+ function statusIsOk(status) {
78
+ return status >= 200 && status < 400;
79
+ }
63
80
  /**
64
81
  * Response metadata that needs no body capture: HTTP status text, content-type, and byte size (from
65
82
  * content-length when the server sent it). Lets an agent tell an HTML error page served as 200 from
@@ -195,7 +212,7 @@ export function installNetwork(emit, opts = {}) {
195
212
  const reinterpret = opts.reinterpret;
196
213
  // Keep the true original for teardown identity, plus a window-bound copy to invoke
197
214
  // (fetch throws "Illegal invocation" if called with the wrong `this`).
198
- const origFetch = window.fetch;
215
+ const origFetch = requireCapturedMethod(window, 'fetch');
199
216
  const callFetch = origFetch.bind(window);
200
217
  // Declare it if we are not the first to wrap fetch.
201
218
  //
@@ -218,10 +235,15 @@ export function installNetwork(emit, opts = {}) {
218
235
  let seq = 0;
219
236
  const nextId = () => `n${++seq}`;
220
237
  window.fetch = async (input, init) => {
238
+ const rawUrl = urlOf(input);
239
+ // Reticle's own call: pass it straight through, observed by nobody. Emitting even the PENDING
240
+ // half would be wrong twice over — it lands in the agent's network evidence, and `settle` would
241
+ // wait on the SDK's own screenshot as if the app had a request in flight.
242
+ if (true === opts.ignore?.(rawUrl))
243
+ return callFetch(input, init);
221
244
  const id = nextId();
222
245
  const start = performance.now();
223
246
  const method = methodOf(input, init);
224
- const rawUrl = urlOf(input);
225
247
  const url = redactUrl(rawUrl);
226
248
  const initiatorStack = initiatorFrame();
227
249
  const initiatorFields = initiatorStack === undefined ? {} : { initiatorStack };
@@ -240,7 +262,7 @@ export function installNetwork(emit, opts = {}) {
240
262
  method,
241
263
  url,
242
264
  status: res.status,
243
- ok: res.ok,
265
+ ok: statusIsOk(res.status),
244
266
  durationMs: Math.round(headersAt - start),
245
267
  initiator: 'fetch',
246
268
  ...initiatorFields,
@@ -320,8 +342,9 @@ export function installNetwork(emit, opts = {}) {
320
342
  }
321
343
  };
322
344
  // Remember it, so a later install recognises our own wrapper instead of reporting the app.
323
- OURS.add(window.fetch);
324
- const patchedFetch = window.fetch;
345
+ // Our own wrapper, read the same way — it is a value we stored a line ago, not a method call.
346
+ const patchedFetch = requireCapturedMethod(window, 'fetch');
347
+ OURS.add(patchedFetch);
325
348
  const meta = new WeakMap();
326
349
  const proto = XMLHttpRequest.prototype;
327
350
  const origOpen = captureMethod(proto, 'open');
@@ -382,7 +405,7 @@ export function installNetwork(emit, opts = {}) {
382
405
  method: cur.method,
383
406
  url: cur.url,
384
407
  status: this.status,
385
- ok: this.status >= 200 && this.status < 400,
408
+ ok: statusIsOk(this.status),
386
409
  durationMs: Math.round(performance.now() - cur.start),
387
410
  initiator: 'xhr',
388
411
  ...(cur.initiatorStack === undefined ? {} : { initiatorStack: cur.initiatorStack }),
@@ -1,4 +1,4 @@
1
- import { EventType, REDACTED_VALUE } from '@reticlehq/core';
1
+ import { BlindSpotKind, 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) {
@@ -68,9 +68,28 @@ export function installStoreState(emit) {
68
68
  // Stores already registered...
69
69
  for (const entry of subscribableStores())
70
70
  watch(entry);
71
+ /**
72
+ * Declare an unwatched state channel — and clear it the moment one is watched.
73
+ *
74
+ * With nothing subscribed, every act reports `stateDiffs: []`, which reads as "the app changed no
75
+ * state" when the truth is "nobody was looking". Emitted as a BLIND_SPOT so it travels the path
76
+ * that already exists for everything the layer cannot see: bounding, never impeaching, so no
77
+ * verdict turns UNKNOWN over it.
78
+ */
79
+ if (0 === active.size) {
80
+ emit(EventType.BLIND_SPOT, { kind: BlindSpotKind.UNWATCHED_STATE, count: 1 });
81
+ }
71
82
  //...and any registered LATER. The SDK installs observers during connect, but apps call
72
83
  // registerStore after that, so without this the common case subscribes to nothing.
73
- const offRegistered = onStoreRegistered(watch);
84
+ const offRegistered = onStoreRegistered((entry) => {
85
+ const wasDark = 0 === active.size;
86
+ watch(entry);
87
+ // The channel just lit up. A blind spot that is never withdrawn is a permanent lie, and this one
88
+ // is withdrawn in the ordinary case: apps register their stores AFTER connect() installs us.
89
+ if (wasDark && active.size > 0) {
90
+ emit(EventType.BLIND_SPOT, { kind: BlindSpotKind.UNWATCHED_STATE, count: 0 });
91
+ }
92
+ });
74
93
  return () => {
75
94
  offRegistered();
76
95
  for (const unsubscribe of active.values()) {
@@ -1,19 +1,36 @@
1
+ /**
2
+ * The artwork constants are annotated `: string` deliberately.
3
+ *
4
+ * Without the annotation TypeScript infers a literal type, and every one of these SVG and HTML
5
+ * strings is copied verbatim into the emitted `.d.ts` as its own type. That duplicated the artwork
6
+ * into the published package twice over: this file's declarations alone were 16KB of a 900KB
7
+ * budget, carrying type information nobody can use - no caller cares about the literal type of a
8
+ * `<path d=...>`.
9
+ */
1
10
  /**
2
11
  * Brand marks, inlined. The SDK is bundled into someone else's page: it cannot read
3
12
  * `assets/logo/*.svg` from disk at runtime and must never fetch from our own domain, so the two
4
13
  * 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
14
+ * Both are minified - editor metadata, clip paths, masks and sub-0.01 precision stripped - and paint
6
15
  * with `currentColor` so the mark tracks --reticle-fg instead of hardcoding the export's #FAFAFA.
7
16
  */
8
- /** Accessible name of the HUD — the wordmark is artwork, this is what a screen reader reads. */
17
+ /** Accessible name of the HUD - the wordmark is artwork, this is what a screen reader reads. */
9
18
  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>";
19
+ /** Mark alone (shown in the FAB and compact toolbar). */
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;
14
29
  /**
15
30
  * Header brand block. Both marks are always in the DOM and CSS picks one off the collapsed
16
31
  * attribute, so there is no JS state to keep in sync. The artwork is decorative (aria-hidden);
17
32
  * the visually-hidden name is what keeps the HUD identifiable to a screen reader in BOTH states.
18
33
  */
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>";
34
+ export declare const BRAND_HTML: string;
35
+ /** Collapsed FAB control - Reticle mark only. */
36
+ export declare const FAB_TOGGLE_HTML: string;