@neriousy/opencode-browser embeds a real Electron-backed browser workspace in the OpenCode V2 TUI. The bundled
opentui-browser implementation supplies the background Managed Browser Runtime, Shared Browser Pages, DevTools
targets, and raw RGBA-to-Kitty Presentation path. This package supplies OpenCode lifecycle integration, Browser Session
policy, Host Chrome, Browser Control, and MCP registration.
There is one active background Electron runtime per Browser Generation. Every logical browser tab in that generation is a Shared Browser Page in the same runtime. Chrome DevTools MCP connects to the runtime's loopback DevTools endpoint; it does not own the runtime or carry Presentation frames.
| Domain | Responsibility | Lifetime |
|---|---|---|
| Browser Plugin | Compose the integration and expose the Promise TUI boundary | One plugin installation |
| Browser Generation | Own one Managed Browser Runtime, initial Shared Browser Page, session, and slot | One root-scope child |
| Browser Session | Track logical tabs, selection, explicit page creation, closure, and replacement | One generation |
| Browser Control | Expose the authenticated loopback API and dispatch to the current session | Plugin root |
| MCP Registry Lease | Own fixed OpenCode MCP names and refresh generation-specific endpoint config | Plugin root |
| Host Chrome | Render the OpenCode tab title, toolbar, and nested browser-tab strip | One app slot |
| Browser Surface | Mount the selected Shared Browser Page in Solid | One visible surface |
| Presentation | Start, focus, and close one OpenTUI browser renderable without owning its page | One mounted page view |
The ownership tree is:
Browser Plugin root scope
├── Browser Control
├── MCP Registry Lease
└── current Browser Generation child scope
├── background Electron Managed Browser Runtime
│ └── one or more Shared Browser Pages
├── Browser Session and page/disconnect listeners
└── OpenCode app slot
└── Host Chrome → Browser Surface → Presentation
Closing the root closes the active child before the stable control and MCP resources. A disconnected generation is detached and closed before recovery. Every replacement candidate is built in a separate child scope, checked for usability, installed transactionally, and only then becomes current; a failed candidate is closed. Callbacks carry a generation ID so stale disconnects and late work cannot mutate a newer generation.
tabIdis the stable logical identity used by Host Chrome and Browser Control.targetIdis the current ElectronwebContentsCDP target identity. Replacing a failed Shared Browser Page preservestabIdand changestargetId.- Chrome DevTools MCP assigns a third, process-local numeric
pageId. It cannot be derived from either plugin identity; list pages and match by live URL/title before using page-scoped DevTools tools. - The top-level OpenCode tab title follows the selected page title. The nested browser tab strip represents Shared Browser Pages inside that one OpenCode tab.
- The control endpoint and MCP server names remain stable across recovery. Requests resolve the current generation at execution time; the Chrome DevTools MCP registration is refreshed with the replacement Electron endpoint under the same lease.
Browser Session owns explicit page lifecycle through Host Chrome and Browser Control. It observes pages created through
the shared runtime API, but it does not implement popup adoption. New windows requested by page content are not a second
tab-creation policy. Browser Control uses open for ordinary navigation and reserves new_tab for additional pages.
Browser Control and Host Chrome new-tab actions are both append-only. Browser Control changes logical selection and
page state in the background but never activates Host Chrome; /browser, the tab strip, and browser-mode keybindings
are the explicit foreground boundary.
Closing the final tracked page closes Host Chrome and leaves Browser Session empty; it does not invent a hidden
replacement target. The next explicit /browser, open, or new_tab operation creates the next page. A plain
/browser creates the local welcome page on demand, renders it as New tab with an empty address, and can retry a
failed page creation on the next invocation.
Electron runs as a background accessory with hidden offscreen windows. It is the Managed Browser Runtime, not Host Chrome: the visible address bar, navigation buttons, and tab strip are terminal UI owned by this package.
Only the selected visible Shared Browser Page has an active Presentation. Background pages stay alive for the user and agent but do not continuously send raw frames to Kitty. Presentation remains outside CDP:
Electron offscreen paint → raw RGBA file → Kitty image → terminal placement
Host Chrome foreground navigation waits for the exact selected Browser Surface to finish viewport reconciliation and
attach its Presentation before sending goto. Browser Control navigation does not wait for Presentation because agent
work remains offscreen and must not activate the Browser workspace.
The fixed Electron main/GPU/network/profile/DevTools cost is therefore paid once per Browser Generation, page renderer
cost scales with the number and complexity of Shared Browser Pages, and active Presentation cost scales with viewport
pixels and frame rate. The plugin defaults to 30 fps: 960×600 RGBA is about 66 MiB/s and 1920×1080 is about 237 MiB/s
of local raw-frame traffic. Raising the rate to 60 doubles those ceilings. Prefer one shared runtime and tune
frameRate before inventing another runtime path.
chrome-devtools-mcp is a direct dependency. src/mcp.ts resolves its installed entry point and launches it with the
configured Node.js command against the current runtime's loopback WebSocket endpoint. Do not add an npx launcher,
first-run package download, Chrome installer, browser cache, or system-Chrome discovery path.
The two MCP roles stay separate:
opencode-browser-controlowns background tracked-tab lifecycle, logical selection, and stabletabIdpolicy. It does not expose visibility mutations.opencode-browser-devtoolsinspects and operates the same live pages through Chrome DevTools MCP.
Use Browser Control for new_tab and close_tab; letting a DevTools client invent page lifecycle would bypass Browser
Session and leave Host Chrome incoherent.
src/plugin.tsis the public facade for the OpenCode V2 TUI adapter.src/plugin/runtime.tscreates the root scope and bridges the Promise TUI contract to Effect services.src/plugin/supervisor.tsowns stable control/MCP resources, the current child scope, and serialized recovery.src/plugin/host.tsdecodes host input and acquires the scoped Electron Managed Browser Runtime.src/plugin/generation.tsconstructs one scoped runtime/session/slot generation.src/plugin/error.tsdefines lifecycle and recovery failures.src/browser-session.tsowns logical tab state andtabId/targetIdmapping.src/control/defines the authenticated protocol, client, server, and command executor.src/mcp.tsowns OpenCode V2 MCP wire schemas and direct local subprocess registrations.src/plugin/mcp-lease.tsversions process-local lease ownership so a disposed root cannot remove or overwrite a replacement root's fixed MCP names. Reactivation takes over only names left ambiguous by timed-out, failed, or still-running cleanup; definitive cleanup expires ownership and restores OpenCode's external conflict check.src/browser-tab.ts,src/browser-toolbar.tsx,src/browser-tab-strip.tsx, andsrc/browser-workspace.tsximplement Host Chrome.src/browser-surface.tsxandsrc/presentation.tsbridge Solid to the OpenTUI renderable lifecycle.
Keep this map aligned when a domain moves. Prefer domain names over technical buckets.
Use Effect for services, layers, scopes, acquisition/release, recovery coordination, interruption, tagged errors, and Schema decoding of unknown data. Use named services and operations so traces read like the domain.
Keep Promises at the OpenCode TUI contract and inside existing interactive rendering APIs. Keep plain TypeScript for session projections, controller decisions, view state, and encoded wire values. Keep Solid, Presentation delivery, resize, keyboard, mouse, and scroll paths free of Effect allocation.
OpenCode V2 references live in ../opencode/packages/core/src/plugin.ts,
../opencode/packages/core/src/plugin/supervisor.ts, and ../opencode/packages/core/src/plugin/promise.ts. Copy their
explicit domain/service/scope ideas, not the application-wide LayerNode graph, location service machinery, persistence
layers, or monorepo topology.
- Name the domain and its single owner before editing.
- Put every acquired handle, listener, slot, process, profile, page, server, and MCP lease in the correct scope immediately.
- Preserve one shared Electron runtime per generation and one active Presentation for the selected visible page.
- Decode unknown input once, then operate on typed values.
- Preserve stable logical identities across Shared Browser Page replacement.
- Add a focused regression for the observable lifecycle or UI invariant.
- Check failure, interruption, disposal, and late-completion paths.
- Run focused tests, static checks, build, and packed-runtime verification.