@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,247 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveElement, buildRefNotFoundError, RefNotFoundError } from "./element-utils.js";
|
|
3
|
+
import { wrapCdpError } from "./error-utils.js";
|
|
4
|
+
import { a11yTree } from "../cache/a11y-tree.js";
|
|
5
|
+
// --- Schema (Task 2) ---
|
|
6
|
+
export const typeSchema = z.object({
|
|
7
|
+
ref: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Element reference from read_page (e.g. 'e12') — preferred over selector"),
|
|
11
|
+
selector: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("CSS selector as fallback (e.g. 'input[name=email]')"),
|
|
15
|
+
text: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe("Text to type into the element"),
|
|
18
|
+
clear: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.optional()
|
|
21
|
+
.default(false)
|
|
22
|
+
.describe("Clear existing field content before typing (default: false)"),
|
|
23
|
+
});
|
|
24
|
+
// --- Constants ---
|
|
25
|
+
const INPUT_ROLES = new Set(["textbox", "searchbox", "combobox", "spinbutton"]);
|
|
26
|
+
/** FR-023: Emit a fill_form hint after this many consecutive type calls within the window. */
|
|
27
|
+
const FILL_FORM_HINT_THRESHOLD = 2;
|
|
28
|
+
/** FR-023: Time window (ms) in which consecutive type calls are considered "in the same form session". */
|
|
29
|
+
const FILL_FORM_HINT_WINDOW_MS = 10_000;
|
|
30
|
+
// --- Helpers ---
|
|
31
|
+
function truncate(text, maxLen) {
|
|
32
|
+
return text.length > maxLen ? text.slice(0, maxLen) + "..." : text;
|
|
33
|
+
}
|
|
34
|
+
const typeStreaks = new Map();
|
|
35
|
+
/** Exported for unit tests — clears the streak state. */
|
|
36
|
+
export function _resetTypeStreaks() {
|
|
37
|
+
typeStreaks.clear();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* FR-023 scope-fix: Probe the closest <form> ancestor of the resolved element
|
|
41
|
+
* and tag it with a stable, per-form identifier (persisted via a dataset
|
|
42
|
+
* marker so subsequent probes on the same form return the same ID).
|
|
43
|
+
*
|
|
44
|
+
* Returns null when:
|
|
45
|
+
* - the element has no <form> ancestor (e.g. a loose input field), or
|
|
46
|
+
* - the probe fails (detached node, hostile page, etc.).
|
|
47
|
+
*
|
|
48
|
+
* A null return suppresses the streak hint, which is the safe default —
|
|
49
|
+
* we'd rather miss a hint than show one for unrelated form fills.
|
|
50
|
+
*/
|
|
51
|
+
async function probeFormScope(cdpClient, objectId, targetSession) {
|
|
52
|
+
try {
|
|
53
|
+
const res = (await cdpClient.send("Runtime.callFunctionOn", {
|
|
54
|
+
functionDeclaration: `function() {
|
|
55
|
+
const f = this.closest('form');
|
|
56
|
+
if (!f) return null;
|
|
57
|
+
if (!f.dataset.__silbercueFormId) {
|
|
58
|
+
f.dataset.__silbercueFormId = '__sc_' + Math.random().toString(36).slice(2, 10);
|
|
59
|
+
}
|
|
60
|
+
return f.dataset.__silbercueFormId;
|
|
61
|
+
}`,
|
|
62
|
+
objectId,
|
|
63
|
+
returnByValue: true,
|
|
64
|
+
silent: true,
|
|
65
|
+
}, targetSession));
|
|
66
|
+
return res?.result?.value ?? null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function recordTypeCallAndMaybeHint(sessionId, formId) {
|
|
73
|
+
if (!sessionId)
|
|
74
|
+
return null;
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const existing = typeStreaks.get(sessionId);
|
|
77
|
+
// The streak continues only when ALL of the following hold:
|
|
78
|
+
// 1. We are still inside the time window.
|
|
79
|
+
// 2. The current call has a non-null form scope.
|
|
80
|
+
// 3. That form scope matches the previous call's form scope.
|
|
81
|
+
// Otherwise we reset the streak with the current call as the new baseline.
|
|
82
|
+
if (existing &&
|
|
83
|
+
now - existing.lastAt <= FILL_FORM_HINT_WINDOW_MS &&
|
|
84
|
+
formId !== null &&
|
|
85
|
+
existing.formId === formId) {
|
|
86
|
+
existing.count += 1;
|
|
87
|
+
existing.lastAt = now;
|
|
88
|
+
if (existing.count >= FILL_FORM_HINT_THRESHOLD && !existing.hintShown) {
|
|
89
|
+
existing.hintShown = true;
|
|
90
|
+
return `\n\nTip: ${existing.count} consecutive type calls into the same form in ${Math.round(FILL_FORM_HINT_WINDOW_MS / 1000)}s — next time try fill_form({ fields: [...] }) for one-round-trip form fills. It handles text inputs, <select>, checkbox, and radio natively, so you don't need evaluate or separate click calls.`;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
typeStreaks.set(sessionId, { count: 1, lastAt: now, hintShown: false, formId });
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
// --- Main handler ---
|
|
98
|
+
export async function typeHandler(params, cdpClient, sessionId, sessionManager) {
|
|
99
|
+
const start = performance.now();
|
|
100
|
+
// Story 16.5: Extract optional humanType callback injected by the
|
|
101
|
+
// `enhanceTool` Pro-Hook. The field is NOT part of the Zod schema — it is
|
|
102
|
+
// read from the raw params map via type-guard and stripped from the params
|
|
103
|
+
// object before downstream code uses it.
|
|
104
|
+
const rawParams = params;
|
|
105
|
+
const maybeHuman = rawParams.humanType;
|
|
106
|
+
const humanType = typeof maybeHuman === "function" ? maybeHuman : undefined;
|
|
107
|
+
if ("humanType" in rawParams) {
|
|
108
|
+
const { humanType: _humanType, ...rest } = rawParams;
|
|
109
|
+
void _humanType;
|
|
110
|
+
params = rest;
|
|
111
|
+
}
|
|
112
|
+
// Validation: require text parameter (defensive — Zod enforces this at schema level,
|
|
113
|
+
// but handler may be called directly without schema parsing)
|
|
114
|
+
if (params.text === undefined || params.text === null) {
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: "type requires a 'text' parameter",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
isError: true,
|
|
123
|
+
_meta: { elapsedMs: 0, method: "type" },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Validation: require at least ref or selector
|
|
127
|
+
if (!params.ref && !params.selector) {
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: "type requires either 'ref' or 'selector' to identify the target element",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
isError: true,
|
|
136
|
+
_meta: { elapsedMs: 0, method: "type" },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
// Step 1: Resolve element (ref preferred over selector, with OOPIF routing)
|
|
141
|
+
const target = params.ref ? { ref: params.ref } : { selector: params.selector };
|
|
142
|
+
const element = await resolveElement(cdpClient, sessionId, target, sessionManager);
|
|
143
|
+
const targetSession = element.resolvedSessionId;
|
|
144
|
+
// FR-023 scope-fix: Probe the form ancestor BEFORE we mutate the DOM via
|
|
145
|
+
// focus/clear/insertText. We need a stable form-scope ID up-front so the
|
|
146
|
+
// streak detector at Step 6 only fires when both type calls land in the
|
|
147
|
+
// SAME <form>. Probe failures return null and silently suppress the hint.
|
|
148
|
+
const formScopeId = sessionId
|
|
149
|
+
? await probeFormScope(cdpClient, element.objectId, targetSession)
|
|
150
|
+
: null;
|
|
151
|
+
// Step 2: Role check — only for ref-resolved elements (CSS path skips check)
|
|
152
|
+
if (element.resolvedVia === "ref" && element.role && !INPUT_ROLES.has(element.role)) {
|
|
153
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
154
|
+
return {
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: `Element ${params.ref} is not a text input (role: ${element.role}). Expected textbox, searchbox, combobox, or spinbutton.`,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
isError: true,
|
|
162
|
+
_meta: { elapsedMs, method: "type" },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Step 3: Focus the element (use resolved session for OOPIF)
|
|
166
|
+
// Try DOM.focus first, fall back to JS this.focus() for Shadow-DOM/post-mutation nodes (BUG-006)
|
|
167
|
+
try {
|
|
168
|
+
await cdpClient.send("DOM.focus", { backendNodeId: element.backendNodeId }, targetSession);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Fallback: JS focus via Runtime.callFunctionOn (handles Shadow-DOM and stale backendNodeIds)
|
|
172
|
+
try {
|
|
173
|
+
await cdpClient.send("Runtime.callFunctionOn", {
|
|
174
|
+
functionDeclaration: "function() { this.focus(); }",
|
|
175
|
+
objectId: element.objectId,
|
|
176
|
+
silent: true,
|
|
177
|
+
}, targetSession);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
181
|
+
return {
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: "text",
|
|
185
|
+
text: `Could not focus element ${params.ref ?? params.selector}. Element may be hidden or not focusable.`,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
isError: true,
|
|
189
|
+
_meta: { elapsedMs, method: "type" },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Step 4: Clear field if requested (use resolved session for OOPIF)
|
|
194
|
+
if (params.clear) {
|
|
195
|
+
await cdpClient.send("Runtime.callFunctionOn", {
|
|
196
|
+
objectId: element.objectId,
|
|
197
|
+
functionDeclaration: "function() { this.value = ''; this.dispatchEvent(new Event('input', { bubbles: true })); }",
|
|
198
|
+
returnByValue: true,
|
|
199
|
+
}, targetSession);
|
|
200
|
+
}
|
|
201
|
+
// Step 5: Insert text (use resolved session for OOPIF — no auto-settle)
|
|
202
|
+
// Story 16.5: If humanType callback is injected, delegate to it.
|
|
203
|
+
if (params.text.length > 0) {
|
|
204
|
+
if (humanType) {
|
|
205
|
+
await humanType(cdpClient, targetSession, params.text);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
await cdpClient.send("Input.insertText", { text: params.text }, targetSession);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Step 6: Success response
|
|
212
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
213
|
+
const displayName = element.name
|
|
214
|
+
? `${element.role} '${element.name}'`
|
|
215
|
+
: (params.ref ?? params.selector);
|
|
216
|
+
// FR-023: Emit fill_form hint once per streak when the LLM makes consecutive
|
|
217
|
+
// type calls into the SAME form (form-scoped via probeFormScope above).
|
|
218
|
+
const fillFormHint = recordTypeCallAndMaybeHint(sessionId, formScopeId) ?? "";
|
|
219
|
+
return {
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: "text",
|
|
223
|
+
text: `Typed "${truncate(params.text, 50)}" into ${displayName}${fillFormHint}`,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
_meta: { elapsedMs, method: "type", cleared: params.clear, elementClass: params.ref ? a11yTree.classifyRef(params.ref) : "clickable" },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
// RefNotFoundError — contextual error with input-field alternatives
|
|
231
|
+
if (err instanceof RefNotFoundError && params.ref) {
|
|
232
|
+
const errorText = buildRefNotFoundError(params.ref, INPUT_ROLES);
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: errorText }],
|
|
235
|
+
isError: true,
|
|
236
|
+
_meta: { elapsedMs: 0, method: "type" },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// Generic error
|
|
240
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: "text", text: wrapCdpError(err, "type") }],
|
|
243
|
+
isError: true,
|
|
244
|
+
_meta: { elapsedMs, method: "type" },
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CdpClient } from "../cdp/cdp-client.js";
|
|
3
|
+
import type { ToolResponse, ConnectionStatus } from "../types.js";
|
|
4
|
+
import type { TabStateCache } from "../cache/tab-state-cache.js";
|
|
5
|
+
export declare const virtualDeskSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
6
|
+
export type VirtualDeskParams = z.infer<typeof virtualDeskSchema>;
|
|
7
|
+
export declare function virtualDeskHandler(_params: VirtualDeskParams, cdpClient: CdpClient, sessionId: string | undefined, tabStateCache: TabStateCache, connectionStatus?: ConnectionStatus): Promise<ToolResponse>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { wrapCdpError } from "./error-utils.js";
|
|
3
|
+
export const virtualDeskSchema = z.object({});
|
|
4
|
+
function truncateUrl(url, maxLen) {
|
|
5
|
+
if (url.length <= maxLen)
|
|
6
|
+
return url;
|
|
7
|
+
const short = url.replace(/^https?:\/\//, "");
|
|
8
|
+
if (short.length <= maxLen)
|
|
9
|
+
return short;
|
|
10
|
+
return short.slice(0, maxLen - 3) + "...";
|
|
11
|
+
}
|
|
12
|
+
function truncate(text, maxLen) {
|
|
13
|
+
if (text.length <= maxLen)
|
|
14
|
+
return text;
|
|
15
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Infer loading state from CDP target info when no cache entry exists.
|
|
19
|
+
* about:blank or empty title typically indicate a tab that's still loading.
|
|
20
|
+
*/
|
|
21
|
+
function inferLoadingState(target) {
|
|
22
|
+
if (target.url === "" || target.url === "about:blank")
|
|
23
|
+
return "loading";
|
|
24
|
+
if (target.title === "")
|
|
25
|
+
return "loading";
|
|
26
|
+
return "ready";
|
|
27
|
+
}
|
|
28
|
+
export async function virtualDeskHandler(_params, cdpClient, sessionId, tabStateCache, connectionStatus) {
|
|
29
|
+
const start = performance.now();
|
|
30
|
+
const method = "virtual_desk";
|
|
31
|
+
// If disconnected or reconnecting, report status immediately
|
|
32
|
+
if (connectionStatus && connectionStatus !== "connected") {
|
|
33
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: `Connection: ${connectionStatus} — tool calls may fail until reconnected` }],
|
|
36
|
+
isError: true,
|
|
37
|
+
_meta: { elapsedMs, method },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
// Single CDP call for ALL tabs — no N+1 problem
|
|
42
|
+
const { targetInfos } = await cdpClient.send("Target.getTargets");
|
|
43
|
+
const pageTabs = targetInfos.filter((t) => t.type === "page");
|
|
44
|
+
if (pageTabs.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: "No open tabs" }],
|
|
47
|
+
_meta: { elapsedMs: Math.round(performance.now() - start), method },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Fetch window info for each tab (parallel CDP calls)
|
|
51
|
+
const windowInfos = await Promise.all(pageTabs.map(async (tab) => {
|
|
52
|
+
try {
|
|
53
|
+
return await cdpClient.send("Browser.getWindowForTarget", { targetId: tab.targetId });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
// Group tabs by windowId
|
|
60
|
+
const windowMap = new Map();
|
|
61
|
+
for (let i = 0; i < pageTabs.length; i++) {
|
|
62
|
+
const wInfo = windowInfos[i];
|
|
63
|
+
const key = wInfo?.windowId ?? -1;
|
|
64
|
+
if (!windowMap.has(key)) {
|
|
65
|
+
windowMap.set(key, { info: wInfo, tabs: [] });
|
|
66
|
+
}
|
|
67
|
+
windowMap.get(key).tabs.push({ tab: pageTabs[i], index: i });
|
|
68
|
+
}
|
|
69
|
+
// Build output grouped by window
|
|
70
|
+
const activeId = tabStateCache.activeTargetId;
|
|
71
|
+
const lines = [];
|
|
72
|
+
let tabCounter = 0;
|
|
73
|
+
for (const [windowId, group] of windowMap) {
|
|
74
|
+
// Window header with bounds
|
|
75
|
+
if (group.info) {
|
|
76
|
+
const b = group.info.bounds;
|
|
77
|
+
const stateLabel = b.windowState !== "normal" ? ` — ${b.windowState}` : "";
|
|
78
|
+
lines.push(`Window ${windowId} (${b.width}x${b.height} at ${b.left},${b.top}${stateLabel}):`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
lines.push(`Window (unknown):`);
|
|
82
|
+
}
|
|
83
|
+
// Tabs within this window
|
|
84
|
+
for (const { tab } of group.tabs) {
|
|
85
|
+
tabCounter++;
|
|
86
|
+
const isActive = tab.targetId === activeId;
|
|
87
|
+
const cached = tabStateCache.get(tab.targetId);
|
|
88
|
+
const url = truncateUrl(tab.url, 80);
|
|
89
|
+
const title = truncate(cached?.title || tab.title, 40);
|
|
90
|
+
const status = cached?.loadingState ?? inferLoadingState(tab);
|
|
91
|
+
const marker = isActive ? ">" : " ";
|
|
92
|
+
lines.push(`${marker} Tab ${tabCounter}: ${tab.targetId} | ${status} | ${title} | ${url}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
97
|
+
_meta: { elapsedMs: Math.round(performance.now() - start), method, tabCount: pageTabs.length },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const elapsedMs = Math.round(performance.now() - start);
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: wrapCdpError(err, method) }],
|
|
104
|
+
isError: true,
|
|
105
|
+
_meta: { elapsedMs, method },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Shared constants used by dom-snapshot and a11y-tree visual enrichment.
|
|
2
|
+
export const CLICKABLE_TAGS = new Set(["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA"]);
|
|
3
|
+
export const CLICKABLE_ROLES = new Set([
|
|
4
|
+
"button", "link", "checkbox", "radio", "tab", "menuitem",
|
|
5
|
+
"switch", "slider", "option", "treeitem",
|
|
6
|
+
]);
|
|
7
|
+
export const COMPUTED_STYLES = [
|
|
8
|
+
"display", "visibility", "color", "background-color",
|
|
9
|
+
"font-size", "position", "z-index",
|
|
10
|
+
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CdpClient } from "../cdp/cdp-client.js";
|
|
3
|
+
import type { ToolResponse } from "../types.js";
|
|
4
|
+
export declare const waitForSchema: z.ZodObject<{
|
|
5
|
+
condition: z.ZodEnum<["element", "network_idle", "js"]>;
|
|
6
|
+
selector: z.ZodOptional<z.ZodString>;
|
|
7
|
+
expression: z.ZodOptional<z.ZodString>;
|
|
8
|
+
timeout: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
timeout: number;
|
|
11
|
+
condition: "element" | "network_idle" | "js";
|
|
12
|
+
expression?: string | undefined;
|
|
13
|
+
selector?: string | undefined;
|
|
14
|
+
}, {
|
|
15
|
+
condition: "element" | "network_idle" | "js";
|
|
16
|
+
timeout?: number | undefined;
|
|
17
|
+
expression?: string | undefined;
|
|
18
|
+
selector?: string | undefined;
|
|
19
|
+
}>;
|
|
20
|
+
export type WaitForParams = z.infer<typeof waitForSchema>;
|
|
21
|
+
/**
|
|
22
|
+
* Extract the first CSS selector from a querySelector/getElementById call in a JS expression.
|
|
23
|
+
* Returns the CSS selector string, or null if none found.
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractSelector(expression: string): string | null;
|
|
26
|
+
export declare function waitForHandler(params: WaitForParams, cdpClient: CdpClient, sessionId?: string): Promise<ToolResponse>;
|