---
title: OpenCode provider (run sessions in OpenCode's harness)
---

# OpenCode Provider

## What it is

Omniscio can spawn Claude-Code-style sessions backed by the **OpenCode CLI** (`opencode`) as an alternative provider, alongside `claude` (the default), `codex`, `gemini`, `antigravity`, the Anthropic-compatible `deepseek` / `kimi` / `glm` / `minimax` / `meta`, `cursor`, `hermes`, `grok`, `pi`, and `openclaw` (the registry defines 26 session-provider keys, 20 of them user-selectable/pickable; the non-pickable ones include the managed-PTY `terminal` engine and the internal-only `openclaw`). OpenCode is a model-agnostic local CLI harness: by **default** it runs **Claude (Sonnet)** through an Anthropic key, but it can also run **any hosted model** OpenCode supports (Kimi, GPT, DeepSeek, …) via the **OpenCode model** picker — Omniscio sends the chosen model to the shared server as the `{providerID, id}` selector on `POST /session` and routes the matching provider credential by the model-id prefix. So an OpenCode session is "an agent harness you can point at any model"; left on the default it's "Claude, driven by a different harness." Model routing is locked by [provider-registry-contract.md](../../.claude/memory/contracts/provider-registry-contract.md).

## Where to find it

**Settings → Accounts** — the OpenCode section stays hidden until you flip **Show alternative AI providers**. Once revealed it appears as a provider in the normal session-creation pickers.

## How it behaves

### What the user sees

An OpenCode session looks almost identical in the sidebar and main pane to a Claude session — same status dots, same streaming bubbles, same Ctrl+Enter to send. The differences:

- **OpenCode icon in the header.** An OpenCode session shows the OpenCode mark (brand orange, no text label) next to its title — like every non-default provider (Codex, Gemini, Anti-Gravity, Cursor, DeepSeek, Kimi, GLM, MiniMax, Hermes, Pi). Only the internal-only engines (OpenClaw, orchestrator) show no icon — every other session, Claude included, shows its engine mark. A fresh session's **Change-provider button** shows the same mark while OpenCode is the selected provider.
- **Streaming is token-by-token.** Omniscio subscribes to the shared `opencode serve` server's Server-Sent-Events stream, so the reply appears incrementally as it generates (like Gemini, unlike Anti-Gravity's block mode). When the turn completes, one final push replaces the bubble with the full accumulated text.
- **Compactions are visible (2026-09-11).** When a long OpenCode session fills its context window, OpenCode rewrites the history into a summary and older turns stop existing. It announces that with a `session.compacted` event, which Omniscio used to drop as an unknown frame — so the rewrite left no trace in the transcript at all. Now it writes the same **“Conversation compacted”** divider a Claude or Codex session shows, carrying the context size from the last assistant message. Two differences from Claude: there is no pulsing “Compacting…” pill (OpenCode announces the rewrite only once it is DONE — there is no start signal to hang one on), and the divider has no **“Show summary”** button, because that reads Claude's own on-disk transcript and OpenCode keeps none. See [compaction-summary.md](compaction-summary.md).
- **Tool-use auto-approval defaults to ON.** OpenCode runs its tools (file writes, shell commands, network) without per-call prompts. The "Allow OpenCode sessions" toggle gates whether you can spawn it at all; once spawned, it does not ask before acting.

### How to enable

> **Master toggle required first.** OpenCode is an alternative provider — by default Omniscio ships as a Claude-only product, and the entire OpenCode setup section is hidden in Settings → Accounts. Flip **Settings → Accounts → Show alternative AI providers** to ON (off by default) and the **OpenCode (CLI provider)** panel appears. With the master off, the per-launch provider switcher (ChangeProviderButton) hides OpenCode — it effectively does not exist in the UI even if every gate below is configured. Your "Allow OpenCode sessions" toggle and saved key are preserved across master-toggle flips.

> **Fastest path — "Set it up for me."** The OpenCode panel leads with a **Set it up for me** button: one click turns OpenCode on, **installs the `opencode` CLI for you** (npm `opencode-ai`, with a Windows winget fallback), and **reuses an Anthropic API key you already have saved** (with a one-click confirm — never a silent copy), defaulting to Claude Sonnet so only that one key is needed. If you have no key saved, it installs + enables OpenCode and stops with a **durable in-card status line** ("one step left — paste your Anthropic API key below to finish") and the key field revealed. Every outcome — ready, one-step-left, or an install error — shows that persistent result line right in the card (not just a transient toast), so the click is never silent. The three manual gates below are exactly what that button automates — governed by [provider-registry-contract.md](../../.claude/memory/contracts/provider-registry-contract.md).

Three gates must all pass (`getProviderSpawnReadiness` enforces them in this order):

> **The toggle leads and gates the panel UI.** The CLI-install status card and the API key field render only once "Allow OpenCode sessions" is on — so a key can't be entered into a disabled provider. When off, a hint stands in and keeps the `opencode-binary` / `opencode-api-key` Settings-search anchors landable; a saved key is preserved.

1. **Allow OpenCode sessions** toggle — off by default. Same security stance as the other auto-approving providers: enable only because you intend to use it. Lives in the OpenCode panel as **Allow OpenCode sessions in any project**, and it's the first thing you see (turning it on reveals the steps below).
2. **OpenCode CLI binary** — the `opencode` binary must be on your PATH. The **Set it up for me** button installs it for you (npm `opencode-ai`, bin `opencode`; a Windows winget `SST.opencode` fallback runs only if npm fails) and clears the binary-resolver cache so the fresh install is seen immediately. The Settings panel shows **OpenCode CLI: Installed (version X)** in the binary status card, or a "not found on your PATH" hint. To install by hand, run `npm install -g opencode-ai`. The resolver also discovers an npm-global install via `npm root -g`, so a GUI-launched Omniscio with a narrow PATH still finds it. (OpenCode is registered in Omniscio's toolchain installer with `defaultSelected:false` — installable on demand, never auto-installed in the first-run "install recommended tools" sweep.)
3. **The API key for the selected model's provider.** Readiness is now **model-aware**: the key required depends on the **OpenCode model** you pick (gate 4). On the default (no model chosen) it's the **OpenCode API Key (Anthropic)** — OpenCode runs Claude through it. Pick a non-Claude model and the gate asks for that provider's key instead (e.g. an OpenRouter key for `openrouter/…`). All keys are stored encrypted (`safeStorage` + `enc:` prefix) and injected as the matching environment variable on spawn (`ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY` / `OPENAI_API_KEY` / `MOONSHOT_API_KEY`) — never on the command line.

**Gate 4 — OpenCode model (optional).** A dropdown of curated models (Claude Sonnet, GPT-4o, Kimi K2, DeepSeek…) plus a **Custom…** box for any `provider/model` id OpenCode supports. **Leave it on the default and nothing changes** — OpenCode keeps its own Claude-Sonnet default with your Anthropic key (the same outcome as before this feature; the server still gets the resolved default model selector, never a literal `--model` arg). Choosing a model routes the matching key by its prefix:

| Model prefix               | Env var injected     | Key read from (your settings)            |
| -------------------------- | -------------------- | ---------------------------------------- |
| `anthropic/…` (default)    | `ANTHROPIC_API_KEY`  | OpenCode API Key                         |
| `openrouter/…` (catch-all) | `OPENROUTER_API_KEY` | OpenRouter API Key (field in this panel) |
| `openai/…`                 | `OPENAI_API_KEY`     | OpenAI key (Codex's)                     |
| `moonshotai/…`             | `MOONSHOT_API_KEY`   | Kimi key                                 |

**OpenRouter is the catch-all** — one OpenRouter key unlocks Kimi, GPT, Claude, and hundreds more as `openrouter/<vendor>/<model>`, so you rarely need per-vendor keys. **Cost guard:** the OpenRouter route reads _your_ saved key only — never Omniscio's bundled internal OpenRouter key — so a coding session can't bill Omniscio's shared account. **Local models** (Ollama/LM Studio) are not yet supported in v1 → they surface a `model-unsupported` gap rather than a confusing CLI failure.

Once all three are green, you launch an OpenCode session two ways:

- **Per-launch override** — on a fresh (zero-message) session, the **Change-provider button** in the new session's main panel (the launch-config pickers on a fresh session) lets you pick a one-off provider before sending the first message. Pick OpenCode and the first send spawns it. Non-ready providers appear disabled with a "Set up…" / "Install…" / "Add key" suffix that deep-links to the relevant Settings panel.
- **Programmatically** — anything that creates a session with `provider: 'opencode'` (recipes, the CLI control server, agent-driven sessions). The same three readiness gates apply on the backend.

### No per-project default, no virtual project

OpenCode is intentionally **excluded** from two surfaces that the persistent providers (Codex, Gemini, Anti-Gravity) have:

- **Not a per-project default.** The Edit-Project "Default provider" radios and the sidebar "+ New Session" split-button omit OpenCode (it is `pickable: false`, so it is absent from `PROVIDER_ORDER`). It is now a persistent shared-server engine (like Codex / Gemini / Anti-Gravity), but it has simply not been wired into the project-default surface yet. Launch it explicitly per-session instead.
- **No dedicated virtual project.** There is no `__opencode__` sidebar entry (unlike `__codex__` / `__gemini__` / `__antigravity__`), and therefore no sentinel bypass of the "Allow OpenCode sessions" toggle. The toggle always applies.

### Error states and fixes

Gap codes (toggle + binary as before; `key-missing` is now per-selected-model; `model-unsupported` is new):

| Gap                 | What it means                                              | Click-to-fix lands you at…                                              |
| ------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------- |
| `toggle-off`        | "Allow OpenCode sessions" is OFF in Settings               | Settings → Accounts → Allow OpenCode sessions toggle                    |
| `binary-missing`    | `opencode` CLI not found on PATH                           | Settings → Accounts → OpenCode CLI binary panel                         |
| `key-missing`       | No saved key for the **selected model's** provider         | Settings → the key field for that provider (Anthropic / OpenRouter / …) |
| `model-unsupported` | The model id is local/unknown/malformed (v1 = hosted only) | Settings → Accounts → OpenCode model picker                             |

There is no virtual-project escape hatch, so the gaps apply to every OpenCode session. A resolver gap fails the turn **before** the server is touched (the model/key is resolved during the lazy start) with a humanized transcript line — it never sends a turn with a bad model/key.

### Comparison with other providers

| Concern             | OpenCode                                                    | Gemini                            | Anti-Gravity                      |
| ------------------- | ----------------------------------------------------------- | --------------------------------- | --------------------------------- |
| Auth                | Anthropic API key (env `ANTHROPIC_API_KEY`)                 | Google API key (`GEMINI_API_KEY`) | Google Sign-In (OS keyring)       |
| Underlying model    | Claude (Sonnet) by default; any hosted model via the picker | Gemini                            | Gemini / Google models            |
| Streaming           | Token-by-token (shared `opencode serve` SSE)                | Token-by-token (ACP JSON-RPC)     | Block mode (full reply at end)    |
| Process model       | Shared persistent `opencode serve` server (HTTP/SSE)        | Persistent `gemini --acp` child   | Per-turn spawn                    |
| Multi-turn resume   | Native (opencode `ses_…`, persisted in the shared DB)       | Native `--resume <uuid>`          | Synthesized (prepend prior turns) |
| Cost emitted        | Yes — authoritative, written to `api_cost_log`              | No (Google-billed)                | No                                |
| Readiness gates     | 3 (toggle + binary + key)                                   | 3 (toggle + binary + key)         | 2 (toggle + binary)               |
| Per-project default | No (excluded from radios — historical, not yet wired)       | Yes                               | Yes                               |
| Virtual project     | No                                                          | Yes (`__gemini__`)                | Yes (`__antigravity__`)           |
| Session-header icon | OpenCode mark (orange)                                      | Gemini bloom (gradient)           | Anti-Gravity "A" (blue)           |

## For agents

### How it works under the hood

**One shared `opencode serve` server, not a spawn per turn.** OpenCode is now a `persistent-external` engine (like Codex, Gemini, Pi). Omniscio runs ONE long-lived `opencode serve` HTTP/SSE server that multiplexes EVERY OpenCode session; per-turn work is just HTTP calls to it, not a fresh process. The server is owned by a **supervisor** (`opencode-server-supervisor.ts`) and is **lazy** — it is NOT started at app launch; `ensureServer()` starts it on the FIRST session's first turn, single-flighted so two concurrent first-turns can't spawn two servers. It is **ref-counted** (each live session `acquire`s it; the last `release` disposes it after a short idle grace, and a quick close-then-reopen cancels the pending dispose), and **watchdog'd** (while ≥1 session needs it, a health probe restarts a wedged/dead server, firing ONE deduped inbox alert — never resurrecting a server with zero sessions). `launch()` still just registers an in-memory session with no child; the first `sendMessage()` is what lazily starts the shared server and creates this session's opencode session.

**Why the shared server (the bug it fixes).** The old `opencode run` one-shot cold-booted OpenCode's WHOLE runtime — server + SQLite DB + Drizzle migrations — for EVERY turn, all against ONE shared `~/.local/share/opencode/opencode.db`. Two concurrent turns collided on that DB's WAL recovery lock (`SQLITE_BUSY_RECOVERY`, errno 261) at startup and BOTH wedged — **measured 0/24 success under 8-concurrent load**. `opencode serve` is a client/server app: one process owns the DB and serves many sessions, so the collision cannot happen (a live proof ran **6/6** concurrent across two directories) — and turns are far faster (no per-turn boot). See [opencode-server-contract.md](../../.claude/memory/contracts/opencode-server-contract.md).

**Loopback-bound + Basic-auth secured.** The server is launched on `127.0.0.1` with a random `OPENCODE_SERVER_PASSWORD`, and every request carries `Authorization: Basic base64("opencode:<password>")` (the only scheme that returned 200 against opencode v1.15.2 — Bearer/none both 401). The serve child launches only INSIDE Omniscio's Windows job object — the job-gate launcher holds it until Omniscio has put it there, and a launch that cannot be contained is refused — so it dies with Omniscio (clean exit OR crash) with no separate orphan reaper. Kill switches: `AMC_DISABLE_OPENCODE_SERVER=1` turns the whole feature off (OpenCode sessions cleanly error); `AMC_DISABLE_OPENCODE_SERVER_WATCHDOG=1` disables just the auto-restart.

**One opencode `ses_…` per Omniscio session, reused across restarts.** On a session's first turn Omniscio `POST /session?directory=<cwd>`s to mint an opencode `ses_…` id, pinned to the project cwd, and persists it as `cli_session_id`. Because the shared opencode DB persists the conversation, that id is **reused** on app re-register (reload/restart) — the next prompt continues with full context and NO Omniscio replay (unlike Codex/Pi, whose in-memory child state dies). Only a row with Omniscio history but no stored opencode id mints a fresh one and replays its history as a one-time context prefix (rare).

**Turns go via `prompt_async` + a per-directory SSE stream.** Each turn `POST /session/{id}/prompt_async?directory=<cwd>`s (parts + the `{providerID, modelID}` selector) and returns **204** immediately — fire-and-stream. Output then arrives over a Server-Sent-Events stream: `GET /event?directory=<cwd>` is **directory-scoped** (verified live — directory A's stream never carries directory B's sessions), so the client opens ONE stream PER DISTINCT DIRECTORY and demuxes every frame back to the right Omniscio session by `properties.sessionID`. Streamed text = `message.part.delta` (`field:'text'`) pushed to the bubble via `SESSION_OUTPUT` with `streaming:true` (APPEND); turn-end = `session.idle`; at end one final `SESSION_OUTPUT` with `streaming:false` carries the full accumulated text (REPLACE semantics — the same contract every other provider uses). A reconnect that re-reads a buffered terminal frame is deduped so it can't double-fire turn-end.

**Cost tracking.** Unlike Gemini (Google-billed, emits no cost), OpenCode reports authoritative `cost` and `tokens` (input / output / reasoning / cache) on the turn's final assistant `message.updated` frame. Omniscio captures that exact figure and applies it once at turn-end: each turn writes per-session totals (`updateSessionCost`) AND an `api_cost_log` row via `trackApiCostRaw` with a synthetic accountId (`opencode-shared`) and source `'opencode'`, so the cost dashboard matches the provider console (per the CLAUDE.md "CLI spend MUST hit `api_cost_log`" rule). The model label logged is the **resolved model id** (e.g. `openrouter/moonshotai/kimi-k2`) — so the dashboard reflects what actually ran. For the _default_ (unset) case the label is OpenCode's own default; the dollar figure (sourced from OpenCode itself) is always correct.

**Interrupt.** Pressing stop calls `POST /session/{id}/abort` on the shared server — it aborts JUST that session's in-flight turn, never the server (other sessions keep using it). The turn's `session.idle` then settles the session to `ready` as a clean stop (not a crash); the stall watchdog is the backstop if the server never emits one. Partial streamed text is preserved and the opencode `ses_…` id is kept so the next turn still resumes. No cost row is written for an interrupted turn (no final assistant cost frame).

**Routing.** A `SESSION_LAUNCH` (and subsequent send / interrupt / terminate) with `provider: 'opencode'` is dispatched to `opencodeSessionManager` instead of the Claude `processManager`, in both `session-service.ts` and `session-handlers.ts` (and the CLI server's alt-manager dispatch). The manager now extends `BaseExternalSessionManager` directly (like the Gemini-ACP manager) — shared status transitions, the stall watchdog, the first-response timer, and the streaming-finalize template come from the base; the manager owns only the OpenCode-specific surface (the shared server + the `ses_…` mapping + the demuxed SSE consumer + exact cost). Because it is a registry `persistent-external` engine, it sits in `SESSION_MANAGERS` (NOT `ONE_SHOT_MANAGERS`) and in `CRASH_RESUMABLE_EXTERNAL_MANAGERS`. The session row stores `provider: 'opencode'` in its DB column — accepted by migration **v238**, which extends the provider validation trigger to allow the value.

**Crash-restart via the base.** An app-crash victim (`crash_reconciled=1`) auto-resumes through the base `resumeAfterCrash` → it REUSES the stored opencode `ses_id`, so the conversation continues from the shared opencode DB with no Omniscio context replay. A session that errored on its own is never auto-resumed.

**MCP servers — OFF in the shared-server model (a capability regression, called out honestly).** The shared-server move turned MCP **off** (`mcp: false`), and the OpenCode MCP connector was **removed**. The old mechanism handed each turn's per-session config to a fresh `opencode run` child via the `OPENCODE_CONFIG_CONTENT` environment variable — but a single shared `opencode serve` process has ONE launch env, and OpenCode config is per-session, so a single shared-server launch env can't safely carry per-session MCP without leaking one project's servers across projects. So unlike the one-shot mode (where OpenCode sessions did receive your enabled MemPalace / KMS / Google Workspace / Zapier / custom servers), an OpenCode session in serve mode currently gets **no MCP servers**. This is an intentional, honest regression vs the old one-shot mode — not a lying flag. Serve-mode MCP (a per-session `/mcp` route on the shared server) is a tracked follow-up; the `toOpencodeMcpConfig` translator is retained for it. Connector seam background: [provider-mcp-connector-contract.md](../../.claude/memory/contracts/provider-mcp-connector-contract.md).

**Auth.** Keys are stored encrypted (all on `CLI_SETTINGS_SENSITIVE_KEYS` + `ENCRYPTED_APP_SETTINGS_KEYS`) and injected into the **shared server's launch env** — `ANTHROPIC_API_KEY` (default/Claude), `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, and/or `MOONSHOT_API_KEY` — never on argv. The launch env first STRIPS any ambient provider-key copy from the Omniscio process (`OPENCODE_SERVER_ENV_KEYS`) and then re-injects ONLY the user's saved keys plus the server password, so a stray shell `OPENROUTER_API_KEY` can never bill the wrong account. The OpenRouter route reads the **raw user setting only** (never Omniscio's bundled internal key — the cost guard). One consequence of a single shared server: all sessions share that one key set, so per-session provider-key isolation isn't possible (Omniscio keys are global anyway).

**Binary status IPC.** The Settings panel's binary status card calls `OPENCODE_GET_STATUS` (`opencode:get-status`), which runs `resolveOpencodeBinary()` and returns `{ installed, version }` — mirroring `ANTIGRAVITY_GET_STATUS`. This isolates the "is the binary on PATH" question from the toggle/key gates (which `PROVIDER_GET_READINESS` would short-circuit), so the card can tell you to install OpenCode even while the allow-toggle is still off.

**Capabilities.** OpenCode is now a `persistent-external` engine in the registry, declared with `{ images: false, mcp: false, ssh: false, permissionPrompts: false, multiTurn: true }` — `images: false` is no inline-image protocol, no SSH remote, no permission prompts (auto-approves), and `mcp: false` is honest about the serve-mode regression (see "MCP servers" above — the connector was removed, serve-mode MCP is a follow-up). Multi-turn is native (the opencode `ses_…` persists in the shared DB). Send is **text-only** in serve v1 (`makeTextOnlySendStrategy`) — no image/doc attachments yet.

### Files

- [.claude/memory/contracts/opencode-server-contract.md](../../.claude/memory/contracts/opencode-server-contract.md) — **the shared-server source of truth**: the supervisor/client/manager split, the live-captured wire protocol, and every invariant (lazy start, ref-count, watchdog, Basic auth, per-directory SSE, cost-guard env, MCP-off)
- [src/main/services/engines/opencode-server-supervisor.ts](../../src/main/services/engines/opencode-server-supervisor.ts) — owns the ONE `opencode serve` child: lazy single-flight start, ref-counted idle-dispose, watchdog restart, Basic-auth + cost-guard launch env, job-object orphan kill (singleton `opencodeServerSupervisor`)
- [src/main/process/opencode-server-client.ts](../../src/main/process/opencode-server-client.ts) — typed HTTP + SSE transport: `createSession` / `sendPrompt` (204) / `abort` / `dispose`, one reconnecting `/event` stream per directory demuxed by `sessionID`, terminal-event dedupe; owns no process and no session bookkeeping
- [src/main/services/engines/opencode-session-manager.ts](../../src/main/services/engines/opencode-session-manager.ts) — extends `BaseExternalSessionManager`; maps Omniscio sessions ↔ opencode `ses_…` ids, drives turns, demuxes the shared SSE stream, records exact cost (singleton `opencodeSessionManager`)
- [src/main/services/engines/opencode-model-resolver.ts](../../src/main/services/engines/opencode-model-resolver.ts) — `routeOpencodeModel` (pure prefix→env+key map) + `resolveOpencodeModel` (settings-backed) + `toServerModelRef` (`{providerID, id}`) + `buildOpencodeServerKeyEnv` (user-keys-only cost guard); single source of truth for model→credential routing
- [.claude/memory/contracts/provider-registry-contract.md](../../.claude/memory/contracts/provider-registry-contract.md) — model-routing invariants (default-preserving, cost guard) + the one-click "Set it up for me" flow (toolchain install + Anthropic-key reuse in main + arm-toggle-last) that automates the gates above
- [src/main/services/engines/opencode-binary-resolver.ts](../../src/main/services/engines/opencode-binary-resolver.ts) — locate `opencode` via the shared `createPathBinaryResolver` factory (PATH → `npm root -g` → known dirs; caches only successes and re-probes on miss — no stale "not installed" after an install); `resolveOpencodeSpawnTarget` resolves the real `opencode.exe` the supervisor launches
- [src/main/services/engines/opencode-credential-store.ts](../../src/main/services/engines/opencode-credential-store.ts) — encrypted `opencodeApiKey` get/set/has/clear
- [src/main/services/provider-setup/provider-readiness.ts](../../src/main/services/provider-setup/provider-readiness.ts) — 3-gate readiness (toggle + binary + key); single source of truth for ChangeProviderButton + spawn guard
- [src/main/ipc/openai-handlers.ts](../../src/main/ipc/openai-handlers.ts) — `OPENCODE_GET_STATUS` binary-status handler
- [src/main/db/incremental-migrations.ts](../../src/main/db/incremental-migrations.ts) — migration **v238** (provider trigger accepts `'opencode'`; the frozen integer migration ladder lives here, not in `database.ts`)
- [src/main/services/session/session-service.ts](../../src/main/services/session/session-service.ts) + [src/main/ipc/session-handlers.ts](../../src/main/ipc/session-handlers.ts) — routing to `opencodeSessionManager`
- [src/shared/types.ts](../../src/shared/types.ts) (`ProviderId`, `opencodeApiKey`, `allowOpencodeSessionSpawn`), [src/shared/provider-capabilities.ts](../../src/shared/provider-capabilities.ts), [src/renderer/src/lib/visible-providers.ts](../../src/renderer/src/lib/visible-providers.ts) — provider enumeration / visibility
- [src/renderer/src/components/ui/OpenCodeIcon.tsx](../../src/renderer/src/components/ui/OpenCodeIcon.tsx), [src/renderer/src/features/sessions/ChangeProviderButton.tsx](../../src/renderer/src/features/sessions/ChangeProviderButton.tsx), [src/renderer/src/features/sessions/ProviderSplitButton.tsx](../../src/renderer/src/features/sessions/ProviderSplitButton.tsx) — icon + per-launch switcher (OpenCode excluded from the project-default split-button list)
- [src/renderer/src/features/settings/sections/accounts/AccountSettings.tsx](../../src/renderer/src/features/settings/sections/accounts/AccountSettings.tsx) — the OpenCode (CLI provider) Settings section (binary card + API key + allow toggle)
- [tests/unit/process/opencode-server-client.test.ts](../../tests/unit/process/opencode-server-client.test.ts) + [tests/unit/services/opencode-server-supervisor.test.ts](../../tests/unit/services/opencode-server-supervisor.test.ts) — lock the SSE demux / terminal-dedupe / cost-extraction (client) and the lazy-start / ref-count / watchdog / kill-switch lifecycle (supervisor); built against the live-captured opencode v1.15.2 protocol

## Related

### Related

- [gemini-provider.md](gemini-provider.md) — closest analog: token-by-token streaming (ACP JSON-RPC) + API-key auth, but Google-billed and a project-default-capable persistent provider
- [antigravity-provider.md](antigravity-provider.md) — a per-turn one-shot spawn (OpenCode is no longer one-shot — it now runs a shared persistent server), plus block-mode streaming and Google Sign-In auth
- [codex-provider.md](codex-provider.md) — long-lived JSON-RPC child (different process model)
- [deepseek-provider.md](deepseek-provider.md) / [kimi-provider.md](kimi-provider.md) — also icon-badged, but Anthropic-compat API providers with no binary (reuse the `claude` CLI)

