lsd-pi 1.3.2 → 1.3.6

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 (169) hide show
  1. package/dist/resources/extensions/browser-tools/tools/codegen.js +5 -5
  2. package/dist/resources/extensions/browser-tools/tools/navigation.js +107 -178
  3. package/dist/resources/extensions/browser-tools/tools/network-mock.js +112 -167
  4. package/dist/resources/extensions/browser-tools/tools/pages.js +182 -234
  5. package/dist/resources/extensions/browser-tools/tools/refs.js +202 -461
  6. package/dist/resources/extensions/browser-tools/tools/session.js +176 -323
  7. package/dist/resources/extensions/browser-tools/tools/state-persistence.js +91 -154
  8. package/dist/resources/extensions/browser-tools/utils.js +1 -1
  9. package/dist/resources/extensions/slash-commands/extension-manifest.json +2 -2
  10. package/dist/resources/extensions/slash-commands/fast.js +73 -0
  11. package/dist/resources/extensions/slash-commands/index.js +2 -0
  12. package/dist/resources/extensions/slash-commands/plan.js +37 -12
  13. package/dist/resources/extensions/subagent/background-job-manager.js +13 -0
  14. package/dist/resources/extensions/subagent/in-process-runner.js +387 -0
  15. package/dist/resources/extensions/subagent/index.js +278 -626
  16. package/dist/resources/extensions/subagent/legacy-runner.js +503 -0
  17. package/dist/resources/extensions/voice/index.js +96 -36
  18. package/dist/resources/extensions/voice/push-to-talk.js +26 -0
  19. package/package.json +1 -1
  20. package/packages/pi-agent-core/dist/agent.d.ts +19 -0
  21. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  22. package/packages/pi-agent-core/dist/agent.js +16 -0
  23. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  24. package/packages/pi-agent-core/src/agent.ts +32 -2
  25. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +34 -1
  26. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  27. package/packages/pi-ai/dist/providers/openai-codex-responses.js +32 -4
  28. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  29. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js +127 -16
  30. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js.map +1 -1
  31. package/packages/pi-ai/dist/providers/openai-responses.d.ts +8 -1
  32. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  33. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.d.ts +2 -0
  34. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.d.ts.map +1 -0
  35. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.js +67 -0
  36. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.js.map +1 -0
  37. package/packages/pi-ai/dist/providers/openai-responses.js +21 -3
  38. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  39. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  40. package/packages/pi-ai/dist/providers/simple-options.js +2 -0
  41. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  42. package/packages/pi-ai/dist/types.d.ts +5 -0
  43. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  44. package/packages/pi-ai/dist/types.js.map +1 -1
  45. package/packages/pi-ai/src/providers/openai-codex-responses.test.ts +143 -20
  46. package/packages/pi-ai/src/providers/openai-codex-responses.ts +47 -4
  47. package/packages/pi-ai/src/providers/openai-responses.fast-mode.test.ts +73 -0
  48. package/packages/pi-ai/src/providers/openai-responses.ts +26 -3
  49. package/packages/pi-ai/src/providers/simple-options.ts +2 -0
  50. package/packages/pi-ai/src/types.ts +5 -0
  51. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  52. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  53. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  54. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  55. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  56. package/packages/pi-coding-agent/dist/core/sdk.js +4 -2
  57. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  58. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  59. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  60. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.d.ts +2 -0
  61. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.d.ts.map +1 -0
  62. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.js +35 -0
  63. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.js.map +1 -0
  64. package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
  65. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  68. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -1
  71. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/tool-priority.d.ts +4 -0
  73. package/packages/pi-coding-agent/dist/core/tool-priority.d.ts.map +1 -0
  74. package/packages/pi-coding-agent/dist/core/tool-priority.js +18 -0
  75. package/packages/pi-coding-agent/dist/core/tool-priority.js.map +1 -0
  76. package/packages/pi-coding-agent/dist/core/tool-priority.test.d.ts +2 -0
  77. package/packages/pi-coding-agent/dist/core/tool-priority.test.d.ts.map +1 -0
  78. package/packages/pi-coding-agent/dist/core/tool-priority.test.js +27 -0
  79. package/packages/pi-coding-agent/dist/core/tool-priority.test.js.map +1 -0
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.d.ts +2 -0
  81. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.d.ts.map +1 -0
  82. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +26 -0
  83. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -0
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.d.ts +45 -0
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.js +314 -0
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.d.ts +2 -0
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.d.ts.map +1 -0
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.js +122 -0
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.js.map +1 -0
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts +2 -0
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +7 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +26 -2
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +6 -0
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +18 -4
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +13 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -0
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +49 -0
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -0
  108. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.d.ts +2 -0
  109. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.d.ts.map +1 -0
  110. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +197 -0
  111. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -0
  112. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +97 -0
  114. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +7 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +3 -0
  119. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  122. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +35 -0
  124. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +41 -0
  127. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  128. package/packages/pi-coding-agent/package.json +1 -1
  129. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  130. package/packages/pi-coding-agent/src/core/sdk.ts +4 -2
  131. package/packages/pi-coding-agent/src/core/settings-manager.fast-mode.test.ts +46 -0
  132. package/packages/pi-coding-agent/src/core/settings-manager.ts +18 -0
  133. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  134. package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -1
  135. package/packages/pi-coding-agent/src/core/tool-priority.test.ts +30 -0
  136. package/packages/pi-coding-agent/src/core/tool-priority.ts +17 -0
  137. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +31 -0
  138. package/packages/pi-coding-agent/src/modes/interactive/components/btw-overlay.test.ts +172 -0
  139. package/packages/pi-coding-agent/src/modes/interactive/components/btw-overlay.ts +402 -0
  140. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +8 -0
  141. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +32 -2
  142. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +1154 -1136
  143. package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +64 -0
  144. package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +228 -0
  145. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +494 -398
  146. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +7 -0
  147. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +3 -0
  148. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +38 -0
  149. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +60 -1
  150. package/pkg/package.json +1 -1
  151. package/src/resources/extensions/browser-tools/tools/codegen.ts +5 -5
  152. package/src/resources/extensions/browser-tools/tools/navigation.ts +118 -196
  153. package/src/resources/extensions/browser-tools/tools/network-mock.ts +114 -205
  154. package/src/resources/extensions/browser-tools/tools/pages.ts +183 -237
  155. package/src/resources/extensions/browser-tools/tools/refs.ts +193 -507
  156. package/src/resources/extensions/browser-tools/tools/session.ts +182 -321
  157. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +94 -172
  158. package/src/resources/extensions/browser-tools/utils.ts +1 -1
  159. package/src/resources/extensions/slash-commands/extension-manifest.json +2 -2
  160. package/src/resources/extensions/slash-commands/fast.ts +89 -0
  161. package/src/resources/extensions/slash-commands/index.ts +2 -0
  162. package/src/resources/extensions/slash-commands/plan.ts +42 -12
  163. package/src/resources/extensions/subagent/background-job-manager.ts +28 -0
  164. package/src/resources/extensions/subagent/in-process-runner.ts +534 -0
  165. package/src/resources/extensions/subagent/index.ts +489 -799
  166. package/src/resources/extensions/subagent/legacy-runner.ts +607 -0
  167. package/src/resources/extensions/voice/index.ts +308 -238
  168. package/src/resources/extensions/voice/push-to-talk.ts +42 -0
  169. package/src/resources/extensions/voice/tests/push-to-talk.test.ts +109 -0
@@ -14,84 +14,39 @@ import {
14
14
 
15
15
  export function registerPageTools(pi: ExtensionAPI, deps: ToolDeps): void {
16
16
  // -------------------------------------------------------------------------
17
- // browser_list_pages
17
+ // browser_pages — list, switch, close tabs
18
18
  // -------------------------------------------------------------------------
19
19
  pi.registerTool({
20
- name: "browser_list_pages",
21
- label: "Browser List Pages",
20
+ name: "browser_pages",
21
+ label: "Browser Pages",
22
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.",
23
+ "Manage browser tabs: list open pages, switch to a page by ID, or close a page by ID. " +
24
+ "Cannot close the last remaining page — use browser_close for that.",
72
25
  parameters: Type.Object({
73
- id: Type.Number({ description: "Page ID to switch to (from browser_list_pages)" }),
26
+ action: Type.Union([
27
+ Type.Literal("list"),
28
+ Type.Literal("switch"),
29
+ Type.Literal("close"),
30
+ ], { description: "'list' — show all pages, 'switch' — activate a page by ID, 'close' — close a page by ID" }),
31
+ id: Type.Optional(Type.Number({ description: "Page ID (required for switch/close, from list action)." })),
74
32
  }),
75
33
 
76
34
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
77
35
  try {
78
36
  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
- };
37
+
38
+ if (params.action === "list") {
39
+ return await listPages();
40
+ } else if (params.action === "switch") {
41
+ if (!params.id) return { content: [{ type: "text" as const, text: "Switch requires 'id' parameter." }], details: { error: "missing_id" }, isError: true };
42
+ return await switchPage(params.id);
43
+ } else {
44
+ if (!params.id) return { content: [{ type: "text" as const, text: "Close requires 'id' parameter." }], details: { error: "missing_id" }, isError: true };
45
+ return await closePage(params.id);
46
+ }
92
47
  } catch (err: any) {
93
48
  return {
94
- content: [{ type: "text", text: `Switch page failed: ${err.message}` }],
49
+ content: [{ type: "text" as const, text: `Pages action '${params.action}' failed: ${err.message}` }],
95
50
  details: { error: err.message },
96
51
  isError: true,
97
52
  };
@@ -100,57 +55,36 @@ export function registerPageTools(pi: ExtensionAPI, deps: ToolDeps): void {
100
55
  });
101
56
 
102
57
  // -------------------------------------------------------------------------
103
- // browser_close_page
58
+ // browser_frames — list and select iframes
104
59
  // -------------------------------------------------------------------------
105
60
  pi.registerTool({
106
- name: "browser_close_page",
107
- label: "Browser Close Page",
61
+ name: "browser_frames",
62
+ label: "Browser Frames",
108
63
  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.",
64
+ "Manage browser frames (iframes): list all frames in the active page, or select a frame to operate on. " +
65
+ "Pass action='select' with name, urlPattern, or index. Use name='main' to reset to main page.",
110
66
  parameters: Type.Object({
111
- id: Type.Number({ description: "Page ID to close (from browser_list_pages)" }),
67
+ action: Type.Union([
68
+ Type.Literal("list"),
69
+ Type.Literal("select"),
70
+ ], { description: "'list' — show all frames, 'select' — activate a frame" }),
71
+ name: Type.Optional(Type.String({ description: "Frame name to select. Use 'main' to reset to main frame." })),
72
+ urlPattern: Type.Optional(Type.String({ description: "URL substring to match for frame selection." })),
73
+ index: Type.Optional(Type.Number({ description: "Frame index from list action." })),
112
74
  }),
113
75
 
114
76
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
115
77
  try {
116
78
  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 { /* non-fatal — page may have been closed or navigated away */ }
79
+
80
+ if (params.action === "list") {
81
+ return await listFrames();
82
+ } else {
83
+ return await selectFrame(params);
141
84
  }
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
85
  } catch (err: any) {
152
86
  return {
153
- content: [{ type: "text", text: `Close page failed: ${err.message}` }],
87
+ content: [{ type: "text" as const, text: `Frames action '${params.action}' failed: ${err.message}` }],
154
88
  details: { error: err.message },
155
89
  isError: true,
156
90
  };
@@ -158,146 +92,158 @@ export function registerPageTools(pi: ExtensionAPI, deps: ToolDeps): void {
158
92
  },
159
93
  });
160
94
 
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({}),
95
+ // ── page helpers ──
170
96
 
171
- async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
97
+ async function listPages() {
98
+ const pageRegistry = getPageRegistry();
99
+ for (const entry of pageRegistry.pages) {
172
100
  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
- });
101
+ entry.title = await entry.page.title();
102
+ entry.url = entry.page.url();
103
+ } catch { /* Page may have been closed */ }
104
+ }
105
+ const pages = registryListPages(pageRegistry);
106
+ if (pages.length === 0) {
107
+ return { content: [{ type: "text" as const, text: "No pages open." }], details: { pages: [], count: 0 } };
108
+ }
109
+ const lines = pages.map((p: any) => {
110
+ const active = p.isActive ? " ← active" : "";
111
+ const opener = p.opener !== null ? ` (opener: ${p.opener})` : "";
112
+ return ` [${p.id}] ${p.title || "(untitled)"} — ${p.url}${opener}${active}`;
113
+ });
114
+ return {
115
+ content: [{ type: "text" as const, text: `${pages.length} page(s):\n${lines.join("\n")}` }],
116
+ details: { pages, count: pages.length },
117
+ };
118
+ }
210
119
 
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
- }),
120
+ async function switchPage(id: number) {
121
+ const pageRegistry = getPageRegistry();
122
+ registrySetActive(pageRegistry, id);
123
+ setActiveFrame(null);
124
+ const entry = registryGetActive(pageRegistry);
125
+ await entry.page.bringToFront();
126
+ const title = await entry.page.title().catch(() => "");
127
+ const url = entry.page.url();
128
+ entry.title = title;
129
+ entry.url = url;
130
+ return {
131
+ content: [{ type: "text" as const, text: `Switched to page ${id}: ${title || "(untitled)"} — ${url}` }],
132
+ details: { id, title, url },
133
+ };
134
+ }
224
135
 
225
- async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
136
+ async function closePage(id: number) {
137
+ const pageRegistry = getPageRegistry();
138
+ if (pageRegistry.pages.length <= 1) {
139
+ return {
140
+ content: [{ type: "text" as const, text: "Cannot close the last remaining page. Use browser_close to close the entire browser." }],
141
+ details: { error: "last_page", pageCount: pageRegistry.pages.length },
142
+ isError: true,
143
+ };
144
+ }
145
+ const entry = pageRegistry.pages.find((e: any) => e.id === id);
146
+ if (!entry) {
147
+ const available = pageRegistry.pages.map((e: any) => e.id);
148
+ return {
149
+ content: [{ type: "text" as const, text: `Page ${id} not found. Available page IDs: [${available.join(", ")}].` }],
150
+ details: { error: "not_found", available },
151
+ isError: true,
152
+ };
153
+ }
154
+ await entry.page.close();
155
+ setActiveFrame(null);
156
+ for (const remaining of pageRegistry.pages) {
226
157
  try {
227
- await deps.ensureBrowser();
228
- const p = deps.getActivePage();
229
- const frames = p.frames();
158
+ remaining.title = await remaining.page.title();
159
+ remaining.url = remaining.page.url();
160
+ } catch { /* non-fatal */ }
161
+ }
162
+ const pages = registryListPages(pageRegistry);
163
+ const lines = pages.map((p: any) => {
164
+ const active = p.isActive ? " ← active" : "";
165
+ return ` [${p.id}] ${p.title || "(untitled)"} — ${p.url}${active}`;
166
+ });
167
+ return {
168
+ content: [{ type: "text" as const, text: `Closed page ${id}. ${pages.length} page(s) remaining:\n${lines.join("\n")}` }],
169
+ details: { closedId: id, pages, count: pages.length },
170
+ };
171
+ }
230
172
 
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
- }
173
+ // ── frame helpers ──
238
174
 
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
- }
175
+ async function listFrames() {
176
+ const p = deps.getActivePage();
177
+ const frames = p.frames();
178
+ const mainFrame = p.mainFrame();
179
+ const activeFrame = getActiveFrame();
180
+ const frameList = frames.map((f, index) => {
181
+ const isMain = f === mainFrame;
182
+ const parentName = f.parentFrame()?.name() || (f.parentFrame() === mainFrame ? "main" : "");
183
+ return {
184
+ index,
185
+ name: f.name() || (isMain ? "main" : `(unnamed-${index})`),
186
+ url: f.url(),
187
+ isMain,
188
+ parentName: isMain ? null : (parentName || "main"),
189
+ isActive: f === activeFrame,
190
+ };
191
+ });
192
+ const lines = frameList.map((f) => {
193
+ const main = f.isMain ? " [main]" : "";
194
+ const active = f.isActive ? " ← selected" : "";
195
+ const parent = f.parentName ? ` (parent: ${f.parentName})` : "";
196
+ return ` [${f.index}] "${f.name}" — ${f.url}${main}${parent}${active}`;
197
+ });
198
+ const activeInfo = activeFrame ? `Active frame: "${activeFrame.name() || "(unnamed)"}"` : "No frame selected (operating on main page)";
199
+ return {
200
+ content: [{ type: "text" as const, text: `${frameList.length} frame(s) in active page:\n${lines.join("\n")}\n\n${activeInfo}` }],
201
+ details: { frames: frameList, count: frameList.length, activeFrame: activeFrame?.name() ?? null },
202
+ };
203
+ }
255
204
 
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
- }
205
+ async function selectFrame(params: { name?: string; urlPattern?: string; index?: number }) {
206
+ const p = deps.getActivePage();
207
+ const frames = p.frames();
272
208
 
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
- }
209
+ if (params.name === "main" || params.name === "null" || params.name === null) {
210
+ setActiveFrame(null);
211
+ return { content: [{ type: "text" as const, text: "Reset to main page frame." }], details: { activeFrame: null } };
212
+ }
288
213
 
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
- };
214
+ if (params.name) {
215
+ const frame = frames.find((f) => f.name() === params.name);
216
+ if (!frame) {
217
+ const available = frames.map((f, i) => `[${i}] "${f.name() || "(unnamed)"}" — ${f.url()}`);
218
+ return { content: [{ type: "text" as const, text: `Frame "${params.name}" not found.\n${available.join("\n ")}` }], details: { error: "not_found" }, isError: true };
300
219
  }
301
- },
302
- });
220
+ setActiveFrame(frame);
221
+ return { content: [{ type: "text" as const, text: `Selected frame "${frame.name()}" — ${frame.url()}` }], details: { name: frame.name(), url: frame.url() } };
222
+ }
223
+
224
+ if (params.urlPattern) {
225
+ const frame = frames.find((f) => f.url().includes(params.urlPattern!));
226
+ if (!frame) {
227
+ const available = frames.map((f, i) => `[${i}] "${f.name() || "(unnamed)"}" — ${f.url()}`);
228
+ return { content: [{ type: "text" as const, text: `No frame URL matches "${params.urlPattern}".\n${available.join("\n ")}` }], details: { error: "not_found" }, isError: true };
229
+ }
230
+ setActiveFrame(frame);
231
+ return { content: [{ type: "text" as const, text: `Selected frame "${frame.name() || "(unnamed)"}" — ${frame.url()}` }], details: { name: frame.name(), url: frame.url() } };
232
+ }
233
+
234
+ if (params.index !== undefined) {
235
+ if (params.index < 0 || params.index >= frames.length) {
236
+ return { content: [{ type: "text" as const, text: `Frame index ${params.index} out of range (0-${frames.length - 1}).` }], details: { error: "index_out_of_range" }, isError: true };
237
+ }
238
+ const frame = frames[params.index];
239
+ setActiveFrame(frame);
240
+ return { content: [{ type: "text" as const, text: `Selected frame [${params.index}] "${frame.name() || "(unnamed)"}" — ${frame.url()}` }], details: { index: params.index, name: frame.name(), url: frame.url() } };
241
+ }
242
+
243
+ return {
244
+ content: [{ type: "text" as const, text: "Provide name, urlPattern, or index to select a frame. Use name='main' to reset." }],
245
+ details: { error: "no_criteria" },
246
+ isError: true,
247
+ };
248
+ }
303
249
  }