@relictombs/opencode-browser
v0.2.0
Published
Relictombs Browser: a shared browser workspace for the OpenCode v2 TUI
Maintainers
Readme
Relictombs Browser
@relictombs/opencode-browser is a shared browser workspace for the OpenCode V2 TUI. In the production remote layout, the server owns
one hidden Electron runtime while the client renders the selected page in the terminal and forwards input. An explicit local
fallback remains available when remoteHost is omitted.
There is no separate Electron window. The address bar, navigation controls, loading state, and tab strip are OpenTUI
UI inside OpenCode. Rendering is provided by
@relictombs/opentui-browser, whose terminal-browser
direction is inspired by
zenbu-labs/terminal-browser.
What it provides
- Real interactive pages with keyboard, paste, mouse, wheel, hover, links, and forms.
- Multiple browser tabs sharing one Electron runtime and profile.
- Responsive viewports that follow OpenCode and terminal resizes.
- Hidden background tabs that stay alive while only the selected page is presented.
- Automatic OpenCode registration for browser lifecycle tools and, locally or through an explicit CDP forward, Chrome DevTools MCP.
- Scoped Effect lifecycle, bounded recovery, and cleanup for Electron, pages, MCP servers, and the OpenTUI surface.
Requirements
- OpenCode V2
0.0.0-next-16650or a newer compatible@nextbuild, launched asopencode2. - A local Kitty-capable terminal. Ghostty is the primary tested terminal.
- Node.js 24 or newer for the controller and Chrome DevTools MCP subprocesses.
- Bun 1.3 or newer only when developing this repository.
The terminal and OpenCode TUI run on the client. Electron may run on the server through the authenticated remote host. Plaintext
WebSockets must be carried through SSH loopback tunnels; a trusted TLS edge may use wss:. Tmux and screen still need
Kitty graphics passthrough and are not part of the supported baseline.
The OpenCode V2 plugin API is still beta. This release is tested against 0.0.0-next-16650; upgrade OpenCode and
this plugin together if that API changes.
Install OpenCode V2
Install the current V2 prerelease and allow its package install script:
npm install -g @opencode-ai/cli@next --allow-scripts='@opencode-ai/cli'
opencode2 --versionIf npm previously installed the CLI without its platform binary:
npm rebuild -g @opencode-ai/cli --allow-scripts='@opencode-ai/cli'Set up Portal
Run the interactive setup wizard for a complete local or remote configuration:
npx --yes @relictombs/[email protected]Choose browser or both surfaces, then choose where the runtime should run. For remote use, the wizard accepts a direct SSH destination or a Tailscale machine name or address, verifies FFmpeg and connectivity, starts the server, and owns the authenticated tunnel. Local and existing-host flows can print generated JSON instead of writing it; setup-owned SSH sessions apply the global or project configuration before optionally launching OpenCode.
Advanced manual configuration
OpenCode V2 reads ~/.config/opencode/cli.json, or $OPENCODE_CONFIG_DIR/cli.json when that environment variable
is set. Add the published TUI plugin:
{
"tabs": {
"enabled": true,
"scope": "global"
},
"mouse": true,
"plugins": [
{
"package": "@relictombs/[email protected]",
"options": {
"url": "https://example.com",
"profile": "project"
}
}
]
}The exact version keeps the client aligned with the remote host protocol. If no options are needed, the short form also works:
{
"plugins": ["@relictombs/[email protected]"]
}OpenCode resolves the package's ./tui export. There is no separate plugin file, MCP configuration, Electron
window, Chrome installation, or browser-server setup.
Portal's shared MCP and OpenCode registry packages are private bundled implementation details. Install only Portal; they do not need separate package entries or releases.
Electron is a package dependency. If its install script did not provision the platform binary, first activation runs Electron's installer asynchronously in a bounded worker and validates the result before starting the browser. That first activation can take longer and needs either a cached binary or network access.
Restart opencode2 after changing the package or its options.
The configuration above is the local setup: omitting remoteHost launches Electron on the same machine as OpenCode.
It needs no Relictombs host, FFmpeg, or SSH tunnel. Prefer the wizard for end-to-end setup; edit this JSON directly only
when integrating Relictombs into an independently supervised deployment.
Use
/browser reveals the currently selected browser tab when one exists; it does not create another tab. When the browser
is empty, it creates the local New tab page on demand. /browser https://example.com reuses the selected tab and
starts navigation only after its terminal surface has the final viewport. Use
/browser-new or /browser-new https://example.com when you explicitly want an additional tab. Both commands are
available from the slash-command picker and command palette.
| Action | Keyboard or UI |
| --------------------------- | -------------------------------------------------------- |
| Focus the address | Ctrl-L / Cmd-L |
| New browser tab | /browser-new [url], Ctrl-T / Cmd-T, or + |
| Close selected browser tab | Ctrl-W / Cmd-W, or the tab's × |
| Next / previous browser tab | Ctrl-Tab / Ctrl-Shift-Tab |
| Reload or stop loading | Ctrl-R / Cmd-R, or the toolbar button |
| Back / forward | Cmd-[ / Cmd-], or Alt-Left / Alt-Right |
| Reset an unresponsive page | Ctrl-Shift-R / Cmd-Shift-R |
| Return to OpenCode | Ctrl-Shift-B, a session tab, or Return to OpenCode |
While the address field is focused, Escape restores the live page URL. Ctrl-W keeps its normal word-deletion
behavior there; Cmd-W still closes the page.
Returning to OpenCode hides the browser without closing Electron or its pages, so reopening is fast. Remove browser
tabs from strip also hides every browser page item while keeping the runtime ready; /browser restores them.
Closing the final tracked page closes the browser workspace and leaves no hidden replacement target. The Electron
runtime stays ready; the next explicit /browser, open, or new_tab operation creates the next page. A plain
/browser creates the local welcome page on demand and presents it as New tab with an empty address instead of
exposing its internal data URL.
Popup windows are deliberately denied. Use + or the controller's new_tab tool when a new tracked page is
required.
Options
| Option | Purpose |
| ------------------------ | ------------------------------------------------------------------------------- |
| url | Initial HTTP(S) URL and default new-tab page. Defaults to a local welcome page. |
| profile | Cookie/profile isolation policy. Defaults to "project"; see below. |
| electronExecutablePath | Explicit Electron executable. Normally resolved from the package dependency. |
| frameRate | Active Electron paint ceiling from 1 to 240 fps. Defaults to 30. |
| maxViewportPixels | Optional raw viewport area cap; lowers terminal traffic while preserving input. |
| nodeCommand | Node.js executable used for both local MCP subprocesses. |
| remoteHost | Authenticated server control/media endpoints; selects the remote runtime path. |
remoteHost accepts endpoint, a 64-character lowercase hexadecimal token, optional requestTimeoutMs, optional
mediaBaseEndpoint for rewriting server media URLs to a client tunnel, and optional devToolsWebSocketEndpoint for a
separate CDP tunnel. Local Electron/profile/frame-rate options cannot be combined with remoteHost; those resources
belong to the server.
Advanced manual remote configuration
The wizard is the supported start-to-finish remote path. The following shape is only for operators who supervise the server and tunnel themselves:
{
"tabs": { "enabled": true, "scope": "global" },
"mouse": true,
"plugins": [
{
"package": "@relictombs/[email protected]",
"options": {
"url": "https://example.com",
"remoteHost": {
"endpoint": "ws://127.0.0.1:43100/control",
"mediaBaseEndpoint": "ws://127.0.0.1:43101",
"token": "<copy the 64-character token printed by the server>"
}
}
}
]
}Do not include profile, frameRate, or electronExecutablePath with remoteHost; the server owns those resources.
The wizard verifies the client's H.264 decoder and the server's libx264 encoder, and provides installation guidance
when either is missing.
The control/media tunnel transports the browser workspace. Chrome DevTools is deliberately separate, and the stock
host CLI neither prints nor configures Electron's dynamic CDP endpoint. A custom host operator can expose a known server
loopback endpoint, forward it separately, and set devToolsWebSocketEndpoint. Otherwise Portal registers the control
MCP only and does not pretend DevTools is available. The stock remote browser profile is temporary and is deleted when
the host stops; persistent remote-profile selection requires custom host composition.
Exact setup, plugin, and host versions remain a coordinated release set; current peers negotiate ALV2 while older peers retain the ALV1/NUT compatibility path. The npx setup and host commands are available from the public Relictombs packages on npm.
Host composition API
Custom server launchers can import @relictombs/opencode-browser/host-mcp. Use
serveBrowserControllerHostMcp(controllerCall, httpOptions) to expose the same bounded browser controller tools on an
authenticated loopback Streamable HTTP endpoint. Use browserDevToolsHostRegistration(...) to describe a
server-local Chrome DevTools MCP process for a validated loopback browser CDP endpoint. Both functions return strict
OpenCode registration descriptors; the HTTP server remains scoped to the caller's Effect scope.
Cookies, logins, and profiles
For the local fallback, the default "project" profile persists cookies, logins, local storage, history, and cache for one stable OpenCode
project. It survives browser-tab closure, Electron recovery, plugin reloads, and TUI restarts. Project identifiers are
hashed before being used in the profile path.
Choose the narrowest sharing policy that fits the work:
| profile value | Persistence and sharing |
| ----------------------------------------- | ---------------------------------------------------------------------------- |
| "project" | Persistent and shared by workspaces belonging to one OpenCode project. |
| "workspace" | Persistent and isolated to one OpenCode project/workspace pair. |
| "global" | Persistent and intentionally shared across every project using this profile. |
| "temporary" | Private for this activation and deleted when its root scope closes. |
| { "mode": "custom", "directory": "/…" } | Persistent in an explicit absolute directory dedicated to this plugin. |
Persistent profiles live below $XDG_STATE_HOME/opencode/relictombs/browser/profiles/v1, falling back to
~/.local/state/opencode/relictombs/browser/profiles/v1. A mode-0700 directory and exclusive owner lock prevent two
Electron runtimes from opening the same profile concurrently. The UI displays only a safe mode label, never the path
or project identity.
Agent access
The plugin always registers browser control. Local generations also register DevTools; remote generations do so only
when remoteHost.devToolsWebSocketEndpoint names an explicit client loopback tunnel:
| MCP server | Responsibility |
| ----------------------------- | ------------------------------------------------------------------------------------- |
| relictombs-browser-control | Navigate, create, list, select, and close tracked browser tabs in the background. |
| relictombs-browser-devtools | Inspect and operate the same live Electron pages through [email protected]. |
The control server exposes six tools: open, new_tab, list_tabs, select_tab, close_tab, and status. Agent
operations never select the Browser workspace. They can add a browser item to the terminal tab strip and update its
loading state, but OpenCode stays active until the user runs /browser or selects that browser tab.
For reliable agent automation:
- Use
openfor an ordinary single-page request; it reuses the selected tab in the background, including the initial welcome tab. - Use
new_tabonly when the user requests an additional tab or the task must keep multiple pages open concurrently; the new page is also created and selected in the background. - Read its stable logical
tabId, current CDPtargetId, URL, and title. - Call Chrome DevTools MCP
list_pagesand match the live page by URL/title. - Use Chrome DevTools MCP's numeric
pageIdfor page-scoped DevTools operations.
tabId, targetId, and Chrome DevTools MCP's pageId are different identities:
tabIdbelongs to the plugin and survives replacement of an unresponsive page.targetIdbelongs to the current ElectronwebContentsand changes when that page is replaced.pageIdbelongs to the current Chrome DevTools MCP process and must be obtained fromlist_pages.
Use the controller for new_tab and close_tab; creating page lifecycle through DevTools would bypass the Browser
Session and leave the terminal tab strip stale. Give new_tab a caller-generated requestId and reuse it when
retrying the same timed-out call.
The plugin launches the installed Chrome DevTools MCP entry with Node.js. It does not use npx, download an MCP
package during activation, discover system Chrome, or create a second browser runtime.
DevTools screenshots default to WebP at quality 80 and are bounded to 640×640 during capture. The bound keeps even an explicit PNG capture below Chrome DevTools MCP's inline-image threshold, so screenshot results remain typed image attachments that OpenCode can render in the session feed instead of opaque temporary-file messages.
If the plugin becomes active during an existing agent turn, begin a new turn so OpenCode can refresh its MCP tool catalog.
How it works
One Browser Generation owns one hidden Electron Managed Browser Runtime. Every browser tab is a long-lived Shared Browser Page inside that runtime. Only the selected visible page owns an active Presentation:
OpenCode Host UI
-> selected Shared Browser Page
-> Electron offscreen paint
-> owned in-memory RGBA
-> local: Kitty direct-data image
-> remote: negotiated ALV2/H.264 media socket (ALV1/NUT fallback) -> client decode -> Kitty direct-data imageBackground pages remain available to the user and agent, idle their offscreen paint rate at 1 fps, and do not send frames to Kitty. Changing terminal geometry updates the selected page viewport and placement without stretching a stale frame. Explicit foreground URL commands wait for that viewport and Presentation to be ready before navigation; agent navigation remains immediate and offscreen.
OpenCode V2 currently exposes session tabs rather than arbitrary native plugin tabs. The plugin therefore renders a composite strip in the global app slot: OpenCode sessions and logical browser pages remain independently owned, while the current OpenCode route stays mounted underneath the browser workspace.
If a page Presentation fails, the plugin retries it and can replace only that Shared Browser Page. If Electron disconnects, it retires the Browser Generation, launches a clean replacement, and refreshes the DevTools endpoint without replacing the stable controller or MCP names.
Contributor-level lifecycle, identity, and Effect boundaries are documented in
CONTEXT.md.
Resource use
Electron's fixed main, GPU, network-service, profile, and DevTools cost is paid once per Browser Generation. Each additional tab adds a page renderer; only the selected visible page pays active Presentation cost.
On Apple M5/macOS with a 960×600 animated page:
| State | Electron memory | Electron CPU | Bun + Electron memory | | -------------------------------------- | --------------: | --------------------: | --------------------: | | Hidden/no Presentation, 1 fps idle cap | 223–225 MiB | 0.25–0.6% of one core | 276–278 MiB | | Active Presentation, 30 fps | 227–232 MiB | 26–29% of one core | 281–283 MiB |
This short measurement predates the encoded remote path and covers a local in-memory RGBA path; it excludes Ghostty's image storage and a full OpenCode host baseline. Real websites, video, WebGL, extra tabs, viewport size, and terminal image storage can use more.
Uncompressed capture work scales with width × height × 4 × frameRate, but the server → client link carries encoded
video rather than RGBA files. Keep the visible viewport bounded when reducing network or decode cost; the stock host
CLI does not currently expose a remote frame-rate option.
Troubleshooting and limitations
- No browser appears: run
/browser, then check OpenCode's Plugins view for an activation error. - First activation is slow: Electron may be provisioning its platform binary after lifecycle scripts were skipped.
- Terminal becomes unresponsive: set
maxViewportPixelsto144000to reduce raw Kitty traffic by about 75% for a 960×600 surface without enabling terminal-side compression. - Blank remote surface: verify both the control and media SSH forwards, and ensure rewritten media URLs resolve on the client. A successful control connection alone does not carry pixels.
- DevTools missing remotely: the stock host CLI neither prints nor configures Electron's dynamic CDP endpoint. A
custom host operator must first expose a known server loopback endpoint, then forward it separately and set
devToolsWebSocketEndpoint. - Input goes to OpenCode: click the page or use
/browserto focus the Browser Surface;Ctrl-Lintentionally focuses the address field. - MCP registration fails: remove or rename pre-existing servers called
relictombs-browser-controlorrelictombs-browser-devtools. The plugin will not overwrite them. - Duplicate plugin behavior: configure the package once. Do not combine
cli.jsonregistration with a local auto-discovered copy.
Page-created popup windows, browser permission prompts, and terminal multiplexers are not supported. Remote browser rendering is supported through the authenticated server path described above.
Cleanup and security
Hiding the workspace keeps Electron ready. Deactivating the plugin from OpenCode's Plugins view—or removing it from
cli.json and restarting—detaches that TUI and removes its app slot and authenticated controller. The machine-local
Browser daemon and Electron remain ready for other clients and later TUI restarts.
Electron's DevTools server and the controller bind only to loopback. The controller requires a random bearer token
passed only to its stdio sidecar, bounds request bodies and concurrent work, and accepts only HTTP(S) navigation.
Treat every persistent browser profile as sensitive: an attached DevTools agent can inspect the cookies and storage of
the live profile. "global" deliberately broadens that access across projects; "workspace" provides the strongest
persistent isolation.
Local development
Install the workspace dependencies once, then launch the checkout from the repository root:
bun install
bun run dev:browserThe launcher packs the checkout into a temporary loopback registry and starts a TUI against a managed local OpenCode
core; it does not publish any package. Run the same command in a second terminal to attach another TUI. Both terminals
share one OpenCode core and one machine-local Electron daemon, while each TUI keeps its own browser pages. To open a
different project, pass --project /absolute/path.
Closing the last TUI leaves the shared development core and Browser daemon ready for another terminal. While Browser
has focus, Ctrl-C is sent to Chromium; close each hand-test TUI with Ctrl-Shift-B, then /exit, and only after every
TUI exits stop the shared processes with:
bun run dev:browser -- --stopbun run test:packed uses a sibling ../opencode checkout by default. Set
OPENCODE_REPO=/absolute/path/to/opencode when that checkout is elsewhere. bun run test:mcp launches the real
Electron runtime and directly installed Chrome DevTools MCP.
Release verification:
bun run check
bun run test:packed
bun run --cwd packages/opencode/browser test:mcp