Skip to content
9 changes: 9 additions & 0 deletions .changeset/bright-clients-route.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@opencode-ai/browser-control": patch
---

Isolate CDP client state so concurrent clients retain their own auto-attach
settings, invalidate target aliases when ownership hides a tab, reject hidden
session routing, avoid arbitrary target fallback, and detach child targets when
their root disappears. Centralize target and alias routing so stale root and
child sessions fail closed.
6 changes: 6 additions & 0 deletions .changeset/calm-sessions-select.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@opencode-ai/browser-control": patch
---

Accept `--session`, `-s`, and `BROWSER_CONTROL_SESSION` for session reset and
delete while retaining positional and current-session selection.
8 changes: 8 additions & 0 deletions .changeset/calm-spiders-connect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@opencode-ai/browser-control": patch
---

Prepare an unlisted Chrome Web Store extension with protocol-based relay
compatibility, deterministic packaging, and more reliable cold-start target
creation. Session reset and delete now recover relay-owned targets whose
debugger attachment was permanently lost during an extension update.
5 changes: 5 additions & 0 deletions .changeset/clean-recordings-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opencode-ai/browser-control": minor
---

Stream tab-capture recordings to disk with intrinsically framed, sequenced binary messages instead of buffering complete recordings in relay memory.
6 changes: 6 additions & 0 deletions .changeset/shiny-forms-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@opencode-ai/browser-control": patch
---

Search recursively through open shadow roots in `fillInput` and `fillInputs`,
and report the closed-root boundary when a selector has no match.
6 changes: 6 additions & 0 deletions .changeset/tidy-inputs-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@opencode-ai/browser-control": patch
---

Preserve page focus while `fillInput` and `fillInputs` update controlled fields,
preventing focus-sensitive extensions from making the target unresponsive.
5 changes: 5 additions & 0 deletions .changeset/warm-spoons-reconnect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opencode-ai/browser-control": patch
---

Keep the Chrome extension connected across idle service-worker suspension, repair missing reconnect alarms whenever the worker starts, start the managed relay correctly when MCP runs through a package-manager bin symlink, and make Doctor compare the runtime extension with the manifest shipped in the npm package.
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ local Node relay.
- Extension shim changes require reloading the unpacked extension once in Brave.
- Relay-only changes should not require reloading the extension.
- Use `termctrl` for long-running relay sessions during testing.
- Run `SMOKE_CASE=local-forms,local-cart,local-checkout,reconnect-evaluate,redirect-reconnect-evaluate,execute-target-url,execute-page-recovery,execute-page-detach-recovery,execute-fill-helpers,execute-snapshot-refs,handoff-navigation,handoff-cross-tab,handoff-target-detach,oopif-reconnect,dedicated-worker,network-capture,session-download-capability,execute-ghost-cursor,session-isolation,multi-client,stale-client-checkout,raw-first-checkout pnpm smoke`
- Run `SMOKE_CASE=local-forms,local-cart,local-checkout,reconnect-evaluate,redirect-reconnect-evaluate,session-missing-selector,execute-target-url,execute-page-recovery,execute-page-detach-recovery,execute-fill-helpers,execute-snapshot-refs,handoff-navigation,handoff-cross-tab,handoff-target-detach,oopif-reconnect,dedicated-worker,network-capture,session-download-capability,execute-ghost-cursor,session-isolation,multi-client,stale-client-checkout,raw-first-checkout pnpm smoke`
before claiming the current smoke set is green.
- CDP target visibility is scoped per client (`src/cdp-visibility.ts`):
session-owned tabs are announced and their events delivered only to that
Expand Down Expand Up @@ -242,11 +242,14 @@ browser-control skill

- Load `extension/dist` as the unpacked extension.
- The relay listens on `127.0.0.1:19989` by default.
- Current shim version is `0.0.23` and extension protocol version is `1`.
- Store and npm versions may differ when protocol `1` remains compatible.
- Current shim version is `0.0.23` and extension protocol version is `2`.
- Store and npm versions may differ while their extension protocol versions remain compatible.
- On socket open the shim sends `hello` and then re-announces every tab it still
has `chrome.debugger` attached to (`debugger.attached` events), so a restarted
relay rebuilds its target registry without the user re-clicking the toolbar.
- Repair the reconnect alarm whenever the MV3 worker starts and send heartbeat
traffic every 20 seconds while its relay socket is open. Chrome may clear
persisted alarms and retires idle extension workers even with an open socket.
- The relay dedupes target announcements per CDP client by targetId: a
re-announce under a new sessionId emits `Target.detachedFromTarget` for the
old session first. Never announce the same targetId twice to one client
Expand Down
98 changes: 58 additions & 40 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ Agent / MCP client / CLI
-> user's Chromium-family browser tabs
```

The end-to-end path is working. Current work should finish CDP routing
correctness, simplify the relay, and make recording robust. New features
The end-to-end path is working. Current work should simplify the relay and make
recording robust. New features
should not weaken the code-first interface or move behavior into the extension
without a concrete browser-API reason.

Expand All @@ -27,60 +27,39 @@ without a concrete browser-API reason.
Work these in order unless field evidence changes the priority. Every item
should land with unit or smoke evidence appropriate to the behavior.

### 1. Finish CDP routing correctness

- Emit child `Target.detachedFromTarget` events when a root target detaches so
clients cannot retain orphaned child sessions.
- Remove arbitrary-first-target fallbacks from `Target.getTargetInfo` and other
sessionless CDP routing.
- Store `autoAttachParams` per client instead of using global
last-writer-wins state.

Verification:

- Extend reconnect, OOPIF, and multi-client smoke cases to cover root detach and
conflicting client auto-attach settings.

### 2. Split the relay into testable responsibilities
### 1. Split the relay into testable responsibilities

Extract cohesive modules from `makeRelay` without changing the protocol:

- `CdpRouter`: command routing, guardrails, and compatibility shims.
- Deepen `CdpRouter` with command classification, guardrails, and compatibility
shims.
- `ExtensionEventHandler`: extension event decoding and registry mutation.
- `CdpClientPool`: client sockets, per-client attachment sets, and connection
generations.

`CdpClientPool` now owns client sockets, per-client attachment sets, aliases,
auto-attach settings, and connection generations. `CdpRouter` now owns
client-relative visibility, target inventory, target and alias resolution, and
exact root-versus-child Chrome session routing.

The goal is browser-free testing of routing and lifecycle behavior, not smaller
files for their own sake. Keep orchestration in `makeRelay` and avoid exposing
internal protocol details to the CLI or MCP server.

### 3. Stream recordings with unambiguous framing
Verification:

- Extend reconnect, OOPIF, and multi-client smoke cases to cover root detach and
conflicting client auto-attach settings.

### 2. Extend recording surfaces

- Include the tab id and sequence number in each binary websocket frame instead
of pairing a JSON metadata frame with the next binary frame.
- Stream chunks to disk instead of buffering complete recordings in relay
memory.
- Add MCP recording start, stop, status, and cancel tools after the relay path is
robust.
- Build the flight-recorder ring buffer only after chunk streaming lands.

Verification:

- Exercise interleaved recordings and a recording larger than the intended
in-memory bound.
- Confirm CLI and MCP recording behavior match.

### 4. Resolve smaller agent-experience gaps

- Make `fillInput` and `fillInputs` search open shadow roots recursively. Closed
shadow roots remain unsupported. When no DOM match exists, the error should
explain the helper's boundary and suggest `locator.fill()` if Playwright can
resolve the field. The current zero-match error was observed on
api.data.gov's signup component.
- Accept `--session` and `-s` on `session reset` and `session delete`, matching
`execute` and `journal`.
- Add smoke coverage proving explicit missing session ids fail for both
`--session x` and `BROWSER_CONTROL_SESSION=x`.
### 3. Resolve smaller agent-experience gaps

Verification:

Expand All @@ -92,6 +71,46 @@ Verification:

## Recently Shipped

### Tab-capture recordings stream with intrinsic framing

Extension protocol `2` sends each recording chunk as a sequenced `BCRD` binary
frame containing its tab id. The relay validates framing and sequence, bounds
pending writes, streams each tab to an adjacent temporary file, and atomically
renames complete recordings. Interleaving, oversized queues, malformed frames,
and output larger than a single frame have direct coverage.

### Fill helpers traverse open shadow roots

String selectors passed to `fillInput` and `fillInputs` now search recursively
through open shadow roots. A zero-match error explains that closed roots remain
unavailable and suggests `locator.fill()` when Playwright can resolve the field.

### Session lifecycle selectors are consistent

`session reset` and `session delete` accept positional ids, `--session`/`-s`,
and `BROWSER_CONTROL_SESSION` before falling back to the saved current session.
Smoke coverage verifies explicit missing flag and environment ids fail instead
of falling back to the saved current session.

### CDP routing fails closed

Identity-free `Target.getTargetInfo` no longer returns an arbitrary tab, and
otherwise-unhandled sessionless CDP commands require an explicit session. All
explicit target and session routing now rechecks client visibility, including
session-scoped auto-attach. Root teardown emits each announced child detach
before detaching the root so clients cannot retain orphaned sessions. The
browser-free `CdpRouter` module keeps these visibility, alias, and generation
rules out of relay transport orchestration.

### CDP client state is isolated per connection

`CdpClientPool` now owns each CDP client's session identity, target
announcements, aliases, auto-attach settings, and idle-reset generation. New
targets use the originating client's auto-attach settings instead of global
last-writer-wins state. Ownership visibility changes also invalidate target
aliases, so a client cannot continue routing commands to a tab after it becomes
hidden.

### Wedged session pages recover or fail fast

A 2026-07-09 field failure left a relay-owned page open but unusable after its
Expand Down Expand Up @@ -437,8 +456,7 @@ restarts can be distinguished from session eviction.
- Native `locator.fill()` can hang on login-style fields when installed browser
extensions inject focus handlers or overlays. `fillInput` is the explicit
fallback for ordinary `input` and `textarea` elements.
- `fillInput` currently uses `querySelector` semantics and cannot reach fields
in shadow roots.
- `fillInput` cannot reach fields inside closed shadow roots.
- OOPIF behavior is guaranteed only by the current reconnect smoke scenarios.
- Clipboard automation on insecure origins is not guaranteed.
- Playwright download events and `download.saveAs()` are unavailable in
Expand Down
Loading