all repos — sz-chrome-remote-cdp @ 1b11036e34f5ba6b2184369e2514a48ec8ba5565

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

feat(domains): migrate page_info, wait, wait_for_load with typed timeout
amankumarsingh77 amankumarsingh7702@gmal.com
Sat, 02 May 2026 16:14:52 +0530
commit

1b11036e34f5ba6b2184369e2514a48ec8ba5565

parent

307f9e746c5288e12c62f16efdd6a0868f84bc59

3 files changed, 92 insertions(+), 115 deletions(-)

jump to
A src/domains/page.ts

@@ -0,0 +1,88 @@

+import { Type } from "typebox"; +import { type Result, err, ok } from "../util/result"; +import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool"; +import { sleep } from "../util/time"; +import { safeJs } from "../util/js-template"; + +export const pageInfoTool = defineBrowserTool({ + name: "browser_page_info", + label: "Browser Page Info", + description: + "Get current page state: URL, title, viewport size, scroll position, page dimensions. If a JS dialog is open, returns dialog info instead.", + promptSnippet: "Get current page URL, title, viewport, and scroll position", + promptGuidelines: [ + "Use browser_page_info to quickly check what page you're on and whether a JS dialog is blocking interaction.", + "If browser_page_info returns a dialog, use browser_handle_dialog before any other browser actions.", + "JS dialogs freeze the page's JS thread, so no other interaction works until the dialog is handled.", + "browser_page_info auto-detects alert, confirm, prompt, and beforeunload dialogs.", + ], + parameters: Type.Object({}), + async handler(_args, { client }): Promise<Result<ToolOk, ToolErr>> { + const r = await client.pageInfo(); + if (!r.success) return err({ kind: "cdp_error", message: r.error.message }); + if ("dialog" in r.data) { + const d = r.data.dialog; + return ok({ + text: `⚠️ DIALOG OPEN: ${d.type}\nMessage: ${d.message}${d.defaultPrompt ? `\nDefault: ${d.defaultPrompt}` : ""}\n\nUse browser_handle_dialog to accept or dismiss.`, + details: { dialog: d }, + }); + } + const i = r.data; + return ok({ + text: `URL: ${i.url}\nTitle: ${i.title}\nViewport: ${i.width}x${i.height}\nScroll: (${i.scrollX}, ${i.scrollY})\nPage size: ${i.pageWidth}x${i.pageHeight}`, + details: { ...i }, + }); + }, +}); + +const WaitArgs = Type.Object({ + seconds: Type.Number({ minimum: 0, maximum: 60, description: "Seconds to wait" }), +}); + +export const waitTool = defineBrowserTool({ + name: "browser_wait", + label: "Browser Wait", + description: "Wait N seconds before continuing.", + promptSnippet: "Wait N seconds", + promptGuidelines: ["Use sparingly — prefer browser_wait_for_load when waiting for a page load."], + parameters: WaitArgs, + async handler(args, { signal }): Promise<Result<ToolOk, ToolErr>> { + try { + await sleep(Math.round(args.seconds * 1000), signal); + return ok({ text: `Waited ${args.seconds}s` }); + } catch (e) { + return err({ kind: "internal", message: e instanceof Error ? e.message : String(e) }); + } + }, +}); + +const WaitForLoadArgs = Type.Object({ + timeout: Type.Optional(Type.Number({ default: 15, minimum: 1, maximum: 120, description: "Max seconds to wait. Default: 15." })), +}); + +export const waitForLoadTool = defineBrowserTool({ + name: "browser_wait_for_load", + label: "Browser Wait For Load", + description: + "Poll until the current page reports document.readyState === 'complete'. Returns a typed timeout error if the deadline elapses.", + promptSnippet: "Wait for the page to finish loading", + promptGuidelines: [ + "Call after browser_navigate / browser_open_urls before extracting data.", + "Returns when readyState becomes 'complete' OR the timeout elapses.", + ], + parameters: WaitForLoadArgs, + async handler(args, { client, signal }): Promise<Result<ToolOk, ToolErr>> { + const timeoutMs = (args.timeout ?? 15) * 1000; + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (signal?.aborted) return err({ kind: "internal", message: "aborted" }); + const r = await client.evaluateJs(safeJs`document.readyState`); + if (r.success && r.data === "complete") { + const ms = Date.now() - start; + return ok({ text: `Page loaded in ${Math.round(ms / 100) / 10}s`, details: { ms } }); + } + await new Promise((res) => setTimeout(res, 50)); + } + return err({ kind: "timeout", message: `Page did not finish loading in ${args.timeout ?? 15}s` }); + }, +});
M src/registry.ts → src/registry.ts

@@ -3,6 +3,7 @@ import type { BrowserClient } from "./client";

import { type AnyBrowserToolDefinition, registerBrowserTool } from "./util/tool"; import { clickTool } from "./domains/click"; import { typeTool, pressKeyTool, dispatchKeyTool } from "./domains/keyboard"; +import { pageInfoTool, waitTool, waitForLoadTool } from "./domains/page"; import { scrollTool } from "./domains/scroll"; const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [

@@ -11,6 +12,9 @@ typeTool,

pressKeyTool, dispatchKeyTool, scrollTool, + pageInfoTool, + waitTool, + waitForLoadTool, ]; export const registerAllTools = (pi: ExtensionAPI, client: BrowserClient): void => {
M src/tools.ts → src/tools.ts

@@ -170,54 +170,6 @@ },

}); // ═══════════════════════════════════════════════════════════════════════════ - // browser_page_info - // ═══════════════════════════════════════════════════════════════════════════ - pi.registerTool({ - name: "browser_page_info", - label: "Browser Page Info", - description: - "Get current page state: URL, title, viewport size, scroll position, page dimensions. If a JS dialog (alert/confirm/prompt) is open, returns dialog info instead.", - promptSnippet: "Get current page URL, title, viewport, and scroll position", - promptGuidelines: [ - "Use browser_page_info to quickly check what page you're on and whether a JS dialog is blocking interaction.", - "If browser_page_info returns a dialog, use browser_handle_dialog before any other browser actions.", - "JS dialogs freeze the page's JS thread, so no other interaction works until the dialog is handled.", - "browser_page_info auto-detects alert, confirm, prompt, and beforeunload dialogs.", - ], - parameters: Type.Object({}), - async execute() { - try { - await daemon.ensureAlive(); - const info = await daemon.getPageInfo(); - - if ("dialog" in info) { - return { - content: [{ - type: "text" as const, - text: `⚠️ DIALOG OPEN: ${info.dialog.type}\nMessage: ${info.dialog.message}${info.dialog.defaultPrompt ? `\nDefault: ${info.dialog.defaultPrompt}` : ""}\n\nUse browser_handle_dialog to accept or dismiss.`, - }], - details: undefined, - }; - } - - return { - content: [{ - type: "text" as const, - text: `URL: ${info.url}\nTitle: ${info.title}\nViewport: ${info.width}x${info.height}\nScroll: (${info.scrollX}, ${info.scrollY})\nPage size: ${info.pageWidth}x${info.pageHeight}`, - }], - details: undefined, - }; - } catch (err) { - return { - isError: true, - content: [{ type: "text" as const, text: `Failed: ${err instanceof Error ? err.message : String(err)}` }], - details: undefined, - }; - } - }, - }); - - // ═══════════════════════════════════════════════════════════════════════════ // browser_go_back // ═══════════════════════════════════════════════════════════════════════════ pi.registerTool({

@@ -764,73 +716,6 @@ : summary,

0, 0, ); - }, - }); - - // ═══════════════════════════════════════════════════════════════════════════ - // browser_wait - // ═══════════════════════════════════════════════════════════════════════════ - pi.registerTool({ - name: "browser_wait", - label: "Browser Wait", - description: "Wait for a specified number of seconds.", - promptSnippet: "Wait for N seconds (default 1s)", - promptGuidelines: [ - "Use browser_wait for simple delays. Prefer browser_wait_for_load after navigation.", - ], - parameters: Type.Object({ - seconds: Type.Optional(Type.Number({ description: "Seconds to wait. Default: 1" })), - }), - async execute(_id, params) { - const sec = params.seconds || 1; - await new Promise((r) => setTimeout(r, sec * 1000)); - return { content: [{ type: "text" as const, text: `Waited ${sec}s.` }], details: undefined }; - }, - }); - - // ═══════════════════════════════════════════════════════════════════════════ - // browser_wait_for_load - // ═══════════════════════════════════════════════════════════════════════════ - pi.registerTool({ - name: "browser_wait_for_load", - label: "Browser Wait For Load", - description: - "Wait for the page to finish loading (document.readyState === 'complete'). Returns false on timeout. Note: SPAs may need additional waiting after this.", - promptSnippet: "Wait for document.readyState === 'complete'", - promptGuidelines: [ - "Use browser_wait_for_load after browser_navigate or browser_new_tab.", - "This checks document.readyState only. Single-page apps may still be loading data after this returns true.", - "For SPAs, use browser_execute_js to check for specific elements after browser_wait_for_load.", - ], - parameters: Type.Object({ - timeout: Type.Optional(Type.Number({ description: "Max seconds to wait. Default: 15" })), - }), - async execute(_id, params) { - try { - await daemon.ensureAlive(); - const timeout = params.timeout || 15; - const deadline = Date.now() + timeout * 1000; - - while (Date.now() < deadline) { - const state = await daemon.evaluateJS("document.readyState"); - if (state === "complete") { - return { content: [{ type: "text" as const, text: "Page loaded (readyState: complete)." }], details: undefined }; - } - await new Promise((r) => setTimeout(r, 300)); - } - - return { - isError: true, - content: [{ type: "text" as const, text: `Page load timed out after ${timeout}s. Current state unknown.` }], - details: undefined, - }; - } catch (err) { - return { - isError: true, - content: [{ type: "text" as const, text: `Wait failed: ${err instanceof Error ? err.message : String(err)}` }], - details: undefined, - }; - } }, });