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/index.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/index.ts')
| -rw-r--r-- | src/index.ts | 71 |
1 files changed, 46 insertions, 25 deletions
diff --git a/src/index.ts b/src/index.ts index c2aa2d0..7265263 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ import { registerSetupCommand } from "./setup"; import { type BrowserState, defaultState, persistState, restoreState } from "./state"; import { registerAllTools } from "./registry"; import { cleanupTempDirs } from "./util/truncate"; +import { createDaemonTransport } from "./cdp/daemon-transport"; export default function browserHarnessExtension(pi: ExtensionAPI): void { const flagNs = pi.getFlag("browser-namespace") as string | undefined; @@ -98,25 +99,43 @@ export default function browserHarnessExtension(pi: ExtensionAPI): void { pi.on("session_start", async (_event, ctx) => { state = restoreState(ctx, state.namespace); - const initialOwnership: { ownedTargetIds?: ReadonlyArray<string>; harnessWindowTargetId?: string } = {}; - if (state.ownedTargetIds !== undefined) initialOwnership.ownedTargetIds = state.ownedTargetIds; - if (state.harnessWindowTargetId !== undefined) initialOwnership.harnessWindowTargetId = state.harnessWindowTargetId; - client = createBrowserClient({ - namespace: state.namespace, - ...(Object.keys(initialOwnership).length > 0 ? { initialOwnership } : {}), - onOwnershipChange: (snap) => { - state = { - ...state, - ownedTargetIds: snap.ownedTargetIds, - ...(snap.harnessWindowTargetId !== undefined - ? { harnessWindowTargetId: snap.harnessWindowTargetId } - : {}), - }; - persistState(pi, state); - }, - }); - // failure is fine — surfaced via tool errors when the agent first tries to use the browser - await client.start(); + + // Create the client lazily on first session. The daemon transport + // is created but NOT connected — that only happens when the user + // runs /browser-setup or a browser tool call finds an existing socket. + // This means zero Chrome prompts for non-browser pi sessions. + if (!client) { + const transport = createDaemonTransport(state.namespace); + + const initialOwnership: { ownedTargetIds?: ReadonlyArray<string>; harnessWindowTargetId?: string } = {}; + if (state.ownedTargetIds !== undefined) initialOwnership.ownedTargetIds = state.ownedTargetIds; + if (state.harnessWindowTargetId !== undefined) initialOwnership.harnessWindowTargetId = state.harnessWindowTargetId; + + client = createBrowserClient({ + namespace: state.namespace, + transport, + ...(Object.keys(initialOwnership).length > 0 ? { initialOwnership } : {}), + onOwnershipChange: (snap) => { + state = { + ...state, + ownedTargetIds: snap.ownedTargetIds, + ...(snap.harnessWindowTargetId !== undefined + ? { harnessWindowTargetId: snap.harnessWindowTargetId } + : {}), + }; + // Ownership changes can fire from CDP events at any time, + // including after session replacement when pi's ctx is stale. + // Persistence is best-effort — failure must not crash the event consumer. + // ponytail: try/catch is the simplest guard against stale-ctx. + try { persistState(pi, state); } catch { /* stale ctx — safe to ignore */ } + }, + }); + } + + // Do NOT call client.start() here — browser control is on-demand. + // The first browser tool call or /browser-setup triggers the actual + // daemon socket connection and Chrome prompt. + if (!toolsRegistered) { registerAllTools(pi, client); toolsRegistered = true; @@ -132,15 +151,17 @@ export default function browserHarnessExtension(pi: ExtensionAPI): void { persistState(pi, state); if (client) { try { - await client.stop(); + // Detach from the page target (removes the "Chrome is being controlled" + // banner) but keep the transport alive for the next session. + await client.detach(); } catch (e) { - // Shutdown is best-effort, but a stuck stop() points at a transport - // bug worth surfacing for debugging. - console.warn("[pi-browser-harness] client.stop() failed during shutdown:", e); + console.warn("[pi-browser-harness] client.detach() failed during shutdown:", e); } - client = null; + // ponytail: keep the transport alive across sessions. + // Do NOT call client.stop() or null out client — the daemon connection + // persists and eliminates the per-session "Allow Remote Debugging" prompt. + // Only Chrome restart or daemon death triggers a new prompt. } - toolsRegistered = false; await cleanupTempDirs(); }); |
