@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.
- package/NOTICE +7 -0
- package/README.md +2 -2
- package/dist/actions/actions-dom.js +23 -15
- package/dist/actions/actions.d.ts +17 -0
- package/dist/actions/actions.js +132 -29
- package/dist/actions/synthetic-input.d.ts +9 -0
- package/dist/actions/synthetic-input.js +40 -0
- package/dist/commands/commands.js +18 -1
- package/dist/connect-options.d.ts +9 -9
- package/dist/dom/a11y.d.ts +9 -0
- package/dist/dom/a11y.js +19 -0
- package/dist/dom/desktop-capture.js +20 -3
- package/dist/dom/dom-ignore.d.ts +19 -6
- package/dist/dom/dom-ignore.js +20 -13
- package/dist/dom/query.d.ts +2 -2
- package/dist/dom/query.js +85 -45
- package/dist/dom/refs.d.ts +28 -0
- package/dist/dom/refs.js +87 -0
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +34 -0
- package/dist/dom/source.d.ts +14 -0
- package/dist/dom/source.js +16 -0
- package/dist/dom/theme.d.ts +33 -10
- package/dist/dom/theme.js +109 -50
- package/dist/observers/console.js +5 -2
- package/dist/observers/install-all.js +4 -1
- package/dist/observers/ipc.d.ts +16 -0
- package/dist/observers/ipc.js +19 -1
- package/dist/observers/network.d.ts +8 -0
- package/dist/observers/network.js +29 -6
- package/dist/observers/state.js +21 -2
- package/dist/presenter/presenter-brand.d.ts +24 -7
- package/dist/presenter/presenter-brand.js +23 -6
- package/dist/presenter/presenter-config.d.ts +102 -9
- package/dist/presenter/presenter-config.js +94 -8
- package/dist/presenter/presenter-controls.d.ts +10 -10
- package/dist/presenter/presenter-controls.js +175 -85
- package/dist/presenter/presenter-dock-layout.d.ts +6 -0
- package/dist/presenter/presenter-dock-layout.js +170 -0
- package/dist/presenter/presenter-drag.d.ts +48 -0
- package/dist/presenter/presenter-drag.js +198 -0
- package/dist/presenter/presenter-effects.d.ts +1 -1
- package/dist/presenter/presenter-effects.js +1 -1
- package/dist/presenter/presenter-glow.d.ts +5 -5
- package/dist/presenter/presenter-glow.js +7 -7
- package/dist/presenter/presenter-heroicons-data.d.ts +41 -0
- package/dist/presenter/presenter-heroicons-data.js +38 -0
- package/dist/presenter/presenter-hud-chrome.d.ts +17 -0
- package/dist/presenter/presenter-hud-chrome.js +41 -0
- package/dist/presenter/presenter-icons.d.ts +44 -0
- package/dist/presenter/presenter-icons.js +100 -0
- package/dist/presenter/presenter-log.d.ts +27 -5
- package/dist/presenter/presenter-log.js +114 -28
- package/dist/presenter/presenter-report-copy.d.ts +83 -0
- package/dist/presenter/presenter-report-copy.js +127 -0
- package/dist/presenter/presenter-report-styles.d.ts +7 -0
- package/dist/presenter/presenter-report-styles.js +102 -0
- package/dist/presenter/presenter-report.d.ts +23 -0
- package/dist/presenter/presenter-report.js +217 -0
- package/dist/presenter/presenter-run-state.d.ts +1 -1
- package/dist/presenter/presenter-settings-styles.d.ts +2 -0
- package/dist/presenter/presenter-settings-styles.js +153 -0
- package/dist/presenter/presenter-settings.d.ts +82 -0
- package/dist/presenter/presenter-settings.js +494 -0
- package/dist/presenter/presenter-shell-styles.d.ts +1 -0
- package/dist/presenter/presenter-shell-styles.js +375 -0
- package/dist/presenter/presenter-shell.d.ts +48 -0
- package/dist/presenter/presenter-shell.js +408 -0
- package/dist/presenter/presenter-styles.d.ts +9 -4
- package/dist/presenter/presenter-styles.js +61 -125
- package/dist/presenter/presenter-tally.d.ts +8 -11
- package/dist/presenter/presenter-tally.js +48 -23
- package/dist/presenter/presenter-test-helpers.d.ts +1 -1
- package/dist/presenter/presenter-test-helpers.js +1 -1
- package/dist/presenter/presenter-verbs.js +2 -2
- package/dist/presenter/presenter-workspace.d.ts +15 -0
- package/dist/presenter/presenter-workspace.js +180 -0
- package/dist/presenter/presenter.d.ts +15 -10
- package/dist/presenter/presenter.js +194 -46
- package/dist/registry/emitter.d.ts +2 -2
- package/dist/reticle.d.ts +2 -2
- package/dist/reticle.js +27 -25
- package/dist/review/annotator-styles.d.ts +6 -0
- package/dist/review/annotator-styles.js +81 -0
- package/dist/review/annotator.d.ts +29 -17
- package/dist/review/annotator.js +477 -146
- package/dist/timers/clock.js +8 -5
- package/dist/transport/unreachable-message.d.ts +12 -0
- package/dist/transport/unreachable-message.js +20 -0
- package/dist/util/captured-method.d.ts +37 -0
- package/dist/util/captured-method.js +45 -0
- package/package.json +9 -6
package/dist/dom/source.js
CHANGED
|
@@ -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
|
*
|
package/dist/dom/theme.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
* color/background
|
|
6
|
-
* signal no outside-the-page tool has
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
5
|
-
* color/background
|
|
6
|
-
* signal no outside-the-page tool has
|
|
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
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
+
names.add(prop);
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
return
|
|
53
|
+
return names;
|
|
53
54
|
}
|
|
54
|
-
/**
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const colorOff = !isTransparent(cs.color) &&
|
|
77
|
-
const bgOff = !isTransparent(cs.backgroundColor) &&
|
|
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
|
-
|
|
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:
|
|
83
|
-
tokenCount
|
|
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
|
-
|
|
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 })),
|
package/dist/observers/ipc.d.ts
CHANGED
|
@@ -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.
|
package/dist/observers/ipc.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
324
|
-
const patchedFetch = window
|
|
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
|
|
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 }),
|
package/dist/observers/state.js
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
11
|
-
export declare const MARK_SVG
|
|
12
|
-
/**
|
|
13
|
-
export declare const
|
|
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
|
|
34
|
+
export declare const BRAND_HTML: string;
|
|
35
|
+
/** Collapsed FAB control - Reticle mark only. */
|
|
36
|
+
export declare const FAB_TOGGLE_HTML: string;
|