@reticlehq/browser 2.11.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/actions-dom.js +41 -3
- package/dist/actions/actions.js +37 -5
- package/dist/commands/commands.js +6 -2
- package/dist/dom/a11y.js +92 -9
- package/dist/dom/query.js +113 -94
- package/dist/dom/refs.d.ts +11 -0
- package/dist/dom/refs.js +30 -1
- package/dist/dom/snapshot.d.ts +29 -0
- package/dist/dom/snapshot.js +72 -34
- package/dist/edit-epoch.d.ts +32 -0
- package/dist/edit-epoch.js +108 -0
- package/dist/observers/context-open.d.ts +12 -0
- package/dist/observers/context-open.js +25 -0
- package/dist/observers/install-all.js +2 -0
- package/dist/observers/storage.js +1 -1
- package/dist/presenter/presenter-brand.d.ts +4 -18
- package/dist/presenter/presenter-brand.js +6 -18
- package/dist/presenter/presenter-config.d.ts +0 -6
- package/dist/presenter/presenter-config.js +0 -3
- package/dist/presenter/presenter-controls.d.ts +0 -3
- package/dist/presenter/presenter-controls.js +0 -3
- package/dist/presenter/presenter-settings.js +10 -2
- package/dist/presenter/presenter-shell.d.ts +0 -2
- package/dist/presenter/presenter-shell.js +1 -5
- package/dist/reticle.d.ts +12 -0
- package/dist/reticle.js +32 -1
- package/dist/transport/unreachable-message.js +2 -1
- package/package.json +2 -5
- package/dist/dom/blind-spots.d.ts +0 -13
- package/dist/dom/blind-spots.js +0 -29
- package/dist/observers/subresources.d.ts +0 -10
- package/dist/observers/subresources.js +0 -63
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PresenterReport, reportPanelHtml } from './presenter-report.js';
|
|
2
|
-
import { CHAT_MIN_ATTR, CHAT_PILL_ATTR, REPORT_ATTR, REPORT_BTN_ATTR, ANNOTATE_BTN_ATTR, CHAT_ATTR, CHAT_TOGGLE_ATTR, CHAT_PANEL_ATTR, DOCK_ATTR, FAB_ATTR, MIN_ATTR,
|
|
2
|
+
import { CHAT_MIN_ATTR, CHAT_PILL_ATTR, REPORT_ATTR, REPORT_BTN_ATTR, ANNOTATE_BTN_ATTR, CHAT_ATTR, CHAT_TOGGLE_ATTR, CHAT_PANEL_ATTR, DOCK_ATTR, FAB_ATTR, MIN_ATTR, SETTINGS_ATTR, SETTINGS_BTN_ATTR, } from './presenter-config.js';
|
|
3
3
|
import { BRAND_NAME, FAB_TOGGLE_HTML, MARK_SVG } from './presenter-brand.js';
|
|
4
4
|
import { settleLogAtLatest } from './presenter-log.js';
|
|
5
5
|
import { installHudDragHandles, installHudPositionGuards } from './presenter-drag.js';
|
|
@@ -420,7 +420,3 @@ export class HudShell {
|
|
|
420
420
|
}
|
|
421
421
|
};
|
|
422
422
|
}
|
|
423
|
-
/** Whether the dock has been dragged off the default bottom-right position. */
|
|
424
|
-
export function isDockDragged(dock) {
|
|
425
|
-
return '1' === dock.getAttribute(HUD_DRAGGED_ATTR);
|
|
426
|
-
}
|
package/dist/reticle.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export declare function buildEvent(args: {
|
|
|
55
55
|
sessionId: string;
|
|
56
56
|
data: Record<string, unknown>;
|
|
57
57
|
ref?: string | undefined;
|
|
58
|
+
documentId?: string | undefined;
|
|
59
|
+
editEpoch?: number | undefined;
|
|
58
60
|
}): ReticleEvent;
|
|
59
61
|
/**
|
|
60
62
|
* The browser-side orchestrator. Wires observers -> events -> bridge, and bridge
|
|
@@ -79,5 +81,15 @@ export declare class Reticle {
|
|
|
79
81
|
describe(input: CapabilitiesInput): void;
|
|
80
82
|
/** Live-control: end the session programmatically from the host app (drives the panel to ended). */
|
|
81
83
|
endSession(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Hand the SDK the page's hot-update channel, so a stale ref can say the code changed underneath it.
|
|
86
|
+
*
|
|
87
|
+
* `unknown` on purpose: the only caller that has one is the build integration, the shape it passes
|
|
88
|
+
* is Vite's `import.meta.hot`, and the SDK must not depend on Vite — it ships to Next, Electron,
|
|
89
|
+
* Tauri and plain pages. Anything that is not a subscribable channel is ignored, which is the
|
|
90
|
+
* normal case: with no channel the epoch stays at `NO_EDITS_OBSERVED`, meaning "no edits OBSERVED",
|
|
91
|
+
* never "no edits happened".
|
|
92
|
+
*/
|
|
93
|
+
observeHotUpdates(hot: unknown): void;
|
|
82
94
|
disconnect(): void;
|
|
83
95
|
}
|
package/dist/reticle.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, TRANSPORT_LIMITS, buildRedactionPolicy, isLoopbackHostname, isLocalPage, setActiveRedactionPolicy, wireRedactionKeys, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, CONTRACT_FINGERPRINT, } from '@reticlehq/core';
|
|
1
|
+
import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, TRANSPORT_LIMITS, buildRedactionPolicy, isLoopbackHostname, isLocalPage, setActiveRedactionPolicy, wireRedactionKeys, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, CONTRACT_FINGERPRINT, newDocumentId, NO_EDITS_OBSERVED, } from '@reticlehq/core';
|
|
2
2
|
import { rememberSessionLabel } from './session-continuity.js';
|
|
3
|
+
import { editEpoch } from './edit-epoch.js';
|
|
3
4
|
import { createCommandRegistry, RELOAD_CACHE_BUST_PARAM, } from './commands/commands.js';
|
|
4
5
|
import { Transport } from './transport/transport.js';
|
|
5
6
|
import { unreachableMessage } from './transport/unreachable-message.js';
|
|
@@ -128,6 +129,15 @@ export function buildEvent(args) {
|
|
|
128
129
|
type: args.type,
|
|
129
130
|
sessionId: args.sessionId,
|
|
130
131
|
ref: args.ref,
|
|
132
|
+
// Which document this was observed under, so the server can refuse evidence minted before a
|
|
133
|
+
// navigation replaced the page. Stamped HERE because it is the one place every event passes
|
|
134
|
+
// through: an observer added later would otherwise emit unstamped events, which read as
|
|
135
|
+
// "current" by design and would reintroduce the defect silently for one event type.
|
|
136
|
+
documentId: args.documentId,
|
|
137
|
+
// Which round of source edits this was observed under. Stamped HERE for exactly the reason
|
|
138
|
+
// documentId is, and omitted while nothing has hot-updated: absence already reads as "current"
|
|
139
|
+
// downstream, so `NO_EDITS_OBSERVED` on the wire would be bytes spent saying "unknown".
|
|
140
|
+
editEpoch: NO_EDITS_OBSERVED === args.editEpoch ? undefined : args.editEpoch,
|
|
131
141
|
data: args.data,
|
|
132
142
|
};
|
|
133
143
|
}
|
|
@@ -141,6 +151,13 @@ export class Reticle {
|
|
|
141
151
|
#teardowns = [];
|
|
142
152
|
#connected = false;
|
|
143
153
|
#session = 'default';
|
|
154
|
+
/**
|
|
155
|
+
* Minted once, here, because this class is constructed once per document: a full navigation tears
|
|
156
|
+
* down the JavaScript context and builds a new one, so the id dies with the document it names. An
|
|
157
|
+
* SPA route change does not reconstruct it, which is correct — same context, same in-flight
|
|
158
|
+
* requests, same evidence.
|
|
159
|
+
*/
|
|
160
|
+
#documentId = newDocumentId(Math.random);
|
|
144
161
|
#start = 0;
|
|
145
162
|
#overlay;
|
|
146
163
|
#presenter;
|
|
@@ -321,6 +338,18 @@ export class Reticle {
|
|
|
321
338
|
endSession() {
|
|
322
339
|
this.#presenter?.setState(SessionState.ENDED);
|
|
323
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Hand the SDK the page's hot-update channel, so a stale ref can say the code changed underneath it.
|
|
343
|
+
*
|
|
344
|
+
* `unknown` on purpose: the only caller that has one is the build integration, the shape it passes
|
|
345
|
+
* is Vite's `import.meta.hot`, and the SDK must not depend on Vite — it ships to Next, Electron,
|
|
346
|
+
* Tauri and plain pages. Anything that is not a subscribable channel is ignored, which is the
|
|
347
|
+
* normal case: with no channel the epoch stays at `NO_EDITS_OBSERVED`, meaning "no edits OBSERVED",
|
|
348
|
+
* never "no edits happened".
|
|
349
|
+
*/
|
|
350
|
+
observeHotUpdates(hot) {
|
|
351
|
+
editEpoch.observe(hot);
|
|
352
|
+
}
|
|
324
353
|
disconnect() {
|
|
325
354
|
if (!this.#connected)
|
|
326
355
|
return;
|
|
@@ -347,6 +376,8 @@ export class Reticle {
|
|
|
347
376
|
t: Math.round(performance.now() - this.#start),
|
|
348
377
|
type,
|
|
349
378
|
sessionId: this.#session,
|
|
379
|
+
documentId: this.#documentId,
|
|
380
|
+
editEpoch: editEpoch.current,
|
|
350
381
|
data,
|
|
351
382
|
ref,
|
|
352
383
|
});
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* than a template inlined in a warn call. The rule it exists to hold: report what this page observed
|
|
6
6
|
* and what it could not determine, never a cause. See unreachable-message.test.ts.
|
|
7
7
|
*/
|
|
8
|
+
import { UNREACHABLE_NOTICE_PREFIX } from '@reticlehq/core';
|
|
8
9
|
/**
|
|
9
10
|
* The page tried, failed, and cannot see why. Names the address and the attempt count, refuses to
|
|
10
11
|
* diagnose the daemon, and lists the checks in the order that actually resolves the ambiguity.
|
|
11
12
|
*/
|
|
12
13
|
export function unreachableMessage(url, attempts) {
|
|
13
|
-
return (
|
|
14
|
+
return (`${UNREACHABLE_NOTICE_PREFIX}${url}. ${String(attempts)} attempts, all ` +
|
|
14
15
|
`failed. That is everything the page can see: from inside the browser it cannot tell a daemon ` +
|
|
15
16
|
`that is not there from one it is not allowed to reach, so this is not evidence about the ` +
|
|
16
17
|
`daemon. What answers it: run \`npx @reticlehq/server status\` to see whether one is listening ` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/browser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Reticle browser SDK — installs observers, builds semantic snapshots, executes actions, talks to the bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,21 +13,18 @@
|
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@
|
|
17
|
-
"@reticlehq/core": "2.11.0"
|
|
16
|
+
"@reticlehq/core": "2.12.0"
|
|
18
17
|
},
|
|
19
18
|
"devDependencies": {
|
|
20
19
|
"@iconify-json/heroicons-outline": "^1.2.1",
|
|
21
20
|
"@iconify-json/heroicons-solid": "^1.2.1",
|
|
22
21
|
"@tanstack/query-core": "^5.101.4",
|
|
23
|
-
"@types/jsdom": "^30.0.0",
|
|
24
22
|
"jotai": "^2.20.2",
|
|
25
23
|
"jsdom": "^30.0.1",
|
|
26
24
|
"mobx": "^7.0.0",
|
|
27
25
|
"pinia": "^4.0.3",
|
|
28
26
|
"react": "^19.2.8",
|
|
29
27
|
"recoil": "^0.7.7",
|
|
30
|
-
"sharp": "^0.35.3",
|
|
31
28
|
"svelte": "^5.56.8",
|
|
32
29
|
"valtio": "^2.3.2",
|
|
33
30
|
"vue": "^3.5.41",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Blind-spot detection, the detectable half). Cross-origin iframes are a real observation gap — the
|
|
3
|
-
* SDK cannot see inside them (same-origin policy). We COUNT them so a result can say "coverage: partial —
|
|
4
|
-
* 2 cross-origin frames unobserved" instead of implying it saw the whole page. Closed shadow roots are a
|
|
5
|
-
* blind spot too, but undetectable by design (element.shadowRoot === null), so they are not counted here.
|
|
6
|
-
*/
|
|
7
|
-
/** Pure: how many frames are cross-origin, given each frame's same-origin flag + whether it has a src. */
|
|
8
|
-
export declare function classifyCrossOriginFrames(frames: ReadonlyArray<{
|
|
9
|
-
sameOrigin: boolean;
|
|
10
|
-
hasSrc: boolean;
|
|
11
|
-
}>): number;
|
|
12
|
-
/** Probe the live document: count iframes whose document is unreachable (cross-origin). */
|
|
13
|
-
export declare function countCrossOriginFrames(): number;
|
package/dist/dom/blind-spots.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Blind-spot detection, the detectable half). Cross-origin iframes are a real observation gap — the
|
|
3
|
-
* SDK cannot see inside them (same-origin policy). We COUNT them so a result can say "coverage: partial —
|
|
4
|
-
* 2 cross-origin frames unobserved" instead of implying it saw the whole page. Closed shadow roots are a
|
|
5
|
-
* blind spot too, but undetectable by design (element.shadowRoot === null), so they are not counted here.
|
|
6
|
-
*/
|
|
7
|
-
/** Pure: how many frames are cross-origin, given each frame's same-origin flag + whether it has a src. */
|
|
8
|
-
export function classifyCrossOriginFrames(frames) {
|
|
9
|
-
return frames.filter((f) => f.hasSrc && !f.sameOrigin).length;
|
|
10
|
-
}
|
|
11
|
-
/** Probe the live document: count iframes whose document is unreachable (cross-origin). */
|
|
12
|
-
export function countCrossOriginFrames() {
|
|
13
|
-
if ('undefined' === typeof document)
|
|
14
|
-
return 0;
|
|
15
|
-
const frames = [];
|
|
16
|
-
for (const frame of document.querySelectorAll('iframe')) {
|
|
17
|
-
const hasSrc = frame.getAttribute('src') !== null && frame.getAttribute('src') !== '';
|
|
18
|
-
let sameOrigin;
|
|
19
|
-
try {
|
|
20
|
-
// Reaching contentDocument throws (or yields null) for a cross-origin frame.
|
|
21
|
-
sameOrigin = frame.contentDocument !== null;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
sameOrigin = false;
|
|
25
|
-
}
|
|
26
|
-
frames.push({ sameOrigin, hasSrc });
|
|
27
|
-
}
|
|
28
|
-
return classifyCrossOriginFrames(frames);
|
|
29
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export interface SubresourceObservation {
|
|
2
|
-
url: string;
|
|
3
|
-
method: string;
|
|
4
|
-
status?: number;
|
|
5
|
-
ok?: boolean;
|
|
6
|
-
durationMs: number;
|
|
7
|
-
transferSize?: number;
|
|
8
|
-
initiatorType: string;
|
|
9
|
-
}
|
|
10
|
-
export declare function collectSubresources(seenUrls: ReadonlySet<string>, nowMs: number, sinceMs: number): SubresourceObservation[];
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { redactUrl } from './network-redact.js';
|
|
2
|
-
/**
|
|
3
|
-
* Document-initiated subresource observation.
|
|
4
|
-
*
|
|
5
|
-
* fetch/XHR patches never see requests the DOCUMENT makes on its own: `<link rel=icon>`,
|
|
6
|
-
* `<link rel=manifest>`, stylesheets, `<img src>`, `<script src>`. Those are exactly the loads a
|
|
7
|
-
* branding change touches, so a `{net}` predicate over them used to return `assertion_failed`
|
|
8
|
-
* ("your change is broken") when the truthful answer was "not observable" — and an agent trusting
|
|
9
|
-
* that verdict goes and "fixes" working code.
|
|
10
|
-
*
|
|
11
|
-
* `performance.getEntriesByType('resource')` reports these with no CDP and no patching: URL (name),
|
|
12
|
-
* initiator type (`link`, `css`, `img`, `script`, …), duration, transfer size. The known limitation
|
|
13
|
-
* is the status code — resource timing does not expose one directly (`responseStatus` exists only
|
|
14
|
-
* in newer Chromium). Entries carry `status` ONLY when it could be read; predicates asserting a
|
|
15
|
-
* status over unreadable data are handled at the evaluation seam, which downgrades rather than
|
|
16
|
-
* guesses.
|
|
17
|
-
*
|
|
18
|
-
* Deduplication matters: a fetch/XHR-initiated request ALSO lands in resource timing. Every entry
|
|
19
|
-
* is checked against the URLs the patched transports already reported this page, and only genuinely
|
|
20
|
-
* document-initiated loads survive.
|
|
21
|
-
*/
|
|
22
|
-
/** Resource-timing initiator types that mean "the document itself asked", not a patched transport. */
|
|
23
|
-
const DOCUMENT_INITIATORS = new Set(['link', 'css', 'img', 'script', 'manifest', 'other']);
|
|
24
|
-
/** Cap per call — a pathological page must not blow the event budget. */
|
|
25
|
-
const MAX_SUBRESOURCE_EVENTS = 200;
|
|
26
|
-
export function collectSubresources(seenUrls, nowMs, sinceMs) {
|
|
27
|
-
const out = [];
|
|
28
|
-
try {
|
|
29
|
-
const entries = performance.getEntriesByType('resource');
|
|
30
|
-
for (const entry of entries) {
|
|
31
|
-
const initiatorType = entry.initiatorType || 'other';
|
|
32
|
-
// fetch/XHR already have richer records from their own patches.
|
|
33
|
-
if (!DOCUMENT_INITIATORS.has(initiatorType))
|
|
34
|
-
continue;
|
|
35
|
-
const raw = entry.name;
|
|
36
|
-
if (seenUrls.has(raw))
|
|
37
|
-
continue;
|
|
38
|
-
const startMs = nowMs - entry.duration;
|
|
39
|
-
if (startMs < sinceMs)
|
|
40
|
-
continue;
|
|
41
|
-
const obs = {
|
|
42
|
-
url: redactUrl(raw),
|
|
43
|
-
method: 'GET',
|
|
44
|
-
durationMs: Math.round(entry.duration),
|
|
45
|
-
initiatorType,
|
|
46
|
-
...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
|
|
47
|
-
};
|
|
48
|
-
// responseStatus is Chromium-only and may be 0 when unreadable. Present ONLY when it says
|
|
49
|
-
// something real, so the wire never carries a guessed status.
|
|
50
|
-
const rs = entry.responseStatus;
|
|
51
|
-
if ('number' === typeof rs && rs > 0) {
|
|
52
|
-
obs.status = rs;
|
|
53
|
-
obs.ok = rs >= 200 && rs < 400;
|
|
54
|
-
}
|
|
55
|
-
out.push(obs);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// No resource-timing support / security error: report nothing, honestly.
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
return out.slice(0, MAX_SUBRESOURCE_EVENTS);
|
|
63
|
-
}
|