aboutsummaryrefslogtreecommitdiff
path: root/src/domains
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/domains
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/domains')
-rw-r--r--src/domains/navigate.ts20
-rw-r--r--src/domains/setup.ts41
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 });
+ },
+});