@jobshimo/browser-link 0.35.0 → 0.36.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/agent-instructions/content.js +10 -0
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/cdp/inpage/builders.d.ts +52 -3
- package/dist/cdp/inpage/builders.js +176 -35
- package/dist/cdp/inpage/builders.js.map +1 -1
- package/dist/cdp/inpage/observer.d.ts +21 -1
- package/dist/cdp/inpage/observer.js +106 -2
- package/dist/cdp/inpage/observer.js.map +1 -1
- package/dist/cdp/transport.js +1 -0
- package/dist/cdp/transport.js.map +1 -1
- package/dist/extension/background.js +1 -0
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/inpage/builders.d.ts +52 -3
- package/dist/extension/inpage/builders.js +176 -35
- package/dist/extension/inpage/builders.js.map +1 -1
- package/dist/extension/inpage/observer.d.ts +21 -1
- package/dist/extension/inpage/observer.js +106 -2
- package/dist/extension/inpage/observer.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/tools/browser-definitions.js +13 -3
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.js +14 -2
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.js +10 -0
- package/dist/tools/server-instructions.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,52 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { DEEP_QUERY_JS } from './deep-query.js';
|
|
10
10
|
import { DOM_HELPERS_JS } from './dom-helpers.js';
|
|
11
|
+
import { OBSERVER_GLOBAL } from './observer.js';
|
|
12
|
+
/**
|
|
13
|
+
* In-page access to the page observer's element identity (spec v4 §5.6),
|
|
14
|
+
* injected verbatim into `buildSnapshotJs`, `buildFindJs` and `buildStateJs`.
|
|
15
|
+
* The observer global is looked up ONCE per evaluation and used only when it
|
|
16
|
+
* is present, not torn down, and exposes `refFor`:
|
|
17
|
+
* - `refOf(el)` returns the observer's `e<N>` ref for `el`, or `null`;
|
|
18
|
+
* - `withDocId(result)` adds `doc_id` to `result` and returns it;
|
|
19
|
+
* - `snapshotStore()` returns the observer when it keeps snapshot records
|
|
20
|
+
* (`recordSnapshot` / `getSnapshot`, used by `snapshot({ since })`), else
|
|
21
|
+
* `null`.
|
|
22
|
+
* Without the observer (injection failed, pre-observer document) all are
|
|
23
|
+
* no-ops, so every result is byte-identical to the pre-ref output.
|
|
24
|
+
*/
|
|
25
|
+
export const PAGE_OBSERVER_JS = `
|
|
26
|
+
const pageObserver = (() => {
|
|
27
|
+
try {
|
|
28
|
+
const g = window[${JSON.stringify(OBSERVER_GLOBAL)}];
|
|
29
|
+
if (g && !g.dead && typeof g.refFor === 'function') return g;
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
return null;
|
|
32
|
+
})();
|
|
33
|
+
function refOf(el) {
|
|
34
|
+
if (!pageObserver) return null;
|
|
35
|
+
try {
|
|
36
|
+
const ref = pageObserver.refFor(el);
|
|
37
|
+
return typeof ref === 'string' ? ref : null;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function withDocId(result) {
|
|
43
|
+
if (pageObserver && typeof pageObserver.doc_id === 'string') result.doc_id = pageObserver.doc_id;
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function snapshotStore() {
|
|
47
|
+
if (
|
|
48
|
+
pageObserver &&
|
|
49
|
+
typeof pageObserver.recordSnapshot === 'function' &&
|
|
50
|
+
typeof pageObserver.getSnapshot === 'function'
|
|
51
|
+
) {
|
|
52
|
+
return pageObserver;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
11
57
|
export function buildSnapshotJs(opts = {}) {
|
|
12
58
|
const optsJson = JSON.stringify({
|
|
13
59
|
withinSelector: typeof opts.within_selector === 'string' ? opts.within_selector : null,
|
|
@@ -16,23 +62,25 @@ export function buildSnapshotJs(opts = {}) {
|
|
|
16
62
|
maxInteractive: typeof opts.max_interactive === 'number' && opts.max_interactive > 0
|
|
17
63
|
? Math.min(opts.max_interactive, 500)
|
|
18
64
|
: 120,
|
|
65
|
+
since: typeof opts.since === 'string' ? opts.since : null,
|
|
19
66
|
});
|
|
20
67
|
return `
|
|
21
68
|
(() => {
|
|
22
69
|
${DEEP_QUERY_JS}
|
|
23
70
|
${DOM_HELPERS_JS}
|
|
71
|
+
${PAGE_OBSERVER_JS}
|
|
24
72
|
const opts = ${optsJson};
|
|
25
73
|
let root = document;
|
|
26
74
|
let notice = '';
|
|
27
75
|
if (opts.withinSelector) {
|
|
28
76
|
const sub = deepQueryFirst(opts.withinSelector);
|
|
29
77
|
if (!sub) {
|
|
30
|
-
return {
|
|
78
|
+
return withDocId({
|
|
31
79
|
title: document.title,
|
|
32
80
|
url: location.href,
|
|
33
81
|
interactive: [],
|
|
34
82
|
notice: 'within_selector did not match any element',
|
|
35
|
-
};
|
|
83
|
+
});
|
|
36
84
|
}
|
|
37
85
|
root = sub;
|
|
38
86
|
}
|
|
@@ -75,6 +123,8 @@ export function buildSnapshotJs(opts = {}) {
|
|
|
75
123
|
const role = el.getAttribute('role') || tag;
|
|
76
124
|
const selInfo = genSelectorInfo(el);
|
|
77
125
|
const entry = { tag, role, selector: selInfo.selector };
|
|
126
|
+
const ref = refOf(el);
|
|
127
|
+
if (ref) entry.ref = ref;
|
|
78
128
|
if (selInfo.ambiguous) entry.ambiguous = true;
|
|
79
129
|
const txt = shortText(el);
|
|
80
130
|
if (txt) entry.text = txt;
|
|
@@ -94,26 +144,105 @@ export function buildSnapshotJs(opts = {}) {
|
|
|
94
144
|
if (frame) entry.frame = frame;
|
|
95
145
|
interactive.push(entry);
|
|
96
146
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
url: location.href,
|
|
100
|
-
interactive: interactive.slice(0, opts.maxInteractive),
|
|
101
|
-
};
|
|
147
|
+
let headings = null;
|
|
148
|
+
let visibleText = '';
|
|
102
149
|
if (!opts.onlyInteractive) {
|
|
103
|
-
const
|
|
150
|
+
const found = [];
|
|
104
151
|
deepQueryAll('h1, h2, h3', root).forEach((h) => {
|
|
105
152
|
if (!isVisible(h)) return;
|
|
106
153
|
if (inExcludedLandmark(h)) return;
|
|
107
154
|
const t = shortText(h);
|
|
108
|
-
if (t)
|
|
155
|
+
if (t) found.push({ level: h.tagName, text: t });
|
|
109
156
|
});
|
|
110
|
-
|
|
157
|
+
headings = found.slice(0, 30);
|
|
111
158
|
const textRoot = (root === document) ? (document.body || document) : root;
|
|
112
|
-
|
|
159
|
+
visibleText = (textRoot && textRoot.innerText) ? textRoot.innerText.slice(0, 4000) : '';
|
|
160
|
+
}
|
|
161
|
+
let snapshotId = null;
|
|
162
|
+
if (withDocId({}).doc_id && typeof pageObserver.nextSnapshotId === 'function') {
|
|
163
|
+
const issued = pageObserver.nextSnapshotId();
|
|
164
|
+
if (typeof issued === 'string') snapshotId = issued;
|
|
165
|
+
}
|
|
166
|
+
function fullResult() {
|
|
167
|
+
const result = {
|
|
168
|
+
title: document.title,
|
|
169
|
+
url: location.href,
|
|
170
|
+
interactive: interactive.slice(0, opts.maxInteractive),
|
|
171
|
+
};
|
|
172
|
+
if (headings) result.headings = headings;
|
|
113
173
|
if (visibleText) result.text = visibleText;
|
|
174
|
+
if (notice) result.notice = notice;
|
|
175
|
+
withDocId(result);
|
|
176
|
+
if (snapshotId) result.snapshot_id = snapshotId;
|
|
177
|
+
return result;
|
|
114
178
|
}
|
|
115
|
-
|
|
116
|
-
|
|
179
|
+
// Record every snapshot (with or without since) so a later call can diff
|
|
180
|
+
// against it. Entries are keyed by ref; the hash is the entry serialised
|
|
181
|
+
// without its ref, in emission order. Capped at the first 500 entries.
|
|
182
|
+
const store = snapshotId ? snapshotStore() : null;
|
|
183
|
+
// A snapshot_id is \`<doc_id>:s<N>\`: a since whose prefix (before the LAST
|
|
184
|
+
// ':') is not this document's doc_id, or that has no ':', came from another
|
|
185
|
+
// document and is never looked up.
|
|
186
|
+
const sinceCut = opts.since === null ? -1 : opts.since.lastIndexOf(':');
|
|
187
|
+
const sinceSameDoc =
|
|
188
|
+
sinceCut >= 0 && withDocId({}).doc_id === opts.since.slice(0, sinceCut);
|
|
189
|
+
const previous = store && sinceSameDoc ? store.getSnapshot(opts.since) : null;
|
|
190
|
+
const current = new Map();
|
|
191
|
+
const headingsHash = headings ? JSON.stringify(headings) : null;
|
|
192
|
+
const textHash = opts.onlyInteractive ? null : visibleText;
|
|
193
|
+
if (store) {
|
|
194
|
+
for (const entry of interactive) {
|
|
195
|
+
if (current.size >= 500) break;
|
|
196
|
+
if (typeof entry.ref !== 'string') continue;
|
|
197
|
+
const copy = {};
|
|
198
|
+
for (const key of Object.keys(entry)) if (key !== 'ref') copy[key] = entry[key];
|
|
199
|
+
current.set(entry.ref, { hash: JSON.stringify(copy), entry });
|
|
200
|
+
}
|
|
201
|
+
store.recordSnapshot(snapshotId, { entries: current, headings_hash: headingsHash, text_hash: textHash });
|
|
202
|
+
}
|
|
203
|
+
if (opts.since === null) return fullResult();
|
|
204
|
+
if (!store) {
|
|
205
|
+
const result = fullResult();
|
|
206
|
+
result.since = opts.since;
|
|
207
|
+
result.since_status = 'observer-unavailable';
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
if (!previous || !previous.entries || typeof previous.entries.get !== 'function') {
|
|
211
|
+
const result = fullResult();
|
|
212
|
+
result.since = opts.since;
|
|
213
|
+
result.since_status = sinceSameDoc ? 'evicted' : 'doc-changed';
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
const added = [];
|
|
217
|
+
const changed = [];
|
|
218
|
+
const removed = [];
|
|
219
|
+
current.forEach((item, ref) => {
|
|
220
|
+
const old = previous.entries.get(ref);
|
|
221
|
+
if (!old) added.push(item.entry);
|
|
222
|
+
else if (old.hash !== item.hash) changed.push(item.entry);
|
|
223
|
+
});
|
|
224
|
+
previous.entries.forEach((item, ref) => {
|
|
225
|
+
if (current.has(ref) || removed.length >= 500) return;
|
|
226
|
+
const gone = { ref, tag: item.entry.tag };
|
|
227
|
+
if (item.entry.text) gone.text = item.entry.text;
|
|
228
|
+
removed.push(gone);
|
|
229
|
+
});
|
|
230
|
+
// max_interactive applies after diffing (added first, then changed), so
|
|
231
|
+
// a window shift never masquerades as change.
|
|
232
|
+
const listed = added.concat(changed);
|
|
233
|
+
const kept = listed.slice(0, opts.maxInteractive);
|
|
234
|
+
const diff = withDocId({ title: document.title, url: location.href });
|
|
235
|
+
diff.snapshot_id = snapshotId;
|
|
236
|
+
diff.since = opts.since;
|
|
237
|
+
diff.added = kept.slice(0, Math.min(added.length, kept.length));
|
|
238
|
+
diff.removed = removed;
|
|
239
|
+
diff.changed = kept.slice(diff.added.length);
|
|
240
|
+
if (listed.length > kept.length) diff.overflow = listed.length - kept.length;
|
|
241
|
+
if (!opts.onlyInteractive) {
|
|
242
|
+
if (headingsHash !== previous.headings_hash) diff.headings = headings;
|
|
243
|
+
if (textHash !== previous.text_hash) diff.text = visibleText;
|
|
244
|
+
}
|
|
245
|
+
return diff;
|
|
117
246
|
})()
|
|
118
247
|
`;
|
|
119
248
|
}
|
|
@@ -129,6 +258,7 @@ export function buildFindJs(opts) {
|
|
|
129
258
|
(() => {
|
|
130
259
|
${DEEP_QUERY_JS}
|
|
131
260
|
${DOM_HELPERS_JS}
|
|
261
|
+
${PAGE_OBSERVER_JS}
|
|
132
262
|
const opts = ${optsJson};
|
|
133
263
|
const needle = opts.text.toLowerCase();
|
|
134
264
|
const needleTokens = needle.split(/[^a-z0-9]+/).filter(Boolean);
|
|
@@ -189,6 +319,8 @@ export function buildFindJs(opts) {
|
|
|
189
319
|
}
|
|
190
320
|
function toNearMiss(el) {
|
|
191
321
|
const entry = { text: snippetOf(el), selector: genSelector(el) };
|
|
322
|
+
const ref = refOf(el);
|
|
323
|
+
if (ref) entry.ref = ref;
|
|
192
324
|
const roleAttr = el.getAttribute('role');
|
|
193
325
|
if (roleAttr) entry.role = roleAttr;
|
|
194
326
|
return entry;
|
|
@@ -221,44 +353,46 @@ export function buildFindJs(opts) {
|
|
|
221
353
|
' but none with role "' + opts.role + '"' +
|
|
222
354
|
(closest ? ' — closest: ' + describeForError(closest) + ' "' + snippetOf(closest) + '"' : '');
|
|
223
355
|
result.near_misses = ranked.map(toNearMiss);
|
|
224
|
-
return result;
|
|
356
|
+
return withDocId(result);
|
|
225
357
|
}
|
|
226
358
|
const ranked = rankCandidates(broadAll);
|
|
227
359
|
if (ranked.length > 0) result.near_misses = ranked.map(toNearMiss);
|
|
228
|
-
return result;
|
|
360
|
+
return withDocId(result);
|
|
229
361
|
}
|
|
230
362
|
const ranked = rankCandidates(all);
|
|
231
363
|
if (ranked.length > 0) result.near_misses = ranked.map(toNearMiss);
|
|
232
|
-
return result;
|
|
364
|
+
return withDocId(result);
|
|
233
365
|
}
|
|
234
366
|
if (matches.length > 1) {
|
|
235
|
-
return {
|
|
367
|
+
return withDocId({
|
|
236
368
|
matched: false,
|
|
237
369
|
reason: 'multiple-matches',
|
|
238
|
-
candidates: matches.slice(0, opts.candidateLimit).map((el) =>
|
|
239
|
-
selector: genSelector(el)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
370
|
+
candidates: matches.slice(0, opts.candidateLimit).map((el) => {
|
|
371
|
+
const candidate = { selector: genSelector(el) };
|
|
372
|
+
const ref = refOf(el);
|
|
373
|
+
if (ref) candidate.ref = ref;
|
|
374
|
+
candidate.text = shortText(el);
|
|
375
|
+
candidate.tag = el.tagName.toLowerCase();
|
|
376
|
+
return candidate;
|
|
377
|
+
}),
|
|
378
|
+
});
|
|
244
379
|
}
|
|
245
380
|
const el = matches[0];
|
|
246
381
|
const center = viewportCenterOf(el);
|
|
247
382
|
const selInfo = genSelectorInfo(el);
|
|
248
|
-
const result = {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
tag: el.tagName.toLowerCase(),
|
|
256
|
-
text: shortText(el),
|
|
383
|
+
const result = { matched: true, selector: selInfo.selector };
|
|
384
|
+
const ref = refOf(el);
|
|
385
|
+
if (ref) result.ref = ref;
|
|
386
|
+
result.coords = {
|
|
387
|
+
x: Math.round(center.x),
|
|
388
|
+
y: Math.round(center.y),
|
|
257
389
|
};
|
|
390
|
+
result.tag = el.tagName.toLowerCase();
|
|
391
|
+
result.text = shortText(el);
|
|
258
392
|
if (selInfo.ambiguous) result.ambiguous = true;
|
|
259
393
|
const frame = frameSelectorFor(el);
|
|
260
394
|
if (frame) result.frame = frame;
|
|
261
|
-
return result;
|
|
395
|
+
return withDocId(result);
|
|
262
396
|
})()
|
|
263
397
|
`;
|
|
264
398
|
}
|
|
@@ -623,12 +757,16 @@ export function buildExpectProbeJs(opts) {
|
|
|
623
757
|
* `focused` beyond `<body>`, no `dialogs` when none are open, and no
|
|
624
758
|
* `scroll` when the page is at its default (0,0) position — token-lean by
|
|
625
759
|
* construction, matching the omit-falsy convention the other builders use.
|
|
760
|
+
*
|
|
761
|
+
* With the page observer installed (see `PAGE_OBSERVER_JS`), `focused`
|
|
762
|
+
* carries `ref` right after `selector` and the result carries `doc_id`.
|
|
626
763
|
*/
|
|
627
764
|
export function buildStateJs() {
|
|
628
765
|
return `
|
|
629
766
|
(() => {
|
|
630
767
|
${DEEP_QUERY_JS}
|
|
631
768
|
${DOM_HELPERS_JS}
|
|
769
|
+
${PAGE_OBSERVER_JS}
|
|
632
770
|
function deepActiveElement() {
|
|
633
771
|
let el = document.activeElement;
|
|
634
772
|
let guard = 0;
|
|
@@ -689,7 +827,10 @@ export function buildStateJs() {
|
|
|
689
827
|
const active = deepActiveElement();
|
|
690
828
|
if (active && active !== document.body && active.tagName) {
|
|
691
829
|
const selInfo = genSelectorInfo(active);
|
|
692
|
-
const focused = { selector: selInfo.selector
|
|
830
|
+
const focused = { selector: selInfo.selector };
|
|
831
|
+
const ref = refOf(active);
|
|
832
|
+
if (ref) focused.ref = ref;
|
|
833
|
+
focused.tag = active.tagName.toLowerCase();
|
|
693
834
|
if (selInfo.ambiguous) focused.ambiguous = true;
|
|
694
835
|
result.focused = focused;
|
|
695
836
|
}
|
|
@@ -709,7 +850,7 @@ export function buildStateJs() {
|
|
|
709
850
|
const scrollY = Math.round(window.scrollY);
|
|
710
851
|
if (scrollX !== 0 || scrollY !== 0) result.scroll = { x: scrollX, y: scrollY };
|
|
711
852
|
|
|
712
|
-
return result;
|
|
853
|
+
return withDocId(result);
|
|
713
854
|
})()
|
|
714
855
|
`;
|
|
715
856
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builders.js","sourceRoot":"","sources":["../../src/inpage/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"builders.js","sourceRoot":"","sources":["../../src/inpage/builders.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* `cdp/builders-parity.test.ts` checks the built output.
|
|
23
23
|
*/
|
|
24
24
|
/** Bump whenever the in-page protocol (global shape, messages, methods) changes. */
|
|
25
|
-
export declare const OBSERVER_VERSION =
|
|
25
|
+
export declare const OBSERVER_VERSION = 2;
|
|
26
26
|
/** Name of the `Runtime.addBinding` function the observer pushes messages through. */
|
|
27
27
|
export declare const OBSERVER_BINDING = "__blPageObserverPush";
|
|
28
28
|
/** Well-known global that holds the installed observer instance. */
|
|
@@ -42,6 +42,19 @@ export interface ObserverInstallOpts {
|
|
|
42
42
|
* `Page.addScriptToEvaluateOnNewDocument` source and as a one-off
|
|
43
43
|
* `Runtime.evaluate` expression, which evaluates to
|
|
44
44
|
* `{ ok: true, installed, version, doc_id, coverage }`.
|
|
45
|
+
*
|
|
46
|
+
* The instance also owns element identity (spec v4 §5.6): `refFor(el)` issues
|
|
47
|
+
* `e<N>` (monotonic per document instance, from `e1`) the first time an
|
|
48
|
+
* element is seen and reuses it while the node lives; `resolveRef(ref)`
|
|
49
|
+
* returns the element while it is connected, else `null`. Refs are only valid
|
|
50
|
+
* for the `doc_id` that issued them. Neither method throws. `nextSnapshotId()`
|
|
51
|
+
* issues `<doc_id>:s<N>` (N monotonic per document instance, from 1) for each
|
|
52
|
+
* `snapshot` result and returns `null` once the instance is torn down; the
|
|
53
|
+
* `doc_id` prefix lets a stale id from another document be rejected.
|
|
54
|
+
* `recordSnapshot(id, record)` keeps the last 4 snapshot records per document
|
|
55
|
+
* instance (oldest evicted first) for `snapshot({ since })`; `getSnapshot(id)`
|
|
56
|
+
* returns the record or `null` when unknown or evicted. The record is opaque
|
|
57
|
+
* here. Both are inert after teardown, which clears the store.
|
|
45
58
|
*/
|
|
46
59
|
export declare function buildObserverInstallJs(opts?: ObserverInstallOpts): string;
|
|
47
60
|
/**
|
|
@@ -60,6 +73,13 @@ export declare function buildObserverMarkJs(): string;
|
|
|
60
73
|
* baseline; or `{ ok: false, reason: 'observer-unavailable' | 'doc-changed' }`.
|
|
61
74
|
*/
|
|
62
75
|
export declare function buildObserverChangesJs(mark: number, docId?: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Build the ref-resolve expression, for tests and debugging. Evaluates to
|
|
78
|
+
* `{ ok: true, found: true, tag }` when `ref` names an element of the current
|
|
79
|
+
* document that is still connected, `{ ok: true, found: false }` when it is
|
|
80
|
+
* unknown or stale, or `{ ok: false, reason: 'observer-unavailable' }`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildObserverRefResolveJs(ref: string): string;
|
|
63
83
|
/**
|
|
64
84
|
* Build the teardown expression: disconnects every observer, restores the
|
|
65
85
|
* `attachShadow` and `history` patches when they are still the observer's,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* `cdp/builders-parity.test.ts` checks the built output.
|
|
23
23
|
*/
|
|
24
24
|
/** Bump whenever the in-page protocol (global shape, messages, methods) changes. */
|
|
25
|
-
export const OBSERVER_VERSION =
|
|
25
|
+
export const OBSERVER_VERSION = 2;
|
|
26
26
|
/** Name of the `Runtime.addBinding` function the observer pushes messages through. */
|
|
27
27
|
export const OBSERVER_BINDING = '__blPageObserverPush';
|
|
28
28
|
/** Well-known global that holds the installed observer instance. */
|
|
@@ -41,6 +41,19 @@ function clampInt(value, fallback, min, max) {
|
|
|
41
41
|
* `Page.addScriptToEvaluateOnNewDocument` source and as a one-off
|
|
42
42
|
* `Runtime.evaluate` expression, which evaluates to
|
|
43
43
|
* `{ ok: true, installed, version, doc_id, coverage }`.
|
|
44
|
+
*
|
|
45
|
+
* The instance also owns element identity (spec v4 §5.6): `refFor(el)` issues
|
|
46
|
+
* `e<N>` (monotonic per document instance, from `e1`) the first time an
|
|
47
|
+
* element is seen and reuses it while the node lives; `resolveRef(ref)`
|
|
48
|
+
* returns the element while it is connected, else `null`. Refs are only valid
|
|
49
|
+
* for the `doc_id` that issued them. Neither method throws. `nextSnapshotId()`
|
|
50
|
+
* issues `<doc_id>:s<N>` (N monotonic per document instance, from 1) for each
|
|
51
|
+
* `snapshot` result and returns `null` once the instance is torn down; the
|
|
52
|
+
* `doc_id` prefix lets a stale id from another document be rejected.
|
|
53
|
+
* `recordSnapshot(id, record)` keeps the last 4 snapshot records per document
|
|
54
|
+
* instance (oldest evicted first) for `snapshot({ since })`; `getSnapshot(id)`
|
|
55
|
+
* returns the record or `null` when unknown or evicted. The record is opaque
|
|
56
|
+
* here. Both are inert after teardown, which clears the store.
|
|
44
57
|
*/
|
|
45
58
|
export function buildObserverInstallJs(opts = {}) {
|
|
46
59
|
const optsJson = JSON.stringify({
|
|
@@ -82,6 +95,18 @@ export function buildObserverInstallJs(opts = {}) {
|
|
|
82
95
|
};
|
|
83
96
|
const observers = [];
|
|
84
97
|
const observed = new WeakSet();
|
|
98
|
+
let refs = new WeakMap();
|
|
99
|
+
const refNodes = new Map();
|
|
100
|
+
let refSeq = 0;
|
|
101
|
+
let snapshotSeq = 0;
|
|
102
|
+
// Last SNAPSHOT_KEEP snapshot records (opaque to the observer; the snapshot
|
|
103
|
+
// builder owns their shape), keyed by snapshot_id in insertion order.
|
|
104
|
+
const SNAPSHOT_KEEP = 4;
|
|
105
|
+
const snapshots = new Map();
|
|
106
|
+
// WeakRef keeps the registry from pinning removed nodes; the fallback only
|
|
107
|
+
// matters on engines without it (Chrome has it since 84).
|
|
108
|
+
const holdWeakly =
|
|
109
|
+
typeof WeakRef === 'function' ? (el) => new WeakRef(el) : (el) => ({ deref: () => el });
|
|
85
110
|
const round = (n) => Math.round(n * 10) / 10;
|
|
86
111
|
|
|
87
112
|
function push(message) {
|
|
@@ -268,6 +293,53 @@ export function buildObserverInstallJs(opts = {}) {
|
|
|
268
293
|
|
|
269
294
|
g.flush = flush;
|
|
270
295
|
|
|
296
|
+
g.refFor = function (el) {
|
|
297
|
+
try {
|
|
298
|
+
if (g.dead || !el || el.nodeType !== 1) return null;
|
|
299
|
+
let ref = refs.get(el);
|
|
300
|
+
if (!ref) {
|
|
301
|
+
ref = 'e' + ++refSeq;
|
|
302
|
+
refs.set(el, ref);
|
|
303
|
+
}
|
|
304
|
+
// Same node, same ref: re-register one pruned while it was detached.
|
|
305
|
+
if (!refNodes.has(ref)) refNodes.set(ref, holdWeakly(el));
|
|
306
|
+
return ref;
|
|
307
|
+
} catch (e) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
g.resolveRef = function (ref) {
|
|
313
|
+
try {
|
|
314
|
+
if (typeof ref !== 'string') return null;
|
|
315
|
+
const held = refNodes.get(ref);
|
|
316
|
+
if (!held) return null;
|
|
317
|
+
const el = held.deref();
|
|
318
|
+
if (el && el.isConnected) return el;
|
|
319
|
+
refNodes.delete(ref);
|
|
320
|
+
return null;
|
|
321
|
+
} catch (e) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
g.nextSnapshotId = function () {
|
|
327
|
+
if (g.dead) return null;
|
|
328
|
+
return g.doc_id + ':s' + ++snapshotSeq;
|
|
329
|
+
};
|
|
330
|
+
g.recordSnapshot = function (id, record) {
|
|
331
|
+
if (g.dead || typeof id !== 'string') return;
|
|
332
|
+
snapshots.delete(id);
|
|
333
|
+
snapshots.set(id, record);
|
|
334
|
+
while (snapshots.size > SNAPSHOT_KEEP) snapshots.delete(snapshots.keys().next().value);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
g.getSnapshot = function (id) {
|
|
338
|
+
if (g.dead || typeof id !== 'string') return null;
|
|
339
|
+
const record = snapshots.get(id);
|
|
340
|
+
return record === undefined ? null : record;
|
|
341
|
+
};
|
|
342
|
+
|
|
271
343
|
g.mark = function () {
|
|
272
344
|
flush();
|
|
273
345
|
const mark = g.seq;
|
|
@@ -348,6 +420,10 @@ export function buildObserverInstallJs(opts = {}) {
|
|
|
348
420
|
window.removeEventListener('popstate', onNav);
|
|
349
421
|
window.removeEventListener('hashchange', onNav);
|
|
350
422
|
g.baselines.clear();
|
|
423
|
+
refs = new WeakMap();
|
|
424
|
+
refNodes.clear();
|
|
425
|
+
snapshotSeq = 0;
|
|
426
|
+
snapshots.clear();
|
|
351
427
|
if (window[opts.key] === g) {
|
|
352
428
|
try { delete window[opts.key]; } catch (e) {}
|
|
353
429
|
}
|
|
@@ -390,9 +466,37 @@ export function buildObserverMarkJs() {
|
|
|
390
466
|
*/
|
|
391
467
|
export function buildObserverChangesJs(mark, docId) {
|
|
392
468
|
const markJson = JSON.stringify(clampInt(mark, 0, 0, Number.MAX_SAFE_INTEGER));
|
|
393
|
-
const docJson = typeof docId === 'string' && docId.length > 0 ?
|
|
469
|
+
const docJson = typeof docId === 'string' && docId.length > 0 ? jsStringLiteral(docId) : 'null';
|
|
394
470
|
return callInstance(`g.changes(${markJson}, ${docJson})`);
|
|
395
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* A string literal safe to embed in JS source. `JSON.stringify` alone leaves
|
|
474
|
+
* U+2028 / U+2029 (line terminators in older JS grammars) and `<` (which can
|
|
475
|
+
* close a surrounding `<script>`) unescaped; this escapes them as `\uXXXX`,
|
|
476
|
+
* which parses back to the same string.
|
|
477
|
+
*/
|
|
478
|
+
function jsStringLiteral(value) {
|
|
479
|
+
// The two separators are written as char codes so this source file never
|
|
480
|
+
// carries them raw; the replacement is the six-character escape sequence.
|
|
481
|
+
const lineSeparator = String.fromCharCode(0x2028);
|
|
482
|
+
const paragraphSeparator = String.fromCharCode(0x2029);
|
|
483
|
+
return JSON.stringify(value)
|
|
484
|
+
.split(lineSeparator)
|
|
485
|
+
.join('\\u2028')
|
|
486
|
+
.split(paragraphSeparator)
|
|
487
|
+
.join('\\u2029')
|
|
488
|
+
.replace(/</g, '\\u003c');
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Build the ref-resolve expression, for tests and debugging. Evaluates to
|
|
492
|
+
* `{ ok: true, found: true, tag }` when `ref` names an element of the current
|
|
493
|
+
* document that is still connected, `{ ok: true, found: false }` when it is
|
|
494
|
+
* unknown or stale, or `{ ok: false, reason: 'observer-unavailable' }`.
|
|
495
|
+
*/
|
|
496
|
+
export function buildObserverRefResolveJs(ref) {
|
|
497
|
+
const refJson = jsStringLiteral(typeof ref === 'string' ? ref : '');
|
|
498
|
+
return callInstance(`((el) => el ? { ok: true, found: true, tag: el.localName || String(el.nodeName).toLowerCase() } : { ok: true, found: false })(g.resolveRef(${refJson}))`);
|
|
499
|
+
}
|
|
396
500
|
/**
|
|
397
501
|
* Build the teardown expression: disconnects every observer, restores the
|
|
398
502
|
* `attachShadow` and `history` patches when they are still the observer's,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observer.js","sourceRoot":"","sources":["../../src/inpage/observer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC,sFAAsF;AACtF,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEvD,oEAAoE;AACpE,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAElD,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAClC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAC7B,MAAM,aAAa,GAAG,IAAI,CAAC;AAa3B,SAAS,QAAQ,CAAC,KAAc,EAAE,QAAgB,EAAE,GAAW,EAAE,GAAW;IAC1E,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC1E,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"observer.js","sourceRoot":"","sources":["../../src/inpage/observer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "browser-link",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.36.0",
|
|
5
5
|
"description": "Bridge between Chrome and an MCP client (Claude Code, OpenCode, GitHub Copilot CLI, …). Per-tab manual activation.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"debugger",
|
|
@@ -188,7 +188,7 @@ export const BROWSER_TOOL_DEFINITIONS = [
|
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
name: 'browser.snapshot',
|
|
191
|
-
description: 'Snapshot of the tab: title, url, visible text (truncated) and a list of interactive elements (buttons, links, inputs, selects, textareas) with a CSS selector and labels. Use this to understand page state before clicking or typing. Elements whose computed `pointer-events` is `none` (invisible accessibility layers that can never receive a real click) are excluded from the interactive list, though their text still contributes to headings/text; a descendant that re-enables itself with `pointer-events:auto` is still listed. The scan pierces OPEN Shadow DOM roots and same-origin iframes (nested arbitrarily) — a web component internal or an in-page iframe shows up without extra steps. An entry for an element living inside an iframe carries an extra `frame` field (the CSS selector of the innermost hosting iframe) so you know it is framed; entries in the top document or in Shadow DOM omit it. An entry whose selector could not be made unique across all roots (structurally-identical component twins) carries `ambiguous: true` — that selector resolves first-match-wins, so use it immediately and do NOT store it in the persistent map. Optional filters keep the response small: `within_selector` restricts the scan to a subtree (also deep-search-aware — it can target a subtree inside a shadow root or iframe); `only_interactive` skips headings and the text dump; `exclude` drops landmarks like nav/footer; `max_interactive` overrides the default cap of 120. The per-entry serializer omits empty-string fields, so the same call returns a leaner payload than before — no behavior change for clients that read by key.',
|
|
191
|
+
description: 'Snapshot of the tab: title, url, visible text (truncated) and a list of interactive elements (buttons, links, inputs, selects, textareas) with a CSS selector and labels. Use this to understand page state before clicking or typing. Elements whose computed `pointer-events` is `none` (invisible accessibility layers that can never receive a real click) are excluded from the interactive list, though their text still contributes to headings/text; a descendant that re-enables itself with `pointer-events:auto` is still listed. The scan pierces OPEN Shadow DOM roots and same-origin iframes (nested arbitrarily) — a web component internal or an in-page iframe shows up without extra steps. An entry for an element living inside an iframe carries an extra `frame` field (the CSS selector of the innermost hosting iframe) so you know it is framed; entries in the top document or in Shadow DOM omit it. An entry whose selector could not be made unique across all roots (structurally-identical component twins) carries `ambiguous: true` — that selector resolves first-match-wins, so use it immediately and do NOT store it in the persistent map. Optional filters keep the response small: `within_selector` restricts the scan to a subtree (also deep-search-aware — it can target a subtree inside a shadow root or iframe); `only_interactive` skips headings and the text dump; `exclude` drops landmarks like nav/footer; `max_interactive` overrides the default cap of 120. The per-entry serializer omits empty-string fields, so the same call returns a leaner payload than before — no behavior change for clients that read by key. When the page observer is running in the tab, each interactive entry also carries `ref` (right after `selector`): a per-document element handle `e<N>` issued by the page observer, stable for the same element across snapshot/find/state calls and valid only for the `doc_id` that issued it; unlike `ambiguous` (a selector-level warning), a `ref` names exactly one element of the current document. The result then also carries `doc_id` (the document instance that issued the refs; it changes on every new document) and `snapshot_id` (`<doc_id>:s<N>`, one per snapshot call in that document). All three are additive and ignorable, and are absent when the observer is not in the page. Pass `since: <snapshot_id>` to get a diff instead of the whole page: `{ title, url, doc_id, snapshot_id, since, added, removed, changed, overflow? }`, where `added` and `changed` are full entries (added first) and `removed` is `{ ref, tag, text? }`. `max_interactive` applies after diffing to `added` + `changed` and `overflow` (count dropped) appears only when above 0; `headings` and `text` appear in a diff only when `only_interactive` is false and they changed. When no diff can be made the result is the full snapshot plus `since` and `since_status`: `doc-changed` (the `since` prefix is not the current `doc_id`, or it has no `:`), `evicted` (same document, but not among the last 4 snapshots or never issued) or `observer-unavailable`. An invalid `since` (not a string, or over 64 characters) is dropped and a plain full snapshot is returned; a `within_selector` miss ignores `since`.',
|
|
192
192
|
inputSchema: {
|
|
193
193
|
type: 'object',
|
|
194
194
|
properties: {
|
|
@@ -210,6 +210,11 @@ export const BROWSER_TOOL_DEFINITIONS = [
|
|
|
210
210
|
type: 'number',
|
|
211
211
|
description: 'Cap on the number of interactive entries returned. Default 120, hard ceiling 500.',
|
|
212
212
|
},
|
|
213
|
+
since: {
|
|
214
|
+
type: 'string',
|
|
215
|
+
maxLength: 64,
|
|
216
|
+
description: 'The `snapshot_id` of an earlier snapshot of this document. Format `<doc_id>:s<N>`. Returns only `added`, `removed` and `changed` entries (by `ref`) against it; when it cannot diff, returns the full snapshot plus `since_status` (`evicted`, `doc-changed` or `observer-unavailable`). The observer keeps the last 4 snapshots per document.',
|
|
217
|
+
},
|
|
213
218
|
},
|
|
214
219
|
required: ['tab_id'],
|
|
215
220
|
additionalProperties: false,
|
|
@@ -221,6 +226,7 @@ export const BROWSER_TOOL_DEFINITIONS = [
|
|
|
221
226
|
'Before clicking or typing, to find a stable selector for the target element.',
|
|
222
227
|
'When the user reports a layout or visual issue and you need to ground your reasoning in what is actually rendered.',
|
|
223
228
|
'When you only care about a region of the page, pass `within_selector` so the response stays small.',
|
|
229
|
+
'When you look at the same view again: take one full snapshot, keep its `snapshot_id`, and pass it as `since` on every later look.',
|
|
224
230
|
],
|
|
225
231
|
gotchas: [
|
|
226
232
|
'The snapshot is the source of truth; the persistent map is a cache, not a substitute.',
|
|
@@ -228,13 +234,17 @@ export const BROWSER_TOOL_DEFINITIONS = [
|
|
|
228
234
|
'Empty-string fields (`placeholder`, `aria_label`, etc.) are omitted from each entry. Read by key with optional-chaining or fall back to "".',
|
|
229
235
|
'CLOSED shadow roots (attachShadow({mode:"closed"})) are unreachable from any CDP-based tool — there is no workaround. Cross-origin iframes are also unreachable (same-origin policy); their content is invisible to this scan.',
|
|
230
236
|
'Two structurally-identical component instances in different roots (e.g. twin web components with byte-identical internals) cannot be told apart by any CSS selector — no syntax scopes a selector to one shadow root. Affected entries carry `ambiguous: true`; their selector resolves to the FIRST match in a deterministic traversal order. Use it right away, never cache it.',
|
|
237
|
+
'`ref`, `doc_id` and `snapshot_id` appear only when the page observer is running in the tab. A `ref` dies with its document: compare `doc_id` before trusting a ref from an earlier call, and never store refs in the persistent map. `ref` is informational for now: it is not yet accepted as an action target.',
|
|
238
|
+
'With `since`, a `since_status` of `doc-changed`, `evicted` or `observer-unavailable` means you already received the full snapshot: keep its new `snapshot_id` for the next `since`. Do not diff against the old id again.',
|
|
239
|
+
'The structural selector adds `:nth-of-type` only when siblings force it, so inserting a sibling can change the selector of a neighbour and list that neighbour in `changed`. The selector did change: use the current one, not the one from the earlier snapshot.',
|
|
240
|
+
'A diff covers at most the first 500 interactive entries of each snapshot, and `removed` is capped at 500.',
|
|
231
241
|
],
|
|
232
242
|
example: 'browser.snapshot({ tab_id: "tab_1", within_selector: "main", exclude: ["nav", "footer"] })',
|
|
233
243
|
},
|
|
234
244
|
},
|
|
235
245
|
{
|
|
236
246
|
name: 'browser.find',
|
|
237
|
-
description: 'Locate ONE interactive element by its visible text and return a stable selector plus viewport coordinates. The match is case-insensitive substring by default (set `exact:true` for full-string equality). Pass `role` to narrow to a specific ARIA role (`button`, `link`, `textbox`, `checkbox`, `tab`, `menuitem`) — without it the scan covers buttons, links, inputs, role-bearing elements, contenteditable nodes, and `[onclick]` divs (the "peruvian markup" case). Elements whose computed `pointer-events` is `none` (invisible accessibility layers that can never receive a real click) are never returned as matches, though they can still surface as `near_misses` hints. The scan pierces OPEN Shadow DOM roots and same-origin iframes, same as browser.snapshot. Returns `{ matched: true, selector, coords:{x,y}, tag, text, frame?, ambiguous? }` on a unique hit, `{ matched: false, reason: "not-found", error?, near_misses? }` when nothing matches, or `{ matched: false, reason: "multiple-matches", candidates: [{selector, text, tag}] }` (up to 5) when several elements match — pick one and try again with `exact:true` or a longer text. On `not-found`, `near_misses` carries up to 3 VISIBLE candidates (`{text, selector, role?}`) ranked by substring containment then token overlap — omitted entirely when nothing plausible exists. When `role` excluded matches that the text DID find elsewhere, `error` names it explicitly (e.g. `text matched 2 elements but none with role "button" — closest: <div onclick> "GIF picker"`). `frame`, when present, is the CSS selector of the innermost iframe hosting the match. `ambiguous: true`, when present, means the selector could not be made unique across all roots (structurally-identical twins) and resolves first-match-wins — use it immediately, never cache it. `coords` are already mapped to TOP-LEVEL viewport coordinates even when the match lives inside an iframe. The returned `selector` uses the same heuristic as `browser.snapshot` (id → data-testid → aria-label → name → positional fallback, each verified unique across the full deep search scope), so a subsequent `browser.click` / `browser.type` works without re-querying.',
|
|
247
|
+
description: 'Locate ONE interactive element by its visible text and return a stable selector plus viewport coordinates. The match is case-insensitive substring by default (set `exact:true` for full-string equality). Pass `role` to narrow to a specific ARIA role (`button`, `link`, `textbox`, `checkbox`, `tab`, `menuitem`) — without it the scan covers buttons, links, inputs, role-bearing elements, contenteditable nodes, and `[onclick]` divs (the "peruvian markup" case). Elements whose computed `pointer-events` is `none` (invisible accessibility layers that can never receive a real click) are never returned as matches, though they can still surface as `near_misses` hints. The scan pierces OPEN Shadow DOM roots and same-origin iframes, same as browser.snapshot. Returns `{ matched: true, selector, ref?, coords:{x,y}, tag, text, frame?, ambiguous?, doc_id? }` on a unique hit, `{ matched: false, reason: "not-found", error?, near_misses?, doc_id? }` when nothing matches, or `{ matched: false, reason: "multiple-matches", candidates: [{selector, ref?, text, tag}], doc_id? }` (up to 5) when several elements match — pick one and try again with `exact:true` or a longer text. On `not-found`, `near_misses` carries up to 3 VISIBLE candidates (`{text, selector, ref?, role?}`) ranked by substring containment then token overlap — omitted entirely when nothing plausible exists. When `role` excluded matches that the text DID find elsewhere, `error` names it explicitly (e.g. `text matched 2 elements but none with role "button" — closest: <div onclick> "GIF picker"`). `frame`, when present, is the CSS selector of the innermost iframe hosting the match. `ambiguous: true`, when present, means the selector could not be made unique across all roots (structurally-identical twins) and resolves first-match-wins — use it immediately, never cache it. `coords` are already mapped to TOP-LEVEL viewport coordinates even when the match lives inside an iframe. The returned `selector` uses the same heuristic as `browser.snapshot` (id → data-testid → aria-label → name → positional fallback, each verified unique across the full deep search scope), so a subsequent `browser.click` / `browser.type` works without re-querying. `ref` and `doc_id` are additive, ignorable fields present only when the page observer is running in the tab: `ref` is the same per-document element handle `e<N>` that browser.snapshot reports for that element, valid only for the `doc_id` that issued it.',
|
|
238
248
|
inputSchema: {
|
|
239
249
|
type: 'object',
|
|
240
250
|
properties: {
|
|
@@ -277,7 +287,7 @@ export const BROWSER_TOOL_DEFINITIONS = [
|
|
|
277
287
|
},
|
|
278
288
|
{
|
|
279
289
|
name: 'browser.state',
|
|
280
|
-
description: 'Compact orientation snapshot — cheaper than a full browser.snapshot when the agent only needs to know where it is. Returns { url, title, viewport:{w,h}, focused?, dialogs?, scroll? }. `focused` is the innermost actually-focused element (descending through open Shadow DOM roots and same-origin iframes past `document.activeElement`, which by itself only reports a shadow host or an <iframe> element) as `{ selector, tag, ambiguous? }`. `dialogs` lists visible `[role=dialog]`, `[role=alertdialog]` and open `<dialog>` elements as `{ selector, role, label? }`, `label` best-effort from aria-label / aria-labelledby / the first heading inside. `scroll` is `{x,y}` and is omitted when the page is at its default (0,0) position. Every optional field is omitted when there is nothing to report.',
|
|
290
|
+
description: 'Compact orientation snapshot — cheaper than a full browser.snapshot when the agent only needs to know where it is. Returns { url, title, viewport:{w,h}, focused?, dialogs?, scroll? }. `focused` is the innermost actually-focused element (descending through open Shadow DOM roots and same-origin iframes past `document.activeElement`, which by itself only reports a shadow host or an <iframe> element) as `{ selector, ref?, tag, ambiguous? }`. `dialogs` lists visible `[role=dialog]`, `[role=alertdialog]` and open `<dialog>` elements as `{ selector, role, label? }`, `label` best-effort from aria-label / aria-labelledby / the first heading inside. `scroll` is `{x,y}` and is omitted when the page is at its default (0,0) position. Every optional field is omitted when there is nothing to report. When the page observer is running in the tab, `focused.ref` is the same per-document element handle `e<N>` that browser.snapshot reports for that element and the result carries `doc_id`; both are additive, ignorable, valid only for that `doc_id`, and absent when the observer is not in the page.',
|
|
281
291
|
inputSchema: {
|
|
282
292
|
type: 'object',
|
|
283
293
|
properties: { tab_id: { type: 'string' } },
|