aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/domains/tabs.ts89
-rw-r--r--src/registry.ts5
-rw-r--r--src/tools.ts153
3 files changed, 94 insertions, 153 deletions
diff --git a/src/domains/tabs.ts b/src/domains/tabs.ts
new file mode 100644
index 0000000..c9440fe
--- /dev/null
+++ b/src/domains/tabs.ts
@@ -0,0 +1,89 @@
+import { Type } from "typebox";
+import { type Result, err, ok } from "../util/result";
+import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool";
+
+const ListTabsArgs = Type.Object({
+ includeInternal: Type.Optional(Type.Boolean({ default: true, description: "Include chrome:// pages" })),
+});
+
+export const listTabsTool = defineBrowserTool({
+ name: "browser_list_tabs",
+ label: "Browser List Tabs",
+ description: "List all open browser tabs (page targets).",
+ promptSnippet: "List browser tabs",
+ promptGuidelines: [
+ "Use to find a targetId for browser_switch_tab.",
+ "Internal tabs (chrome://) included by default; pass includeInternal=false to exclude them.",
+ ],
+ parameters: ListTabsArgs,
+ async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
+ const r = await client.listTabs(args.includeInternal ?? true);
+ if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
+ const lines = r.data.map((t, i) => ` [${i}] ${t.targetId.slice(0, 8)}… ${t.url}\n ${t.title}`);
+ return ok({
+ text: `Tabs (${r.data.length}):\n${lines.join("\n")}`,
+ details: { tabs: r.data },
+ });
+ },
+});
+
+export const currentTabTool = defineBrowserTool({
+ name: "browser_current_tab",
+ label: "Browser Current Tab",
+ description: "Get info about the currently attached tab.",
+ promptSnippet: "Get current tab info",
+ promptGuidelines: ["Returns targetId, url, title."],
+ parameters: Type.Object({}),
+ async handler(_a, { client }): Promise<Result<ToolOk, ToolErr>> {
+ const cur = client.current();
+ if (!cur) return err({ kind: "invalid_state", message: "No tab attached." });
+ const ti = await client.session().callBrowser("Target.getTargetInfo", { targetId: cur.targetId });
+ if (!ti.success) return err({ kind: "cdp_error", message: ti.error.message });
+ const info = (ti.data as { targetInfo: { targetId: string; url: string; title: string } }).targetInfo; // CDP boundary cast: Target.getTargetInfo returns { targetInfo: { targetId, url, title } }
+ return ok({
+ text: `Current tab:\n ${info.targetId}\n ${info.url}\n ${info.title}`,
+ details: { targetId: info.targetId, url: info.url, title: info.title },
+ });
+ },
+});
+
+const SwitchTabArgs = Type.Object({
+ targetId: Type.String({ description: "Target ID from browser_list_tabs" }),
+});
+
+export const switchTabTool = defineBrowserTool({
+ name: "browser_switch_tab",
+ label: "Browser Switch Tab",
+ description: "Switch to and attach to a different tab by targetId.",
+ promptSnippet: "Switch tabs",
+ promptGuidelines: ["Get a targetId via browser_list_tabs first."],
+ parameters: SwitchTabArgs,
+ async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
+ const r = await client.switchTab(args.targetId);
+ if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
+ return ok({ text: `Switched to ${args.targetId}`, details: { targetId: args.targetId } });
+ },
+});
+
+const NewTabArgs = Type.Object({
+ url: Type.Optional(Type.String({ description: "Optional URL to navigate to" })),
+});
+
+export const newTabTool = defineBrowserTool({
+ name: "browser_new_tab",
+ label: "Browser New Tab",
+ description: "Open a new tab and switch to it. Optionally navigate to a URL.",
+ promptSnippet: "Open a new tab",
+ promptGuidelines: ["Pass url to navigate immediately."],
+ parameters: NewTabArgs,
+ async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> {
+ const r = await client.newTab(args.url);
+ if (!r.success) return err({ kind: "cdp_error", message: r.error.message });
+ return ok({
+ text: `New tab: ${r.data}${args.url ? ` (${args.url})` : ""}`,
+ details: args.url !== undefined
+ ? { targetId: r.data, url: args.url }
+ : { targetId: r.data },
+ });
+ },
+});
diff --git a/src/registry.ts b/src/registry.ts
index 839ccba..02a8c8e 100644
--- a/src/registry.ts
+++ b/src/registry.ts
@@ -9,6 +9,7 @@ import { handleDialogTool } from "./domains/dialog";
import { screenshotTool } from "./domains/screenshot";
import { navigateTool, openUrlsTool } from "./domains/navigate";
import { goBackTool, goForwardTool, reloadTool } from "./domains/history";
+import { listTabsTool, currentTabTool, switchTabTool, newTabTool } from "./domains/tabs";
const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
clickTool,
@@ -26,6 +27,10 @@ const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
goBackTool,
goForwardTool,
reloadTool,
+ listTabsTool,
+ currentTabTool,
+ switchTabTool,
+ newTabTool,
];
export const registerAllTools = (pi: ExtensionAPI, client: BrowserClient): void => {
diff --git a/src/tools.ts b/src/tools.ts
index 77023cd..581a7ca 100644
--- a/src/tools.ts
+++ b/src/tools.ts
@@ -136,159 +136,6 @@ export function registerTools(pi: ExtensionAPI, daemon: BrowserDaemon): void {
});
// ═══════════════════════════════════════════════════════════════════════════
- // browser_list_tabs
- // ═══════════════════════════════════════════════════════════════════════════
- pi.registerTool({
- name: "browser_list_tabs",
- label: "Browser List Tabs",
- description: "List all open browser tabs.",
- promptSnippet: "List all open browser tabs with URLs and titles",
- promptGuidelines: [
- "Use browser_list_tabs to see what tabs are open before switching.",
- "Use browser_switch_tab to switch to a specific tab by targetId.",
- "Set includeChrome: true to also see Chrome internal pages (devtools, settings, etc).",
- ],
- parameters: Type.Object({
- includeChrome: Type.Optional(Type.Boolean({ description: "Include Chrome internal pages (chrome://, devtools://, etc). Default: true" })),
- }),
- async execute(_id, params) {
- try {
- await daemon.ensureAlive();
- const tabs = await daemon.listTabs(params.includeChrome !== false);
- const listing = tabs
- .map((t, i) => `[${i}] ${t.title || "(no title)"}\n URL: ${t.url}\n ID: ${t.targetId}`)
- .join("\n\n");
- return {
- content: [{ type: "text" as const, text: `${tabs.length} tab(s):\n\n${listing}` }],
- details: { tabs },
- };
- } catch (err) {
- return {
- isError: true,
- content: [{ type: "text" as const, text: `Failed: ${err instanceof Error ? err.message : String(err)}` }],
- details: undefined,
- };
- }
- },
- renderResult(result, { expanded }, theme) {
- const details = result.details as {
- tabs?: TabInfo[];
- fullOutputPath?: string;
- } | undefined;
-
- const tabCount = details?.tabs?.length ?? 0;
-
- if (expanded) {
- // Expanded (CTRL+O): show full tab listing
- const content = result.content[0];
- const raw = content?.type === "text" ? content.text : "";
- return new Text(raw || "(no tabs)", 0, 0);
- }
-
- // Collapsed (default): compact count
- return new Text(
- theme ? theme.fg("dim", `📋 ${tabCount} tab${tabCount !== 1 ? "s" : ""}`) : `📋 ${tabCount} tab${tabCount !== 1 ? "s" : ""}`,
- 0,
- 0,
- );
- },
- });
-
- // ═══════════════════════════════════════════════════════════════════════════
- // browser_current_tab
- // ═══════════════════════════════════════════════════════════════════════════
- pi.registerTool({
- name: "browser_current_tab",
- label: "Browser Current Tab",
- description: "Get info about the currently active tab.",
- promptSnippet: "Get the current active tab's URL, title, and ID",
- promptGuidelines: [
- "Use browser_current_tab to confirm which tab you're controlling.",
- ],
- parameters: Type.Object({}),
- async execute() {
- try {
- await daemon.ensureAlive();
- const tab = await daemon.currentTab();
- return {
- content: [{ type: "text" as const, text: `Current tab: ${tab.title || "(no title)"}\nURL: ${tab.url}\nID: ${tab.targetId}` }],
- details: undefined,
- };
- } catch (err) {
- return {
- isError: true,
- content: [{ type: "text" as const, text: `Failed: ${err instanceof Error ? err.message : String(err)}` }],
- details: undefined,
- };
- }
- },
- });
-
- // ═══════════════════════════════════════════════════════════════════════════
- // browser_switch_tab
- // ═══════════════════════════════════════════════════════════════════════════
- pi.registerTool({
- name: "browser_switch_tab",
- label: "Browser Switch Tab",
- description: "Switch to a different browser tab by targetId (from browser_list_tabs).",
- promptSnippet: "Switch to a browser tab by its targetId",
- promptGuidelines: [
- "Use browser_list_tabs first to find the targetId, then browser_switch_tab to switch.",
- ],
- parameters: Type.Object({
- targetId: Type.String({ description: "Target ID of the tab to switch to (from browser_list_tabs)" }),
- }),
- async execute(_id, params) {
- try {
- await daemon.ensureAlive();
- await daemon.switchTab(params.targetId);
- const tab = await daemon.currentTab();
- return {
- content: [{ type: "text" as const, text: `Switched to: ${tab.title || "(no title)"}\nURL: ${tab.url}` }],
- details: undefined,
- };
- } catch (err) {
- return {
- isError: true,
- content: [{ type: "text" as const, text: `Switch failed: ${err instanceof Error ? err.message : String(err)}` }],
- details: undefined,
- };
- }
- },
- });
-
- // ═══════════════════════════════════════════════════════════════════════════
- // browser_new_tab
- // ═══════════════════════════════════════════════════════════════════════════
- pi.registerTool({
- name: "browser_new_tab",
- label: "Browser New Tab",
- description: "Open a new browser tab and switch to it. Optionally navigate to a URL.",
- promptSnippet: "Open a new browser tab, optionally navigate to a URL",
- promptGuidelines: [
- "Use browser_new_tab for the first navigation in a task — it preserves the user's active tab.",
- "Use browser_navigate for subsequent navigation in the same tab.",
- ],
- parameters: Type.Object({
- url: Type.Optional(Type.String({ description: "URL to navigate to in the new tab. Default: about:blank" })),
- }),
- async execute(_id, params) {
- try {
- await daemon.ensureAlive();
- const targetId = await daemon.newTab(params.url);
- const msg = params.url
- ? `New tab created and navigated to: ${params.url}\nTarget: ${targetId}`
- : `New tab created: about:blank\nTarget: ${targetId}`;
- return { content: [{ type: "text" as const, text: msg }], details: undefined };
- } catch (err) {
- return {
- isError: true,
- content: [{ type: "text" as const, text: `Failed: ${err instanceof Error ? err.message : String(err)}` }],
- details: undefined,
- };
- }
- },
- });
// ═══════════════════════════════════════════════════════════════════════════
// browser_execute_js