all repos — sz-chrome-remote-cdp @ 3dfc4f1150b80bb9a5e332999f7440b6255c4ec0

pi-browser-harness, patched to drive a Chrome on another host over CDP, with castle_cdp_search web search

feat(domains): migrate navigate (NavOutcome) and open_urls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
amankumarsingh77 amankumarsingh7702@gmal.com
Sat, 02 May 2026 16:24:23 +0530
commit

3dfc4f1150b80bb9a5e332999f7440b6255c4ec0

parent

6708da54b1805756908dea84bf65fe73ab03426a

3 files changed, 148 insertions(+), 289 deletions(-)

jump to
A src/domains/navigate.ts

@@ -0,0 +1,145 @@

+import { Type } from "typebox"; +import { Text } from "@mariozechner/pi-tui"; +import { type Result, err, ok } from "../util/result"; +import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool"; +import { applyTruncation } from "../util/truncate"; + +const NavigateArgs = Type.Object({ + url: Type.String({ description: "Full URL to navigate to (e.g. https://github.com)" }), +}); + +export type NavOutcome = + | { readonly kind: "in_place"; readonly targetId: string } + | { readonly kind: "new_tab_created"; readonly targetId: string; readonly reason: "no_tabs" }; + +export const navigateTool = defineBrowserTool({ + name: "browser_navigate", + label: "Browser Navigate", + description: + "Navigate to a URL. Creates a new tab on first call (when no real tabs exist). Otherwise navigates the current tab in place.", + promptSnippet: "Navigate to a URL", + promptGuidelines: [ + "Use browser_navigate to go to URLs.", + "Use browser_wait_for_load after browser_navigate to wait for the page to finish loading.", + "For extracting data from a page you already navigated to, use browser_execute_js or browser_http_get (faster for APIs).", + ], + parameters: NavigateArgs, + renderCall: (a) => new Text(`🌐 Navigate to ${a.url}`, 0, 0), + async handler(args, { client }): Promise<Result<ToolOk, ToolErr>> { + const tabs = await client.listTabs(false); + if (!tabs.success) return err({ kind: "cdp_error", message: tabs.error.message }); + let outcome: NavOutcome; + if (tabs.data.length === 0) { + const created = await client.newTab(args.url); + if (!created.success) return err({ kind: "cdp_error", message: created.error.message }); + outcome = { kind: "new_tab_created", targetId: created.data, reason: "no_tabs" }; + } else { + const nav = await client.session().call("Page.navigate", { url: args.url }); + if (!nav.success) return err({ kind: "cdp_error", message: nav.error.message }); + const cur = client.current(); + if (!cur) return err({ kind: "internal", message: "navigate succeeded but no current target tracked" }); + outcome = { kind: "in_place", targetId: cur.targetId }; + } + const info = await client.pageInfo(); + if (!info.success) return err({ kind: "cdp_error", message: info.error.message }); + if ("dialog" in info.data) { + return ok({ + text: `Navigated to ${args.url}\nTarget: ${outcome.targetId}\n⚠️ Dialog open: ${info.data.dialog.type} β€” "${info.data.dialog.message}"\nUse browser_handle_dialog.`, + details: { outcome, dialog: info.data.dialog }, + }); + } + const prefix = outcome.kind === "new_tab_created" ? "New tab: " : ""; + return ok({ + text: `${prefix}Navigated to: ${info.data.url}\nTitle: ${info.data.title}\nViewport: ${info.data.width}x${info.data.height}\nTarget: ${outcome.targetId}`, + details: { outcome, page: info.data }, + }); + }, +}); + +const OpenUrlsArgs = Type.Object({ + urls: Type.Array(Type.String(), { description: "Array of URLs to open in new tabs" }), +}); + +type TabResult = { + readonly url: string; + readonly targetId: string; + readonly ok: boolean; + readonly error?: string; +}; + +export const openUrlsTool = defineBrowserTool({ + name: "browser_open_urls", + label: "Browser Open URLs", + description: "Open multiple URLs in parallel new tabs. Returns per-URL outcomes.", + promptSnippet: "Open multiple URLs in new tabs (parallel)", + promptGuidelines: [ + "Use after web_search to open citations in parallel.", + "After opening, use browser_list_tabs / browser_switch_tab / browser_screenshot to interact.", + "Use browser_wait_for_load on a tab before extracting data from SPAs.", + ], + parameters: OpenUrlsArgs, + renderCall: (a) => new Text(`🌐 Opening ${a.urls.length} URL${a.urls.length !== 1 ? "s" : ""}…`, 0, 0), + async handler(args, { client, onUpdate }): Promise<Result<ToolOk, ToolErr>> { + const total = args.urls.length; + // Phase 1: create tabs in parallel. + const created = await Promise.all(args.urls.map(async (url): Promise<TabResult> => { + const r = await client.session().callBrowser("Target.createTarget", { url: "about:blank" }); + if (!r.success) return { url, targetId: "", ok: false, error: r.error.message }; + const c = r.data as { targetId: string }; // CDP boundary cast: Target.createTarget returns { targetId: string } + return { url, targetId: c.targetId, ok: true }; + })); + // Phase 2: attach + navigate each created tab in parallel. + let completed = 0; + const settled = await Promise.all( + created.filter((t) => t.ok).map(async (tab): Promise<TabResult> => { + try { + const attached = await client.session().callBrowser("Target.attachToTarget", { targetId: tab.targetId, flatten: true }); + if (!attached.success) return { ...tab, ok: false, error: attached.error.message }; + const sid = (attached.data as { sessionId: string }).sessionId; // CDP boundary cast: Target.attachToTarget returns { sessionId: string } + const enabled = await client.session().callOnTarget("Page.enable", {}, sid); + if (!enabled.success) return { ...tab, ok: false, error: enabled.error.message }; + const nav = await client.session().callOnTarget("Page.navigate", { url: tab.url }, sid); + if (!nav.success) return { ...tab, ok: false, error: nav.error.message }; + return tab; + } finally { + completed++; + // onUpdate must never escape: a renderer crash would otherwise + // tear down the tool call. + try { + onUpdate({ text: `Opening URLs… ${completed}/${created.filter((t) => t.ok).length} navigated` }); + } catch { /* swallow */ } + } + }), + ); + const failures = created.filter((t) => !t.ok); + const all: ReadonlyArray<TabResult> = [...settled, ...failures]; + const okTabs = all.filter((r) => r.ok); + const failTabs = all.filter((r) => !r.ok); + // Phase 3: activate the last successfully-opened tab so the user sees something. + if (okTabs.length > 0) { + const last = okTabs[okTabs.length - 1]; + if (last) { + await client.session().callBrowser("Target.activateTarget", { targetId: last.targetId }); + } + } + const lines: string[] = []; + if (okTabs.length) { + lines.push(`βœ… ${okTabs.length}/${total} tabs opened:`, ...okTabs.map((t, i) => ` [${i}] ${t.url} β†’ ${t.targetId}`)); + } + if (failTabs.length) { + lines.push(`❌ ${failTabs.length}/${total} failed:`, ...failTabs.map((t) => ` ${t.url}: ${t.error ?? "unknown"}`)); + } + const truncated = await applyTruncation(lines.join("\n"), "urls"); + if (okTabs.length === 0) { + return err({ + kind: "cdp_error", + message: `All ${total} URLs failed`, + ...(truncated.fullOutputPath ? { details: { tabs: all, fullOutputPath: truncated.fullOutputPath } } : { details: { tabs: all } }), + }); + } + return ok({ + text: truncated.text, + ...(truncated.fullOutputPath ? { details: { tabs: all, fullOutputPath: truncated.fullOutputPath } } : { details: { tabs: all } }), + }); + }, +});
M src/registry.ts → src/registry.ts

@@ -7,6 +7,7 @@ import { pageInfoTool, waitTool, waitForLoadTool } from "./domains/page";

import { scrollTool } from "./domains/scroll"; import { handleDialogTool } from "./domains/dialog"; import { screenshotTool } from "./domains/screenshot"; +import { navigateTool, openUrlsTool } from "./domains/navigate"; const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [ clickTool,

@@ -19,6 +20,8 @@ waitTool,

waitForLoadTool, handleDialogTool, screenshotTool, + navigateTool, + openUrlsTool, ]; export const registerAllTools = (pi: ExtensionAPI, client: BrowserClient): void => {
M src/tools.ts → src/tools.ts

@@ -97,79 +97,6 @@ // ── Registration ─────────────────────────────────────────────────────────────

export function registerTools(pi: ExtensionAPI, daemon: BrowserDaemon): void { // ═══════════════════════════════════════════════════════════════════════════ - // browser_navigate - // ═══════════════════════════════════════════════════════════════════════════ - pi.registerTool({ - name: "browser_navigate", - label: "Browser Navigate", - description: - "Navigate to a URL. Creates a new tab on first call (safe), navigates current tab on subsequent calls.", - promptSnippet: "Navigate to a URL (creates new tab on first call)", - promptGuidelines: [ - "Use browser_navigate to go to URLs. The first call creates a new tab so it doesn't clobber the user's active tab.", - "Use browser_wait_for_load after browser_navigate to wait for the page to finish loading.", - "For extracting data from a page you already navigated to, use browser_execute_js or browser_http_get (faster for APIs).", - ], - parameters: Type.Object({ - url: Type.String({ description: "Full URL to navigate to (e.g. https://github.com)" }), - }), - async execute(_id, params) { - try { - await daemon.ensureAlive(); - - // Determine if we need a new tab: create one on first-ever call, - // or if the current tab is an internal Chrome page. Otherwise - // navigate the current tab in-place. - let targetId: string; - let isNewTab = false; - try { - const tabs = await daemon.listTabs(false); - if (tabs.length === 0) { - // No real tabs exist β€” create one - targetId = await daemon.newTab(params.url); - isNewTab = true; - } else { - // Navigate the current tab in-place - await daemon.cdp("Page.navigate", { url: params.url }); - const tab = await daemon.currentTab(); - targetId = tab.targetId; - } - } catch { - // Fallback: create a new tab - targetId = await daemon.newTab(params.url); - isNewTab = true; - } - - const info = await daemon.getPageInfo(); - if ("dialog" in info) { - return { - content: [{ type: "text" as const, text: `Navigated to ${params.url}\nTarget: ${targetId}\n⚠️ A dialog is open: ${info.dialog.type} β€” "${info.dialog.message}"\nUse browser_handle_dialog to dismiss or accept it.` }], - details: undefined, - }; - } - - const prefix = isNewTab ? "New tab: " : ""; - return { - content: [{ - type: "text" as const, - text: `${prefix}Navigated to: ${info.url}\nTitle: ${info.title}\nViewport: ${info.width}x${info.height}\nTarget: ${targetId}`, - }], - details: undefined, - }; - } catch (err) { - return { - isError: true, - content: [{ type: "text" as const, text: `Navigation failed: ${err instanceof Error ? err.message : String(err)}` }], - details: undefined, - }; - } - }, - renderCall(args, theme) { - return new Text(`🌐 Navigate to ${(args as { url: string }).url}`, 0, 0); - }, - }); - - // ═══════════════════════════════════════════════════════════════════════════ // browser_go_back // ═══════════════════════════════════════════════════════════════════════════ pi.registerTool({

@@ -933,222 +860,6 @@ content: [{ type: "text" as const, text: `Network log failed: ${err instanceof Error ? err.message : String(err)}` }],

details: undefined, }; } - }, - }); - - // ═══════════════════════════════════════════════════════════════════════ - // browser_open_urls - // ═══════════════════════════════════════════════════════════════════════ - pi.registerTool({ - name: "browser_open_urls", - label: "Browser Open URLs", - description: - "Open multiple URLs in new browser tabs in parallel. Useful after web_search to open citation links for visual inspection. Creates real Chrome tabs with full JS rendering support β€” identical to browser_new_tab but for many URLs at once.", - promptSnippet: "Open multiple URLs in new tabs (parallel, full JS rendering)", - promptGuidelines: [ - "Use browser_open_urls to open multiple URLs in parallel β€” much faster than calling browser_new_tab repeatedly.", - "Use browser_open_urls after web_search to open citation links from the synthesized answer.", - "Each URL opens in a real Chrome tab with full JS rendering β€” same as browser_new_tab.", - "After opening, use browser_list_tabs to see all tabs, browser_switch_tab to switch, and browser_screenshot / browser_execute_js to interact.", - "Use browser_wait_for_load on a tab before extracting data from SPAs.", - ], - parameters: Type.Object({ - urls: Type.Array(Type.String(), { description: "Array of URLs to open in new tabs" }), - }), - async execute(_id, params, _signal, onUpdate) { - await daemon.ensureAlive(); - - const total = params.urls.length; - const results: Array<{ - url: string; - targetId: string; - ok: boolean; - error?: string; - }> = []; - - // ═══════════════════════════════════════════════════════════════════ - // Phase 1: Create all tabs in parallel. - // Target.createTarget is a browser-level CDP call β€” no session - // needed, no shared state mutated. Safe to parallelize. - // ═══════════════════════════════════════════════════════════════════ - const createPromises = params.urls.map(async (url: string) => { - try { - const result = (await daemon.cdp("Target.createTarget", { - url: "about:blank", - })) as { targetId: string }; - return { url, targetId: result.targetId, ok: true as const }; - } catch (err) { - return { - url, - targetId: "", - ok: false as const, - error: err instanceof Error ? err.message : String(err), - details: undefined, - }; - } - }); - - const created = await Promise.all(createPromises); - - // ═══════════════════════════════════════════════════════════════════ - // Phase 2: Attach a CDP session to each tab and navigate. - // Each tab gets its own sessionId passed explicitly to cdp(), - // avoiding the shared daemon._sessionId. Safe to parallelize. - // ═══════════════════════════════════════════════════════════════════ - let completed = 0; - const attachAndNav = created - .filter((t) => t.ok) - .map(async (tab) => { - try { - // Attach to target to get a session for this tab - const attachResult = (await daemon.cdp( - "Target.attachToTarget", - { targetId: tab.targetId, flatten: true }, - )) as { sessionId: string }; - const sid = attachResult.sessionId; - - // Enable the Page domain so navigation events fire - await daemon.cdp("Page.enable", {}, sid); - - // Navigate to the URL using the tab's own session - await daemon.cdp("Page.navigate", { url: tab.url }, sid); - - return { ...tab, ok: true as const, targetId: tab.targetId }; - } catch (err) { - return { - ...tab, - ok: false as const, - error: err instanceof Error ? err.message : String(err), - details: undefined, - }; - } finally { - completed++; - // Stream progress so the TUI shows live per-URL status - const okCount = created.filter((t) => t.ok).length; - onUpdate?.({ - content: [ - { - type: "text" as const, - text: `Opening URLs… ${completed}/${okCount} navigated`, - }, - ], - details: undefined, - }); - } - }); - - // Also collect failures from Phase 1 - const failures = created.filter((t) => !t.ok); - - const settled = await Promise.all(attachAndNav); - results.push(...settled, ...failures); - - // ═══════════════════════════════════════════════════════════════════ - // Phase 3: Activate the last successfully opened tab so the user - // sees something meaningful in the foreground. - // ═══════════════════════════════════════════════════════════════════ - const lastOk = [...settled].reverse().find((r) => r.ok); - if (lastOk) { - try { - await daemon.cdp("Target.activateTarget", { - targetId: lastOk.targetId, - }); - } catch { - // best-effort β€” the tabs are still open - } - } - - // Build result text - const okTabs = results.filter((r) => r.ok); - const failedTabs = results.filter((r) => !r.ok); - - const lines: string[] = []; - if (okTabs.length > 0) { - lines.push( - `βœ… ${okTabs.length}/${total} tabs opened:`, - ...okTabs.map( - (t, i) => ` [${i}] ${t.url} β†’ ${t.targetId}`, - ), - ); - } - if (failedTabs.length > 0) { - lines.push( - `❌ ${failedTabs.length}/${total} failed:`, - ...failedTabs.map( - (t) => ` ${t.url}: ${t.error || "unknown error"}`, - ), - ); - } - - const isTotalFailure = okTabs.length === 0; - const fullOutput = lines.join("\n"); - const { text, fullOutputPath } = await applyTruncation(fullOutput, "urls"); - - return { - isError: isTotalFailure, - content: [{ type: "text" as const, text }], - details: { tabs: results, fullOutputPath }, - }; - }, - renderCall(args, _theme) { - const { urls } = args as { urls: string[] }; - return new Text( - `🌐 Opening ${urls.length} URL${urls.length !== 1 ? "s" : ""}…`, - 0, - 0, - ); - }, - renderResult(result, { expanded }, theme) { - const details = result.details as { - tabs?: Array<{ - url: string; - targetId: string; - ok: boolean; - error?: string; - }>; - fullOutputPath?: string; - } | undefined; - - const tabs = details?.tabs || []; - const ok = tabs.filter((t) => t.ok).length; - const fail = tabs.filter((t) => !t.ok).length; - const total = tabs.length; - - if (expanded) { - // Expanded (CTRL+O): show the full URLβ†’targetId mapping - const content = result.content[0]; - const raw = content?.type === "text" ? content.text : ""; - // Strip the truncation footer for cleaner display, show it separately - const truncIdx = raw.indexOf("\n\n[Output truncated:"); - const displayText = truncIdx >= 0 ? raw.slice(0, truncIdx) : raw; - - let expandedText = displayText || "(no output)"; - if (details?.fullOutputPath && truncIdx < 0) { - // Truncated but we stripped the footer β€” add a compact note - expandedText += `\n\n${theme ? theme.fg("dim", `Full output: ${details.fullOutputPath}`) : `Full output: ${details.fullOutputPath}`}`; - } else if (details?.fullOutputPath) { - expandedText += `\n${theme ? theme.fg("dim", `Full output: ${details.fullOutputPath}`) : `Full output: ${details.fullOutputPath}`}`; - } - return new Text(expandedText, 0, 0); - } - - // Collapsed (default): compact summary - let text: string; - if (total === 0) { - text = "No URLs provided."; - } else if (fail === 0) { - text = `βœ… ${ok} tab${ok !== 1 ? "s" : ""} opened`; - } else if (ok === 0) { - text = `❌ All ${fail} URL${fail !== 1 ? "s" : ""} failed`; - } else { - text = `⚠️ ${ok} opened, ${fail} failed`; - } - - return new Text( - theme ? theme.fg(fail === 0 ? "success" : ok === 0 ? "error" : "warning", text) : text, - 0, - 0, - ); }, });