diff options
| author | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-06-21 00:46:44 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-21 00:46:44 +0530 |
| commit | da5d31af999b728c31ab0169373531cab4543f0c (patch) | |
| tree | 5d350ad1428ff332372a9c724ac46c964d8ca477 /src/setup.ts | |
| parent | 8db684d43366d335760e106eae39a74376bd7640 (diff) | |
release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init (#6)
* feat: Unix socket daemon with auto-reconnect for persistent Chrome CDP
Eliminates the 'Allow Remote Debugging' Chrome prompt that appeared on
every pi session by multiplexing pi instances through a single persistent
daemon process.
Key additions:
- Unix socket IPC (net.createServer + readline JSON framing)
- CdpBridge with ID multiplexer + event router
- DaemonTransport implementing CdpTransport interface
- Auto-spawn with ensureDaemon() (lazy, checks socket)
Fixes since initial implementation:
- Bridge auto-reconnects to Chrome with exponential backoff (1s-60s).
Previously the bridge tried once at startup and permanently failed
if Chrome wasn't ready — causing 'Chrome not connected' on every
browser tool call.
- Daemon spawn uses project-local tsx binary instead of global --import
flag, fixing silent spawn failure (ERR_MODULE_NOT_FOUND).
- Stale ctx crash: persistState in onOwnershipChange wrapped in
try/catch to prevent CDP event consumer crashes.
49 test assertions across 4 test files. Zero TypeScript errors.
* feat: on-demand browser initialization
Browser daemon no longer auto-starts on session_start. Instead:
- session_start creates the client lazily but does NOT connect or spawn
the daemon. Zero Chrome prompts for non-browser pi sessions.
- Browser tool wrapper checks for daemon socket before each call.
Socket missing → clear error: 'Run /browser-setup first'.
Socket exists → proceeds silently (daemon already running).
- /browser-setup is the explicit, one-time init point. It spawns
the daemon and connects to Chrome only when the user opts in.
- SKILL.md updated with Connection Setup instructions telling agents
to direct users to /browser-setup when socket is missing.
* fix: checkChromeRunning on macOS — match by substring not exact
ps -A -o comm= returns full executable paths on modern macOS
(e.g. /Applications/Google Chrome.app/Contents/MacOS/Google Chrome),
not the basename. The old exact-match check (browserProcesses.includes(l))
never matched, causing false 'No browser instance running' errors.
Changed to substring includes() with helper/renderer/crashpad filtering.
* feat: browser_setup as agent-callable tool
The agent can now self-recover from 'Browser harness not initialized'
errors by calling browser_setup directly — no user mediation needed.
- Extracted shared performSetup() from the slash command into setup.ts
- Created src/domains/setup.ts with ensureAlive:false to bypass the
socket guard (this tool IS the setup, can't require itself)
- Registered browser_setup in the tool registry
- Updated SKILL.md: agent calls browser_setup directly, not ask user
- Removed stale test/manual/spawn-test.ts
* release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init
Diffstat (limited to 'src/setup.ts')
| -rw-r--r-- | src/setup.ts | 84 |
1 files changed, 45 insertions, 39 deletions
diff --git a/src/setup.ts b/src/setup.ts index 3909603..561a343 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,37 +1,50 @@ /** - * Setup command — verifies Chrome is running with remote debugging - * and connects the pi agent to it. + * Setup — verifies Chrome is running with remote debugging and connects + * the pi agent to it. Exposed as both a slash command (/browser-setup) for + * users and a tool (browser_setup) for the agent to self-recover. */ -import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { execSync } from "node:child_process"; import type { BrowserClient } from "./client"; +import { ensureDaemon } from "./daemon/spawn"; + +// ── Public: register the /browser-setup slash command ────────────────────── export function registerSetupCommand(pi: ExtensionAPI, client: BrowserClient): void { pi.registerCommand("browser-setup", { description: "Connect pi to your Chrome browser", handler: async (_args, ctx) => { - await runSetup(ctx, client); + const result = await performSetup(client); + if (result.success) { + ctx.ui.notify(result.data, "info"); + } else { + ctx.ui.notify(result.error, "error"); + } }, }); } -async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<void> { - ctx.ui.notify("browser setup: checking Chrome...", "info"); +// ── Shared setup result ──────────────────────────────────────────────────── + +export type SetupResult = { success: true; data: string } | { success: false; error: string }; + +// ── Shared setup logic (used by both the command and the tool) ───────────── +export async function performSetup(client: BrowserClient): Promise<SetupResult> { // Step 1: Check Chrome is running const chromeRunning = checkChromeRunning(); if (!chromeRunning) { - ctx.ui.notify( - "No browser instance running. Please open your browser and then run /browser-setup.", - "error", - ); - return; + return { success: false, error: "No browser instance running. Please open your browser and then run /browser-setup." }; } - ctx.ui.notify("Chrome is running ✓", "info"); - // Step 2: Try to connect to Chrome DevTools - ctx.ui.notify("Connecting to Chrome DevTools...", "info"); + // Step 2: Start the browser daemon (spawns if not running, silently reuses if alive) + const daemonReady = await ensureDaemon(); + if (!daemonReady) { + return { success: false, error: "Could not start the browser daemon. Check /tmp/pi-browser-daemon.sock." }; + } + + // Step 3: Connect to Chrome DevTools const startResult = await client.start(); if (!startResult.success) { const msg = startResult.error.message; @@ -43,30 +56,23 @@ async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<v lower.includes("econnrefused") || lower.includes("cannot reach chrome devtools") ) { - ctx.ui.notify( - "Chrome remote debugging needs to be enabled.\n\n" + - "Open chrome://inspect/#remote-debugging in your browser, tick the\n" + - "\"Discover network targets\" / Allow checkbox, then run /browser-setup again.\n\n" + + return { + success: false, + error: + "Chrome remote debugging needs to be enabled.\n\n" + + 'Open chrome://inspect/#remote-debugging in your browser, tick the\n' + + '"Discover network targets" / Allow checkbox, then retry.\n\n' + "Or set BU_CDP_WS to a remote browser WebSocket URL.", - "warning", - ); - return; + }; } - ctx.ui.notify(`Connection failed: ${msg}`, "error"); - return; + return { success: false, error: `Connection failed: ${msg}` }; } - ctx.ui.notify("Connected to Chrome ✓", "info"); - // Step 3: Verify with test navigation - ctx.ui.notify("Testing browser control...", "info"); + // Step 4: Verify with test navigation const tabResult = await client.newTab("https://github.com"); if (!tabResult.success) { - ctx.ui.notify( - `Browser connected but test navigation failed: ${tabResult.error.message}`, - "warning", - ); - return; + return { success: false, error: `Browser connected but test navigation failed: ${tabResult.error.message}` }; } const info = await client.pageInfo(); @@ -75,24 +81,24 @@ async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<v } const pageUrl = info.success && !("dialog" in info.data) ? info.data.url : "github.com"; - ctx.ui.notify(`Browser control verified ✓\nNavigated to: ${pageUrl}`, "info"); + return { success: true, data: `Browser connected ✓\nNavigated to: ${pageUrl}` }; } +// ── Chrome process detection ─────────────────────────────────────────────── + function checkChromeRunning(): boolean { try { if (process.platform === "darwin") { - // Exact line matching — excludes "Google Chrome Helper" etc. - // that linger after the user quits the browser. + // ps -o comm= returns the full executable path on modern macOS. + // We check for "Google Chrome" anywhere in the path (case-insensitive), + // excluding helper/renderer subprocesses. const out = execSync("ps -A -o comm=", { timeout: 5000 }).toString().toLowerCase(); const lines = out.split("\n").map((l) => l.trim()).filter(Boolean); - const browserProcesses = ["google chrome", "chromium", "microsoft edge"]; return lines.some((l) => { - if (l.includes("helper") || l.includes("renderer")) return false; - return browserProcesses.includes(l); + if (l.includes("helper") || l.includes("renderer") || l.includes("crashpad")) return false; + return l.includes("google chrome") || l.includes("chromium") || l.includes("microsoft edge"); }); } else if (process.platform === "linux") { - // Use args to distinguish the main browser process from sub-processes - // (GPU, renderer, utility, etc.) which carry --type= flags. const out = execSync("ps -A -o comm=,args=", { timeout: 5000 }).toString().toLowerCase(); const lines = out.split("\n").map((l) => l.trim()).filter(Boolean); const browserComms = ["chrome", "chromium", "chromium-browser", "msedge", "microsoft-edge", "google-chrome"]; |
