@silbercue/chrome 0.2.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/LICENSE +21 -0
- package/README.md +229 -0
- package/build/cache/a11y-tree.d.ts +252 -0
- package/build/cache/a11y-tree.js +1956 -0
- package/build/cache/index.d.ts +8 -0
- package/build/cache/index.js +4 -0
- package/build/cache/selector-cache.d.ts +47 -0
- package/build/cache/selector-cache.js +119 -0
- package/build/cache/session-defaults.d.ts +27 -0
- package/build/cache/session-defaults.js +130 -0
- package/build/cache/tab-state-cache.d.ts +39 -0
- package/build/cache/tab-state-cache.js +171 -0
- package/build/cdp/cdp-client.d.ts +25 -0
- package/build/cdp/cdp-client.js +146 -0
- package/build/cdp/chrome-launcher.d.ts +85 -0
- package/build/cdp/chrome-launcher.js +502 -0
- package/build/cdp/console-collector.d.ts +53 -0
- package/build/cdp/console-collector.js +147 -0
- package/build/cdp/debug.d.ts +1 -0
- package/build/cdp/debug.js +6 -0
- package/build/cdp/dialog-handler.d.ts +54 -0
- package/build/cdp/dialog-handler.js +129 -0
- package/build/cdp/dom-watcher.d.ts +45 -0
- package/build/cdp/dom-watcher.js +195 -0
- package/build/cdp/emulation.d.ts +12 -0
- package/build/cdp/emulation.js +17 -0
- package/build/cdp/index.d.ts +11 -0
- package/build/cdp/index.js +6 -0
- package/build/cdp/network-collector.d.ts +77 -0
- package/build/cdp/network-collector.js +257 -0
- package/build/cdp/protocol.d.ts +20 -0
- package/build/cdp/protocol.js +1 -0
- package/build/cdp/session-manager.d.ts +62 -0
- package/build/cdp/session-manager.js +205 -0
- package/build/cdp/settle.d.ts +16 -0
- package/build/cdp/settle.js +71 -0
- package/build/cli/license-commands.d.ts +19 -0
- package/build/cli/license-commands.js +199 -0
- package/build/cli/top-level-commands.d.ts +49 -0
- package/build/cli/top-level-commands.js +222 -0
- package/build/hooks/index.d.ts +2 -0
- package/build/hooks/index.js +1 -0
- package/build/hooks/pro-hooks.d.ts +126 -0
- package/build/hooks/pro-hooks.js +17 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +86 -0
- package/build/license/free-tier-config.d.ts +14 -0
- package/build/license/free-tier-config.js +18 -0
- package/build/license/index.d.ts +4 -0
- package/build/license/index.js +2 -0
- package/build/license/license-status.d.ts +15 -0
- package/build/license/license-status.js +9 -0
- package/build/overlay/session-overlay.d.ts +22 -0
- package/build/overlay/session-overlay.js +372 -0
- package/build/plan/index.d.ts +7 -0
- package/build/plan/index.js +4 -0
- package/build/plan/plan-conditions.d.ts +12 -0
- package/build/plan/plan-conditions.js +242 -0
- package/build/plan/plan-executor.d.ts +49 -0
- package/build/plan/plan-executor.js +259 -0
- package/build/plan/plan-state-store.d.ts +24 -0
- package/build/plan/plan-state-store.js +43 -0
- package/build/plan/plan-variables.d.ts +16 -0
- package/build/plan/plan-variables.js +71 -0
- package/build/registry.d.ts +124 -0
- package/build/registry.js +884 -0
- package/build/server.d.ts +1 -0
- package/build/server.js +245 -0
- package/build/tools/click.d.ts +34 -0
- package/build/tools/click.js +293 -0
- package/build/tools/configure-session.d.ts +15 -0
- package/build/tools/configure-session.js +45 -0
- package/build/tools/console-logs.d.ts +18 -0
- package/build/tools/console-logs.js +44 -0
- package/build/tools/dom-snapshot.d.ts +13 -0
- package/build/tools/dom-snapshot.js +259 -0
- package/build/tools/element-utils.d.ts +23 -0
- package/build/tools/element-utils.js +133 -0
- package/build/tools/error-utils.d.ts +8 -0
- package/build/tools/error-utils.js +27 -0
- package/build/tools/evaluate.d.ts +34 -0
- package/build/tools/evaluate.js +217 -0
- package/build/tools/file-upload.d.ts +20 -0
- package/build/tools/file-upload.js +174 -0
- package/build/tools/fill-form.d.ts +39 -0
- package/build/tools/fill-form.js +256 -0
- package/build/tools/handle-dialog.d.ts +15 -0
- package/build/tools/handle-dialog.js +48 -0
- package/build/tools/index.d.ts +35 -0
- package/build/tools/index.js +18 -0
- package/build/tools/navigate.d.ts +18 -0
- package/build/tools/navigate.js +111 -0
- package/build/tools/network-monitor.d.ts +18 -0
- package/build/tools/network-monitor.js +66 -0
- package/build/tools/observe.d.ts +44 -0
- package/build/tools/observe.js +339 -0
- package/build/tools/press-key.d.ts +33 -0
- package/build/tools/press-key.js +155 -0
- package/build/tools/read-page.d.ts +22 -0
- package/build/tools/read-page.js +100 -0
- package/build/tools/run-plan.d.ts +205 -0
- package/build/tools/run-plan.js +215 -0
- package/build/tools/screenshot.d.ts +16 -0
- package/build/tools/screenshot.js +283 -0
- package/build/tools/scroll.d.ts +28 -0
- package/build/tools/scroll.js +143 -0
- package/build/tools/switch-tab.d.ts +26 -0
- package/build/tools/switch-tab.js +355 -0
- package/build/tools/tab-status.d.ts +7 -0
- package/build/tools/tab-status.js +50 -0
- package/build/tools/type.d.ts +31 -0
- package/build/tools/type.js +247 -0
- package/build/tools/virtual-desk.d.ts +7 -0
- package/build/tools/virtual-desk.js +108 -0
- package/build/tools/visual-constants.d.ts +3 -0
- package/build/tools/visual-constants.js +10 -0
- package/build/tools/wait-for.d.ts +26 -0
- package/build/tools/wait-for.js +323 -0
- package/build/transport/index.d.ts +3 -0
- package/build/transport/index.js +2 -0
- package/build/transport/pipe-transport.d.ts +18 -0
- package/build/transport/pipe-transport.js +63 -0
- package/build/transport/transport.d.ts +8 -0
- package/build/transport/transport.js +1 -0
- package/build/transport/websocket-transport.d.ts +22 -0
- package/build/transport/websocket-transport.js +200 -0
- package/build/types.d.ts +21 -0
- package/build/types.js +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { EMULATED_WIDTH, EMULATED_HEIGHT, isHeadless } from "../cdp/emulation.js";
|
|
3
|
+
import { wrapCdpError } from "./error-utils.js";
|
|
4
|
+
import { a11yTree } from "../cache/a11y-tree.js";
|
|
5
|
+
import { CLICKABLE_TAGS, CLICKABLE_ROLES, COMPUTED_STYLES } from "./visual-constants.js";
|
|
6
|
+
export const screenshotSchema = z.object({
|
|
7
|
+
full_page: z
|
|
8
|
+
.boolean()
|
|
9
|
+
.optional()
|
|
10
|
+
.default(false)
|
|
11
|
+
.describe("Capture full scrollable page instead of just viewport"),
|
|
12
|
+
som: z
|
|
13
|
+
.boolean()
|
|
14
|
+
.optional()
|
|
15
|
+
.default(false)
|
|
16
|
+
.describe("Overlay numbered labels on interactive elements matching read_page ref IDs (Set-of-Mark)"),
|
|
17
|
+
});
|
|
18
|
+
const MAX_WIDTH = 800;
|
|
19
|
+
const QUALITY = 80;
|
|
20
|
+
const RETRY_QUALITY = 50;
|
|
21
|
+
const MAX_BYTES = 100_000; // 100 KB size guard (promised in tool description)
|
|
22
|
+
const SOM_MAX_LABELS = 80;
|
|
23
|
+
const SOM_MIN_SIZE = 10;
|
|
24
|
+
// --- SoM Pipeline Helpers ---
|
|
25
|
+
function collectSomLabels(snapshot) {
|
|
26
|
+
if (!snapshot.documents || snapshot.documents.length === 0)
|
|
27
|
+
return [];
|
|
28
|
+
const doc = snapshot.documents[0];
|
|
29
|
+
const strings = snapshot.strings;
|
|
30
|
+
// Build layout index map: nodeIndex → layoutIndex
|
|
31
|
+
const layoutMap = new Map();
|
|
32
|
+
for (let li = 0; li < doc.layout.nodeIndex.length; li++) {
|
|
33
|
+
layoutMap.set(doc.layout.nodeIndex[li], li);
|
|
34
|
+
}
|
|
35
|
+
const labels = [];
|
|
36
|
+
const totalNodes = doc.nodes.backendNodeId.length;
|
|
37
|
+
for (let ni = 0; ni < totalNodes; ni++) {
|
|
38
|
+
const backendNodeId = doc.nodes.backendNodeId[ni];
|
|
39
|
+
// Must have an A11y ref
|
|
40
|
+
const ref = a11yTree.getRefForBackendNodeId(backendNodeId);
|
|
41
|
+
if (!ref)
|
|
42
|
+
continue;
|
|
43
|
+
// Must have layout data
|
|
44
|
+
const li = layoutMap.get(ni);
|
|
45
|
+
if (li === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
// Read bounds
|
|
48
|
+
const boundsArr = doc.layout.bounds[li];
|
|
49
|
+
if (!boundsArr || boundsArr.length < 4)
|
|
50
|
+
continue;
|
|
51
|
+
const [x, y, w, h] = boundsArr;
|
|
52
|
+
// Visibility check via computed styles (display, visibility are indices 0, 1)
|
|
53
|
+
const styleProps = doc.layout.styles[li] ?? [];
|
|
54
|
+
const displayIdx = styleProps[0];
|
|
55
|
+
const visibilityIdx = styleProps[1];
|
|
56
|
+
const displayVal = (displayIdx !== undefined && displayIdx >= 0 && displayIdx < strings.length)
|
|
57
|
+
? strings[displayIdx] : "";
|
|
58
|
+
const visibilityVal = (visibilityIdx !== undefined && visibilityIdx >= 0 && visibilityIdx < strings.length)
|
|
59
|
+
? strings[visibilityIdx] : "";
|
|
60
|
+
if (displayVal === "none" || visibilityVal === "hidden")
|
|
61
|
+
continue;
|
|
62
|
+
// Viewport check
|
|
63
|
+
if (x + w <= 0 || y + h <= 0 || x >= EMULATED_WIDTH || y >= EMULATED_HEIGHT)
|
|
64
|
+
continue;
|
|
65
|
+
// Minimum size
|
|
66
|
+
if (w < SOM_MIN_SIZE || h < SOM_MIN_SIZE)
|
|
67
|
+
continue;
|
|
68
|
+
// isClickable heuristic — check tag name from snapshot (M1: shared constants)
|
|
69
|
+
const tagIdx = doc.nodes.nodeName[ni];
|
|
70
|
+
const tag = (tagIdx !== undefined && tagIdx >= 0 && tagIdx < strings.length) ? strings[tagIdx] : "";
|
|
71
|
+
const nodeInfo = a11yTree.getNodeInfo(backendNodeId);
|
|
72
|
+
const isClickable = CLICKABLE_TAGS.has(tag) || (nodeInfo ? CLICKABLE_ROLES.has(nodeInfo.role) : false);
|
|
73
|
+
// C1: Only label clickable/interactive elements
|
|
74
|
+
if (!isClickable)
|
|
75
|
+
continue;
|
|
76
|
+
const paintOrder = doc.layout.paintOrders?.[li] ?? 0;
|
|
77
|
+
labels.push({
|
|
78
|
+
ref,
|
|
79
|
+
bounds: { x: Math.round(x), y: Math.round(y), w: Math.round(w), h: Math.round(h) },
|
|
80
|
+
isClickable,
|
|
81
|
+
paintOrder,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Anti-clutter: limit to SOM_MAX_LABELS, prioritize clickable + front paint order
|
|
85
|
+
if (labels.length > SOM_MAX_LABELS) {
|
|
86
|
+
labels.sort((a, b) => {
|
|
87
|
+
if (a.isClickable !== b.isClickable)
|
|
88
|
+
return a.isClickable ? -1 : 1;
|
|
89
|
+
return b.paintOrder - a.paintOrder;
|
|
90
|
+
});
|
|
91
|
+
labels.length = SOM_MAX_LABELS;
|
|
92
|
+
}
|
|
93
|
+
return labels;
|
|
94
|
+
}
|
|
95
|
+
function buildSomInjectScript(labels) {
|
|
96
|
+
const labelData = labels.map((l) => ({
|
|
97
|
+
ref: l.ref,
|
|
98
|
+
x: l.bounds.x,
|
|
99
|
+
y: l.bounds.y,
|
|
100
|
+
}));
|
|
101
|
+
return `(() => {
|
|
102
|
+
const data = ${JSON.stringify(labelData)};
|
|
103
|
+
const style = document.createElement('style');
|
|
104
|
+
style.id = '__som_style__';
|
|
105
|
+
style.textContent = \`
|
|
106
|
+
.__som_label {
|
|
107
|
+
position: absolute;
|
|
108
|
+
z-index: 2147483647;
|
|
109
|
+
pointer-events: none;
|
|
110
|
+
background: rgba(255, 87, 34, 0.85);
|
|
111
|
+
color: white;
|
|
112
|
+
font-size: 10px;
|
|
113
|
+
font-family: monospace;
|
|
114
|
+
font-weight: bold;
|
|
115
|
+
line-height: 1;
|
|
116
|
+
padding: 1px 3px;
|
|
117
|
+
border-radius: 2px;
|
|
118
|
+
white-space: nowrap;
|
|
119
|
+
}
|
|
120
|
+
\`;
|
|
121
|
+
document.head.appendChild(style);
|
|
122
|
+
const container = document.createElement('div');
|
|
123
|
+
container.id = '__som_overlay__';
|
|
124
|
+
container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none;';
|
|
125
|
+
for (const d of data) {
|
|
126
|
+
const el = document.createElement('div');
|
|
127
|
+
el.className = '__som_label';
|
|
128
|
+
el.style.top = d.y + 'px';
|
|
129
|
+
el.style.left = d.x + 'px';
|
|
130
|
+
el.textContent = d.ref;
|
|
131
|
+
container.appendChild(el);
|
|
132
|
+
}
|
|
133
|
+
document.body.appendChild(container);
|
|
134
|
+
})()`;
|
|
135
|
+
}
|
|
136
|
+
const SOM_REMOVE_SCRIPT = `(() => {
|
|
137
|
+
const overlay = document.getElementById('__som_overlay__');
|
|
138
|
+
if (overlay) overlay.remove();
|
|
139
|
+
const style = document.getElementById('__som_style__');
|
|
140
|
+
if (style) style.remove();
|
|
141
|
+
})()`;
|
|
142
|
+
// --- Handler ---
|
|
143
|
+
export async function screenshotHandler(params, cdpClient, sessionId, sessionManager) {
|
|
144
|
+
const start = performance.now();
|
|
145
|
+
try {
|
|
146
|
+
// In headed mode (no Emulation.setDeviceMetricsOverride), clip coordinates are
|
|
147
|
+
// in page/document space. We must offset by scroll position to capture the
|
|
148
|
+
// visible viewport, otherwise we capture the top of the page (which may be
|
|
149
|
+
// off-screen and return a black image from the compositor).
|
|
150
|
+
let clipX = 0;
|
|
151
|
+
let clipY = 0;
|
|
152
|
+
if (!isHeadless() && !params.full_page) {
|
|
153
|
+
const scrollResult = await cdpClient.send("Runtime.evaluate", { expression: "JSON.stringify({x:window.scrollX,y:window.scrollY})", returnByValue: true }, sessionId);
|
|
154
|
+
try {
|
|
155
|
+
const scroll = JSON.parse(scrollResult.result.value);
|
|
156
|
+
clipX = scroll.x || 0;
|
|
157
|
+
clipY = scroll.y || 0;
|
|
158
|
+
}
|
|
159
|
+
catch { /* fallback to 0,0 */ }
|
|
160
|
+
}
|
|
161
|
+
const captureParams = {
|
|
162
|
+
format: "webp",
|
|
163
|
+
quality: QUALITY,
|
|
164
|
+
optimizeForSpeed: true,
|
|
165
|
+
clip: {
|
|
166
|
+
x: clipX,
|
|
167
|
+
y: clipY,
|
|
168
|
+
width: EMULATED_WIDTH,
|
|
169
|
+
height: EMULATED_HEIGHT,
|
|
170
|
+
scale: MAX_WIDTH / EMULATED_WIDTH,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
if (params.full_page) {
|
|
174
|
+
const metrics = await cdpClient.send("Page.getLayoutMetrics", {}, sessionId);
|
|
175
|
+
const { width, height } = metrics.cssContentSize;
|
|
176
|
+
// H3: Guard against zero/negative dimensions — fall back to viewport
|
|
177
|
+
if (width <= 0 || height <= 0) {
|
|
178
|
+
captureParams.clip = {
|
|
179
|
+
x: 0,
|
|
180
|
+
y: 0,
|
|
181
|
+
width: EMULATED_WIDTH,
|
|
182
|
+
height: EMULATED_HEIGHT,
|
|
183
|
+
scale: MAX_WIDTH / EMULATED_WIDTH,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
captureParams.clip = {
|
|
188
|
+
x: 0,
|
|
189
|
+
y: 0,
|
|
190
|
+
width,
|
|
191
|
+
height,
|
|
192
|
+
scale: MAX_WIDTH / width,
|
|
193
|
+
};
|
|
194
|
+
captureParams.captureBeyondViewport = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// --- SoM Pipeline: inject overlay before screenshot, remove after ---
|
|
198
|
+
let somElements;
|
|
199
|
+
let somFailed = false;
|
|
200
|
+
if (params.som) {
|
|
201
|
+
try {
|
|
202
|
+
// C2: Always refresh A11y refs — after navigation they may be stale
|
|
203
|
+
await a11yTree.getTree(cdpClient, sessionId, {}, sessionManager);
|
|
204
|
+
// Capture DOMSnapshot for bounding boxes
|
|
205
|
+
const snapshot = await cdpClient.send("DOMSnapshot.captureSnapshot", {
|
|
206
|
+
computedStyles: [...COMPUTED_STYLES],
|
|
207
|
+
includeDOMRects: true,
|
|
208
|
+
includeBlendedBackgroundColors: true,
|
|
209
|
+
includePaintOrder: true,
|
|
210
|
+
}, sessionId);
|
|
211
|
+
const labels = collectSomLabels(snapshot);
|
|
212
|
+
somElements = labels.length;
|
|
213
|
+
if (labels.length > 0) {
|
|
214
|
+
// H2: try/finally wraps entire inject+screenshot+cleanup so even inject errors run cleanup
|
|
215
|
+
try {
|
|
216
|
+
// Inject overlay
|
|
217
|
+
await cdpClient.send("Runtime.evaluate", {
|
|
218
|
+
expression: buildSomInjectScript(labels),
|
|
219
|
+
}, sessionId);
|
|
220
|
+
let result = await cdpClient.send("Page.captureScreenshot", captureParams, sessionId);
|
|
221
|
+
let bytes = Math.ceil(result.data.length * 3 / 4);
|
|
222
|
+
// Size guard
|
|
223
|
+
if (bytes > MAX_BYTES) {
|
|
224
|
+
captureParams.quality = RETRY_QUALITY;
|
|
225
|
+
result = await cdpClient.send("Page.captureScreenshot", captureParams, sessionId);
|
|
226
|
+
bytes = Math.ceil(result.data.length * 3 / 4);
|
|
227
|
+
}
|
|
228
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
229
|
+
return {
|
|
230
|
+
content: [{ type: "image", data: result.data, mimeType: "image/webp" }],
|
|
231
|
+
_meta: {
|
|
232
|
+
elapsedMs,
|
|
233
|
+
method: "screenshot",
|
|
234
|
+
bytes,
|
|
235
|
+
somElements,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
// Cleanup guarantee — remove overlay even on error
|
|
241
|
+
await cdpClient.send("Runtime.evaluate", {
|
|
242
|
+
expression: SOM_REMOVE_SCRIPT,
|
|
243
|
+
}, sessionId).catch(() => { });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// If no labels, fall through to normal screenshot with somElements = 0
|
|
247
|
+
}
|
|
248
|
+
catch (somErr) {
|
|
249
|
+
// H1: SoM pipeline failed — fall through to normal screenshot
|
|
250
|
+
somFailed = true;
|
|
251
|
+
somElements = undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Normal screenshot (som === false OR som with 0 labels)
|
|
255
|
+
let result = await cdpClient.send("Page.captureScreenshot", captureParams, sessionId);
|
|
256
|
+
let bytes = Math.ceil(result.data.length * 3 / 4);
|
|
257
|
+
// C1: Size guard — retry once with lower quality if >100KB
|
|
258
|
+
if (bytes > MAX_BYTES) {
|
|
259
|
+
captureParams.quality = RETRY_QUALITY;
|
|
260
|
+
result = await cdpClient.send("Page.captureScreenshot", captureParams, sessionId);
|
|
261
|
+
bytes = Math.ceil(result.data.length * 3 / 4);
|
|
262
|
+
}
|
|
263
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: "image", data: result.data, mimeType: "image/webp" }],
|
|
266
|
+
_meta: {
|
|
267
|
+
elapsedMs,
|
|
268
|
+
method: "screenshot",
|
|
269
|
+
bytes,
|
|
270
|
+
...(somElements !== undefined ? { somElements } : {}),
|
|
271
|
+
...(somFailed ? { somFailed: true } : {}),
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
277
|
+
return {
|
|
278
|
+
content: [{ type: "text", text: wrapCdpError(err, "screenshot") }],
|
|
279
|
+
isError: true,
|
|
280
|
+
_meta: { elapsedMs, method: "screenshot" },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CdpClient } from "../cdp/cdp-client.js";
|
|
3
|
+
import type { SessionManager } from "../cdp/session-manager.js";
|
|
4
|
+
import type { ToolResponse } from "../types.js";
|
|
5
|
+
export declare const scrollSchema: z.ZodObject<{
|
|
6
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
7
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
8
|
+
container_ref: z.ZodOptional<z.ZodString>;
|
|
9
|
+
container_selector: z.ZodOptional<z.ZodString>;
|
|
10
|
+
direction: z.ZodOptional<z.ZodEnum<["up", "down"]>>;
|
|
11
|
+
amount: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
ref?: string | undefined;
|
|
14
|
+
selector?: string | undefined;
|
|
15
|
+
container_ref?: string | undefined;
|
|
16
|
+
container_selector?: string | undefined;
|
|
17
|
+
direction?: "up" | "down" | undefined;
|
|
18
|
+
amount?: number | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
ref?: string | undefined;
|
|
21
|
+
selector?: string | undefined;
|
|
22
|
+
container_ref?: string | undefined;
|
|
23
|
+
container_selector?: string | undefined;
|
|
24
|
+
direction?: "up" | "down" | undefined;
|
|
25
|
+
amount?: number | undefined;
|
|
26
|
+
}>;
|
|
27
|
+
export type ScrollParams = z.infer<typeof scrollSchema>;
|
|
28
|
+
export declare function scrollHandler(params: ScrollParams, cdpClient: CdpClient, sessionId?: string, sessionManager?: SessionManager): Promise<ToolResponse>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveElement, buildRefNotFoundError, RefNotFoundError } from "./element-utils.js";
|
|
3
|
+
import { wrapCdpError } from "./error-utils.js";
|
|
4
|
+
// --- Schema ---
|
|
5
|
+
export const scrollSchema = z.object({
|
|
6
|
+
ref: z
|
|
7
|
+
.string()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("Element ref to scroll into view (e.g. 'e42')"),
|
|
10
|
+
selector: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("CSS selector to scroll into view (e.g. '#item-30')"),
|
|
14
|
+
container_ref: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Scrollable container ref — scroll this container instead of the page (e.g. 'e10')"),
|
|
18
|
+
container_selector: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Scrollable container CSS selector (e.g. '.sidebar-list')"),
|
|
22
|
+
direction: z
|
|
23
|
+
.enum(["up", "down"])
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Scroll direction (when no ref/selector given). Default: down"),
|
|
26
|
+
amount: z
|
|
27
|
+
.number()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Pixels to scroll (default: 500). Only used with direction."),
|
|
30
|
+
});
|
|
31
|
+
// --- Handler ---
|
|
32
|
+
export async function scrollHandler(params, cdpClient, sessionId, sessionManager) {
|
|
33
|
+
const start = performance.now();
|
|
34
|
+
// Mode 1: Scroll element into view
|
|
35
|
+
if (params.ref || params.selector) {
|
|
36
|
+
try {
|
|
37
|
+
const target = params.ref ? { ref: params.ref } : { selector: params.selector };
|
|
38
|
+
const element = await resolveElement(cdpClient, sessionId, target, sessionManager);
|
|
39
|
+
await cdpClient.send("DOM.scrollIntoViewIfNeeded", { backendNodeId: element.backendNodeId }, element.resolvedSessionId);
|
|
40
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: "text", text: `Scrolled ${params.ref ?? params.selector} into view` }],
|
|
43
|
+
_meta: { elapsedMs, method: "scroll" },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err instanceof RefNotFoundError && params.ref) {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: buildRefNotFoundError(params.ref) }],
|
|
50
|
+
isError: true,
|
|
51
|
+
_meta: { elapsedMs: 0, method: "scroll" },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: wrapCdpError(err, "scroll") }],
|
|
57
|
+
isError: true,
|
|
58
|
+
_meta: { elapsedMs, method: "scroll" },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Mode 2: Scroll by direction + amount (page or container)
|
|
63
|
+
const direction = params.direction ?? "down";
|
|
64
|
+
const amount = params.amount ?? 500;
|
|
65
|
+
const scrollY = direction === "down" ? amount : -amount;
|
|
66
|
+
// Resolve container element if specified
|
|
67
|
+
let containerObjectId;
|
|
68
|
+
let containerSessionId = sessionId;
|
|
69
|
+
if (params.container_ref || params.container_selector) {
|
|
70
|
+
try {
|
|
71
|
+
const target = params.container_ref
|
|
72
|
+
? { ref: params.container_ref }
|
|
73
|
+
: { selector: params.container_selector };
|
|
74
|
+
const container = await resolveElement(cdpClient, sessionId, target, sessionManager);
|
|
75
|
+
containerObjectId = container.objectId;
|
|
76
|
+
containerSessionId = container.resolvedSessionId;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err instanceof RefNotFoundError && params.container_ref) {
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: buildRefNotFoundError(params.container_ref) }],
|
|
82
|
+
isError: true,
|
|
83
|
+
_meta: { elapsedMs: Math.round(performance.now() - start), method: "scroll" },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: wrapCdpError(err, "scroll") }],
|
|
89
|
+
isError: true,
|
|
90
|
+
_meta: { elapsedMs, method: "scroll" },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
if (containerObjectId) {
|
|
96
|
+
// Scroll inside container element
|
|
97
|
+
const result = await cdpClient.send("Runtime.callFunctionOn", {
|
|
98
|
+
functionDeclaration: `function() {
|
|
99
|
+
this.scrollBy(0, ${scrollY});
|
|
100
|
+
return { scrollTop: Math.round(this.scrollTop), scrollHeight: this.scrollHeight, clientHeight: this.clientHeight };
|
|
101
|
+
}`,
|
|
102
|
+
objectId: containerObjectId,
|
|
103
|
+
returnByValue: true,
|
|
104
|
+
}, containerSessionId);
|
|
105
|
+
const pos = result.result.value;
|
|
106
|
+
const containerLabel = params.container_ref ?? params.container_selector;
|
|
107
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
108
|
+
return {
|
|
109
|
+
content: [{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `Scrolled ${direction} ${amount}px in ${containerLabel} — position: ${pos.scrollTop}/${pos.scrollHeight - pos.clientHeight}px`,
|
|
112
|
+
}],
|
|
113
|
+
_meta: { elapsedMs, method: "scroll" },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Page scroll (default)
|
|
117
|
+
const result = await cdpClient.send("Runtime.evaluate", {
|
|
118
|
+
expression: `(() => {
|
|
119
|
+
window.scrollBy(0, ${scrollY});
|
|
120
|
+
return { scrollY: Math.round(window.scrollY), scrollHeight: document.documentElement.scrollHeight, clientHeight: window.innerHeight };
|
|
121
|
+
})()`,
|
|
122
|
+
returnByValue: true,
|
|
123
|
+
awaitPromise: false,
|
|
124
|
+
}, sessionId);
|
|
125
|
+
const pos = result.result.value;
|
|
126
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
127
|
+
return {
|
|
128
|
+
content: [{
|
|
129
|
+
type: "text",
|
|
130
|
+
text: `Scrolled ${direction} ${amount}px — position: ${pos.scrollY}/${pos.scrollHeight - pos.clientHeight}px`,
|
|
131
|
+
}],
|
|
132
|
+
_meta: { elapsedMs, method: "scroll" },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: "text", text: wrapCdpError(err, "scroll") }],
|
|
139
|
+
isError: true,
|
|
140
|
+
_meta: { elapsedMs, method: "scroll" },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CdpClient } from "../cdp/cdp-client.js";
|
|
3
|
+
import type { SessionManager } from "../cdp/session-manager.js";
|
|
4
|
+
import type { ToolResponse } from "../types.js";
|
|
5
|
+
import type { TabStateCache } from "../cache/tab-state-cache.js";
|
|
6
|
+
export declare const switchTabSchema: z.ZodObject<{
|
|
7
|
+
action: z.ZodDefault<z.ZodOptional<z.ZodEnum<["open", "switch", "close"]>>>;
|
|
8
|
+
url: z.ZodOptional<z.ZodString>;
|
|
9
|
+
tab: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
action: "close" | "switch" | "open";
|
|
12
|
+
url?: string | undefined;
|
|
13
|
+
tab?: string | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
url?: string | undefined;
|
|
16
|
+
tab?: string | undefined;
|
|
17
|
+
action?: "close" | "switch" | "open" | undefined;
|
|
18
|
+
}>;
|
|
19
|
+
export type SwitchTabParams = z.infer<typeof switchTabSchema>;
|
|
20
|
+
/** Visible for testing — resets the internal lock to a resolved promise. */
|
|
21
|
+
export declare function _resetSwitchLock(): void;
|
|
22
|
+
/** Visible for testing — resets the origin tab tracking. */
|
|
23
|
+
export declare function _resetOriginTab(): void;
|
|
24
|
+
/** Visible for testing — reads the current origin tab. */
|
|
25
|
+
export declare function _getOriginTabId(): string | undefined;
|
|
26
|
+
export declare function switchTabHandler(params: SwitchTabParams, cdpClient: CdpClient, sessionId: string | undefined, tabStateCache: TabStateCache, onSessionChange: (newSessionId: string) => void, sessionManager?: SessionManager): Promise<ToolResponse>;
|