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/domains | |
| 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/domains')
| -rw-r--r-- | src/domains/navigate.ts | 20 | ||||
| -rw-r--r-- | src/domains/setup.ts | 41 |
2 files changed, 58 insertions, 3 deletions
diff --git a/src/domains/navigate.ts b/src/domains/navigate.ts index 7012d15..4477485 100644 --- a/src/domains/navigate.ts +++ b/src/domains/navigate.ts @@ -3,6 +3,7 @@ import { Text } from "@mariozechner/pi-tui"; import { type Result, err, ok } from "../util/result"; import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool"; import { applyTruncation } from "../util/truncate"; +import { safeJs } from "../util/js-template"; const NavigateArgs = Type.Object({ url: Type.String({ description: "Full URL to navigate to (e.g. https://github.com)" }), @@ -82,11 +83,18 @@ export const openUrlsTool = defineBrowserTool({ renderCall: (a) => new Text(`🌐 Opening ${a.urls.length} URL${a.urls.length !== 1 ? "s" : ""}…`, 0, 0), async handler(args, { client, onUpdate }): Promise<Result<ToolOk, ToolErr>> { const total = args.urls.length; - // Phase 1: create tabs in parallel. + // Phase 1: create tabs in parallel. Own them so they appear in + // browser_list_tabs scope:'owned' and group in the harness window. + const hw = client.ownership().harnessWindow(); const created = await Promise.all(args.urls.map(async (url): Promise<TabResult> => { - const r = await client.session().callBrowser("Target.createTarget", { url: "about:blank" }); + const params: Record<string, unknown> = { url: "about:blank" }; + if (hw) params["openerId"] = hw; + else params["newWindow"] = true; + const r = await client.session().callBrowser("Target.createTarget", params); if (!r.success) return { url, targetId: "", ok: false, error: r.error.message }; - const c = r.data as { targetId: string }; // CDP boundary cast: Target.createTarget returns { targetId: string } + const c = r.data as { targetId: string }; + if (params["newWindow"]) client.ownership().setHarnessWindow(c.targetId); + client.ownership().add(c.targetId); return { url, targetId: c.targetId, ok: true }; })); // Phase 2: attach + navigate each created tab in parallel. @@ -101,6 +109,12 @@ export const openUrlsTool = defineBrowserTool({ if (!enabled.success) return { ...tab, ok: false, error: enabled.error.message }; const nav = await client.session().callOnTarget("Page.navigate", { url: tab.url }, sid); if (!nav.success) return { ...tab, ok: false, error: nav.error.message }; + // Mark the tab with a green circle so the user can see which + // tabs the agent opened. Best-effort; don't fail on CSP blocks. + client.evaluateJs( + safeJs`if(!document.title.startsWith('🟢'))document.title='🟢 '+document.title`, + sid + ).catch(() => {}); return tab; } finally { completed++; diff --git a/src/domains/setup.ts b/src/domains/setup.ts new file mode 100644 index 0000000..bbd767f --- /dev/null +++ b/src/domains/setup.ts @@ -0,0 +1,41 @@ +/** + * browser_setup tool — agent-callable browser initialization. + * + * The agent can call this directly when browser tools fail with + * "not initialized" instead of asking the user. Idempotent: safe + * to call even when already connected. + */ + +import { Type } from "typebox"; +import { Text } from "@mariozechner/pi-tui"; +import type { Result } from "../util/result"; +import { err, ok } from "../util/result"; +import { defineBrowserTool, type ToolErr, type ToolOk } from "../util/tool"; +import { performSetup } from "../setup"; + +const SetupArgs = Type.Object({}); + +export const setupTool = defineBrowserTool({ + name: "browser_setup", + label: "Browser Setup", + description: + "Initialize browser control. Spawns the daemon, connects to Chrome, opens a test tab. " + + "Call when browser tools fail with 'Browser harness not initialized'. Idempotent.", + promptSnippet: "Initialize browser connection", + promptGuidelines: [ + "Call browser_setup when you get a 'Browser harness not initialized' error from any browser tool.", + "This tool is idempotent — calling it when already connected is harmless.", + "After browser_setup succeeds, retry the browser tool that failed.", + ], + parameters: SetupArgs, + // ensureAlive:false — this tool IS the setup; it must bypass the socket guard + ensureAlive: false, + renderCall: () => new Text("🔧 Initializing browser...", 0, 0), + async handler(_args, { client }): Promise<Result<ToolOk, ToolErr>> { + const result = await performSetup(client); + if (result.success) { + return ok({ text: result.data }); + } + return err({ kind: "internal", message: result.error }); + }, +}); |
