@mehmoodqureshi/chrome-mcp 0.9.2 → 0.9.3
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/README.md +4 -2
- package/dist/shared/page-fns.js +208 -182
- package/dist/shared/screenshot.d.ts +14 -0
- package/dist/shared/screenshot.js +19 -4
- package/dist/shared/snapshot.d.ts +24 -2
- package/dist/shared/snapshot.js +117 -33
- package/dist/src/bridge/connection.d.ts +13 -0
- package/dist/src/bridge/connection.js +55 -1
- package/dist/src/bridge/server.d.ts +2 -0
- package/dist/src/bridge/server.js +5 -0
- package/dist/src/bridge/workspace.d.ts +1 -1
- package/dist/src/bridge/workspace.js +2 -2
- package/dist/src/executor/cdp-executor.d.ts +3 -2
- package/dist/src/executor/cdp-executor.js +21 -6
- package/dist/src/executor/extension-executor.d.ts +6 -2
- package/dist/src/executor/extension-executor.js +18 -5
- package/dist/src/executor/types.d.ts +25 -2
- package/dist/src/mcp/locate.d.ts +7 -4
- package/dist/src/mcp/locate.js +15 -8
- package/dist/src/mcp/tools.js +16 -9
- package/docs/BLUEPRINT.md +1 -1
- package/extension-dist/background.js +494 -263
- package/extension-dist/manifest.json +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -502,5 +502,7 @@ the fallback.
|
|
|
502
502
|
> `chrome.debugger` is used only where it's needed and worth it: `trusted: true`
|
|
503
503
|
> input (real OS-level events on React/Vue inputs) and `screenshot` (captures a
|
|
504
504
|
> specific tab **without** activating it — safe under parallel `batch` — with
|
|
505
|
-
> true full-page and element capture). Those ops
|
|
506
|
-
>
|
|
505
|
+
> true full-page and element capture). Those ops show the debug banner while
|
|
506
|
+
> attached; the session lingers 1.5s after the last op so a burst of them
|
|
507
|
+
> attaches once. Screenshots are JPEG (quality 70) at CSS-pixel size by
|
|
508
|
+
> default — pass `format: "png"`, `quality`, or `scale` to change that.
|
package/dist/shared/page-fns.js
CHANGED
|
@@ -87,8 +87,6 @@ function pageOp(a) {
|
|
|
87
87
|
return { dx, dy, exact: true, scrollX: win.scrollX, scrollY: win.scrollY };
|
|
88
88
|
};
|
|
89
89
|
const sel = typeof a.selector === 'string' && a.selector.length > 0 ? a.selector : null;
|
|
90
|
-
const el = sel ? deepQuery(sel) : null;
|
|
91
|
-
const missing = sel !== null && el === null;
|
|
92
90
|
/** Set a value the way React/Vue see it (they patch the instance setter). */
|
|
93
91
|
const setValue = (node, next) => {
|
|
94
92
|
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), 'value')?.set;
|
|
@@ -98,195 +96,223 @@ function pageOp(a) {
|
|
|
98
96
|
node.value = next;
|
|
99
97
|
node.dispatchEvent(new Event('input', { bubbles: true }));
|
|
100
98
|
};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return { found:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return { found: false };
|
|
113
|
-
const root = el ?? document.documentElement;
|
|
114
|
-
if (!root)
|
|
115
|
-
return { found: true, html: '' };
|
|
116
|
-
const outer = a.outer === true || !sel;
|
|
117
|
-
return { found: true, html: outer ? root.outerHTML : root.innerHTML };
|
|
118
|
-
}
|
|
119
|
-
case 'click': {
|
|
120
|
-
if (!el)
|
|
121
|
-
return { found: false };
|
|
122
|
-
el.scrollIntoView({ block: 'center' });
|
|
123
|
-
el.click();
|
|
124
|
-
return { found: true };
|
|
125
|
-
}
|
|
126
|
-
case 'type': {
|
|
127
|
-
const node = el;
|
|
128
|
-
if (!node)
|
|
129
|
-
return { found: false };
|
|
130
|
-
node.focus();
|
|
131
|
-
const next = (a.clear ? '' : node.value ?? '') + (a.text ?? '');
|
|
132
|
-
setValue(node, next);
|
|
133
|
-
node.dispatchEvent(new Event('change', { bubbles: true }));
|
|
134
|
-
return { found: true };
|
|
135
|
-
}
|
|
136
|
-
case 'focus': {
|
|
137
|
-
const node = el;
|
|
138
|
-
if (!node)
|
|
139
|
-
return { found: false };
|
|
140
|
-
node.focus();
|
|
141
|
-
if (a.clear)
|
|
142
|
-
setValue(node, '');
|
|
143
|
-
return { found: true };
|
|
144
|
-
}
|
|
145
|
-
case 'point': {
|
|
146
|
-
if (!el)
|
|
147
|
-
return { found: false };
|
|
148
|
-
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
149
|
-
const r = el.getBoundingClientRect();
|
|
150
|
-
const off = frameOffset();
|
|
151
|
-
return {
|
|
152
|
-
found: true,
|
|
153
|
-
x: r.left + r.width / 2 + off.dx,
|
|
154
|
-
y: r.top + r.height / 2 + off.dy,
|
|
155
|
-
// false => this frame's coordinates cannot be mapped to the top viewport.
|
|
156
|
-
exact: off.exact,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
case 'hover': {
|
|
160
|
-
if (!el)
|
|
161
|
-
return { found: false };
|
|
162
|
-
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
163
|
-
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
164
|
-
return { found: true };
|
|
165
|
-
}
|
|
166
|
-
case 'select': {
|
|
167
|
-
const node = el;
|
|
168
|
-
if (!node || !node.options)
|
|
169
|
-
return { found: false };
|
|
170
|
-
const want = new Set(a.values ?? []);
|
|
171
|
-
let matched = false;
|
|
172
|
-
for (const opt of Array.from(node.options)) {
|
|
173
|
-
const on = want.has(opt.value) || want.has(opt.label) || want.has(opt.text);
|
|
174
|
-
opt.selected = on;
|
|
175
|
-
if (on)
|
|
176
|
-
matched = true;
|
|
99
|
+
/** The op proper, against an element already resolved (or null). */
|
|
100
|
+
const perform = (el) => {
|
|
101
|
+
const missing = sel !== null && el === null;
|
|
102
|
+
switch (a.op) {
|
|
103
|
+
case 'probe':
|
|
104
|
+
return { found: true, url: location.href, title: document.title };
|
|
105
|
+
case 'text': {
|
|
106
|
+
if (missing)
|
|
107
|
+
return { found: false };
|
|
108
|
+
const root = el ?? document.body;
|
|
109
|
+
return { found: true, text: root ? root.innerText ?? '' : '' };
|
|
177
110
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
h: window.innerHeight,
|
|
187
|
-
fullW: Math.max(d.scrollWidth, d.clientWidth),
|
|
188
|
-
fullH: Math.max(d.scrollHeight, d.clientHeight),
|
|
189
|
-
};
|
|
190
|
-
if (!sel)
|
|
191
|
-
return { found: true, dims, element: null, missing: false };
|
|
192
|
-
if (!el)
|
|
193
|
-
return { found: true, dims, element: null, missing: true };
|
|
194
|
-
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
195
|
-
const r = el.getBoundingClientRect();
|
|
196
|
-
const off = frameOffset();
|
|
197
|
-
// viewport rect + this frame's offset + the TOP document's scroll ->
|
|
198
|
-
// document coordinates of the page the screenshot actually captures.
|
|
199
|
-
return {
|
|
200
|
-
found: true,
|
|
201
|
-
dims,
|
|
202
|
-
element: {
|
|
203
|
-
x: r.left + off.dx + off.scrollX,
|
|
204
|
-
y: r.top + off.dy + off.scrollY,
|
|
205
|
-
w: r.width,
|
|
206
|
-
h: r.height,
|
|
207
|
-
},
|
|
208
|
-
missing: false,
|
|
209
|
-
exact: off.exact,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
case 'scroll': {
|
|
213
|
-
if (el)
|
|
214
|
-
el.scrollIntoView({ block: 'center' });
|
|
215
|
-
else if (a.x != null || a.y != null)
|
|
216
|
-
window.scrollTo(a.x ?? 0, a.y ?? 0);
|
|
217
|
-
else
|
|
218
|
-
window.scrollBy(a.deltaX ?? 0, a.deltaY ?? 0);
|
|
219
|
-
return { found: !missing };
|
|
220
|
-
}
|
|
221
|
-
case 'storage': {
|
|
222
|
-
const store = a.session ? window.sessionStorage : window.localStorage;
|
|
223
|
-
const op = a.storageOp;
|
|
224
|
-
if (op === 'set') {
|
|
225
|
-
store.setItem(String(a.key), String(a.value ?? ''));
|
|
226
|
-
return { found: true, ok: true };
|
|
111
|
+
case 'html': {
|
|
112
|
+
if (missing)
|
|
113
|
+
return { found: false };
|
|
114
|
+
const root = el ?? document.documentElement;
|
|
115
|
+
if (!root)
|
|
116
|
+
return { found: true, html: '' };
|
|
117
|
+
const outer = a.outer === true || !sel;
|
|
118
|
+
return { found: true, html: outer ? root.outerHTML : root.innerHTML };
|
|
227
119
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
120
|
+
case 'click': {
|
|
121
|
+
if (!el)
|
|
122
|
+
return { found: false };
|
|
123
|
+
el.scrollIntoView({ block: 'center' });
|
|
124
|
+
el.click();
|
|
125
|
+
return { found: true };
|
|
231
126
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
127
|
+
case 'type': {
|
|
128
|
+
const node = el;
|
|
129
|
+
if (!node)
|
|
130
|
+
return { found: false };
|
|
131
|
+
node.focus();
|
|
132
|
+
const next = (a.clear ? '' : node.value ?? '') + (a.text ?? '');
|
|
133
|
+
setValue(node, next);
|
|
134
|
+
node.dispatchEvent(new Event('change', { bubbles: true }));
|
|
135
|
+
return { found: true };
|
|
235
136
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
137
|
+
case 'focus': {
|
|
138
|
+
const node = el;
|
|
139
|
+
if (!node)
|
|
140
|
+
return { found: false };
|
|
141
|
+
node.focus();
|
|
142
|
+
if (a.clear)
|
|
143
|
+
setValue(node, '');
|
|
144
|
+
return { found: true };
|
|
243
145
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return resolve({ found: false });
|
|
257
|
-
setTimeout(tick, every);
|
|
146
|
+
case 'point': {
|
|
147
|
+
if (!el)
|
|
148
|
+
return { found: false };
|
|
149
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
150
|
+
const r = el.getBoundingClientRect();
|
|
151
|
+
const off = frameOffset();
|
|
152
|
+
return {
|
|
153
|
+
found: true,
|
|
154
|
+
x: r.left + r.width / 2 + off.dx,
|
|
155
|
+
y: r.top + r.height / 2 + off.dy,
|
|
156
|
+
// false => this frame's coordinates cannot be mapped to the top viewport.
|
|
157
|
+
exact: off.exact,
|
|
258
158
|
};
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
159
|
+
}
|
|
160
|
+
case 'hover': {
|
|
161
|
+
if (!el)
|
|
162
|
+
return { found: false };
|
|
163
|
+
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
164
|
+
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
165
|
+
return { found: true };
|
|
166
|
+
}
|
|
167
|
+
case 'select': {
|
|
168
|
+
const node = el;
|
|
169
|
+
if (!node || !node.options)
|
|
170
|
+
return { found: false };
|
|
171
|
+
const want = new Set(a.values ?? []);
|
|
172
|
+
let matched = false;
|
|
173
|
+
for (const opt of Array.from(node.options)) {
|
|
174
|
+
const on = want.has(opt.value) || want.has(opt.label) || want.has(opt.text);
|
|
175
|
+
opt.selected = on;
|
|
176
|
+
if (on)
|
|
177
|
+
matched = true;
|
|
178
|
+
}
|
|
179
|
+
node.dispatchEvent(new Event('input', { bubbles: true }));
|
|
180
|
+
node.dispatchEvent(new Event('change', { bubbles: true }));
|
|
181
|
+
return { found: true, matched };
|
|
182
|
+
}
|
|
183
|
+
case 'measure': {
|
|
184
|
+
const d = document.documentElement;
|
|
185
|
+
const dims = {
|
|
186
|
+
w: window.innerWidth,
|
|
187
|
+
h: window.innerHeight,
|
|
188
|
+
fullW: Math.max(d.scrollWidth, d.clientWidth),
|
|
189
|
+
fullH: Math.max(d.scrollHeight, d.clientHeight),
|
|
190
|
+
dpr: window.devicePixelRatio || 1,
|
|
191
|
+
scrollX: window.scrollX,
|
|
192
|
+
scrollY: window.scrollY,
|
|
277
193
|
};
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
194
|
+
if (!sel)
|
|
195
|
+
return { found: true, dims, element: null, missing: false };
|
|
196
|
+
if (!el)
|
|
197
|
+
return { found: true, dims, element: null, missing: true };
|
|
198
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
199
|
+
const r = el.getBoundingClientRect();
|
|
200
|
+
const off = frameOffset();
|
|
201
|
+
// viewport rect + this frame's offset + the TOP document's scroll ->
|
|
202
|
+
// document coordinates of the page the screenshot actually captures.
|
|
203
|
+
return {
|
|
204
|
+
found: true,
|
|
205
|
+
dims,
|
|
206
|
+
element: {
|
|
207
|
+
x: r.left + off.dx + off.scrollX,
|
|
208
|
+
y: r.top + off.dy + off.scrollY,
|
|
209
|
+
w: r.width,
|
|
210
|
+
h: r.height,
|
|
211
|
+
},
|
|
212
|
+
missing: false,
|
|
213
|
+
exact: off.exact,
|
|
284
214
|
};
|
|
285
|
-
|
|
286
|
-
|
|
215
|
+
}
|
|
216
|
+
case 'scroll': {
|
|
217
|
+
if (el)
|
|
218
|
+
el.scrollIntoView({ block: 'center' });
|
|
219
|
+
else if (a.x != null || a.y != null)
|
|
220
|
+
window.scrollTo(a.x ?? 0, a.y ?? 0);
|
|
221
|
+
else
|
|
222
|
+
window.scrollBy(a.deltaX ?? 0, a.deltaY ?? 0);
|
|
223
|
+
return { found: !missing };
|
|
224
|
+
}
|
|
225
|
+
case 'storage': {
|
|
226
|
+
const store = a.session ? window.sessionStorage : window.localStorage;
|
|
227
|
+
const op = a.storageOp;
|
|
228
|
+
if (op === 'set') {
|
|
229
|
+
store.setItem(String(a.key), String(a.value ?? ''));
|
|
230
|
+
return { found: true, ok: true };
|
|
231
|
+
}
|
|
232
|
+
if (op === 'remove') {
|
|
233
|
+
store.removeItem(String(a.key));
|
|
234
|
+
return { found: true, ok: true };
|
|
235
|
+
}
|
|
236
|
+
if (op === 'clear') {
|
|
237
|
+
store.clear();
|
|
238
|
+
return { found: true, ok: true };
|
|
239
|
+
}
|
|
240
|
+
if (a.key)
|
|
241
|
+
return { found: true, ok: true, value: store.getItem(a.key) };
|
|
242
|
+
const entries = {};
|
|
243
|
+
for (let i = 0; i < store.length; i++) {
|
|
244
|
+
const k = store.key(i);
|
|
245
|
+
if (k)
|
|
246
|
+
entries[k] = store.getItem(k) ?? '';
|
|
247
|
+
}
|
|
248
|
+
return { found: true, ok: true, entries };
|
|
249
|
+
}
|
|
250
|
+
// -- the two polling ops: one injection that resolves in-page, rather than
|
|
251
|
+
// one executeScript round-trip per tick --
|
|
252
|
+
case 'waitSelector': {
|
|
253
|
+
const deadline = Date.now() + (a.timeoutMs ?? 5_000);
|
|
254
|
+
const every = a.interval ?? 120;
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
const tick = () => {
|
|
257
|
+
if (sel && deepQuery(sel))
|
|
258
|
+
return resolve({ found: true });
|
|
259
|
+
if (Date.now() > deadline)
|
|
260
|
+
return resolve({ found: false });
|
|
261
|
+
setTimeout(tick, every);
|
|
262
|
+
};
|
|
263
|
+
tick();
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
case 'waitFor': {
|
|
267
|
+
const deadline = Date.now() + (a.timeoutMs ?? 30_000);
|
|
268
|
+
const every = a.interval ?? 150;
|
|
269
|
+
const want = typeof a.textContains === 'string' && a.textContains.length > 0 ? a.textContains : null;
|
|
270
|
+
const gone = a.gone === true;
|
|
271
|
+
return new Promise((resolve) => {
|
|
272
|
+
const hit = () => {
|
|
273
|
+
let present;
|
|
274
|
+
if (sel)
|
|
275
|
+
present = !!deepQuery(sel);
|
|
276
|
+
else if (want)
|
|
277
|
+
present = (document.body?.innerText ?? '').includes(want);
|
|
278
|
+
else
|
|
279
|
+
present = true;
|
|
280
|
+
return gone ? !present : present;
|
|
281
|
+
};
|
|
282
|
+
const tick = () => {
|
|
283
|
+
if (hit())
|
|
284
|
+
return resolve({ found: true, matched: true });
|
|
285
|
+
if (Date.now() > deadline)
|
|
286
|
+
return resolve({ found: true, matched: false });
|
|
287
|
+
setTimeout(tick, every);
|
|
288
|
+
};
|
|
289
|
+
tick();
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
default:
|
|
293
|
+
return { found: false, error: `unknown page op: ${String(a.op)}` };
|
|
287
294
|
}
|
|
288
|
-
|
|
289
|
-
|
|
295
|
+
};
|
|
296
|
+
const el = sel ? deepQuery(sel) : null;
|
|
297
|
+
// Element ops wait for their target IN THIS SAME injection when `timeoutMs`
|
|
298
|
+
// is set: a not-yet-rendered button costs a few in-page ticks, not a second
|
|
299
|
+
// executeScript round-trip (the old waitSelector-then-act pair).
|
|
300
|
+
const waits = a.op === 'click' || a.op === 'type' || a.op === 'focus' || a.op === 'point' || a.op === 'hover' || a.op === 'select';
|
|
301
|
+
if (waits && sel && !el && a.timeoutMs && a.timeoutMs > 0) {
|
|
302
|
+
const deadline = Date.now() + a.timeoutMs;
|
|
303
|
+
const every = a.interval ?? 120;
|
|
304
|
+
return new Promise((resolve) => {
|
|
305
|
+
const tick = () => {
|
|
306
|
+
const hit = deepQuery(sel);
|
|
307
|
+
if (hit)
|
|
308
|
+
return resolve(perform(hit));
|
|
309
|
+
if (Date.now() > deadline)
|
|
310
|
+
return resolve({ found: false });
|
|
311
|
+
setTimeout(tick, every);
|
|
312
|
+
};
|
|
313
|
+
setTimeout(tick, every);
|
|
314
|
+
});
|
|
290
315
|
}
|
|
316
|
+
return perform(el);
|
|
291
317
|
}
|
|
292
318
|
//# sourceMappingURL=page-fns.js.map
|
|
@@ -14,7 +14,20 @@ export interface PageDims {
|
|
|
14
14
|
/** Full content box (document) width/height. */
|
|
15
15
|
fullW: number;
|
|
16
16
|
fullH: number;
|
|
17
|
+
/** window.devicePixelRatio (default 1). Output pixels = CSS px * dpr * clip.scale. */
|
|
18
|
+
dpr?: number;
|
|
19
|
+
/** Current scroll offset of the top document (default 0,0); a viewport clip starts here. */
|
|
20
|
+
scrollX?: number;
|
|
21
|
+
scrollY?: number;
|
|
17
22
|
}
|
|
23
|
+
export type ScreenshotFormat = 'png' | 'jpeg';
|
|
24
|
+
/** Default encoding: JPEG at this quality is ~5-10x smaller than PNG on a
|
|
25
|
+
* typical page and still perfectly legible to a vision model. */
|
|
26
|
+
export declare const DEFAULT_SCREENSHOT_FORMAT: ScreenshotFormat;
|
|
27
|
+
export declare const DEFAULT_JPEG_QUALITY = 70;
|
|
28
|
+
/** Output size multiplier relative to CSS pixels. 1 = CSS px (so a Retina
|
|
29
|
+
* viewport is NOT captured at 2x); 2 = device px on a 2x display. */
|
|
30
|
+
export declare const DEFAULT_SCREENSHOT_SCALE = 1;
|
|
18
31
|
/** An element's box in DOCUMENT coordinates (viewport rect + scroll offset), CSS px. */
|
|
19
32
|
export interface ElementRect {
|
|
20
33
|
x: number;
|
|
@@ -55,4 +68,5 @@ export declare const MAX_CAPTURE_PX = 16384;
|
|
|
55
68
|
export declare function planScreenshot(dims: PageDims, opts?: {
|
|
56
69
|
fullPage?: boolean;
|
|
57
70
|
element?: ElementRect | null;
|
|
71
|
+
scale?: number;
|
|
58
72
|
}): ScreenshotPlan;
|
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
* `ScreenshotResult` reports. No chrome.* calls — just arithmetic.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.MAX_CAPTURE_PX = void 0;
|
|
11
|
+
exports.MAX_CAPTURE_PX = exports.DEFAULT_SCREENSHOT_SCALE = exports.DEFAULT_JPEG_QUALITY = exports.DEFAULT_SCREENSHOT_FORMAT = void 0;
|
|
12
12
|
exports.planScreenshot = planScreenshot;
|
|
13
|
+
/** Default encoding: JPEG at this quality is ~5-10x smaller than PNG on a
|
|
14
|
+
* typical page and still perfectly legible to a vision model. */
|
|
15
|
+
exports.DEFAULT_SCREENSHOT_FORMAT = 'jpeg';
|
|
16
|
+
exports.DEFAULT_JPEG_QUALITY = 70;
|
|
17
|
+
/** Output size multiplier relative to CSS pixels. 1 = CSS px (so a Retina
|
|
18
|
+
* viewport is NOT captured at 2x); 2 = device px on a 2x display. */
|
|
19
|
+
exports.DEFAULT_SCREENSHOT_SCALE = 1;
|
|
13
20
|
/**
|
|
14
21
|
* Practical single-capture height ceiling. Skia/CDP cannot encode arbitrarily
|
|
15
22
|
* tall images; beyond this we clamp the clip and flag `truncated`.
|
|
@@ -20,11 +27,15 @@ exports.MAX_CAPTURE_PX = 16384;
|
|
|
20
27
|
* viewport capture. Heights are clamped to MAX_CAPTURE_PX with `truncated` set.
|
|
21
28
|
*/
|
|
22
29
|
function planScreenshot(dims, opts = {}) {
|
|
30
|
+
// CDP's clip.scale multiplies on top of the device scale factor, so dividing
|
|
31
|
+
// by the DPR yields exactly `opts.scale` output pixels per CSS pixel.
|
|
32
|
+
const dpr = dims.dpr && dims.dpr > 0 ? dims.dpr : 1;
|
|
33
|
+
const scale = (opts.scale && opts.scale > 0 ? opts.scale : exports.DEFAULT_SCREENSHOT_SCALE) / dpr;
|
|
23
34
|
if (opts.element) {
|
|
24
35
|
const realH = Math.max(1, Math.round(opts.element.h));
|
|
25
36
|
const clipH = Math.min(opts.element.h, exports.MAX_CAPTURE_PX);
|
|
26
37
|
return {
|
|
27
|
-
clip: { x: opts.element.x, y: opts.element.y, width: opts.element.w, height: clipH, scale
|
|
38
|
+
clip: { x: opts.element.x, y: opts.element.y, width: opts.element.w, height: clipH, scale },
|
|
28
39
|
captureBeyondViewport: true,
|
|
29
40
|
width: Math.max(1, Math.round(opts.element.w)),
|
|
30
41
|
height: Math.min(realH, exports.MAX_CAPTURE_PX),
|
|
@@ -35,7 +46,7 @@ function planScreenshot(dims, opts = {}) {
|
|
|
35
46
|
if (opts.fullPage) {
|
|
36
47
|
const clipH = Math.min(dims.fullH, exports.MAX_CAPTURE_PX);
|
|
37
48
|
return {
|
|
38
|
-
clip: { x: 0, y: 0, width: dims.fullW, height: clipH, scale
|
|
49
|
+
clip: { x: 0, y: 0, width: dims.fullW, height: clipH, scale },
|
|
39
50
|
captureBeyondViewport: true,
|
|
40
51
|
width: dims.fullW,
|
|
41
52
|
height: clipH,
|
|
@@ -43,8 +54,12 @@ function planScreenshot(dims, opts = {}) {
|
|
|
43
54
|
fullHeight: dims.fullH,
|
|
44
55
|
};
|
|
45
56
|
}
|
|
46
|
-
// Plain viewport:
|
|
57
|
+
// Plain viewport: capture what's visible. A clip is only needed to apply a
|
|
58
|
+
// scale; at scale 1 on a 1x display the bare capture is identical and cheaper.
|
|
47
59
|
return {
|
|
60
|
+
...(scale !== 1
|
|
61
|
+
? { clip: { x: dims.scrollX ?? 0, y: dims.scrollY ?? 0, width: dims.w, height: dims.h, scale } }
|
|
62
|
+
: {}),
|
|
48
63
|
captureBeyondViewport: false,
|
|
49
64
|
width: dims.w,
|
|
50
65
|
height: dims.h,
|
|
@@ -23,6 +23,28 @@ export interface RawSnapshot {
|
|
|
23
23
|
title: string;
|
|
24
24
|
nodes: RawSnapshotNode[];
|
|
25
25
|
truncated: boolean;
|
|
26
|
+
/** Locator mode, nothing matched: up to 8 "role \"name\"" strings of the
|
|
27
|
+
* same role, so the caller can say what WAS there without a second read. */
|
|
28
|
+
nearby?: string[];
|
|
26
29
|
}
|
|
27
|
-
/**
|
|
28
|
-
export
|
|
30
|
+
/** A role/name query resolved IN the page (see `collectSnapshot`). */
|
|
31
|
+
export interface SnapshotLocator {
|
|
32
|
+
role?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Runs IN THE PAGE. Returns interactive (and optionally landmark) elements with
|
|
37
|
+
* fresh refs.
|
|
38
|
+
*
|
|
39
|
+
* With a `locator`, the walk is the same but the SCORING happens here: only the
|
|
40
|
+
* strongest-tier matches come back (and only they get a ref), so resolving
|
|
41
|
+
* "the Sign in button" ships a handful of nodes instead of the whole tree and
|
|
42
|
+
* touches one or two DOM attributes instead of hundreds. The tiers mirror
|
|
43
|
+
* `src/mcp/locate.ts` exactly (exact, case-insensitive, prefix, contains), and
|
|
44
|
+
* the server re-scores what it receives, so both ends always agree.
|
|
45
|
+
*
|
|
46
|
+
* Reads (visibility, names, values) all happen BEFORE the ref attributes are
|
|
47
|
+
* written: interleaving them made every `innerText` after a `setAttribute`
|
|
48
|
+
* re-run style, which on a big page is most of the snapshot's cost.
|
|
49
|
+
*/
|
|
50
|
+
export declare function collectSnapshot(interactiveOnly?: boolean, max?: number, locator?: SnapshotLocator | null): RawSnapshot;
|