aboutsummaryrefslogtreecommitdiff
path: root/src/index.ts
diff options
context:
space:
mode:
authorAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-06-21 00:46:44 +0530
committerGitHub <noreply@github.com>2026-06-21 00:46:44 +0530
commitda5d31af999b728c31ab0169373531cab4543f0c (patch)
tree5d350ad1428ff332372a9c724ac46c964d8ca477 /src/index.ts
parent8db684d43366d335760e106eae39a74376bd7640 (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.ts71
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();
});