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.
Files changed (53) hide show
  1. package/README.md +12 -5
  2. package/dist/loader.js +0 -0
  3. package/dist/modes/interactive/theme/dark.json +85 -0
  4. package/dist/modes/interactive/theme/light.json +84 -0
  5. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  6. package/dist/modes/interactive/theme/theme.d.ts +78 -0
  7. package/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  8. package/dist/modes/interactive/theme/theme.js +949 -0
  9. package/dist/modes/interactive/theme/theme.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  11. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  12. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  13. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  14. package/node_modules/cliui/CHANGELOG.md +121 -0
  15. package/node_modules/color-convert/CHANGELOG.md +54 -0
  16. package/node_modules/esprima/ChangeLog +235 -0
  17. package/node_modules/mz/HISTORY.md +66 -0
  18. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  19. package/node_modules/source-map/CHANGELOG.md +301 -0
  20. package/node_modules/thenify/History.md +11 -0
  21. package/node_modules/thenify-all/History.md +11 -0
  22. package/node_modules/y18n/CHANGELOG.md +100 -0
  23. package/node_modules/yargs/CHANGELOG.md +88 -0
  24. package/node_modules/yargs-parser/CHANGELOG.md +263 -0
  25. package/package.json +5 -2
  26. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +1 -1
  28. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  29. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +1 -1
  30. package/src/resources/extensions/browser-tools/capture.ts +165 -0
  31. package/src/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
  32. package/src/resources/extensions/browser-tools/index.ts +47 -4985
  33. package/src/resources/extensions/browser-tools/lifecycle.ts +265 -0
  34. package/src/resources/extensions/browser-tools/package.json +5 -1
  35. package/src/resources/extensions/browser-tools/refs.ts +264 -0
  36. package/src/resources/extensions/browser-tools/settle.ts +197 -0
  37. package/src/resources/extensions/browser-tools/state.ts +408 -0
  38. package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
  39. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
  40. package/src/resources/extensions/browser-tools/tools/assertions.ts +342 -0
  41. package/src/resources/extensions/browser-tools/tools/forms.ts +801 -0
  42. package/src/resources/extensions/browser-tools/tools/inspection.ts +492 -0
  43. package/src/resources/extensions/browser-tools/tools/intent.ts +614 -0
  44. package/src/resources/extensions/browser-tools/tools/interaction.ts +865 -0
  45. package/src/resources/extensions/browser-tools/tools/navigation.ts +232 -0
  46. package/src/resources/extensions/browser-tools/tools/pages.ts +303 -0
  47. package/src/resources/extensions/browser-tools/tools/refs.ts +541 -0
  48. package/src/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
  49. package/src/resources/extensions/browser-tools/tools/session.ts +400 -0
  50. package/src/resources/extensions/browser-tools/tools/wait.ts +247 -0
  51. package/src/resources/extensions/browser-tools/utils.ts +660 -0
  52. package/src/resources/extensions/gsd/git-service.ts +3 -0
  53. package/src/resources/extensions/shared/interview-ui.ts +1 -1
@@ -0,0 +1,232 @@
1
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import {
4
+ diffCompactStates,
5
+ } from "../core.js";
6
+ import type { ToolDeps, CompactPageState } from "../state.js";
7
+ import {
8
+ setLastActionBeforeState,
9
+ setLastActionAfterState,
10
+ } from "../state.js";
11
+
12
+ export function registerNavigationTools(pi: ExtensionAPI, deps: ToolDeps): void {
13
+ // -------------------------------------------------------------------------
14
+ // browser_navigate
15
+ // -------------------------------------------------------------------------
16
+ pi.registerTool({
17
+ name: "browser_navigate",
18
+ label: "Browser Navigate",
19
+ description:
20
+ "Open the browser (if not already open) and navigate to a URL. Waits for network idle. Returns page title and current URL. Use ONLY for visually verifying locally-running web apps (e.g. http://localhost:3000). Do NOT use for documentation sites, GitHub, search results, or any external URL — use web_search instead. Screenshots are only captured when the `screenshot` parameter is set to true.",
21
+ parameters: Type.Object({
22
+ url: Type.String({ description: "URL to navigate to, e.g. http://localhost:3000" }),
23
+ screenshot: Type.Optional(Type.Boolean({ description: "Capture and return a screenshot (default: false)", default: false })),
24
+ }),
25
+
26
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
27
+ let actionId: number | null = null;
28
+ let beforeState: CompactPageState | null = null;
29
+ try {
30
+ const { page: p } = await deps.ensureBrowser();
31
+ beforeState = await deps.captureCompactPageState(p, { includeBodyText: true });
32
+ actionId = deps.beginTrackedAction("browser_navigate", params, beforeState.url).id;
33
+ await p.goto(params.url, { waitUntil: "domcontentloaded", timeout: 30000 });
34
+ await p.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {});
35
+ await new Promise(resolve => setTimeout(resolve, 300));
36
+
37
+ const title = await p.title();
38
+ const url = p.url();
39
+ const viewport = p.viewportSize();
40
+ const vpText = viewport ? `${viewport.width}x${viewport.height}` : "unknown";
41
+ const afterState = await deps.captureCompactPageState(p, { includeBodyText: true });
42
+ const summary = deps.formatCompactStateSummary(afterState);
43
+ const jsErrors = deps.getRecentErrors(p.url());
44
+ const diff = diffCompactStates(beforeState, afterState);
45
+ setLastActionBeforeState(beforeState);
46
+ setLastActionAfterState(afterState);
47
+ deps.finishTrackedAction(actionId, {
48
+ status: "success",
49
+ afterUrl: afterState.url,
50
+ warningSummary: jsErrors.trim() || undefined,
51
+ diffSummary: diff.summary,
52
+ changed: diff.changed,
53
+ beforeState,
54
+ afterState,
55
+ });
56
+
57
+ let screenshotContent: any[] = [];
58
+ if (params.screenshot) {
59
+ try {
60
+ let buf = await p.screenshot({ type: "jpeg", quality: 80, scale: "css" });
61
+ buf = await deps.constrainScreenshot(p, buf, "image/jpeg", 80);
62
+ screenshotContent = [{ type: "image", data: buf.toString("base64"), mimeType: "image/jpeg" }];
63
+ } catch {}
64
+ }
65
+
66
+ return {
67
+ content: [
68
+ { type: "text", text: `Navigated to: ${url}\nTitle: ${title}\nViewport: ${vpText}\nAction: ${actionId}${jsErrors}\n\nDiff:\n${deps.formatDiffText(diff)}\n\nPage summary:\n${summary}` },
69
+ ...screenshotContent,
70
+ ],
71
+ details: { title, url, status: "loaded", viewport: vpText, actionId, diff },
72
+ };
73
+ } catch (err: any) {
74
+ if (actionId !== null) {
75
+ deps.finishTrackedAction(actionId, { status: "error", afterUrl: deps.getActivePageOrNull()?.url() ?? "", error: err.message, beforeState: beforeState ?? undefined });
76
+ }
77
+ const errorShot = await deps.captureErrorScreenshot(deps.getActivePageOrNull());
78
+ const content: any[] = [{ type: "text", text: `Navigation failed: ${err.message}` }];
79
+ if (errorShot) {
80
+ content.push({ type: "image", data: errorShot.data, mimeType: errorShot.mimeType });
81
+ }
82
+ return {
83
+ content,
84
+ details: { status: "error", error: err.message, actionId },
85
+ isError: true,
86
+ };
87
+ }
88
+ },
89
+ });
90
+
91
+ // -------------------------------------------------------------------------
92
+ // browser_go_back
93
+ // -------------------------------------------------------------------------
94
+ pi.registerTool({
95
+ name: "browser_go_back",
96
+ label: "Browser Go Back",
97
+ description: "Navigate back in browser history. Returns a compact page summary after navigation.",
98
+ parameters: Type.Object({}),
99
+
100
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
101
+ try {
102
+ const { page: p } = await deps.ensureBrowser();
103
+ const response = await p.goBack({ waitUntil: "domcontentloaded", timeout: 10000 });
104
+
105
+ if (!response) {
106
+ return {
107
+ content: [{ type: "text", text: "No previous page in history." }],
108
+ details: {},
109
+ isError: true,
110
+ };
111
+ }
112
+
113
+ await p.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {});
114
+
115
+ const title = await p.title();
116
+ const url = p.url();
117
+ const summary = await deps.postActionSummary(p);
118
+ const jsErrors = deps.getRecentErrors(p.url());
119
+
120
+ return {
121
+ content: [{ type: "text", text: `Navigated back to: ${url}\nTitle: ${title}${jsErrors}\n\nPage summary:\n${summary}` }],
122
+ details: { title, url },
123
+ };
124
+ } catch (err: any) {
125
+ const errorShot = await deps.captureErrorScreenshot(deps.getActivePageOrNull());
126
+ const content: any[] = [{ type: "text", text: `Go back failed: ${err.message}` }];
127
+ if (errorShot) {
128
+ content.push({ type: "image", data: errorShot.data, mimeType: errorShot.mimeType });
129
+ }
130
+ return { content, details: { error: err.message }, isError: true };
131
+ }
132
+ },
133
+ });
134
+
135
+ // -------------------------------------------------------------------------
136
+ // browser_go_forward
137
+ // -------------------------------------------------------------------------
138
+ pi.registerTool({
139
+ name: "browser_go_forward",
140
+ label: "Browser Go Forward",
141
+ description: "Navigate forward in browser history. Returns a compact page summary after navigation.",
142
+ parameters: Type.Object({}),
143
+
144
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
145
+ try {
146
+ const { page: p } = await deps.ensureBrowser();
147
+ const response = await p.goForward({ waitUntil: "domcontentloaded", timeout: 10000 });
148
+
149
+ if (!response) {
150
+ return {
151
+ content: [{ type: "text", text: "No forward page in history." }],
152
+ details: {},
153
+ isError: true,
154
+ };
155
+ }
156
+
157
+ await p.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {});
158
+
159
+ const title = await p.title();
160
+ const url = p.url();
161
+ const summary = await deps.postActionSummary(p);
162
+ const jsErrors = deps.getRecentErrors(p.url());
163
+
164
+ return {
165
+ content: [{ type: "text", text: `Navigated forward to: ${url}\nTitle: ${title}${jsErrors}\n\nPage summary:\n${summary}` }],
166
+ details: { title, url },
167
+ };
168
+ } catch (err: any) {
169
+ const errorShot = await deps.captureErrorScreenshot(deps.getActivePageOrNull());
170
+ const content: any[] = [{ type: "text", text: `Go forward failed: ${err.message}` }];
171
+ if (errorShot) {
172
+ content.push({ type: "image", data: errorShot.data, mimeType: errorShot.mimeType });
173
+ }
174
+ return { content, details: { error: err.message }, isError: true };
175
+ }
176
+ },
177
+ });
178
+
179
+ // -------------------------------------------------------------------------
180
+ // browser_reload
181
+ // -------------------------------------------------------------------------
182
+ pi.registerTool({
183
+ name: "browser_reload",
184
+ label: "Browser Reload",
185
+ description: "Reload the current page. Returns a screenshot, compact page summary, and page metadata (same shape as browser_navigate).",
186
+ parameters: Type.Object({}),
187
+
188
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
189
+ try {
190
+ const { page: p } = await deps.ensureBrowser();
191
+ await p.reload({ waitUntil: "domcontentloaded", timeout: 30000 });
192
+ await p.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => {});
193
+
194
+ const title = await p.title();
195
+ const url = p.url();
196
+ const viewport = p.viewportSize();
197
+ const vpText = viewport ? `${viewport.width}x${viewport.height}` : "unknown";
198
+ const summary = await deps.postActionSummary(p);
199
+ const jsErrors = deps.getRecentErrors(p.url());
200
+
201
+ let screenshotContent: any[] = [];
202
+ try {
203
+ let buf = await p.screenshot({ type: "jpeg", quality: 80, scale: "css" });
204
+ buf = await deps.constrainScreenshot(p, buf, "image/jpeg", 80);
205
+ screenshotContent = [{
206
+ type: "image",
207
+ data: buf.toString("base64"),
208
+ mimeType: "image/jpeg",
209
+ }];
210
+ } catch {}
211
+
212
+ return {
213
+ content: [
214
+ {
215
+ type: "text",
216
+ text: `Reloaded: ${url}\nTitle: ${title}\nViewport: ${vpText}${jsErrors}\n\nPage summary:\n${summary}`,
217
+ },
218
+ ...screenshotContent,
219
+ ],
220
+ details: { title, url, viewport: vpText },
221
+ };
222
+ } catch (err: any) {
223
+ const errorShot = await deps.captureErrorScreenshot(deps.getActivePageOrNull());
224
+ const content: any[] = [{ type: "text", text: `Reload failed: ${err.message}` }];
225
+ if (errorShot) {
226
+ content.push({ type: "image", data: errorShot.data, mimeType: errorShot.mimeType });
227
+ }
228
+ return { content, details: { error: err.message }, isError: true };
229
+ }
230
+ },
231
+ });
232
+ }
@@ -0,0 +1,303 @@
1
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import {
4
+ registryGetActive,
5
+ registryListPages,
6
+ registrySetActive,
7
+ } from "../core.js";
8
+ import type { ToolDeps } from "../state.js";
9
+ import {
10
+ getPageRegistry,
11
+ getActiveFrame,
12
+ setActiveFrame,
13
+ } from "../state.js";
14
+
15
+ export function registerPageTools(pi: ExtensionAPI, deps: ToolDeps): void {
16
+ // -------------------------------------------------------------------------
17
+ // browser_list_pages
18
+ // -------------------------------------------------------------------------
19
+ pi.registerTool({
20
+ name: "browser_list_pages",
21
+ label: "Browser List Pages",
22
+ description:
23
+ "List all open browser pages/tabs with their IDs, titles, URLs, and active status. Use to see what pages are available before switching.",
24
+ parameters: Type.Object({}),
25
+
26
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
27
+ try {
28
+ await deps.ensureBrowser();
29
+ const pageRegistry = getPageRegistry();
30
+ for (const entry of pageRegistry.pages) {
31
+ try {
32
+ entry.title = await entry.page.title();
33
+ entry.url = entry.page.url();
34
+ } catch {
35
+ // Page may have been closed
36
+ }
37
+ }
38
+ const pages = registryListPages(pageRegistry);
39
+ if (pages.length === 0) {
40
+ return {
41
+ content: [{ type: "text", text: "No pages open." }],
42
+ details: { pages: [], count: 0 },
43
+ };
44
+ }
45
+ const lines = pages.map((p: any) => {
46
+ const active = p.isActive ? " ← active" : "";
47
+ const opener = p.opener !== null ? ` (opener: ${p.opener})` : "";
48
+ return ` [${p.id}] ${p.title || "(untitled)"} — ${p.url}${opener}${active}`;
49
+ });
50
+ return {
51
+ content: [{ type: "text", text: `${pages.length} page(s):\n${lines.join("\n")}` }],
52
+ details: { pages, count: pages.length },
53
+ };
54
+ } catch (err: any) {
55
+ return {
56
+ content: [{ type: "text", text: `List pages failed: ${err.message}` }],
57
+ details: { error: err.message },
58
+ isError: true,
59
+ };
60
+ }
61
+ },
62
+ });
63
+
64
+ // -------------------------------------------------------------------------
65
+ // browser_switch_page
66
+ // -------------------------------------------------------------------------
67
+ pi.registerTool({
68
+ name: "browser_switch_page",
69
+ label: "Browser Switch Page",
70
+ description:
71
+ "Switch the active browser page/tab by page ID. Use browser_list_pages to see available IDs. Clears any active frame selection.",
72
+ parameters: Type.Object({
73
+ id: Type.Number({ description: "Page ID to switch to (from browser_list_pages)" }),
74
+ }),
75
+
76
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
77
+ try {
78
+ await deps.ensureBrowser();
79
+ const pageRegistry = getPageRegistry();
80
+ registrySetActive(pageRegistry, params.id);
81
+ setActiveFrame(null);
82
+ const entry = registryGetActive(pageRegistry);
83
+ await entry.page.bringToFront();
84
+ const title = await entry.page.title().catch(() => "");
85
+ const url = entry.page.url();
86
+ entry.title = title;
87
+ entry.url = url;
88
+ return {
89
+ content: [{ type: "text", text: `Switched to page ${params.id}: ${title || "(untitled)"} — ${url}` }],
90
+ details: { id: params.id, title, url },
91
+ };
92
+ } catch (err: any) {
93
+ return {
94
+ content: [{ type: "text", text: `Switch page failed: ${err.message}` }],
95
+ details: { error: err.message },
96
+ isError: true,
97
+ };
98
+ }
99
+ },
100
+ });
101
+
102
+ // -------------------------------------------------------------------------
103
+ // browser_close_page
104
+ // -------------------------------------------------------------------------
105
+ pi.registerTool({
106
+ name: "browser_close_page",
107
+ label: "Browser Close Page",
108
+ description:
109
+ "Close a specific browser page/tab by ID. Cannot close the last remaining page. The page's close event triggers automatic registry cleanup and active-page fallback.",
110
+ parameters: Type.Object({
111
+ id: Type.Number({ description: "Page ID to close (from browser_list_pages)" }),
112
+ }),
113
+
114
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
115
+ try {
116
+ await deps.ensureBrowser();
117
+ const pageRegistry = getPageRegistry();
118
+ if (pageRegistry.pages.length <= 1) {
119
+ return {
120
+ content: [{ type: "text", text: `Cannot close the last remaining page. Use browser_close to close the entire browser.` }],
121
+ details: { error: "last_page", pageCount: pageRegistry.pages.length },
122
+ isError: true,
123
+ };
124
+ }
125
+ const entry = pageRegistry.pages.find((e: any) => e.id === params.id);
126
+ if (!entry) {
127
+ const available = pageRegistry.pages.map((e: any) => e.id);
128
+ return {
129
+ content: [{ type: "text", text: `Page ${params.id} not found. Available page IDs: [${available.join(", ")}].` }],
130
+ details: { error: "not_found", available },
131
+ isError: true,
132
+ };
133
+ }
134
+ await entry.page.close();
135
+ setActiveFrame(null);
136
+ for (const remaining of pageRegistry.pages) {
137
+ try {
138
+ remaining.title = await remaining.page.title();
139
+ remaining.url = remaining.page.url();
140
+ } catch {}
141
+ }
142
+ const pages = registryListPages(pageRegistry);
143
+ const lines = pages.map((p: any) => {
144
+ const active = p.isActive ? " ← active" : "";
145
+ return ` [${p.id}] ${p.title || "(untitled)"} — ${p.url}${active}`;
146
+ });
147
+ return {
148
+ content: [{ type: "text", text: `Closed page ${params.id}. ${pages.length} page(s) remaining:\n${lines.join("\n")}` }],
149
+ details: { closedId: params.id, pages, count: pages.length },
150
+ };
151
+ } catch (err: any) {
152
+ return {
153
+ content: [{ type: "text", text: `Close page failed: ${err.message}` }],
154
+ details: { error: err.message },
155
+ isError: true,
156
+ };
157
+ }
158
+ },
159
+ });
160
+
161
+ // -------------------------------------------------------------------------
162
+ // browser_list_frames
163
+ // -------------------------------------------------------------------------
164
+ pi.registerTool({
165
+ name: "browser_list_frames",
166
+ label: "Browser List Frames",
167
+ description:
168
+ "List all frames in the active page, including the main frame and any iframes. Shows frame name, URL, and parent frame name. Use before browser_select_frame to identify available frames.",
169
+ parameters: Type.Object({}),
170
+
171
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
172
+ try {
173
+ await deps.ensureBrowser();
174
+ const p = deps.getActivePage();
175
+ const frames = p.frames();
176
+ const mainFrame = p.mainFrame();
177
+ const activeFrame = getActiveFrame();
178
+ const frameList = frames.map((f, index) => {
179
+ const isMain = f === mainFrame;
180
+ const parentName = f.parentFrame()?.name() || (f.parentFrame() === mainFrame ? "main" : "");
181
+ return {
182
+ index,
183
+ name: f.name() || (isMain ? "main" : `(unnamed-${index})`),
184
+ url: f.url(),
185
+ isMain,
186
+ parentName: isMain ? null : (parentName || "main"),
187
+ isActive: f === activeFrame,
188
+ };
189
+ });
190
+ const lines = frameList.map((f) => {
191
+ const main = f.isMain ? " [main]" : "";
192
+ const active = f.isActive ? " ← selected" : "";
193
+ const parent = f.parentName ? ` (parent: ${f.parentName})` : "";
194
+ return ` [${f.index}] "${f.name}" — ${f.url}${main}${parent}${active}`;
195
+ });
196
+ const activeInfo = activeFrame ? `Active frame: "${activeFrame.name() || "(unnamed)"}"` : "No frame selected (operating on main page)";
197
+ return {
198
+ content: [{ type: "text", text: `${frameList.length} frame(s) in active page:\n${lines.join("\n")}\n\n${activeInfo}` }],
199
+ details: { frames: frameList, count: frameList.length, activeFrame: activeFrame?.name() ?? null },
200
+ };
201
+ } catch (err: any) {
202
+ return {
203
+ content: [{ type: "text", text: `List frames failed: ${err.message}` }],
204
+ details: { error: err.message },
205
+ isError: true,
206
+ };
207
+ }
208
+ },
209
+ });
210
+
211
+ // -------------------------------------------------------------------------
212
+ // browser_select_frame
213
+ // -------------------------------------------------------------------------
214
+ pi.registerTool({
215
+ name: "browser_select_frame",
216
+ label: "Browser Select Frame",
217
+ description:
218
+ "Select a frame within the active page to operate on. Find frames by name, URL pattern, or index. Pass null or \"main\" to reset back to the main page frame. Once a frame is selected, tools like browser_evaluate, browser_find, and browser_click will operate within that frame (after T03 migration).",
219
+ parameters: Type.Object({
220
+ name: Type.Optional(Type.String({ description: "Frame name to select. Use 'main' or 'null' to reset to main frame." })),
221
+ urlPattern: Type.Optional(Type.String({ description: "URL substring to match against frame URLs." })),
222
+ index: Type.Optional(Type.Number({ description: "Frame index from browser_list_frames." })),
223
+ }),
224
+
225
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
226
+ try {
227
+ await deps.ensureBrowser();
228
+ const p = deps.getActivePage();
229
+ const frames = p.frames();
230
+
231
+ if (params.name === "main" || params.name === "null" || params.name === null) {
232
+ setActiveFrame(null);
233
+ return {
234
+ content: [{ type: "text", text: "Reset to main page frame. Tools will operate on the main page." }],
235
+ details: { activeFrame: null },
236
+ };
237
+ }
238
+
239
+ if (params.name) {
240
+ const frame = frames.find((f) => f.name() === params.name);
241
+ if (!frame) {
242
+ const available = frames.map((f, i) => `[${i}] "${f.name() || "(unnamed)"}" — ${f.url()}`);
243
+ return {
244
+ content: [{ type: "text", text: `Frame with name "${params.name}" not found.\nAvailable frames:\n ${available.join("\n ")}` }],
245
+ details: { error: "frame_not_found", available },
246
+ isError: true,
247
+ };
248
+ }
249
+ setActiveFrame(frame);
250
+ return {
251
+ content: [{ type: "text", text: `Selected frame "${frame.name()}" — ${frame.url()}` }],
252
+ details: { name: frame.name(), url: frame.url() },
253
+ };
254
+ }
255
+
256
+ if (params.urlPattern) {
257
+ const frame = frames.find((f) => f.url().includes(params.urlPattern!));
258
+ if (!frame) {
259
+ const available = frames.map((f, i) => `[${i}] "${f.name() || "(unnamed)"}" — ${f.url()}`);
260
+ return {
261
+ content: [{ type: "text", text: `No frame URL matches "${params.urlPattern}".\nAvailable frames:\n ${available.join("\n ")}` }],
262
+ details: { error: "frame_not_found", available },
263
+ isError: true,
264
+ };
265
+ }
266
+ setActiveFrame(frame);
267
+ return {
268
+ content: [{ type: "text", text: `Selected frame "${frame.name() || "(unnamed)"}" — ${frame.url()}` }],
269
+ details: { name: frame.name(), url: frame.url() },
270
+ };
271
+ }
272
+
273
+ if (params.index !== undefined) {
274
+ if (params.index < 0 || params.index >= frames.length) {
275
+ return {
276
+ content: [{ type: "text", text: `Frame index ${params.index} out of range. ${frames.length} frame(s) available (0-${frames.length - 1}).` }],
277
+ details: { error: "index_out_of_range", count: frames.length },
278
+ isError: true,
279
+ };
280
+ }
281
+ const frame = frames[params.index];
282
+ setActiveFrame(frame);
283
+ return {
284
+ content: [{ type: "text", text: `Selected frame [${params.index}] "${frame.name() || "(unnamed)"}" — ${frame.url()}` }],
285
+ details: { index: params.index, name: frame.name(), url: frame.url() },
286
+ };
287
+ }
288
+
289
+ return {
290
+ content: [{ type: "text", text: "Provide name, urlPattern, or index to select a frame. Use name='main' to reset to main frame." }],
291
+ details: { error: "no_criteria" },
292
+ isError: true,
293
+ };
294
+ } catch (err: any) {
295
+ return {
296
+ content: [{ type: "text", text: `Select frame failed: ${err.message}` }],
297
+ details: { error: err.message },
298
+ isError: true,
299
+ };
300
+ }
301
+ },
302
+ });
303
+ }