| Age | Commit message (Collapse) | Author |
|
Target.attachToTarget was decoded at 5 sites, Browser.getWindowForTarget
at 5, and Runtime.evaluate unwrapped at another. Each is now one typed
helper. Deletes the dead cdp/window-binding.ts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
collapse decode into one path
session-call-test.ts previously only exercised decodeResult (Task 3's
surface, and mostly duplicating commands-test.ts) — call, callOnTarget,
callBrowser, and the session's internal req helper had zero coverage.
Add a stub CdpTransport (no socket, no browser) and test: a valid payload
decodes to typed data, a garbage payload becomes invalid_response without
throwing, a transport-level error passes through with its original kind
(not relabeled), call/callOnTarget/callBrowser route the session id
correctly, and opts.timeoutMs reaches the transport only when provided.
Also collapse the four near-identical transport-then-decode bodies (req,
call, callOnTarget, callBrowser) into one: the three public methods now
delegate to req, which is the sole place unknown ends for every CDP call
the session makes, internal or public. Public signatures are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
call/callOnTarget/callBrowser now take a known method and return its
decoded result type. The session is where unknown ends; transport keeps
returning unknown. Removes the casts in session.ts and target-factory.ts,
including the session's own internal Target.*/Browser.* calls, which now
decode through the same path as the public call methods.
Also completes two params-schema gaps in commands.ts surfaced by this
migration: Target.createTarget was missing openerId and Runtime.evaluate
was missing userGesture, both real CDP fields src/cdp/ already sends.
tsc is red project-wide (6 errors, all in src/domains/) — the sanctioned
state between Tasks 4 and 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
One typebox schema per CDP command is the source of both the TS types and
the runtime validator, so they cannot drift. Malformed responses become
invalid_response errors instead of TypeErrors several frames later.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
* feat: pin the agent to a chosen browser profile (#15)
Harness tabs were created with a bare Target.createTarget, which places
them in Chrome's defaultBrowserContextId — and that value follows window
focus. Which profile the agent acted as therefore depended on which
browser window the user last clicked, so the same task could run as a
work account on one run and a personal account on the next.
/browser-profile lists the connected browser's profiles as "Name (email)"
and pins one to ~/.pi/agent/browser-harness.json, so the choice outlives
the session. The same picker appears on first browser_setup when nothing
is pinned; with no interactive UI, or on cancel, setup continues with a
note and the previous behavior.
Chrome offers no direct route to another profile: Target.createTarget
rejects a foreign browserContextId, and openerId does not inherit the
opener's context. The window is opened through the browser's own command
line (--profile-directory, handed to the running browser by Chromium's
ProcessSingleton) and identified by a unique file:// sentinel page, since
a bare about:blank#token argument is discarded. Further tabs come from
window.open evaluated with userGesture: true — the only CDP-reachable way
to place a tab in a non-default browser context — correlated by a unique
token so concurrent isolated tabs stay distinct. A failed handshake is
reported, never silently redirected to another profile.
All four tab-creation sites now funnel through cdp/target-factory, so
open_urls and the isolated tabs behind web_search / read_page cannot run
with a different profile's cookies than the visible tabs.
Also fixed along the way: snap Chromium is discovered, Windows uses
%LOCALAPPDATA%, browsers launched with an explicit --user-data-dir are
found, the running browser is chosen by ranking candidates against the
connected user-data-dir rather than taking the first process found, and
an installed-but-closed browser no longer reads as running.
CI runs the profile suites and a real-browser end-to-end test on ubuntu,
macOS, and Windows — profile discovery is the one part of the harness
whose behavior genuinely differs per OS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: single-flight harness window creation
Isolated tabs run concurrently (browser_read_page is registered
unserialized), so several could each find "no harness window" and create
one — two blank windows before this feature, two LAUNCHED profile windows
after it. ensureHarnessWindow now shares one in-flight creation per
client.
Sharing introduces a second hazard: freshlyCreated is a claim on the seed
tab, and if every waiter received it, they would all drive the same tab.
Only the caller that started the creation keeps the claim; the rest open
their own tab beside it.
Covered by test/manual/profile-concurrency-test.ts (five concurrent
isolated tabs, pinned and unpinned: distinct targets, one window, correct
profile), wired into CI on Linux only — the coordination is pure JS and
identical across platforms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: tilde expansion differs by purpose on Windows
CI on windows-latest caught this: join("~", "x") yields "~\x" there, and
expandTilde only handled "~/".
The two uses are not the same, so they no longer share a helper. The
agent dir stays byte-compatible with pi's own expandTildePath ("~/" only,
every platform) — being more lenient would put the pin file somewhere pi
never looks. $CHROME_USER_DATA_DIR is ours to interpret, so it accepts
"~\" as well, which is what a Windows user would type.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
- Add forms domain with a universal field setter that auto-detects element
type and fires framework-compatible input events (React native-setter trick)
- Add window-binding helper to capture and bind the real Chrome windowId as the
session's durable window identity on every new-window code path
- Wire supporting changes across CDP session/ownership, client, navigate,
isolated-tab, prompt, and state
- Add manual tests for forms and window binding
- Bump version 0.8.3 -> 0.9.0
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
discovery (#13)
* feat: support Brave + Chrome channels, fix stale DevToolsActivePort discovery
Broadens browser detection and hardens CDP endpoint discovery, based on the
investigation and approach in #4 (Brave Beta on macOS with a leftover Chrome
DevToolsActivePort file).
Browser detection (src/setup.ts, src/cdp/discovery.ts):
- checkChromeRunning() now recognizes Brave (macOS "brave browser", Linux
brave/brave-browser, Windows brave.exe), alongside the existing Chrome/
Chromium/Edge detection. Also excludes gpu/updater sub-processes that can
linger after a browser quits.
- profileDirs() gains Brave (Stable/Beta/Nightly/Dev) and Chrome Beta/Dev/
Canary/Chromium profile paths across macOS, Linux, and Windows.
Stale DevToolsActivePort handling (src/cdp/discovery.ts):
Previously discovery trusted the first readable DevToolsActivePort file. A
browser that has quit leaves its file behind, so if another browser later binds
the same port (e.g. 9222), discovery returned a WS URL carrying the dead
browser's UUID against the live browser's server, and the connection failed.
discoverWsUrl() now:
- collects all readable candidates instead of committing to the first;
- when candidates share a port, keeps the most-recently-written file (mtime);
- skips candidates whose port isn't live via a fast single-shot probe (no
30s waitForPort block on stale files);
- asks each live browser for its canonical webSocketDebuggerUrl via
/json/version, falling back to the file's WS path when that endpoint is
disabled (some browsers only expose it via --remote-debugging-port);
- falls back to probing well-known ports (9222, plus BU_CDP_PORTS) when no
profile file is readable — covering sandboxed harnesses (EPERM/EACCES) and
non-default install locations.
Setup guidance updated to mention brave://inspect / edge://inspect and the
--remote-debugging-port launch flag.
Verified: typecheck clean; queryLiveWsUrl / mtime-dedup / fallback-port logic
exercised against a mock /json/version server; checkChromeRunning() matches the
single main process against live macOS ps output, excluding all helpers.
Closes #4
Co-authored-by: Trevor Whitney <trevorjwhitney@gmail.com>
* fix: harden discovery against malformed port files + stale-then-fallback
Addresses two issues raised in review of the discovery rewrite:
- Validate the port parsed from DevToolsActivePort before building a
candidate. A truncated/corrupt file (non-numeric or out-of-range first
line) yielded NaN, and net.connect throws ERR_SOCKET_BAD_PORT
synchronously for NaN / <=0 / >=65536 — which escaped discoverWsUrl as an
unhandled rejection instead of surfacing as a Result error. Such entries
are now skipped.
- Fall back to well-known ports (9222 / BU_CDP_PORTS) after all discovered
candidates prove stale. Previously the fallback ran only when no port file
was readable, so a leftover file for a quit browser caused a ~30s
waitForPort block and failure without ever trying a live fallback port.
Ports already covered by a candidate are skipped to avoid re-probing.
---------
Co-authored-by: Trevor Whitney <trevorjwhitney@gmail.com>
|
|
|
|
Interaction tools could only target elements by raw viewport coordinates
(browser_click) or model-guessed CSS selectors (browser_fill/focus/
select_option/upload). Neither survives a re-render: on React/SPA forms,
every save or edit reflows the page, so captured @(x,y) coordinates go
stale and guessed selectors miss — leaving the agent to fall back to
hand-rolled execute_js DOM walks and unnoticed failed saves.
Add a stable ref system, mirroring Playwright MCP / browser-use:
- snapshot assigns each interactive element a ref (e1, e2, …), shown as
[eN] in the outline, and publishes a per-tab ref → backendNodeId map
- interaction tools accept an optional `ref` that re-resolves the element
at call time (via DOM.resolveNode / getBoxModel), so it stays valid
across re-renders; coordinates/selectors remain as fallbacks
- mutating tools append a compact diff of changed / newly-appeared (*) /
removed interactive elements, so silent failed saves surface immediately
- stale refs return an actionable "re-snapshot" error instead of a miss
Ref map is replaced per snapshot (bounded memory); fill keeps the native
_valueTracker write path so React controlled inputs still update.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
|
* 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
|
|
Replace singleton dialog, pageInfoDirty, networkBuffer, and consoleBuffer
with per-tab TabSession objects stored in a Map<string, TabSession>.
- Add TabSession type (sessionId, targetId, dialog, pageInfoDirty,
networkBuffer, consoleBuffer) and tabs/sessionIdToTargetId Maps
- Add ev.sessionId filter guard in consumeEvents to prevent cross-tab
event contamination
- Rework switchTo to create/reuse TabSession without clearing buffers,
updating sessionId on re-attach
- Route all CDP events (dialog, network, console) through resolveTab
to the correct TabSession
- takeDialog/drainPageInfoInvalidations/drainNetworkBuffer/
drainConsoleBuffer read from current TabSession
- Target.targetDestroyed prunes dead TabSession entries
- attachFirstPage creates initial TabSession and prunes dead entries
- Per-tab pageCaches Map in client.ts replaces pageCache singleton
- listTabs reconciliation prunes dead pageCaches entries
- console-buffer.ts clear() resets nextSeq to 1 (per-tab seq space)
- Multi-agent warning in prompt.ts Parallel Execution section
|
|
|
|
prompts
Adds two new tools and pivots the agent's default workflow away from
screenshots toward structured page data.
browser_snapshot — CDP accessibility tree (roles, names, states, hierarchy)
with click coordinates @(x,y) attached to every interactive element via
DOM.getBoxModel (parallel, 1.5s aggregate budget). Pass directly to
browser_click — no screenshot round-trip. Optional includeScreenshot:true
for visual confirmation. format:"json" returns a slim structure with full
box per node.
browser_network_requests — replaces the deprecated browser_get_network_log
placeholder. Filters by urlPattern (substring or /regex/), method, status,
resource type, recency. Optional response-body capture under a 5s aggregate
budget with 50KB per-body cap. Buffer is page-scoped (cleared on tab switch),
500 records/tab, with a bufferOverflowed flag on drain.
CDP plumbing: in-process aggregator in src/cdp/network-buffer.ts wired into
the existing single-consumer event loop. Accessibility domain enabled
alongside Page/DOM/Runtime/Network on every attach.
Ctrl+O expand/collapse on three tools' renderResult — browser_snapshot,
browser_network_requests, browser_execute_js. Compact-by-default views with
keyHint("app.tools.expand") that respects user keybinding remaps.
Tool prompts pivoted snapshot-first: browser_screenshot is documented as
LAST RESORT for visual verification only; browser_snapshot is the default
for understanding pages; browser_execute_js is the default for surgical
reads. browser_click and browser_open_urls guidance follows.
SKILL.md collapsed from ~342 to ~53 lines — keeping only what isn't already
in tool prompts: the decision tree, connection rules, browser_run_script
daemon bindings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Track which page targets this harness session opened so the agent can never
attach to, switch to, or close a tab the user opened themselves. New tabs
are spawned in a dedicated Chrome window (newWindow:true on first open,
openerId chaining after) so harness activity stays visually separate.
- new OwnershipRegistry (src/cdp/ownership.ts) tracks owned targetIds and
the harness window seed; mutations notify a single listener that persists
to BrowserState (ownedTargetIds, harnessWindowTargetId)
- attachFirstPage prefers an already-owned live target; otherwise opens a
fresh harness-owned tab in its own window instead of grabbing whatever
page the user is on
- session subscribes Target.setDiscoverTargets and reaps targetDestroyed
events so the registry stays in sync with reality
- listTabs returns owned:boolean per tab and reconciles dead ids
- browser_list_tabs defaults to scope:"owned"; pass scope:"all" to inspect
the user's other tabs (read-only — they cannot be switched to or closed)
- browser_switch_tab refuses non-owned tabs, with a clear remediation hint
- browser_new_tab opens inside the harness window via openerId, falling
back to a fresh window when the seed has been closed
- new browser_close_tab tool — owned tabs only, supports unique hex prefix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
document IIFE-wrap heuristic, log shutdown failures, fix CHANGELOG file-size claim
Five tweaks from the whole-implementation review:
- session.ts: log unexpected event-consumer rejections instead of silent swallow
- client.ts: comment the return-substring IIFE-wrap false-positive case
- transport.ts: comment that ws.onclose post-open settle is no-op-by-design
- index.ts: log client.stop() failures during shutdown
- CHANGELOG.md: correct largest-file claim (transport.ts and client.ts are ~220 each, not "client.ts at ~180")
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
page info shape
|
|
|
|
|
|
|
|
errors
|
|
|
|
|