pi-browser-harness 0.8.1 → 0.9.0

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.
package/src/prompt.ts CHANGED
@@ -2,150 +2,126 @@
2
2
  * System prompt guidance for browser usage.
3
3
  *
4
4
  * Injects a compact browser control reference into the pi system prompt.
5
- * Per-tool guidelines are injected via promptGuidelines on each tool registration.
6
- * This prompt provides only the cross-cutting patterns and tool reference table.
5
+ * This prompt carries only cross-cutting knowledge — the mental model, tool
6
+ * routing, workflows, and gotchas. Per-tool specifics (schemas, detailed usage)
7
+ * are injected separately via promptGuidelines on each tool registration, so this
8
+ * file intentionally does NOT restate full tool descriptions.
7
9
  */
8
10
 
9
11
  export function getBrowserSystemPrompt(): string {
10
12
  return `
11
13
  ## Browser Control
12
14
 
13
- You have full control of a real Chrome browser via \`browser_*\` tools.
14
- The browser connects to the user's running Chrome (not a headless instance).
15
-
16
- **These tools coexist with all other standard pi tools** — read, bash, edit,
17
- write, and any other built-in tools are still available. Use browser_* tools
18
- to interact with pages visually, and other tools alongside them.
19
-
20
- ### Browser-Specific Tools
21
-
22
- | Tool | Purpose |
23
- |------|---------|
24
- | browser_snapshot | Accessibility-tree snapshot. Assigns every interactive element a stable **ref** (\`[eN]\`) plus \`@(x,y)\`. Refs are the primary handle for interaction tools — they survive re-renders, coordinates don't. |
25
- | browser_screenshot | Capture a PNG or JPEG screenshot of the current page (rendered inline in the TUI; the LLM reads the image via the saved file path) |
26
- | browser_click | Click an element by **ref** (preferred — re-resolves position, survives re-renders) or viewport x/y. Appends a compact diff of page changes. |
27
- | browser_type | Type text into the focused element (real keystrokes; requires a focused field) |
28
- | browser_fill | Fill an input/textarea by **ref** (preferred) or selector — updates React/Vue controlled components, verifies the value, appends a page-changes diff |
29
- | browser_select_option | Select an option in a native <select> by **ref** (preferred) or selector, choosing by value/label/index (fires change) |
30
- | browser_focus | Focus an element by **ref** (preferred) or selector (deterministic; use before browser_type) |
31
- | browser_press_key | Press a keyboard key (Enter, Tab, Escape, arrows, etc.) |
32
- | browser_scroll | Scroll the page at coordinates |
33
- | browser_navigate | Navigate to a URL (result details.outcome.kind is "in_place" or "new_tab_created") |
34
- | browser_new_tab | Open a new tab, optionally navigate |
35
- | browser_open_urls | Open multiple URLs in new tabs (parallel) |
36
- | browser_go_back / browser_go_forward / browser_reload | History navigation |
37
- | browser_page_info | Get page URL, title, viewport, scroll position, or dialog info |
38
- | browser_list_tabs | List all open browser tabs |
39
- | browser_current_tab | Get current tab info |
40
- | browser_switch_tab | Switch to a different tab by targetId |
41
- | browser_execute_js | Execute JavaScript and return the result |
42
- | browser_http_get | Direct HTTP GET (outside browser, for APIs) |
43
- | browser_console | Read JS errors / console output on the current tab (diagnostic — use when an action looks broken; pass sinceSeq from the previous nextCursor to see only new messages) |
44
- | browser_wait | Wait N seconds |
45
- | browser_wait_for | Wait until a selector/text appears (or a selector disappears) — use for SPA content before interacting |
46
- | browser_wait_for_load | Wait for document.readyState === 'complete' (returns a typed timeout error if the page doesn't reach readyState=complete in N seconds, default 15) |
47
- | browser_handle_dialog | Accept or dismiss a JS dialog |
48
- | browser_run_script | Execute a temporary script file with daemon access (write script to disk, then run) |
49
-
50
- ### Temporary Scripts
51
-
52
- When the built-in tools aren't enough for a multi-step workflow, write a temporary
53
- script to disk and execute it with browser_run_script. The script runs in the
54
- harness process with direct access to the browser daemon and Node.js APIs.
15
+ You drive a **real Chrome** (the user's running instance, not headless) through
16
+ \`browser_*\` tools. These coexist with every standard pi tool — read, bash, edit,
17
+ write — so mix them freely: \`browser_*\` for the page, everything else alongside.
55
18
 
56
- \`\`\`
57
- write("/tmp/scrape-pages.js", \`
58
- const results = [];
59
- for (const url of params.urls) {
60
- await daemon.cdp("Page.navigate", { url });
61
- await new Promise(r => setTimeout(r, 2000));
62
- const data = await daemon.evaluateJS("document.title");
63
- results.push({ url, title: data });
64
- }
65
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
66
- \`)
67
- browser_run_script("/tmp/scrape-pages.js", { urls: [...] })
68
- \`\`\`
19
+ ### The core handle: refs
69
20
 
70
- Script bindings: params, daemon, require, signal, onUpdate, ctx, console, fetch, JSON, Buffer, setTimeout, clearTimeout.
21
+ Interaction is ref-first. Call \`browser_snapshot\` to get an accessibility tree
22
+ where every interactive element has a stable ref (\`[eN]\`) and \`@(x,y)\`. **Target
23
+ elements by ref, not by coordinates or guessed selectors** — refs are keyed to
24
+ element identity, so they survive the re-renders that React/Vue forms trigger on
25
+ every edit and save. Coordinates go stale; guessed selectors miss.
71
26
 
72
- ### Parallel Execution
27
+ - Prefer \`browser_fill\` for text — it fires input/change so controlled components
28
+ update, and returns the value for verification. Use \`browser_click\` + \`browser_type\`
29
+ only for keystroke-sensitive widgets (autocomplete, masked inputs).
30
+ - Mutating calls append a compact **"Page changes"** diff — read it to confirm the
31
+ change landed (form closed, new \`*[eN]\` appeared).
32
+ - \`"ref is stale"\` means the page changed: re-run \`browser_snapshot\` for fresh refs.
73
33
 
74
- Observation tools (browser_screenshot, browser_page_info, browser_execute_js, browser_list_tabs, browser_http_get, etc.) can run in parallel with each other and with mutation tools. The harness automatically serializes mutation tools (click, type, scroll, navigate, switch_tab, etc.) so they never race on shared state. When operations are independent, emit them in the same turn for better performance.
34
+ ### Choosing a tool
75
35
 
76
- **Examples of safe parallel calls:**
77
- \`\`\`
78
- browser_screenshot() + browser_page_info() + browser_execute_js("document.title")
79
- browser_http_get("https://api.example.com/data") + browser_click(x, y)
80
- \`\`\`
36
+ | To… | Reach for |
37
+ |-----|-----------|
38
+ | See the page structure (for interaction) | \`browser_snapshot\` (refs) |
39
+ | See the page visually | \`browser_screenshot\` |
40
+ | Search the web | \`browser_web_search\` → ranked {title, url, snippet} |
41
+ | Read an article's clean text | \`browser_read_page\` (reader mode; url or owned tab) |
42
+ | Extract specific DOM values | \`browser_execute_js\` |
43
+ | Hit a JSON/API endpoint | \`browser_http_get\` (raw GET, outside the browser) |
44
+ | Click / fill / type / select / focus / press a key | \`browser_click\` · \`browser_fill\` · \`browser_type\` · \`browser_select_option\` · \`browser_focus\` · \`browser_press_key\` |
45
+ | Upload a file · drag · resize viewport | \`browser_upload_file\` · \`browser_drag_and_drop\` · \`browser_viewport_resize\` |
46
+ | Go to a URL / open many / manage tabs | \`browser_navigate\` · \`browser_new_tab\` · \`browser_open_urls\` · \`browser_list_tabs\` · \`browser_switch_tab\` · \`browser_go_back\`/\`go_forward\`/\`reload\` |
47
+ | Wait for content / load / a fixed delay | \`browser_wait_for\` (selector/text) · \`browser_wait_for_load\` (readyState) · \`browser_wait\` |
48
+ | Inspect page / handle a dialog | \`browser_page_info\` · \`browser_current_tab\` · \`browser_handle_dialog\` |
49
+ | Diagnose a broken action | \`browser_console\` (JS errors) · \`browser_get_network_log\` |
50
+ | Save output / config downloads | \`browser_print_to_pdf\` · \`browser_download\` |
51
+ | Anything the tools can't express | \`browser_run_script\` (see Extending) |
81
52
 
82
- **Multi-agent note**: When multiple subagents use the browser, tab switching by one agent changes the active tab for all agents. Per-tab data (console buffers, network traces, dialogs, page info cache) is isolated — switching tabs does not destroy another agent's collected data. Call \`browser_current_tab\` before mutation tools to confirm you're on the expected tab. If \`browser_page_info\` returns a dialog, handle it with \`browser_handle_dialog\` promptly — dialogs block page interaction and are not queued across agents.
53
+ Each tool carries its own detailed guidelines — this table is only for routing.
83
54
 
84
- ### Common Patterns
55
+ ### Workflows
85
56
 
86
- **Navigation:**
57
+ **Research (search → read):**
87
58
  \`\`\`
88
- browser_new_tab("https://example.com") → browser_wait_for_load() → browser_screenshot()
59
+ browser_web_search("query") // ranked links, no page content
60
+ → browser_read_page(url) // clean article text for the top hits
61
+ // fall back to browser_open_urls + browser_execute_js only when reader mode misses
89
62
  \`\`\`
90
63
 
91
- **Form filling (ref-first — this is the reliable path on React/SPA forms):**
64
+ **Form filling (ref-first — the reliable path on SPA forms):**
92
65
  \`\`\`
93
- browser_wait_for({ selector: "#email" }) // ensure SPA field is rendered
94
- browser_snapshot() // each interactive element gets a [eN] ref
95
- → browser_fill({ ref: "e7", value: "a@b.com" }) // target by ref, not a guessed selector
96
- → browser_select_option({ ref: "e9", label: "India" }) // native <select> by ref
97
- → browser_click({ ref: "e12" }) // e.g. Save — re-resolves position even if the form reflowed
98
- // each mutating call appends a compact "Page changes" diff — read it to confirm
99
- // the change landed (e.g. a save closed the form, a new field appeared as *[eN]).
66
+ browser_wait_for({ selector: "#email" }) // ensure the field is rendered
67
+ browser_snapshot() // interactive elements get [eN] refs
68
+ → browser_fill({ ref: "e7", value: "a@b.com" })
69
+ → browser_select_option({ ref: "e9", label: "India" })
70
+ → browser_click({ ref: "e12" }) // re-resolves position even after reflow
71
+ // read each "Page changes" diff to confirm the step landed
100
72
  \`\`\`
101
- **Why ref over selector/coordinates:** refs are keyed to the element's identity, so
102
- they survive the re-renders that React/Vue forms trigger on every edit and save —
103
- coordinates go stale and selectors you guess often don't match. If a call returns
104
- "ref is stale", the page changed: re-run browser_snapshot for fresh refs.
105
-
106
- Prefer browser_fill for text inputs/textareas — it fires input/change so controlled
107
- components update, and returns the field's value for verification. Use browser_click +
108
- browser_type only for keystroke-sensitive widgets (autocomplete, masked inputs);
109
- browser_type requires an already-focused field and errors if none is focused.
110
73
 
111
- **Data extraction:**
74
+ **Navigate + verify:**
112
75
  \`\`\`
113
- browser_execute_js("document.querySelector('.price').innerText")
114
- // or for APIs:
115
- browser_http_get("https://api.example.com/data")
76
+ browser_new_tab("https://example.com") → browser_wait_for_load() → browser_screenshot()
116
77
  \`\`\`
117
78
 
118
- **Scrolling:**
119
- \`\`\`
120
- browser_screenshot() → browser_scroll({ deltaY: 500 }) → browser_screenshot()
121
- \`\`\`
122
- Note: deltaY follows W3C wheel-event convention: positive=down, negative=up. Default deltaY=300 scrolls down.
79
+ **Scroll:** \`browser_scroll({ deltaY: 500 })\` — W3C wheel convention: positive = down,
80
+ negative = up (default 300, down).
123
81
 
124
- **Research Workflow (search + browser):**
82
+ ### Parallelism
83
+
84
+ Observation tools (\`browser_screenshot\`, \`browser_page_info\`, \`browser_execute_js\`,
85
+ \`browser_list_tabs\`, \`browser_http_get\`, \`browser_web_search\`, \`browser_read_page\`, …)
86
+ run in parallel with each other and with mutations. The harness serializes mutations
87
+ (click, type, scroll, navigate, switch_tab) so they never race. Emit independent calls
88
+ in one turn:
125
89
  \`\`\`
126
- browser_navigate("https://google.com/search?q=...") → search engine in a tab
127
- browser_open_urls(urls: ["url1", "url2", ...]) → open result pages in parallel tabs
128
- browser_list_tabs() → see all open tabs with targetIds
129
- browser_switch_tab(targetId: "...") → switch to a tab
130
- browser_screenshot() → visually inspect the page
131
- browser_execute_js("document.querySelector('.main').innerText") → extract content
90
+ browser_screenshot() + browser_page_info() + browser_execute_js("document.title")
132
91
  \`\`\`
133
92
 
134
- **Temporary Scripts (extending the harness):**
93
+ **Multi-agent:** tab switching by one agent changes the active tab for all agents,
94
+ but per-tab data (console, network, dialogs) stays isolated. Call \`browser_current_tab\`
95
+ before a mutation to confirm you're on the expected tab. Handle any dialog reported by
96
+ \`browser_page_info\` promptly — dialogs block interaction and aren't queued across agents.
97
+
98
+ **Session boundary & tab hygiene:** you operate only inside this session's own Chrome
99
+ window. \`browser_switch_tab\`/\`browser_close_tab\` refuse tabs this session didn't open,
100
+ and \`browser_list_tabs\` defaults to owned tabs — never reach into the user's other tabs
101
+ or windows. Close tabs when you're done with them: call \`browser_close_tab\` on any tab
102
+ you opened (via \`browser_new_tab\`/\`browser_open_urls\`) once you've extracted what you
103
+ need. Don't leave a pile of stale tabs behind — keep only what a later step still needs.
104
+ (Leftover tabs are closed automatically when the session ends, but close them yourself
105
+ as you go.)
106
+
107
+ ### Extending: temporary scripts
108
+
109
+ When built-in tools can't express a multi-step workflow, write a script to a temp file
110
+ and run it with \`browser_run_script\`. It executes in the harness process with direct
111
+ daemon + Node access.
135
112
  \`\`\`
136
- write("/tmp/extract.js", "...script with daemon access...") → browser_run_script("/tmp/extract.js")
113
+ write(<tempfile>.js, \`
114
+ const results = [];
115
+ for (const url of params.urls) {
116
+ await daemon.cdp("Page.navigate", { url });
117
+ await new Promise(r => setTimeout(r, 2000));
118
+ results.push({ url, title: await daemon.evaluateJS("document.title") });
119
+ }
120
+ return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
121
+ \`)
122
+ browser_run_script(<tempfile>.js, { urls: [...] })
137
123
  \`\`\`
138
-
139
- ### Additional Tools
140
-
141
- | Tool | Purpose |
142
- |------|---------|
143
- | browser_upload_file | Upload a file to a file input by **ref** (preferred) or selector (bypasses file picker) |
144
- | browser_dispatch_key | Dispatch a DOM KeyboardEvent on an element by **ref** (preferred) or selector (for React/Vue inputs); returns details.matched |
145
- | browser_download | Configure download directory and disable save-as prompts |
146
- | browser_viewport_resize | Resize the viewport for responsive testing |
147
- | browser_drag_and_drop | Perform drag-and-drop from one coordinate to another |
148
- | browser_print_to_pdf | Print the current page to a PDF file |
149
- | browser_get_network_log | Get buffered network request/response events |
124
+ Script bindings: params, daemon, require, signal, onUpdate, ctx, console, fetch, JSON,
125
+ Buffer, setTimeout, clearTimeout. Use a real temp path from the OS — don't hardcode \`/tmp\`.
150
126
  `;
151
127
  }
package/src/registry.ts CHANGED
@@ -19,6 +19,8 @@ import { consoleTool } from "./domains/console";
19
19
  import { snapshotTool } from "./domains/snapshot";
20
20
  import { executeJsTool, runScriptTool } from "./domains/js";
21
21
  import { setupTool } from "./domains/setup";
22
+ import { webSearchTool } from "./domains/search/web-search";
23
+ import { readPageTool } from "./domains/readpage/read-page";
22
24
 
23
25
  // Mutation tools that modify page / browser state are marked `serialized: true`
24
26
  // so they run through a shared async mutex. Observation / read-only tools are
@@ -61,6 +63,10 @@ const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
61
63
  snapshotTool,
62
64
  executeJsTool,
63
65
  runScriptTool,
66
+ // Opens and owns an isolated tab to scrape a SERP — same class as openUrlsTool.
67
+ { ...webSearchTool, serialized: true },
68
+ // Pure isolated read (own sessionId, own tab) — safe concurrent, like executeJsTool.
69
+ readPageTool,
64
70
  ];
65
71
 
66
72
  export const registerAllTools = (pi: ExtensionAPI, client: BrowserClient): void => {
package/src/setup.ts CHANGED
@@ -8,6 +8,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
8
8
  import { execSync } from "node:child_process";
9
9
  import type { BrowserClient } from "./client";
10
10
  import { ensureDaemon } from "./daemon/spawn";
11
+ import { DAEMON_SOCKET_PATH } from "./daemon/protocol";
11
12
 
12
13
  // ── Public: register the /browser-setup slash command ──────────────────────
13
14
 
@@ -41,7 +42,7 @@ export async function performSetup(client: BrowserClient): Promise<SetupResult>
41
42
  // Step 2: Start the browser daemon (spawns if not running, silently reuses if alive)
42
43
  const daemonReady = await ensureDaemon();
43
44
  if (!daemonReady) {
44
- return { success: false, error: "Could not start the browser daemon. Check /tmp/pi-browser-daemon.sock." };
45
+ return { success: false, error: `Could not start the browser daemon. Check ${DAEMON_SOCKET_PATH}.` };
45
46
  }
46
47
 
47
48
  // Step 3: Connect to Chrome DevTools
@@ -59,9 +60,12 @@ export async function performSetup(client: BrowserClient): Promise<SetupResult>
59
60
  return {
60
61
  success: false,
61
62
  error:
62
- "Chrome remote debugging needs to be enabled.\n\n" +
63
- 'Open chrome://inspect/#remote-debugging in your browser, tick the\n' +
63
+ "Browser remote debugging needs to be enabled.\n\n" +
64
+ "Open chrome://inspect/#remote-debugging (or brave://inspect,\n" +
65
+ "edge://inspect) in your browser, tick the\n" +
64
66
  '"Discover network targets" / Allow checkbox, then retry.\n\n' +
67
+ "If that doesn't expose DevTools, relaunch the browser with\n" +
68
+ "--remote-debugging-port=9222.\n\n" +
65
69
  "Or set BU_CDP_WS to a remote browser WebSocket URL.",
66
70
  };
67
71
  }
@@ -89,19 +93,31 @@ export async function performSetup(client: BrowserClient): Promise<SetupResult>
89
93
  function checkChromeRunning(): boolean {
90
94
  try {
91
95
  if (process.platform === "darwin") {
92
- // ps -o comm= returns the full executable path on modern macOS.
93
- // We check for "Google Chrome" anywhere in the path (case-insensitive),
94
- // excluding helper/renderer subprocesses.
96
+ // ps -o comm= returns the full executable path on modern macOS, e.g.
97
+ // "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser". We check
98
+ // for a known Chromium-family name anywhere in the path (case-insensitive),
99
+ // excluding helper/renderer/gpu/crashpad/updater subprocesses that linger
100
+ // after the user quits the browser.
95
101
  const out = execSync("ps -A -o comm=", { timeout: 5000 }).toString().toLowerCase();
96
102
  const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
103
+ const browserNames = ["google chrome", "chromium", "microsoft edge", "brave browser"];
97
104
  return lines.some((l) => {
98
- if (l.includes("helper") || l.includes("renderer") || l.includes("crashpad")) return false;
99
- return l.includes("google chrome") || l.includes("chromium") || l.includes("microsoft edge");
105
+ if (
106
+ l.includes("helper") ||
107
+ l.includes("renderer") ||
108
+ l.includes("crashpad") ||
109
+ l.includes(" gpu") ||
110
+ l.includes("updater")
111
+ ) return false;
112
+ return browserNames.some((name) => l.includes(name));
100
113
  });
101
114
  } else if (process.platform === "linux") {
102
115
  const out = execSync("ps -A -o comm=,args=", { timeout: 5000 }).toString().toLowerCase();
103
116
  const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
104
- const browserComms = ["chrome", "chromium", "chromium-browser", "msedge", "microsoft-edge", "google-chrome"];
117
+ const browserComms = [
118
+ "chrome", "chromium", "chromium-browser", "msedge", "microsoft-edge",
119
+ "google-chrome", "brave", "brave-browser",
120
+ ];
105
121
  return lines.some((line) => {
106
122
  const parts = line.split(/\s+/);
107
123
  const comm = parts[0] ?? "";
@@ -111,7 +127,7 @@ function checkChromeRunning(): boolean {
111
127
  });
112
128
  } else if (process.platform === "win32") {
113
129
  const out = execSync("tasklist", { timeout: 5000 }).toString().toLowerCase();
114
- return ["chrome.exe", "msedge.exe"].some((n) => out.includes(n));
130
+ return ["chrome.exe", "msedge.exe", "brave.exe"].some((n) => out.includes(n));
115
131
  }
116
132
  } catch {
117
133
  // best-effort
package/src/state.ts CHANGED
@@ -12,6 +12,7 @@ export type BrowserState = {
12
12
  readonly remoteBrowserId?: string;
13
13
  readonly ownedTargetIds?: ReadonlyArray<string>;
14
14
  readonly harnessWindowTargetId?: string;
15
+ readonly harnessWindowId?: number;
15
16
  };
16
17
 
17
18
  export const defaultState = (namespace = "default"): BrowserState => ({