From e8650dc566bd0655348e1458772486eaf0ea90d6 Mon Sep 17 00:00:00 2001 From: amankumarsingh77 Date: Sat, 2 May 2026 16:28:15 +0530 Subject: feat(domains): migrate tab tools (list, current, switch, new) --- src/domains/tabs.ts | 89 ++++++++++++++++++++++++++++++ src/registry.ts | 5 ++ src/tools.ts | 153 ---------------------------------------------------- 3 files changed, 94 insertions(+), 153 deletions(-) create mode 100644 src/domains/tabs.ts (limited to 'src') 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> { + 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> { + 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> { + 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> { + 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 = [ clickTool, @@ -26,6 +27,10 @@ const TOOLS: ReadonlyArray = [ 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 -- cgit v1.2.3