gsd-pi 2.7.1 → 2.8.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/README.md +12 -5
- package/dist/loader.js +0 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/dist/modes/interactive/theme/theme.js +949 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/node_modules/cliui/CHANGELOG.md +121 -0
- package/node_modules/color-convert/CHANGELOG.md +54 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/node_modules/mz/HISTORY.md +66 -0
- package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
- package/node_modules/source-map/CHANGELOG.md +301 -0
- package/node_modules/thenify/History.md +11 -0
- package/node_modules/thenify-all/History.md +11 -0
- package/node_modules/y18n/CHANGELOG.md +100 -0
- package/node_modules/yargs/CHANGELOG.md +88 -0
- package/node_modules/yargs-parser/CHANGELOG.md +263 -0
- package/package.json +5 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
- package/src/resources/extensions/browser-tools/capture.ts +165 -0
- package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
- package/src/resources/extensions/browser-tools/index.ts +47 -4985
- package/src/resources/extensions/browser-tools/lifecycle.ts +265 -0
- package/src/resources/extensions/browser-tools/package.json +5 -1
- package/src/resources/extensions/browser-tools/refs.ts +264 -0
- package/src/resources/extensions/browser-tools/settle.ts +197 -0
- package/src/resources/extensions/browser-tools/state.ts +408 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
- package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
- package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
- package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
- package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
- package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
- package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
- package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
- package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
- package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
- package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
- package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
- package/src/resources/extensions/browser-tools/utils.ts +660 -0
- package/src/resources/extensions/gsd/git-service.ts +3 -0
- package/src/resources/extensions/shared/interview-ui.ts +1 -1
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@gsd/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import { StringEnum } from "@gsd/pi-ai";
|
|
4
|
+
import type { ToolDeps } from "../state.js";
|
|
5
|
+
import {
|
|
6
|
+
getConsoleLogs,
|
|
7
|
+
setConsoleLogs,
|
|
8
|
+
getNetworkLogs,
|
|
9
|
+
setNetworkLogs,
|
|
10
|
+
getDialogLogs,
|
|
11
|
+
setDialogLogs,
|
|
12
|
+
} from "../state.js";
|
|
13
|
+
|
|
14
|
+
export function registerInspectionTools(pi: ExtensionAPI, deps: ToolDeps): void {
|
|
15
|
+
// -------------------------------------------------------------------------
|
|
16
|
+
// browser_get_console_logs
|
|
17
|
+
// -------------------------------------------------------------------------
|
|
18
|
+
pi.registerTool({
|
|
19
|
+
name: "browser_get_console_logs",
|
|
20
|
+
label: "Browser Console Logs",
|
|
21
|
+
description:
|
|
22
|
+
"Get all buffered browser console logs and JavaScript errors captured since the last clear. Each entry includes timestamp and page URL. Note: JS errors are also auto-surfaced in interaction tool responses — use this for the full log.",
|
|
23
|
+
parameters: Type.Object({
|
|
24
|
+
clear: Type.Optional(
|
|
25
|
+
Type.Boolean({
|
|
26
|
+
description: "Clear the buffer after returning logs (default: true)",
|
|
27
|
+
})
|
|
28
|
+
),
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
32
|
+
const shouldClear = params.clear !== false;
|
|
33
|
+
const logs = [...getConsoleLogs()];
|
|
34
|
+
|
|
35
|
+
if (shouldClear) {
|
|
36
|
+
setConsoleLogs([]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (logs.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: "No console logs captured." }],
|
|
42
|
+
details: { logs: [], count: 0 },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const formatted = logs
|
|
47
|
+
.map((entry) => {
|
|
48
|
+
const time = new Date(entry.timestamp).toISOString().slice(11, 23);
|
|
49
|
+
return `[${time}] [${entry.type.toUpperCase()}] ${entry.text}`;
|
|
50
|
+
})
|
|
51
|
+
.join("\n");
|
|
52
|
+
|
|
53
|
+
const truncated = deps.truncateText(formatted);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: `${logs.length} console log(s):\n\n${truncated}`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
details: { logs, count: logs.length },
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
// browser_get_network_logs
|
|
69
|
+
// -------------------------------------------------------------------------
|
|
70
|
+
pi.registerTool({
|
|
71
|
+
name: "browser_get_network_logs",
|
|
72
|
+
label: "Browser Network Logs",
|
|
73
|
+
description:
|
|
74
|
+
"Get buffered network requests and responses. Shows method, URL, status code, and resource type for all requests. Includes response body for failed requests (4xx/5xx). Use to debug API failures, CORS issues, missing resources, and auth problems.",
|
|
75
|
+
parameters: Type.Object({
|
|
76
|
+
clear: Type.Optional(
|
|
77
|
+
Type.Boolean({
|
|
78
|
+
description: "Clear the buffer after returning logs (default: true)",
|
|
79
|
+
})
|
|
80
|
+
),
|
|
81
|
+
filter: Type.Optional(
|
|
82
|
+
StringEnum(["all", "errors", "fetch-xhr"] as const)
|
|
83
|
+
),
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
87
|
+
const shouldClear = params.clear !== false;
|
|
88
|
+
let logs = [...getNetworkLogs()];
|
|
89
|
+
|
|
90
|
+
if (shouldClear) {
|
|
91
|
+
setNetworkLogs([]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (params.filter === "errors") {
|
|
95
|
+
logs = logs.filter(e => e.failed || (e.status !== null && e.status >= 400));
|
|
96
|
+
} else if (params.filter === "fetch-xhr") {
|
|
97
|
+
logs = logs.filter(e => e.resourceType === "fetch" || e.resourceType === "xhr");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (logs.length === 0) {
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: "text", text: "No network requests captured." }],
|
|
103
|
+
details: { logs: [], count: 0 },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const formatted = logs
|
|
108
|
+
.map((entry) => {
|
|
109
|
+
const time = new Date(entry.timestamp).toISOString().slice(11, 23);
|
|
110
|
+
const status = entry.failed
|
|
111
|
+
? `FAILED (${entry.failureText})`
|
|
112
|
+
: `${entry.status}`;
|
|
113
|
+
let line = `[${time}] ${entry.method} ${entry.url} → ${status} (${entry.resourceType})`;
|
|
114
|
+
if (entry.responseBody) {
|
|
115
|
+
line += `\n Response: ${entry.responseBody}`;
|
|
116
|
+
}
|
|
117
|
+
return line;
|
|
118
|
+
})
|
|
119
|
+
.join("\n");
|
|
120
|
+
|
|
121
|
+
const truncated = deps.truncateText(formatted);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `${logs.length} network request(s):\n\n${truncated}`,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
details: { count: logs.length },
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// -------------------------------------------------------------------------
|
|
136
|
+
// browser_get_dialog_logs
|
|
137
|
+
// -------------------------------------------------------------------------
|
|
138
|
+
pi.registerTool({
|
|
139
|
+
name: "browser_get_dialog_logs",
|
|
140
|
+
label: "Browser Dialog Logs",
|
|
141
|
+
description:
|
|
142
|
+
"Get buffered JavaScript dialog events (alert, confirm, prompt, beforeunload). Dialogs are auto-accepted to prevent page freezes. Use this to see what dialogs appeared and their messages.",
|
|
143
|
+
parameters: Type.Object({
|
|
144
|
+
clear: Type.Optional(
|
|
145
|
+
Type.Boolean({
|
|
146
|
+
description: "Clear the buffer after returning logs (default: true)",
|
|
147
|
+
})
|
|
148
|
+
),
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
152
|
+
const shouldClear = params.clear !== false;
|
|
153
|
+
const logs = [...getDialogLogs()];
|
|
154
|
+
|
|
155
|
+
if (shouldClear) {
|
|
156
|
+
setDialogLogs([]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (logs.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: "No dialog events captured." }],
|
|
162
|
+
details: { logs: [], count: 0 },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const formatted = logs
|
|
167
|
+
.map((entry) => {
|
|
168
|
+
const time = new Date(entry.timestamp).toISOString().slice(11, 23);
|
|
169
|
+
let line = `[${time}] ${entry.type}: "${entry.message}"`;
|
|
170
|
+
if (entry.defaultValue) {
|
|
171
|
+
line += ` (default: "${entry.defaultValue}")`;
|
|
172
|
+
}
|
|
173
|
+
line += ` → auto-accepted`;
|
|
174
|
+
return line;
|
|
175
|
+
})
|
|
176
|
+
.join("\n");
|
|
177
|
+
|
|
178
|
+
const truncated = deps.truncateText(formatted);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: `${logs.length} dialog(s):\n\n${truncated}`,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
details: { logs, count: logs.length },
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// -------------------------------------------------------------------------
|
|
193
|
+
// browser_evaluate
|
|
194
|
+
// -------------------------------------------------------------------------
|
|
195
|
+
pi.registerTool({
|
|
196
|
+
name: "browser_evaluate",
|
|
197
|
+
label: "Browser Evaluate",
|
|
198
|
+
description:
|
|
199
|
+
"Execute a JavaScript expression in the browser context and return the result. Useful for reading DOM state, checking values, etc.",
|
|
200
|
+
parameters: Type.Object({
|
|
201
|
+
expression: Type.String({
|
|
202
|
+
description: "JavaScript expression to evaluate in the page context",
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
|
|
206
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
207
|
+
try {
|
|
208
|
+
await deps.ensureBrowser();
|
|
209
|
+
const target = deps.getActiveTarget();
|
|
210
|
+
const result = await target.evaluate(params.expression);
|
|
211
|
+
|
|
212
|
+
let serialized: string;
|
|
213
|
+
if (result === undefined) {
|
|
214
|
+
serialized = "undefined";
|
|
215
|
+
} else {
|
|
216
|
+
try {
|
|
217
|
+
serialized = JSON.stringify(result, null, 2) ?? "undefined";
|
|
218
|
+
} catch {
|
|
219
|
+
serialized = `[non-serializable: ${typeof result}]`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const truncated = deps.truncateText(serialized);
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: "text", text: truncated }],
|
|
226
|
+
details: { expression: params.expression },
|
|
227
|
+
};
|
|
228
|
+
} catch (err: any) {
|
|
229
|
+
return {
|
|
230
|
+
content: [
|
|
231
|
+
{
|
|
232
|
+
type: "text",
|
|
233
|
+
text: `Evaluation failed: ${err.message}`,
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
details: { error: err.message },
|
|
237
|
+
isError: true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// -------------------------------------------------------------------------
|
|
244
|
+
// browser_get_accessibility_tree
|
|
245
|
+
// -------------------------------------------------------------------------
|
|
246
|
+
pi.registerTool({
|
|
247
|
+
name: "browser_get_accessibility_tree",
|
|
248
|
+
label: "Browser Accessibility Tree",
|
|
249
|
+
description:
|
|
250
|
+
"Get the accessibility tree of the current page as structured text. Shows roles, names, labels, values, and states of all interactive elements. Use this to understand page structure before clicking — it reveals buttons, inputs, links, and their labels without needing to guess CSS selectors or coordinates. Much more reliable than inspecting the DOM directly.",
|
|
251
|
+
parameters: Type.Object({
|
|
252
|
+
selector: Type.Optional(
|
|
253
|
+
Type.String({
|
|
254
|
+
description:
|
|
255
|
+
"Scope the accessibility tree to a specific element by CSS selector (e.g. 'main', 'form', '#modal'). If omitted, returns the full page tree.",
|
|
256
|
+
})
|
|
257
|
+
),
|
|
258
|
+
}),
|
|
259
|
+
|
|
260
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
261
|
+
try {
|
|
262
|
+
const { page: p } = await deps.ensureBrowser();
|
|
263
|
+
const target = deps.getActiveTarget();
|
|
264
|
+
|
|
265
|
+
let snapshot: string;
|
|
266
|
+
if (params.selector) {
|
|
267
|
+
const locator = target.locator(params.selector).first();
|
|
268
|
+
snapshot = await locator.ariaSnapshot();
|
|
269
|
+
} else {
|
|
270
|
+
snapshot = await target.locator("body").ariaSnapshot();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const truncated = deps.truncateText(snapshot);
|
|
274
|
+
const scope = params.selector ? `element "${params.selector}"` : "full page";
|
|
275
|
+
const viewport = p.viewportSize();
|
|
276
|
+
const vpText = viewport ? `${viewport.width}x${viewport.height}` : "unknown";
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: "text",
|
|
282
|
+
text: `Accessibility tree for ${scope} (viewport: ${vpText}):\n\n${truncated}`,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
details: { scope, snapshot, viewport: vpText },
|
|
286
|
+
};
|
|
287
|
+
} catch (err: any) {
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: "text",
|
|
292
|
+
text: `Accessibility tree failed: ${err.message}`,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
details: { error: err.message },
|
|
296
|
+
isError: true,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// -------------------------------------------------------------------------
|
|
303
|
+
// browser_find
|
|
304
|
+
// -------------------------------------------------------------------------
|
|
305
|
+
pi.registerTool({
|
|
306
|
+
name: "browser_find",
|
|
307
|
+
label: "Browser Find",
|
|
308
|
+
description:
|
|
309
|
+
"Find elements on the page by text content, ARIA role, or CSS selector. Returns only the matched nodes as a compact accessibility snapshot — far cheaper than browser_get_accessibility_tree. Use this after any action to locate a specific button, input, heading, or link before clicking it.",
|
|
310
|
+
promptGuidelines: [
|
|
311
|
+
"Use browser_find for cheap targeted discovery before requesting the full accessibility tree.",
|
|
312
|
+
"Prefer browser_find when you need one button, input, heading, dialog, or alert rather than a full-page structure dump.",
|
|
313
|
+
],
|
|
314
|
+
parameters: Type.Object({
|
|
315
|
+
text: Type.Optional(
|
|
316
|
+
Type.String({
|
|
317
|
+
description: "Find elements whose visible text contains this string (case-insensitive).",
|
|
318
|
+
})
|
|
319
|
+
),
|
|
320
|
+
role: Type.Optional(
|
|
321
|
+
Type.String({
|
|
322
|
+
description: "ARIA role to filter by, e.g. 'button', 'link', 'heading', 'textbox', 'dialog', 'alert'.",
|
|
323
|
+
})
|
|
324
|
+
),
|
|
325
|
+
selector: Type.Optional(
|
|
326
|
+
Type.String({
|
|
327
|
+
description: "CSS selector to scope the search. If omitted, searches the full page.",
|
|
328
|
+
})
|
|
329
|
+
),
|
|
330
|
+
limit: Type.Optional(
|
|
331
|
+
Type.Number({
|
|
332
|
+
description: "Maximum number of results to return (default: 20).",
|
|
333
|
+
})
|
|
334
|
+
),
|
|
335
|
+
}),
|
|
336
|
+
|
|
337
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
338
|
+
try {
|
|
339
|
+
await deps.ensureBrowser();
|
|
340
|
+
const target = deps.getActiveTarget();
|
|
341
|
+
const limit = params.limit ?? 20;
|
|
342
|
+
|
|
343
|
+
const results = await target.evaluate(({ text, role, selector, limit }) => {
|
|
344
|
+
const root = selector ? document.querySelector(selector) : document.body;
|
|
345
|
+
if (!root) return [];
|
|
346
|
+
|
|
347
|
+
let candidates: Element[];
|
|
348
|
+
if (role) {
|
|
349
|
+
const roleMap: Record<string, string> = {
|
|
350
|
+
button: 'button,[role="button"]',
|
|
351
|
+
link: 'a[href],[role="link"]',
|
|
352
|
+
heading: 'h1,h2,h3,h4,h5,h6,[role="heading"]',
|
|
353
|
+
textbox: 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]),textarea,[role="textbox"]',
|
|
354
|
+
checkbox: 'input[type="checkbox"],[role="checkbox"]',
|
|
355
|
+
radio: 'input[type="radio"],[role="radio"]',
|
|
356
|
+
combobox: 'select,[role="combobox"]',
|
|
357
|
+
dialog: 'dialog,[role="dialog"]',
|
|
358
|
+
alert: '[role="alert"]',
|
|
359
|
+
navigation: 'nav,[role="navigation"]',
|
|
360
|
+
listitem: 'li,[role="listitem"]',
|
|
361
|
+
};
|
|
362
|
+
const cssForRole = roleMap[role.toLowerCase()] ?? `[role="${role}"]`;
|
|
363
|
+
candidates = Array.from(root.querySelectorAll(cssForRole));
|
|
364
|
+
} else {
|
|
365
|
+
candidates = Array.from(root.querySelectorAll('*'));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (text) {
|
|
369
|
+
const lower = text.toLowerCase();
|
|
370
|
+
candidates = candidates.filter(el =>
|
|
371
|
+
(el.textContent ?? "").toLowerCase().includes(lower) ||
|
|
372
|
+
(el.getAttribute("aria-label") ?? "").toLowerCase().includes(lower) ||
|
|
373
|
+
(el.getAttribute("placeholder") ?? "").toLowerCase().includes(lower) ||
|
|
374
|
+
(el.getAttribute("value") ?? "").toLowerCase().includes(lower)
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return candidates.slice(0, limit).map(el => {
|
|
379
|
+
const tag = el.tagName.toLowerCase();
|
|
380
|
+
const id = el.id ? `#${el.id}` : "";
|
|
381
|
+
const classes = Array.from(el.classList).slice(0, 2).map(c => `.${c}`).join("");
|
|
382
|
+
const ariaLabel = el.getAttribute("aria-label") ?? "";
|
|
383
|
+
const placeholder = el.getAttribute("placeholder") ?? "";
|
|
384
|
+
const textContent = (el.textContent ?? "").trim().slice(0, 80);
|
|
385
|
+
const role = el.getAttribute("role") ?? "";
|
|
386
|
+
const type = el.getAttribute("type") ?? "";
|
|
387
|
+
const href = el.getAttribute("href") ?? "";
|
|
388
|
+
const value = (el as HTMLInputElement).value ?? "";
|
|
389
|
+
|
|
390
|
+
return { tag, id, classes, ariaLabel, placeholder, textContent, role, type, href, value };
|
|
391
|
+
});
|
|
392
|
+
}, { text: params.text, role: params.role, selector: params.selector, limit });
|
|
393
|
+
|
|
394
|
+
if (results.length === 0) {
|
|
395
|
+
return {
|
|
396
|
+
content: [{ type: "text", text: "No elements found matching the criteria." }],
|
|
397
|
+
details: { count: 0 },
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const lines = results.map((r: any) => {
|
|
402
|
+
const parts: string[] = [`${r.tag}${r.id}${r.classes}`];
|
|
403
|
+
if (r.role) parts.push(`role="${r.role}"`);
|
|
404
|
+
if (r.type) parts.push(`type="${r.type}"`);
|
|
405
|
+
if (r.ariaLabel) parts.push(`aria-label="${r.ariaLabel}"`);
|
|
406
|
+
if (r.placeholder) parts.push(`placeholder="${r.placeholder}"`);
|
|
407
|
+
if (r.href) parts.push(`href="${r.href.slice(0, 60)}"`);
|
|
408
|
+
if (r.value) parts.push(`value="${r.value.slice(0, 40)}"`);
|
|
409
|
+
if (r.textContent && !r.ariaLabel) parts.push(`"${r.textContent}"`);
|
|
410
|
+
return " " + parts.join(" ");
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const criteria: string[] = [];
|
|
414
|
+
if (params.role) criteria.push(`role="${params.role}"`);
|
|
415
|
+
if (params.text) criteria.push(`text="${params.text}"`);
|
|
416
|
+
if (params.selector) criteria.push(`within="${params.selector}"`);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
content: [
|
|
420
|
+
{
|
|
421
|
+
type: "text",
|
|
422
|
+
text: `Found ${results.length} element(s) [${criteria.join(", ")}]:\n${lines.join("\n")}`,
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
details: { count: results.length, results },
|
|
426
|
+
};
|
|
427
|
+
} catch (err: any) {
|
|
428
|
+
return {
|
|
429
|
+
content: [{ type: "text", text: `Find failed: ${err.message}` }],
|
|
430
|
+
details: { error: err.message },
|
|
431
|
+
isError: true,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// -------------------------------------------------------------------------
|
|
438
|
+
// browser_get_page_source
|
|
439
|
+
// -------------------------------------------------------------------------
|
|
440
|
+
pi.registerTool({
|
|
441
|
+
name: "browser_get_page_source",
|
|
442
|
+
label: "Browser Page Source",
|
|
443
|
+
description:
|
|
444
|
+
"Get the current HTML source of the page (or a specific element). Use when you need to inspect the actual DOM structure — verify semantic HTML, check that elements rendered correctly, debug why a selector isn't matching, or audit accessibility markup. Output is truncated for large pages.",
|
|
445
|
+
parameters: Type.Object({
|
|
446
|
+
selector: Type.Optional(
|
|
447
|
+
Type.String({
|
|
448
|
+
description:
|
|
449
|
+
"CSS selector to scope the output to a specific element (e.g. 'main', 'form', '#app'). If omitted, returns the full page HTML.",
|
|
450
|
+
})
|
|
451
|
+
),
|
|
452
|
+
}),
|
|
453
|
+
|
|
454
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
455
|
+
try {
|
|
456
|
+
await deps.ensureBrowser();
|
|
457
|
+
const target = deps.getActiveTarget();
|
|
458
|
+
|
|
459
|
+
let html: string;
|
|
460
|
+
if (params.selector) {
|
|
461
|
+
html = await target.locator(params.selector).first().evaluate((el: Element) => el.outerHTML);
|
|
462
|
+
} else {
|
|
463
|
+
html = await target.content();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const truncated = deps.truncateText(html);
|
|
467
|
+
const scope = params.selector ? `element "${params.selector}"` : "full page";
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: "text",
|
|
473
|
+
text: `HTML source of ${scope}:\n\n${truncated}`,
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
details: { scope },
|
|
477
|
+
};
|
|
478
|
+
} catch (err: any) {
|
|
479
|
+
return {
|
|
480
|
+
content: [
|
|
481
|
+
{
|
|
482
|
+
type: "text",
|
|
483
|
+
text: `Get page source failed: ${err.message}`,
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
details: { error: err.message },
|
|
487
|
+
isError: true,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
}
|