oh-my-opencode-slim installs and runs on both OpenCode v1 (opencode)
and OpenCode v2 (opencode2) from a single published package. This document
describes how each host loads the plugin, what is supported where, and how to
register it.
The package's default export is an object:
export default {
id: 'oh-my-opencode-slim',
server: OhMyOpenCodeLite, // v1 plugin function (PluginInput) => Promise<Hooks>
setup: createV2Setup(), // v2 promise-plugin setup (ctx) => Promise<cleanup>
};There is deliberately no tui key on this export: hosts validate a
server plugin module's tui field (it must be a function and must not
coexist with server), so a boolean tui: true marker gets the whole
plugin rejected with "invalid tui export".
- v1 loader (
readV1Plugininpackages/opencode/src/plugin/shared.ts) detects an object with aserverfield and callsplugin.server(input)with the full v1PluginInput. Extra keys (such assetup) are ignored on this path. - Embedded v2 pass on v1 hosts. Every v1 host (≥ v1.17.10) also boots
the v2 core, which reads the same config (migrating
plugin:entries toplugins:) and callssetup(ctx)with a registration-only context (agent/aisdk/catalog/command/integration/plugin/reference/skill — no tool/session/event/mcp/generate). A dual-export plugin registered via the v1plugin:key therefore gets both invocations: full v1 functionality flows throughserver(), while the parallel pass produces the expected[v2] … failed/bridges: 4log noise (see Environment caveats). A v2plugins:entry yields the setup pass alone — v1 does not convert v2 plugin declarations into v1 hooks. - v2 loader (
PluginModuleschema inpackages/core/src/plugin/supervisor.ts) decodesdefaultas{ id, setup }(Effect Schema 4 rejects function defaults) and callssetup(ctx)via the promise-plugin bridge. - v2 TUI loads the
./tuientry unconditionally: the TUI runtime runs its ownkind: "tui"loader pass over the same plugin list and resolves the entry through the package'sexports["./tui"]map — the server-side export plays no role in that discovery.
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/index.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", ""]), which the exports map resolves directly to
dist/server/index.js — the self-contained v2 server bundle, and also the
entrypoint v2 loads when the dist/server directory is registered directly
(see Installing on v2); the release artifact check
requires it. v1 uses the main entry.
Verified against opencode2 beta-18743 (all bridges green — health check
bridges:10). Every v2 API the adapter touches is capability-probed at
runtime (typeof ctx.mcp?.transform === 'function', s.switchModel,
ctx.generate, …), so a host lacking one capability degrades that single
feature with a log line instead of breaking the load.
setup(ctx) wraps the existing v1 factory rather than reimplementing it:
- Builds a v1-shaped
PluginInputfrom the v2 context (src/v2/client-shim.ts): the project directory fromctx.location, and a shimclientthat really delegates the v1 SDK call shapes to v2 flat session calls —session.get,session.abort→interrupt,session.messages→context,session.prompt(asdelivery: "steer"), andsession.update→rename. The shim marks the inputhostFlavor: 'v2'and never fakes success shapes: methods the host lacks degrade with an honest log (or are omitted entirely, as withsession.get, so capability probes see the truth). - Invokes
OhMyOpenCodeLite(pluginInput)to reuse all existing build logic (config, agents, tools, hooks, job board, multiplexer, companion). - Runs the v1
config()hook against a synthesized config to resolve agent models and the slash commands. - Bridges the returned v1
Hooksinto v2 registrations:agent→ctx.agent.transform(model/prompt/permission adaptation +subagent/executepermission mapping + prompt rewritetask→subagentdraft.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)command→ctx.command.transform— v2 command drafts are add-only:draft.add({name, description, execute}).executesubmits a<omos-cmd-command data-name="...">marker as a user prompt; the session context hook recovers it and dispatches to the v1command.execute.beforehook (deepwork/reflect/loop)- a single
ctx.session.hook("context")handles the system/messages transforms (SystemPart[]/Message.content shape conversion),chat.messageagent 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.hookviacreateToolExecuteBridges(src/v2/setup.ts): the hostsubagenttool is normalized to v1tasksemantics (name mapping,agent→subagent_type,sessionID→task_id, and back after the hook so v2 executes the repaired input). A throwingexecute.beforerethrows — v2 rejects the tool call, which is how the v1 anti-duplicate / relaunch-lease guards enforce on v2event→ctx.event.subscribe()loop feedingmapV2EventToV1(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: idlesession.status→session.idle, flat childsession.created→ v1 early-registration{info: {id, parentID, agent?}}, and usage telemetry (session.usage.updated/session.step.ended) → a deduplicated completed-assistantmessage.updatedfor the cache monitorgenerate.text→ one-shot generation channel probed onctx.generateand threaded asexperimental_v2.generateText, powering the webfetch secondary-model summaries without a temp sessiondispose→ returned cleanup
Each 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) |
Notes |
|---|---|---|---|
| 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 | — |
/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 |
— |
| webfetch secondary-model summaries | ✅ | ✅ via ctx.generate.text |
host without ctx.generate → summaries unavailable (logged) |
| Foreground model fallback (rate-limit failover) | ✅ | ✅ shim translates re-prompt into session.switchModel + delivery:"steer" prompt |
— |
/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 |
| TUI default agent | ✅ orchestrator | ✅ orchestrator — draft.default("orchestrator"); the v2 TUI honors default_agent and hoists the default to the head of the agent list |
— |
| 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 | ✅ | ❌ intentionally not ported | v2's built-in subagent tool posts completion notifications to the parent natively, covering the scheduler's job |
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 | ✅ | independent desktop app; test separately against v2 |
Behaviors of v2 itself that plugin authors should know about — none currently break this plugin:
- Duplicate idle delivery. v2 favors
session.statusoversession.idle; the adapter synthesizessession.idleadditively, so a consumer watching both events sees idle twice per session. Current consumers are idempotent per session (idle-reconciliation's per-session timer guards); new idle consumers must tolerate duplicate delivery. - MCP tool-name namespaces are host-generated. This plugin never
matches raw MCP tool names: MCP access is granted per server name
(
"mcps": ["context7", "!gh_grep"]in agent config), and registration uses its own server names viadraft.set(name, ...).
Add the npm package, pinned to an exact version — v2 auto-refreshes
unpinned npm plugins on every startup, so @latest effectively means
"silently upgrade whenever a new version ships". The global config root is
~/.config/opencode/opencode.json, shared with v1 (~/.config/opencode2/
is not read for plugin config):
{
"plugin": ["oh-my-opencode-slim@2.2.17"]
}For local development, point the config at the built dist/server
directory:
{
"plugin": ["/path/to/oh-my-opencode-slim/dist/server"]
}Then build:
bun install
bun run build # produces dist/index.js (v1), dist/server/index.js (v2
# server bundle, also served via the ./server subpath),
# 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.
- Directory or package entries only. File-path entries (e.g.
…/dist/server.js) are rejected with the WARNconfigured plugin path must be a directory. A directory entry'sindex.jsis the entrypoint — hencedist/serverabove. - Single-file plugins need a wrapper dir whose
index.jsre-exports the original file, e.g.~/.config/opencode/plugins-dev/<name>/index.jscontainingexport { default } from "/abs/path/to/plugin.js";. Do not use the auto-scanned dir namesplugin/pluginsfor wrapper dirs — a scanned duplicate next to an explicit registration hard-dies on duplicate plugin ID.
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 config or the plugin's config file
so delegated subagents can run.
When the foreground model hits a rate limit, the plugin switches the
session's model (session.switchModel) and steers the re-prompt through
delivery: "steer".
/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.
- Multiplexer panes. tmux/zellij/herdr integration is a v1-TUI feature;
v2 renders subagents natively, so the multiplexer is host-gated off on v2
(
shouldEnableMultiplexer/sessionManagerMultiplexerConfiginsrc/index.ts). - Orchestrator-wake scheduler (
backgroundJobs.orchestratorWake). Intentionally not ported: v2's built-insubagenttool already nudges an idle parent with unfinished work by posting completion notifications natively. The capability also depends on hosttodo/childrensurfaces the v2 shim does not provide. chat.headers. Not bridged (low value on v2 — an HTTP request hook exists if demand appears).
- Reduced/TUI-side hosts. Some host processes load the plugin's
setupwith a reduced, TUI-side context that lacksagent.transform(and other domains). The adapter capability-guardssetupand skips registration gracefully for those hosts instead of crashing or retry-storming. The same applies to the embedded v2 pass inside every v1 host: it invokessetupwith registration-only domains, so a v1 session's plugin log shows[v2] tool.transform failed-style lines andhealth check passed {"bridges":4}— expected noise from that parallel pass, not breakage. The classicserver()path (a separate plugin-log instance a few seconds apart) carries the full v1 functionality. - Local-checkout loading. When the plugin is registered from a local
build, the externalized
jsdomimport must resolve from the plugin'snode_modules(webfetch imports it lazily, so the plugin still loads without it — install as a package or ensurejsdomis resolvable to enable webfetch locally). AST-grep resolves its CLI independently and lazily downloads a binary when no package or system binary is available. - Companion app unverified on v2. The companion is an independent desktop app; test it separately against v2 hosts.
- Prompt-cache rules unchanged. The v2 bridges reuse the v1 transform
pipeline under the same cache-safety contract: only trailing messages are
mutated, earlier content stays byte-identical, and the v1 enforcement
suite (
src/hooks/cache-safety.property.test.tsand friends) covers the shared transform code the v2 context hook invokes.