pi-browser-harness 0.4.0 → 0.6.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
@@ -36,6 +36,7 @@ to interact with pages visually, and other tools alongside them.
36
36
  | browser_switch_tab | Switch to a different tab by targetId |
37
37
  | browser_execute_js | Execute JavaScript and return the result |
38
38
  | browser_http_get | Direct HTTP GET (outside browser, for APIs) |
39
+ | 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) |
39
40
  | browser_wait | Wait N seconds |
40
41
  | 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) |
41
42
  | browser_handle_dialog | Accept or dismiss a JS dialog |
@@ -73,6 +74,8 @@ browser_screenshot() + browser_page_info() + browser_execute_js("document.title"
73
74
  browser_http_get("https://api.example.com/data") + browser_click(x, y)
74
75
  \`\`\`
75
76
 
77
+ **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.
78
+
76
79
  ### Common Patterns
77
80
 
78
81
  **Navigation:**
package/src/registry.ts CHANGED
@@ -14,13 +14,17 @@ import { uploadFileTool, downloadTool, printToPdfTool } from "./domains/files";
14
14
  import { viewportResizeTool } from "./domains/viewport";
15
15
  import { dragAndDropTool } from "./domains/drag";
16
16
  import { httpGetTool, networkRequestsTool } from "./domains/network";
17
+ import { consoleTool } from "./domains/console";
17
18
  import { snapshotTool } from "./domains/snapshot";
18
19
  import { executeJsTool, runScriptTool } from "./domains/js";
20
+ import { setupTool } from "./domains/setup";
19
21
 
20
22
  // Mutation tools that modify page / browser state are marked `serialized: true`
21
23
  // so they run through a shared async mutex. Observation / read-only tools are
22
24
  // left unmarked and can execute in parallel.
23
25
  const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
26
+ // setup comes first — the agent calls this to self-recover from not-initialized
27
+ setupTool,
24
28
  { ...clickTool, serialized: true },
25
29
  { ...typeTool, serialized: true },
26
30
  { ...pressKeyTool, serialized: true },
@@ -48,6 +52,7 @@ const TOOLS: ReadonlyArray<AnyBrowserToolDefinition> = [
48
52
  { ...dragAndDropTool, serialized: true },
49
53
  httpGetTool,
50
54
  networkRequestsTool,
55
+ consoleTool,
51
56
  snapshotTool,
52
57
  executeJsTool,
53
58
  runScriptTool,
package/src/setup.ts CHANGED
@@ -1,37 +1,50 @@
1
1
  /**
2
- * Setup command — verifies Chrome is running with remote debugging
3
- * and connects the pi agent to it.
2
+ * Setup — verifies Chrome is running with remote debugging and connects
3
+ * the pi agent to it. Exposed as both a slash command (/browser-setup) for
4
+ * users and a tool (browser_setup) for the agent to self-recover.
4
5
  */
5
6
 
6
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
8
  import { execSync } from "node:child_process";
8
9
  import type { BrowserClient } from "./client";
10
+ import { ensureDaemon } from "./daemon/spawn";
11
+
12
+ // ── Public: register the /browser-setup slash command ──────────────────────
9
13
 
10
14
  export function registerSetupCommand(pi: ExtensionAPI, client: BrowserClient): void {
11
15
  pi.registerCommand("browser-setup", {
12
16
  description: "Connect pi to your Chrome browser",
13
17
  handler: async (_args, ctx) => {
14
- await runSetup(ctx, client);
18
+ const result = await performSetup(client);
19
+ if (result.success) {
20
+ ctx.ui.notify(result.data, "info");
21
+ } else {
22
+ ctx.ui.notify(result.error, "error");
23
+ }
15
24
  },
16
25
  });
17
26
  }
18
27
 
19
- async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<void> {
20
- ctx.ui.notify("browser setup: checking Chrome...", "info");
28
+ // ── Shared setup result ────────────────────────────────────────────────────
29
+
30
+ export type SetupResult = { success: true; data: string } | { success: false; error: string };
31
+
32
+ // ── Shared setup logic (used by both the command and the tool) ─────────────
21
33
 
34
+ export async function performSetup(client: BrowserClient): Promise<SetupResult> {
22
35
  // Step 1: Check Chrome is running
23
36
  const chromeRunning = checkChromeRunning();
24
37
  if (!chromeRunning) {
25
- ctx.ui.notify(
26
- "No browser instance running. Please open your browser and then run /browser-setup.",
27
- "error",
28
- );
29
- return;
38
+ return { success: false, error: "No browser instance running. Please open your browser and then run /browser-setup." };
30
39
  }
31
- ctx.ui.notify("Chrome is running ✓", "info");
32
40
 
33
- // Step 2: Try to connect to Chrome DevTools
34
- ctx.ui.notify("Connecting to Chrome DevTools...", "info");
41
+ // Step 2: Start the browser daemon (spawns if not running, silently reuses if alive)
42
+ const daemonReady = await ensureDaemon();
43
+ if (!daemonReady) {
44
+ return { success: false, error: "Could not start the browser daemon. Check /tmp/pi-browser-daemon.sock." };
45
+ }
46
+
47
+ // Step 3: Connect to Chrome DevTools
35
48
  const startResult = await client.start();
36
49
  if (!startResult.success) {
37
50
  const msg = startResult.error.message;
@@ -43,30 +56,23 @@ async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<v
43
56
  lower.includes("econnrefused") ||
44
57
  lower.includes("cannot reach chrome devtools")
45
58
  ) {
46
- ctx.ui.notify(
47
- "Chrome remote debugging needs to be enabled.\n\n" +
48
- "Open chrome://inspect/#remote-debugging in your browser, tick the\n" +
49
- "\"Discover network targets\" / Allow checkbox, then run /browser-setup again.\n\n" +
59
+ return {
60
+ success: false,
61
+ error:
62
+ "Chrome remote debugging needs to be enabled.\n\n" +
63
+ 'Open chrome://inspect/#remote-debugging in your browser, tick the\n' +
64
+ '"Discover network targets" / Allow checkbox, then retry.\n\n' +
50
65
  "Or set BU_CDP_WS to a remote browser WebSocket URL.",
51
- "warning",
52
- );
53
- return;
66
+ };
54
67
  }
55
68
 
56
- ctx.ui.notify(`Connection failed: ${msg}`, "error");
57
- return;
69
+ return { success: false, error: `Connection failed: ${msg}` };
58
70
  }
59
- ctx.ui.notify("Connected to Chrome ✓", "info");
60
71
 
61
- // Step 3: Verify with test navigation
62
- ctx.ui.notify("Testing browser control...", "info");
72
+ // Step 4: Verify with test navigation
63
73
  const tabResult = await client.newTab("https://github.com");
64
74
  if (!tabResult.success) {
65
- ctx.ui.notify(
66
- `Browser connected but test navigation failed: ${tabResult.error.message}`,
67
- "warning",
68
- );
69
- return;
75
+ return { success: false, error: `Browser connected but test navigation failed: ${tabResult.error.message}` };
70
76
  }
71
77
 
72
78
  const info = await client.pageInfo();
@@ -75,24 +81,24 @@ async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<v
75
81
  }
76
82
 
77
83
  const pageUrl = info.success && !("dialog" in info.data) ? info.data.url : "github.com";
78
- ctx.ui.notify(`Browser control verified ✓\nNavigated to: ${pageUrl}`, "info");
84
+ return { success: true, data: `Browser connected ✓\nNavigated to: ${pageUrl}` };
79
85
  }
80
86
 
87
+ // ── Chrome process detection ───────────────────────────────────────────────
88
+
81
89
  function checkChromeRunning(): boolean {
82
90
  try {
83
91
  if (process.platform === "darwin") {
84
- // Exact line matching — excludes "Google Chrome Helper" etc.
85
- // that linger after the user quits the browser.
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.
86
95
  const out = execSync("ps -A -o comm=", { timeout: 5000 }).toString().toLowerCase();
87
96
  const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
88
- const browserProcesses = ["google chrome", "chromium", "microsoft edge"];
89
97
  return lines.some((l) => {
90
- if (l.includes("helper") || l.includes("renderer")) return false;
91
- return browserProcesses.includes(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");
92
100
  });
93
101
  } else if (process.platform === "linux") {
94
- // Use args to distinguish the main browser process from sub-processes
95
- // (GPU, renderer, utility, etc.) which carry --type= flags.
96
102
  const out = execSync("ps -A -o comm=,args=", { timeout: 5000 }).toString().toLowerCase();
97
103
  const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
98
104
  const browserComms = ["chrome", "chromium", "chromium-browser", "msedge", "microsoft-edge", "google-chrome"];
package/src/util/tool.ts CHANGED
@@ -9,6 +9,8 @@ import type {
9
9
  ToolRenderResultOptions,
10
10
  } from "@mariozechner/pi-coding-agent";
11
11
  import type { Component } from "@mariozechner/pi-tui";
12
+ import { access } from "node:fs/promises";
13
+ import { DAEMON_SOCKET_PATH } from "../daemon/protocol";
12
14
  import type { BrowserClient } from "../client";
13
15
  import type { Result } from "./result";
14
16
 
@@ -142,6 +144,23 @@ export const registerBrowserTool = (
142
144
  : {}),
143
145
  async execute(_toolCallId, args, signal, onUpdate, extensionCtx) {
144
146
  if (def.ensureAlive !== false) {
147
+ // Check daemon socket exists before attempting browser control.
148
+ // If missing, the user hasn't run /browser-setup yet. Return a clear
149
+ // error so the agent knows to direct the user to initialize.
150
+ try {
151
+ await access(DAEMON_SOCKET_PATH);
152
+ } catch {
153
+ return toToolResult(
154
+ {
155
+ success: false,
156
+ error: {
157
+ kind: "not_connected",
158
+ message: "Browser harness not initialized. Run /browser-setup first",
159
+ },
160
+ },
161
+ def.name,
162
+ );
163
+ }
145
164
  const alive = await client.ensureAlive();
146
165
  if (!alive.success) {
147
166
  return toToolResult(