- Read
CONTEXT.mdbefore structural work. Use its domain language: Browser Plugin, Browser Generation, Managed Browser Runtime, Shared Browser Page, Browser Session, Browser Control, MCP Registry Lease, Host Chrome, Browser Surface, and Presentation. - Keep the package specific to the OpenCode V2 TUI. The public TUI plugin boundary remains Promise-based even when Effect owns the internal lifecycle.
- Organize runtime code by named domains. Follow the OpenCode V2 shape where it improves clarity: a domain namespace,
an exported
Interface, aContext.Service, alayer, and namedEffect.fn("Domain.operation")operations. - Bind yielded services to named variables before using them. Do not hide dependency reads inside nested
yield*expressions. - Keep synchronous parsing, option construction, state projection, and wire encoding synchronous. Use Effect for dependency composition, scoped ownership, interruption, typed failures, and decoding untrusted boundaries.
- Do not introduce Effect into Solid components or Presentation, frame, keyboard, mouse, or scroll hot paths.
- Add a domain module when it owns policy or a lifecycle. Do not create generic
utils,helpers, orcommonmodules. - Reuse OpenCode's domain vocabulary and scoped-service pattern, but do not copy its
LayerNode,LocationServiceMap, durable service graph, or monorepo/Turbo structure into this focused package.
- Keep one runtime architecture: one background Electron Managed Browser Runtime per Browser Generation, one or more Shared Browser Pages in that runtime, and one active Presentation for the selected visible page.
- Do not add system-Chrome discovery, managed Chrome downloads or caches, Chrome launch arguments, CDP screencast presentation, popup adoption, or alternate browser backends.
- Electron is an installed package dependency. Chrome DevTools MCP is also a direct installed dependency; resolve its
package entry and launch it with Node.js. Do not add
npx, first-run downloads, or an npm command-cache dependency. - Keep Chrome DevTools MCP out of the pixel path. It connects to the generation's exact loopback Electron DevTools endpoint and operates the same Shared Browser Pages that Host Chrome presents.
- Share the generation runtime across every Browser Session tab. Never launch one Electron process per tab or Browser Surface. Resource cost scales per runtime, per page, and per active Presentation in different ways.
- Treat
frameRateand viewport area as the primary Presentation cost controls. Hiding a Browser Surface must release its Presentation while leaving the Shared Browser Page alive.
Browser Pluginowns one root scope for a plugin installation.Browser Controland theMCP Registry Leaselive for the root scope. Their externally visible names and control endpoint stay stable across Managed Browser Runtime recovery.Browser Generationowns one replaceable child scope: the Electron Managed Browser Runtime, its initial Shared Browser Page, Browser Session, page and disconnect listeners, and the OpenCode app slot. Detach and close a disconnected generation before recovery. Build every replacement in a fresh child scope, publish it only after it is usable, and close a failed candidate.Browser Sessionowns logical browser tabs created through explicit session/control lifecycle. AtabIdis stable identity; a CDPtargetIdis replaceable transport identity. Never exposetargetIdas the UI tab identity, and do not treat Chrome DevTools MCP's numericpageIdas either one.Host Chromeowns the OpenCode-integrated tab title, navigation controls, and browser tab strip. It owns neither Electron nor Presentation resources.Browser Surfaceowns only the visual mount.Presentationowns only the renderable view's start, focus, and close lifecycle; neither owns a Shared Browser Page or Browser Session.
Prefer Effect.acquireRelease, Effect.addFinalizer, child Scopes, and scoped forks over parallel booleans, stored
unsubscribe callbacks, or hand-written cleanup chains. Make cleanup idempotent and order finalizers so the app slot,
listeners, session, pages, and Managed Browser Runtime close before the root-scoped MCP/control resources. Bound
retries, timeouts, queues, and all input-driven work.
- Decode plugin options, control requests/responses, and OpenCode MCP responses exactly once at their ingress with
Effect Schema. Pass typed values inside the package. Do not substitute
isRecord,typeof value === "object"plus property probing, or unchecked response casts for a domain schema. - Use domain-tagged errors at Effect boundaries and preserve the original cause. Translate to rejected Promises only in explicit adapters such as the exported TUI boundary and the control handler boundary.
- Recovery must be serialized and generation-aware. A stale disconnect or late completion may not replace the current generation. Disposal always wins recovery and must close late acquisitions.
- Registration is transactional: do not publish a generation or MCP configuration until its required resources are ready. On partial failure, close the candidate scope. Keep a still-usable current generation when possible; after a disconnect, report the browser as unavailable until a candidate succeeds.
- Keep the control server stable and route each request to the current generation at execution time. Never retain a poisoned Shared Browser Page or session handler after a generation swap.
- Browser Control owns
new_tabandclose_tablifecycle for agents. Chrome DevTools MCP owns inspection and page operations, but must not bypass Browser Session lifecycle and leave Host Chrome stale.
- Use
$opencode-browser-architecturefrom.opencode/skills/opencode-browser-architecture/SKILL.mdfor changes or reviews in runtime source and tests. - Use Bun for dependency management and scripts:
bun install,bun run <script>, andbun test. - Use
oxfmtas the formatting source of truth (semi: false,printWidth: 120). Avoid unrelated formatting churn. - For a bug fix, first add a focused regression test of observable behavior. Test ownership failures: interrupted acquisition, partial startup, stale disconnect, bounded recovery, late completion after dispose, cleanup order, and repeated disposal.
- Prefer injected domain dependencies and real scoped services over global mocks. Assert that listeners, slots, Presentations, Shared Browser Pages, Electron runtimes, control servers, and MCP leases are released.
- Run the narrowest test first, then
bun run typecheck,bun test,bun run build, andbun run test:packedas appropriate. Runbun run test:mcpfor the real Electron/Chrome DevTools MCP integration. Runbun run checkbefore handing off a release-sized change.