35 KiB
Changelog
All notable changes to pi-browser-harness will be documented in this file.
0.11.0 — 2026-08-02
Fixed
--browser-debug-clicksnow does something. The flag was registered and documented but never read; the only working switch was the undocumentedBH_DEBUG_CLICKSenvironment variable. Both now drive the same setting.BU_CDP_WSnow actually attaches to a remote browser. The client read the variable and passed the URL totransport.connect(), but the daemon transport discards that argument and always dials the daemon socket — and the daemon's own discovery never looked at the variable, so setting it changed nothing. The override moved intodiscoverEndpoint, which both processes go through. It is read when the daemon starts, so a running daemon must be stopped first.- A disconnected client's pending requests no longer fire at a dead socket.
removeClientcleared the daemon's id multiplexer but not its callback map, so a gone client's command timeouts stayed armed and later tried to answer it.
Changed
- One route from a tool to the browser. Four coexisted;
src/domains/cdp-call.tsis now the only one, and two boundary-scanner rules scoped tosrc/domains/keep it that way. A CDP timeout now surfaces askind: "timeout"instead ofcdp_errorat the sites that previously flattened it. - Shared logic moved out of tool files into
ax-tree.ts,element-call.ts, andscreenshot-capture.ts, so importing one tool no longer pulls in another tool's module.cdp/attach.tsandcdp/window.tsbecamesession.attach()andsession.windowId();cdp/daemon-transport.tsbecamedaemon/transport.ts; the request-timeout half ofcdp/event-queue.tsbecamecdp/pending-requests.ts. - The daemon's request bookkeeping is one map instead of two, and a request that arrives while Chrome is down awaits a single connect signal rather than spinning its own 250 ms poll loop.
Removed
-
The second CDP transport.
createCdpTransportwas a full WebSocket-to-Chrome implementation reachable only through a fallback inclient.tsthat production never took.cdp/transport.tsnow holds just theCdpTransportinterface, and a client must be given a transport. -
Dead exports and a fake config knob:
isInternalUrl,SPECIAL_KEYS,andThen,mapErr,BrowserState.remoteBrowserId,IpcServer.disconnectClient,IpcServer.clients(), andDAEMON_STALE_SOCKET_CLEANUP(a hardcodedtruewith a branch around it). -
The
deep-researchskill, the/deep-researchcommand, and theweb-search-researchersubagent. The harness ships browser tools; orchestrating multi-agent research on top of them belongs to the agent, not to this extension.browser_web_searchandbrowser_read_pageare unaffected — search then read still works, it is just no longer wrapped in a fan-out workflow.
0.10.3 — 2026-07-26
Fixed
- Profile binding after an in-place browser upgrade. Linux appends
" (deleted)"to/proc/<pid>/exeonce the running binary is unlinked — which is exactly what agoogle-chromepackage update does while the browser stays open.detectRunningBrowserreturned that literal string as the executable path, so every profile launch tried to spawn/opt/google/chrome/chrome (deleted)and failed withENOENT. Executable paths are now validated against disk, with the marker stripped (and argv[0] used) when the linked binary is gone. - A failed browser launch no longer reports as a sentinel timeout.
spawn()signals a missing or non-executable binary asynchronously via anerrorevent rather than by throwing, and that event was discarded — soopenProfileWindowreturned success, no window ever opened, and the caller blamed the browser 15 seconds later withcouldn't open a window in "<profile>" automatically. The launch now waits forspawn/errorand returns the real cause, and removes its handshake page on the failure path.
0.10.2 — 2026-07-26
Fixed
- Page info no longer fails on a missing
documentElement. The page-info probe readdocument.documentElement.scrollWidth/scrollHeightdirectly, which throws whendocumentElementis absent — leaving every caller with an evaluation error instead of a page snapshot. The probe now falls back to zeroed scroll dimensions and returns a usablePageInfo.
0.10.1 — 2026-07-26
Fixed
- Chrome process leak on profile seed failure. When a profile is pinned and
seedProfileWindowspawns Chrome viaopenProfileWindow, the spawned child is kept asdetached: true+unref(). If Chromium's ProcessSingleton fails to delegate (user-data-dir mismatch, multiple browsers, Chrome busy/crashed), the spawned process starts a full second browser instance. The sentinel never appears, the seed times out, but the spawned Chrome process was never killed — accumulating zombie browser instances that consume RAM. The fix stores the child process reference, exposes akill()on the returned handle, and calls it on seed timeout. - Unnecessary Chrome re-spawns on every
start().profileContextIdwas unconditionally cleared on everystart(), forcing a new profile-window launch via ProcessSingleton even when Chrome hadn't restarted and the context was still valid. Now the context is cleared only when the browser UUID in the WebSocket URL changes — which Chrome re-mints on every launch. setProfilePinno longer clears context when the pin hasn't changed. Loading the same pin from disk onsession_startwas clearingprofileContextId, forcing yet another Chrome spawn on the nextstart().
0.10.0 — 2026-07-25
Added
/browser-profile— choose which browser profile the agent works in. The command lists every profile in the connected browser, labelledName (email)(orName (Profile 3)when the profile has no signed-in account), marks the current selection, ordered the way the browser orders them. The choice is saved to~/.pi/agent/browser-harness.json, so it survives session termination, pi restarts, and applies across projects; a trailing— Clear selection —row restores the previous behavior. Picking a profile mid-session takes effect immediately — the harness closes its tabs and reopens its window in the chosen profile.- First-run profile prompt. When no profile has been chosen,
/browser-setupand the agent-callablebrowser_setupshow the same picker before connecting, and report the result asBrowser profile: <label>. With no interactive UI (print/RPC mode) or when the user cancels, setup continues with a one-line note and the pre-existing behavior, so non-interactive usage is unaffected. /browser-statusreports the selected profile.
Fixed
- The agent no longer lands in an arbitrary browser profile. Harness tabs were created with a bare
Target.createTarget, which places them in Chrome'sdefaultBrowserContextId— a value that follows window focus. The 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. With a profile pinned, the harness opens its window inside that profile and keeps every tab there. Chrome offers no direct route for this:Target.createTargetrejects another profile'sbrowserContextIdoutright, andopenerIddoes not inherit the opener's context. The window is opened through the browser's own command line (--profile-directory, which Chromium's ProcessSingleton hands to the already-running browser) and identified by a uniquefile://sentinel page; subsequent tabs come fromwindow.openevaluated withuserGesture: true, which is the only CDP-reachable way to place a tab in a non-default browser context. When the window cannot be opened, the harness reports it and stops rather than silently using another profile. - Tab creation is funnelled through one place (
src/cdp/target-factory.ts).browser_open_urlsand the isolated tabs behindbrowser_web_search/browser_read_pagepreviously calledTarget.createTargetthemselves, so under a pinned profile they would have run with a different profile's cookies than the visible tabs. - Snap-installed Chromium is discovered. Ubuntu's default Chromium keeps its user data in
~/snap/chromium/common/chromium, which was absent from the discovery list, making it invisible to the harness. - Windows profile discovery honours
%LOCALAPPDATA%instead of assumingAppData\Localunder the home directory — the two diverge on roaming and managed accounts. Browsers launched with an explicit--user-data-dir(and Linux's$CHROME_USER_DATA_DIR) are now found as well. - The right browser is identified when several are running. Browser detection now ranks candidate processes against the user-data-dir the harness is actually connected to, instead of taking the first Chromium-family process it finds; with Chrome and Brave both open, a profile window could otherwise be opened in a browser the harness is not attached to. Detection also reports the executable path and any explicit
--user-data-dir, and on Windows uses PowerShell CIM plus the App Paths registry key rather thanwmic, which Microsoft removed by default in Windows 11 24H2. - Installed-but-closed browsers are no longer mistaken for running ones — liveness is derived only from live-process evidence.
Tests
- Fixture-driven unit suites for profile enumeration, pin persistence, per-OS path resolution, and browser-process ranking (
test/profile/), plus a real-browser end-to-end test (test/manual/profile-e2e-test.ts) that creates two profiles in a throwaway user-data-dir and asserts distinct browser contexts, sentinel-based window identification, and that spawned tabs stay in the pinned profile and window. - CI now runs the profile suites and the end-to-end test on ubuntu, macOS, and Windows (Linux under Xvfb). Profile discovery is the one part of the harness whose behavior genuinely differs per OS, and the runner images ship real Chrome, so the cross-platform launch handshake is verified rather than assumed.
0.9.0 — 2026-07-24
Added
browser_web_search— new tool. Query → ranked links by scraping a real Google SERP in the user's own Chrome, so no API key or search subscription is involved. Runs in an isolated tab whose lifecycle mirrorsbrowser_open_urls(never touches the user's current tab). The SERP parser is pure and fixture-tested (9 scenarios), extracts results semantically (a h3) and unwraps redirect URLs. CAPTCHA walls and empty result sets surface asinvalid_statewithdetails.reasonrather than an empty list.browser_read_page— new tool. URL (or an ownedtargetId) → clean main-article text with nav/ad/footer boilerplate stripped, via a dependency-free readability heuristic. The DOM walk runs as an in-page capture expression; scoring and selection are a pure, fixture-tested function (link-density plus boilerplate-ancestor filtering, with a body-text fallback for pages that have no article structure). Registered unserialized, so concurrent reads are safe.deep-researchskill and/deep-researchcommand. Fans out to isolatedweb-search-researchersubagents, runs a coverage-driven loop with a hard iteration ceiling, and synthesizes a cited Markdown report. The researcher agent now usesbrowser_web_search+browser_read_page; it previously referencedweb_search/web_fetchtools that do not exist in this harness.- Forms domain with a universal field setter.
browser_fill,browser_fill_form,browser_select_option, andbrowser_set_checkednow live insrc/domains/forms.tsbehind one setter that auto-detects the element type and fires framework-compatible input events (the React native-setter trick), so controlled inputs register the change instead of silently reverting. - Durable window binding. Every new-window code path now captures the real Chrome
windowIdand binds it as the session's window identity, so per-session tab ownership survives navigation and tab churn instead of being inferred fresh each time. - Brave support and additional Chrome channels.
checkChromeRunning()recognizes Brave (macOSbrave browser, Linuxbrave/brave-browser, Windowsbrave.exe) alongside Chrome/Chromium/Edge, and now excludesgpu/updatersub-processes that linger after a browser quits. Profile discovery covers Brave Stable/Beta/Nightly/Dev and Chrome Beta/Dev/Canary/Chromium across macOS, Linux, and Windows. - Expand/collapse (Ctrl+O) rendering for
browser_web_searchandbrowser_read_page, via a sharedrenderExpandableTexthelper (src/domains/render.ts) mirroringbrowser_execute_js. Compact preview by default, full body on expand; the complete text still reaches the model regardless of render state. Theweb_searchsummary line surfaces engine and result count.
Fixed
- Daemon setup now works on Windows. The Unix-socket daemon introduced in 0.6.0 carried several POSIX-only assumptions that broke setup entirely on Windows:
DAEMON_SOCKET_PATHis now a named pipe (\\.\pipe\pi-browser-daemon) on win32, since a/tmppath is not a validnetlisten/connect target there;spawnDaemon()resolvestsx.cmd/npx.cmdand runs them through a shell with per-token quoting (paths with spaces) andwindowsVerbatimArguments, because npm's.cmdshims fail with EINVAL/ENOENT when spawned directly;isDaemonRunning()skips thefs.accesspre-check and probes the pipe directly, as named pipes are not filesystem entries andaccess()always fails on them; and stale-socketunlink()is a no-op on Windows, where pipes self-clean. (#7) - Stale
DevToolsActivePortfiles no longer break CDP discovery. Discovery previously trusted the first readable port file — a browser that has quit leaves its file behind, so when another browser later bound 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, keeps the most recently written file when candidates share a port, skips ports that aren't live via a fast single-shot probe (no 30swaitForPortblock on stale files), and asks each live browser for its canonicalwebSocketDebuggerUrlvia/json/version, falling back to the file's WS path when that endpoint is disabled. Well-known ports (9222, plusBU_CDP_PORTS) are probed both when no profile file is readable — covering sandboxed harnesses hitting EPERM/EACCES and non-default install locations — and after all discovered candidates prove stale. Ports parsed from a truncated or corrupt file are validated before use, sincenet.connectthrowsERR_SOCKET_BAD_PORTsynchronously forNaN/out-of-range values, which previously escaped as an unhandled rejection. (#4) - Footer browser status indicator removed. The chip set during
session_startwent stale: a successful/browser-setuponly calledctx.ui.notify()and never updatedctx.ui.setStatus("browser", …), so the red "Browser — run /browser-setup" nudge persisted while the browser was in fact connected. Browser control is on-demand and setup already reports its own outcome, so the persistent chip was removed rather than resynced. (#10)
Changed
- Setup guidance now mentions
brave://inspect/edge://inspectand the--remote-debugging-portlaunch flag.
0.8.3 — 2026-07-08
Fixed
- Stale socket detection no longer blocks daemon spawn after a crash.
isDaemonRunning()only checked that the Unix socket file existed; a dead daemon leaves a stale socket behind, soensureDaemon()skipped spawning andclient.start()failed with "Chrome not connected". It now runs a liveness probe — connect, register, verify the ack, disconnect — and cleans up the stale socket on failure. The bridge'shandleRequestis also async, polling for a Chrome connection for up to 15s before rejecting.
0.8.1 — 2026-07-05
Fixed
browser_dispatch_keynow populateskeyCode/whichon the synthesizedKeyboardEvent(both the ref and selector paths). Legacy React/Vue key handlers commonly branch one.keyCode === 13rather thane.key, so Enter on tag/autocomplete inputs previously did nothing. The event remains untrusted (isTrusted === false), so a few libraries may still ignore it.browser_execute_jsIIFE auto-wrap no longer false-positives. The wrap now triggers only when the trimmed source starts with areturnstatement, instead of whenever the string merely contained the substring"return "(which silently turned bare expressions — including ones mentioningreturninside a string literal or comment — intoundefined).
Changed
browser_fillguidelines now point atbrowser_dispatch_key({ ref, key: 'Enter' })for submitting tag/autocomplete inputs (notbrowser_press_key, which targets the focused element), and at the open →browser_snapshot→browser_clickrecipe for custom (div-based) dropdowns.
0.6.0 — 2026-06-21
Added
- Per-tab isolation for multi-agent safety. Each agent's browser session now operates in a dedicated Chrome window with tab ownership tracking. Tabs opened via
browser_open_urlsandbrowser_new_tabare automatically registered in the ownership registry and tagged with a 🟢 prefix in the document title for user visibility.browser_list_tabsscoped to"owned"(default) only shows the current session's tabs. - Unix socket daemon with auto-reconnect. The browser daemon now binds to a Unix-domain socket (
pi-browser.sock) in the harness temp directory instead of a TCP port. The transport layer reconnects automatically when Chrome restarts — no manual/browser-reload-daemonneeded. - On-demand browser initialization. Chrome is no longer launched eagerly at harness startup. The harness attaches lazily on the first browser tool call, reducing resource usage when the agent isn't using the browser.
browser_setupas agent-callable tool. The setup tool can now be called programmatically by agents, not just via slash command. Idempotent — safe to call when already connected.
Fixed
- Chrome detection on macOS now matches by process-name substring instead of exact binary path comparison, fixing false negatives when Chrome is running from different installation paths (e.g.
/Applications/Google Chrome.app/vs user-local copies).
Added
browser_console— new tool. Reads JS errors and console messages from the active tab via two CDP sources merged into one buffer:Runtime.consoleAPICalled(pageconsole.*calls and uncaught exceptions) andLog.entryAdded(browser-level entries — CSP violations, mixed content, deprecations, network errors). Filters:levels(log/info/warn/error/debug),textPattern(substring; wrap in slashes for regex),sinceMs,limit(default 50, cap 500). Each record carries a monotonicseq; the response includesnextCursorso callers can passsinceSeqto see only what's new since the previous drain — the cursor pattern that makes "what did this action cause?" answerable in one call. Buffer is page-scoped (cleared on tab switch) and bounded at 500 records;bufferOverflowedflag reports drops since the last drain. Stack traces (top 3 frames) are preserved for error/warn records. Per-arg cap of 2 KB prevents a singleconsole.log(hugeBlob)from blowing the buffer.LogCDP domain enabled alongside Page/DOM/Runtime/Network/Accessibility on every attach. (Runtimewas already enabled, soRuntime.consoleAPICalledwas reachable; this adds the missing domain for browser-level entries.)browser_consoleships with a customrenderResultfollowing the established pattern — collapsed: header counts (3 errors · 2 warnings · 47 logs) + last 5 rows. Expanded: full list with stack traces inline as code fences. AppendskeyHint("app.tools.expand", ...)so the binding label adapts to user remaps.
Internal
- New pure module
src/cdp/console-buffer.tsmirrors thenetwork-buffer.tspattern (insertion-ordered Map, FIFO eviction at capacity 500, overflow flag reset per drain). Wired into the existing single-consumer event loop insrc/cdp/session.ts.
Known follow-ups
- No tests added in this release. Verification is manual against real Chrome.
0.4.0 — 2026-05-06
Added
browser_snapshot— new tool. Returns the structured CDP accessibility tree (roles, names, states, hierarchy) for the current page. For every interactive element (button, link, textbox, checkbox, etc.) the outline includes click coordinates as@(x,y), fetched viaDOM.getBoxModelper node in parallel under a 1.5s aggregate budget. Pass these straight tobrowser_click— nobrowser_screenshotround-trip needed.format:"json"returns the slim structure withbox: {x,y,width,height,cx,cy}per node. OptionalincludeScreenshot:trueattaches a JPEG (q=80) when visual confirmation is also wanted.browser_network_requests— new tool, replacing the deprecatedbrowser_get_network_logplaceholder. Lists requests captured on the current tab since attach with filters:urlPattern(substring; wrap in slashes for regex),methodFilter,statusFilter,resourceTypes,sinceMs,limit(default 50, cap 500).includeResponseBodies:truefetchesNetwork.getResponseBodyper matched record under a 5s aggregate budget with a 50 KB per-body cap. Buffer is page-scoped (cleared on tab switch) and bounded at 500 records;bufferOverflowedflag in the result reports drops since the last drain.- Tab ownership / harness-window isolation. New
OwnershipRegistrytracks which page targets this session opened. The harness now creates a dedicated Chrome window on first attach (newWindow:true) instead of grabbing the user's foreground tab; subsequentbrowser_new_tabcalls open inside that window viaopenerId.browser_list_tabsdefaults toscope:"owned"; passscope:"all"to see the user's other tabs read-only.browser_switch_taband the newbrowser_close_tabrefuse non-owned tabs with a clear remediation hint. Ownership is persisted across session reloads viaBrowserState. The session also subscribesTarget.setDiscoverTargetsand reapstargetDestroyedevents so the registry stays in sync. AccessibilityCDP domain enabled alongside Page/DOM/Runtime/Network on every attach.Ctrl+O(app.tools.expand) expand/collapse on tool output. Three tools now ship customrenderResult:browser_snapshot— collapsed: 4-line summary (node count, URL, landmarks/buttons/inputs, screenshot status). Expanded: full indented outline + inline screenshot whenincludeScreenshot:true.browser_network_requests— collapsed: header + first 5 rows. Expanded: full markdown table + per-request body sections when bodies were requested.browser_execute_js— collapsed: size + first 120-char preview. Expanded: pretty-printed JSON if value parses, otherwise raw value, in a code fence. All three append akeyHint("app.tools.expand", ...)so the binding label adapts to user remaps.
Changed
- Tool prompts pivoted from screenshot-first to snapshot-first.
browser_screenshot's description now explicitly says "NOT a default exploration tool".browser_snapshotis documented as the default for understanding pages;browser_execute_jsas the default for surgical reads.browser_clickguidance no longer instructs the agent to screenshot for coordinates — it points atbrowser_snapshot's@(x,y)hints.browser_open_urlspost-step flipped from screenshot to snapshot. SKILL.mdrewritten (~342 lines → ~53 lines). Frontmatter description carries the tool-hierarchy hint (always in context per pi's progressive-disclosure model). Body keeps only what isn't already in tool prompts: the decision tree, the connection rules (real Chrome, no creds, dialog-first), and thebrowser_run_scriptdaemon bindings (the only tool whose API can't be inferred from its prompt). Pattern reference, parallelization details, troubleshooting, and tool enumeration removed as duplication.- CDP
Network.*events are now consumed by an in-process aggregator (src/cdp/network-buffer.ts) wired into the existing single-consumer event loop insrc/cdp/session.ts. Pure module, ring-buffered, non-destructive drain.
Removed
browser_get_network_log— was a deprecated placeholder in v0.3 that returned a "use PerformanceObserver" message because the CDP event stream had no public drain API. Replaced bybrowser_network_requests.
Known follow-ups
- Live network streaming (
browser_network_monitorreal-time during a click) — deferred; reuses the same buffer plumbing once the post-hoc form is proven. - No tests added in this release. Verification is manual against real Chrome.
0.3.2 — 2026-05-05
Added
- Parallel tool execution with automatic mutation serialization. Observation tools (
browser_screenshot,browser_page_info,browser_execute_js,browser_list_tabs,browser_http_getetc.) can now run in parallel with each other and with mutation tools. Mutation tools (browser_click,browser_type,browser_scroll,browser_navigate,browser_switch_tab, etc.) are automatically serialized through a shared async mutex so they never race on shared CDP session/page state. LLMs can emit independent operations in the same turn for better latency. - New
src/util/mutex.ts— lightweight async mutex (~25 LOC) with FIFO queue.serialized?: booleanflag added toBrowserToolDefinition;mutationMutex()exposed onBrowserClient. - Prompt and SKILL.md updated with parallel-execution guidance and safe-parallel-call examples.
Fixed
- Scroll tool deltaY sign convention corrected. Previously
deltaYwas documented as positive=up (inverted vs W3C wheel events). Now follows the W3C convention: positive=down, negative=up. Default changed from-300to300(scroll down). Prompt snippets, tool descriptions, and guidelines all updated. - Scroll tool now calls
Page.bringToFrontbefore dispatching mouse events, preventing silent-drop when the target page is not the active browser tab. Mouse events now include explicitbutton:"none",buttons:0,pointerType:"mouse". - Screenshot TUI render no longer crashes the host on long file paths. The
Imagetext-fallback render did not respect terminal width, so a long path could overflow and crash the host TUI. Each rendered line is now truncated withtruncateToWidthto fit the available width.
0.3.1 — 2026-05-02
Bug fixes
browser_list_tabsnow shows full 32-character targetIds instead of truncatedBE9DD1DC…prefixes. Thebrowser_list_tabs→browser_switch_tabround-trip is repaired.browser_switch_tabnow supports prefix matching: pass a unique hex prefix (≥8 chars) and it resolves to the full targetId automatically. Ambiguous prefixes return a clear error listing all matching tabs.browser_downloadauto-creates the download directory withmkdir -pif it doesn't exist. Previously it required a pre-existing writable directory.ensureAlive()now probes the page session withRuntime.evaluate("1")after the transport health check. If the page target has crashed (e.g. localhost server died), it reattaches automatically instead of returning a crypticsession_not_founderror on the next tool call.
Docs & metadata
browser_dispatch_keyprompt guidelines now explicitly note it dispatches a synthetic DOMKeyboardEventand does NOT type text. Point users tobrowser_type/browser_press_keyfor actual text input.browser_navigateprompt guidelines now warn that Google and strict-anti-bot sites may reject CDP navigation.browser_http_getis the recommended workaround.browser_get_network_logprompt snippet updated with explicit workaround (browser_execute_jswithPerformanceObserver).sharpadded tooptionalDependenciessonpm installattempts it (enablesbrowser_screenshotmaxDimauto-resize).SKILL.mdscript bindings section updated to document the actual daemon API:daemon.evaluateJs(),daemon.pageInfo(),daemon.listTabs(),daemon.session().call(). Example script updated to match.SKILL.mdtroubleshooting section now coverssharp/maxDimand Google anti-bot navigation.
0.3.0 — 2026-05-02
Internal rewrite
- Per-domain module split: every tool now lives in its own
src/domains/<name>.tsfile. The 1140-linedaemon.tsand 2277-linetools.tsare gone; the three largest files are nowsrc/cdp/transport.ts(~220 LOC),src/client.ts(~220 LOC), andsrc/domains/js.ts(~200 LOC). - New transport/session/client split:
BrowserDaemonclass replaced withcreateBrowserClient()factory composing aCdpTransport(factory) andCdpSession(factory). - All tool handlers now return
Result<T, E>; onedefineBrowserToolhelper converts to pi'sToolDefinitionand supplies a uniformdetailsshape:{ ok: true, ... }on success,{ ok: false, kind, message, ... }on error. - Strict TypeScript flags enabled (
noUncheckedIndexedAccess,exactOptionalPropertyTypes,noPropertyAccessFromIndexSignature,noUnusedLocals,noUnusedParameters). Zeroanyin the codebase. All boundaryascasts documented.
Bug fixes (predictability)
browser_navigateno longer silently creates a new tab when navigation fails; outcome is reported explicitly viadetails.outcome.kind("in_place"|"new_tab_created").- Page-info cache invalidation is now automatic: the session subscribes to CDP
Page.frameNavigated/Page.loadEventFiredevents. The 5 manualinvalidatePageInfoCache()calls are gone. - Dialog read no longer mutates state on
Page.javascriptDialogClosed— the dialog persists in the buffer untiltakeDialog()is called, fixing a fast-dismiss race that dropped dialogs. browser_dispatch_keynow returnsdetails.matched(count of elements the synthetic event was dispatched to). Zero matches is aninvalid_stateerror instead of a false success.browser_http_gettimeout now covers the response body read (legacy aborted headers butawait response.text()could hang indefinitely).sharpfailures are distinguished from "sharp not installed" — actual errors no longer masked by the install hint.- Screenshot paths use
randomUUID()per-namespace — no more concurrent-write collisions fromDate.now() + global counter. - Reconnect is lazy (via
ensureAlive) — no background reconnect, no stacked Chrome consent popups. - WebSocket events are routed through an
AsyncIterablebound to each connection — stale events from a previous connection can no longer leak across reconnects. browser_wait_for_loadreturns a typedtimeouterror if the deadline elapses (legacy returned a soft string).
Security fixes
- All JS evaluation source is built via
safeJs\...`(always JSON.stringify-safe). The previousreplace(/'/g, "\'")selector escaping (broken for backslashes, newlines, unicode quotes,</script>`) is gone. browser_run_scriptnow requires:- script path inside
tmpdir(),cwd(), orBH_SCRIPT_DIR(other paths rejected withinvalid_state) - a mandatory timeout (default 60s, max 600s, enforced via
Promise.race) - the AbortSignal is honored even if the script ignores its
signalparameter - source size ≤ 1 MB
- return shape validated structurally (each content item must be
{ type: "text", text: string })
- script path inside
browser_downloadvalidates the directory exists and is writable before calling CDP (Chrome was silently downloading to nowhere if the dir was bogus).browser_upload_fileverifies the file exists and is readable before any CDP call (prevents half-set state on the input).pdfPath()/screenshotPath()validate the namespace against a strict regex so a hostile namespace cannot escapetmpdir().
Parameter renames (saved scripts must be updated)
browser_click:clicks→countbrowser_dispatch_key:event→eventType
Removed
- The unused
tabHistory,screenshotDir, anddebugClicksfields on persisted state. - The dead
tool_resulthook inindex.tsfor tab-history tracking (details.targetIdwas never set by any tool). src/protocol.ts,src/renderers.ts,src/daemon.ts,src/tools.ts(replaced by per-domain files andclient.ts).
Known follow-ups
browser_get_network_logreturns a structured deprecation note. The new transport routes events through anAsyncIterableconsumed by the session manager; a synchronousrecentEvents()API is deferred. Usebrowser_execute_jswithPerformanceObserverorperformance.getEntries()as a workaround.browser_run_scriptscript binding is nameddaemonfor back-compat, but the underlying object is now aBrowserClient. Scripts usingdaemon.cdp(method, params)should switch todaemon.session().call(method, params).- No tests added in this rewrite; that's a separate workstream.
[0.2.0] - 2026-05-02
Changed
- Performance: fast
ensureAlive()— skips CDPTarget.getTargetshealth-check roundtrip on every tool call. Uses WebSocket state check + 30s TTL. 96% faster per-call setup. - Performance: event-based
waitForLoad()— replacesreadyStatepolling (300ms interval) with CDPPage.loadEventFired/frameStoppedLoadingevent draining (50ms interval). Detects already-loaded pages in ~1ms (99.5% faster). - Performance: JPEG screenshot support —
captureScreenshot()acceptsformat(png/jpeg) andquality(1-100). JPEG q80 is 29-49% smaller than PNG for complex pages, speeding up CDP transfer and reducing LLM context cost. - Performance: page info caching —
getPageInfo()caches results for 1 second, eliminating redundantevaluateJSCDP roundtrips on back-to-back calls. - Performance: parallel domain enables —
switchTab()enables Page/DOM/Runtime/Network domains viaPromise.allinstead of sequential await. - Tools:
browser_screenshotnow acceptsformat(png/jpeg) andqualityparameters. - Tools:
browser_wait_for_loadnow usesdaemon.waitForLoad()(event-based) instead of polling.
[0.1.0] - 2026-05-02
Added
- Initial release of pi-browser-harness.
- 20 browser control tools (
browser_navigate,browser_screenshot,browser_click,browser_type,browser_press_key,browser_scroll,browser_execute_js,browser_http_get,browser_new_tab,browser_open_urls,browser_switch_tab,browser_list_tabs,browser_current_tab,browser_page_info,browser_go_back,browser_go_forward,browser_reload,browser_wait,browser_wait_for_load,browser_handle_dialog). - Self-extending harness:
list_dynamic_tools,register_tool,remove_tool— the agent can write new browser tools at runtime. - Guided setup command (
/browser-setup) with Chrome detection, automatic browser-harness installation viauvorgit clone. /browser-statusand/browser-reload-daemoncommands for daemon health monitoring.--browser-namespaceand--browser-debug-clicksCLI flags.- Session persistence for tab history and daemon namespace across reloads and branch navigation.
- System prompt injection with browser usage guidance and common workflow patterns.
- Custom TUI renderers for screenshots and tab listings.
- Dialog detection and handling for JS
alert/confirm/prompt/beforeunload. - Parallel URL opening via
browser_open_urlswith live progress streaming. - Output truncation with temp-file fallback for large JS evaluation and HTTP responses.