opencode2) Compatibilityoh-my-opencode-slim installs and runs on both OpenCode v1 (opencode) and
OpenCode v2 (opencode2) from a single published package. This document
explains how the dual-compatibility works, what is supported on each host, the
minimum v2 build each feature needs, and how it degrades on older builds.
The package's default export is an object:
export default {
id: 'oh-my-opencode-slim',
tui: true, // marker: this package ships a `./tui` entry for v2 TUI hosts
server: OhMyOpenCodeLite, // v1 plugin function (PluginInput) => Promise<Hooks>
setup: createV2Setup(), // v2 promise-plugin setup (ctx) => Promise<cleanup>
};
readV1Plugin in packages/opencode/src/plugin/shared.ts)
detects an object with a server field and calls plugin.server(input).
This is the original, unchanged v1 code path — v1 behavior is identical to
previous releases.PluginModule schema in
packages/core/src/plugin/supervisor.ts) decodes default as
{ id, setup } (Effect Schema 4 rejects function defaults) and calls
setup(ctx) via the promise-plugin bridge../tui entry (below) when the server-side
export declares tui: true.Three builds are produced:
| Export | File | Build | Externals |
|---|---|---|---|
. (main) |
dist/index.js |
build:plugin |
zod, jsdom, @opencode-ai/, @opentui/ (shared with v1 host) |
./server |
dist/server.js |
build:v2 |
jsdom only (self-contained for v2) |
./tui |
dist/tui2.js |
build:tui |
same external set as build:plugin (composes the v1 TUI entry; inlines zod) |
v2's plugin resolver tries the server subpath first
(subpaths: ["server", ""]), so a v2 package install loads the self-contained
dist/server.js. v1 uses the main entry.
Supported v2 builds: verified on beta-18269 and beta-18286 (add-only
command drafts; flat session prompt/synthetic/rename/switchAgent).
Newer builds add the capabilities listed under
Capability floors.
src/v2/setup.ts)setup(ctx) wraps the existing v1 factory rather than reimplementing it:
PluginInput from the v2 context
(src/v2/client-shim.ts): the project directory from ctx.location
(v2 ≥ #45403; process.cwd() on older builds), and a shim client that
really delegates the v1 SDK call shapes to v2 flat session calls —
session.get, session.abort→interrupt, session.messages→context,
session.prompt (as delivery: "steer"), and session.update→rename.
The shim marks the input hostFlavor: 'v2' and never fakes success
shapes: methods the host lacks degrade with an honest log (or are omitted
entirely, as with session.get, so capability probes see the truth).OhMyOpenCodeLite(pluginInput) to reuse all existing build
logic (config, agents, tools, hooks, job board, multiplexer, companion).config() hook against a synthesized config to resolve agent
models and the slash commands.Hooks into v2 registrations:
agent → ctx.agent.transform (model/prompt/permission adaptation +
subagent/execute permission mapping + prompt rewrite task→subagent
draft.default("orchestrator"))tool → ctx.tool.transform (zod shape → JSON schema; execute shimmed)mcp → ctx.mcp.transform (draft.set(name, adaptMcpServer(cfg)) for
the built-in MCPs; v2 ≥ #45408, see below)command → ctx.command.transform — v2 command drafts are add-only:
draft.add({name, description, execute}). execute submits a
<omos-cmd-command data-name="..."> marker as a user prompt; the session
context hook recovers it and dispatches to the v1
command.execute.before hook (deepwork/reflect/loop)ctx.session.hook("context") handles the system/messages
transforms (SystemPart[]/Message.content shape conversion),
chat.message agent tracking, and interview + generic command marker
dispatch — mutating only the trailing message so earlier content stays
byte-identical (provider prompt-cache prefix reuse)tool.execute.before/after → ctx.tool.hook via
createToolExecuteBridges (src/v2/setup.ts): the host subagent tool
is normalized to v1 task semantics (name mapping, agent→
subagent_type, sessionID→task_id, and back after the hook so v2
executes the repaired input). A throwing execute.before rethrows —
v2 rejects the tool call, which is how the v1 anti-duplicate /
relaunch-lease guards enforce on v2event → ctx.event.subscribe() loop feeding mapV2EventToV1
(src/v2/event-adapter.ts): additive synthesis only — the raw v2 event
is always dispatched first (the interview bridge depends on it), then
synthesized v1 shapes: idle session.status → session.idle, flat
child session.created → v1 early-registration
{info: {id, parentID, agent?}}, and usage telemetry
(session.usage.updated/session.step.ended) → a deduplicated
completed-assistant message.updated for the cache monitorgenerate.text → one-shot generation channel probed on ctx.generate
and threaded as experimental_v2.generateText, powering the webfetch
secondary-model summaries without a temp sessiondispose → returned cleanupEach bridge is independently try/catch-guarded so one failure cannot disable the rest, and a zero-registration load logs a loud health-check warning.
| Capability | v1 (opencode) |
v2 (opencode2) |
Minimum v2 capability / degradation |
|---|---|---|---|
| Orchestrator + specialist agents, prompts & permission mapping | ✅ | ✅ ctx.agent.transform |
— |
Delegation + background job board + task_* tools |
✅ task tool |
✅ host subagent (auto-bridged: name/args normalization in src/v2/delegation.ts, output parsing in the execute bridges) |
— |
| Tools (ast-grep, webfetch, task_message/task_cancel/task_revive, wait_for_user, acp_run) | ✅ | ✅ ctx.tool.transform |
ast-grep needs its CLI binary (package, system, or lazy download); webfetch needs jsdom resolvable |
Slash commands /deepwork /reflect /loop |
✅ | ✅ marker round-trip | command execute callbacks ≥ beta-18269; older builds register no commands |
/interview |
✅ | ✅ marker command + trailing-message context bridge | — |
| Message transforms (phase reminder, skills filter, image routing, display-name rewrite) | ✅ | ✅ via the single context hook | — |
| Event handling (session tracking, lifecycle, cache telemetry) | ✅ | ✅ event pump + additive v2→v1 synthesis | — |
| Tool execute hooks (apply-patch recovery, task-session, json-recovery) | ✅ | ✅ createToolExecuteBridges with subagent→task normalization |
— |
| Built-in MCPs (context7, gh_grep) auto-registered | ✅ | ✅ ctx.mcp.transform |
mcp.transform ≥ #45408; older builds degrade to config-only — see the snippet |
| webfetch secondary-model summaries | ✅ | ✅ via ctx.generate.text |
absent → summaries fall back to the session pipeline |
| Foreground model fallback (rate-limit failover) | ✅ | ✅ shim translates re-prompt into session.switchModel + delivery:"steer" prompt |
switchModel ≥ #43718; older builds steer on the current model with an honest log (fallback inactive) |
/preset (interactive switcher) |
✅ | ✅ TUI plugin entry (./tui → dist/tui2.js): sidebar + /preset dialog or /preset <name> fast path |
TUI host needs keymap.layer + ui.dialog.select; config-file preset still applies at load |
| Project directory | ✅ | ✅ ctx.location.directory |
≥ #45403; older builds use process.cwd() |
| TUI default agent | ✅ orchestrator | ✅ orchestrator — draft.default("orchestrator"); recent v2 TUI builds honor default_agent and hoist the default to the head of the agent list |
needs a recent v2 build |
| Multiplexer (tmux/zellij/herdr/cmux panes) | ✅ | ❌ host-gated off (hostFlavor: 'v2' → shouldEnableMultiplexer returns false and the session manager is forced to type: "none") |
by design — v2 renders subagents natively |
| Orchestrator-wake scheduler | ✅ | ❌ evaluated, intentionally not ported | v2's built-in subagent tool posts completion notifications to the parent natively, which covers the wake scheduler's job; see Limitations |
chat.headers (custom request headers) |
✅ | ❌ unbridged | low value: v2 exposes an HTTP request hook (session.hook("http.request")) — will bridge only if asked for |
| Companion app | ✅ | ⚠️ unverified | independent desktop app; test separately against v2 |
v2 moves fast and beta numbers are CI build IDs, so floors are stated as upstream PR/commit references rather than versions:
| Feature | Floor | Older-build behavior |
|---|---|---|
Programmatic MCP registration (ctx.mcp.transform) |
≥ #45408 | MCPs stay config-only (logged); add them manually with the snippet |
Foreground model switch (ctx.session.switchModel) |
≥ #43718 | re-prompt steers on the current model (logged) — rate-limit failover inactive |
Project directory (ctx.location) |
≥ #45403 | resolved from process.cwd() — run opencode2 from your project root |
Command execute callbacks (add-only drafts) |
≥ beta-18269 |
slash commands do not register |
Every v2 API the adapter touches is capability-probed at runtime
(typeof ctx.mcp?.transform === 'function', s.switchModel, ctx.generate,
…), so one missing capability degrades that single feature with a log line
instead of breaking the load.
v2 auto-refreshes unpinned npm plugins on every startup (#45118) —
"oh-my-opencode-slim@latest" effectively means "silently upgrade whenever a
new version ships". During the current rapid-evolution window (both v2 and
this adapter are changing quickly), pin an exact version:
{
"plugin": ["oh-my-opencode-slim@2.0.3"]
}
and bump it deliberately. The plugin logs its active capability set on load, so a pinned older build behaves exactly the same tomorrow as it does today.
Behaviors of v2 itself that plugin authors should know about — none currently break this plugin:
"mcps": ["context7", "!gh_grep"] in agent config), and the registration
uses our own server names via draft.set(name, ...).tool.execute.before carries no inputSchema: the v2 before-hook
event has no tool schema. We don't consume one — the bridge passes a mutable
args view and writes back what hooks produce.execute receives a prompt object, not a string: v2 hands
the handler a PromptInput.Prompt. The command bridge reads .text
(invocation?.prompt?.text ?? '') and never assumes a string.Add to ~/.config/opencode2/opencode.json:
{
"plugin": ["oh-my-opencode-slim@2.0.3"]
}
(Pin the exact current version — see version policy.)
For local development, point at the built dist/server.js directly:
{
"plugin": ["/path/to/oh-my-opencode-slim/dist/server.js"]
}
Then build:
bun install
bun run build # produces dist/index.js (v1), dist/server.js (v2 server),
# dist/tui2.js (v2 TUI), dist/cli/
Verify with opencode2 run "list your specialist agents" --standalone — the
orchestrator should name explorer, librarian, oracle, designer, fixer.
Agent models are resolved the same way as v1 (per-agent model in
oh-my-opencode-slim.json, or inherited from the session/host default). On v2,
set a working provider+model in your v2 config or the plugin's config file so
delegated subagents can run.
Rate-limit fallback works on v2 builds with switchModel (≥ #43718): when the
foreground model hits a rate limit, the plugin switches the session's model and
steers the re-prompt through delivery: "steer". On older builds the fallback
stays inactive and logs honestly — switch the model manually there.
On v2 builds without ctx.mcp.transform (< #45408) the two built-in remote
MCPs are not auto-registered. They are plain remote URLs — copy this into your
~/.config/opencode2/opencode.json to restore them:
{
"mcp": {
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
"headers": { "CONTEXT7_API_KEY": "$CONTEXT7_API_KEY" }
},
"gh_grep": { "type": "remote", "url": "https://mcp.grep.app" }
}
}
(context7 needs CONTEXT7_API_KEY; gh_grep needs nothing. Drop either key
if unused.) The librarian agent uses these for library-docs lookup and
GitHub-wide code search; without them it still works via webfetch.
/interview is supported on v2 through a marker command and a trailing-message
context bridge. The bridge keeps an in-memory transcript projection from v2
context and streamed text events, and uses the v2 session methods for prompts,
notifications, and renames. The markdown document remains the durable source
of truth; completion responses without <interview_state> rewrite the current
spec while retaining frontmatter and Q&A history.
shouldEnableMultiplexer / sessionManagerMultiplexerConfig in
src/index.ts).backgroundJobs.orchestratorWake).
Evaluated and intentionally not ported: the scheduler's job — nudging an
idle parent with unfinished work — is covered on v2 by the host's built-in
subagent tool, which posts completion notifications to the parent session
natively (verified in the v2 source: notifyWhenDone in
packages/core/src/tool/plugin/subagent.ts sends a
session.synthetic message with a <subagent sessionID state …>
envelope to the parent). The capability stays v1-only (it also requires
host session.get/todo/children/status/promptAsync surfaces the
v2 shim does not shim).chat.headers. Not bridged (low value on v2 — an HTTP request hook
exists if demand appears).setup
with a reduced, TUI-side context that lacks agent.transform (and other
domains). The adapter capability-guards setup and skips registration
gracefully for those hosts instead of crashing or retry-storming.?mtime= cache-busting query, which can break resolution of the
externalized jsdom import from the plugin's node_modules. The plugin still
loads because webfetch imports it lazily; install as a package or ensure
jsdom is resolvable to enable webfetch locally. AST-grep resolves its CLI
independently and lazily downloads a binary when no package or system binary
is available.src/hooks/cache-safety.property.test.ts and friends) covers the shared
transform code the v2 context hook invokes.