Repository navigation
Description
Summary
OpenCode V2 loads server plugins from config, but external TUI plugins configured in tui.json do not appear to be loaded by the installed opencode2 TUI. The TUI config schema still accepts plugin / plugin_enabled, but the TUI command handler only passes a pluginHost that calls loadBuiltinPlugins, so package/local TUI plugins cannot register keymap commands or dialogs.
Environment
- opencode version:
opencode2 v0.0.0-next-15356 - OS:
Linux 7.0.0-27-generic x86_64 GNU/Linux - Terminal:
TERM_PROGRAM=ghostty,TERM=xterm-256color,COLORTERM=truecolor - Shell:
zsh - Install/channel:
next - Active plugins:
opencode-key-rotator
Reproduction
- Create or use a TUI plugin module exporting a default object with
{ id, tui }. - Add it to
~/.config/opencode/tui.json:{ "plugin": [ "/path/to/opencode-key-rotator/dist/tui.js" ], "plugin_enabled": { "opencode-key-rotator": true } } - Verify the TUI plugin module can be imported by Bun:
Output:
bun -e "import('./dist/tui.js').then(m=>console.log({id:m.default?.id,tui:typeof m.default?.tui}))"{"id":"opencode-key-rotator","tui":"function"} - Start a fresh TUI:
opencode2
- Try to use the slash commands or UI registered by the TUI plugin.
Expected Behavior
The external TUI plugin from tui.json should be loaded, and its tui(api) function should be called so it can register keymap commands, slash names, routes, slots, and dialogs.
For this plugin, /key-save, /key-switch, /key-status, and /key-manage should open TUI dialogs/panels rather than being treated as server-side prompt templates.
Actual Behavior
The TUI plugin in tui.json is not loaded. Its tui(api) function is never called, so no TUI dialogs/panels are registered.
Server-side V2 plugins do load correctly. For the same package, /api/plugin lists opencode-key-rotator, and /api/command lists server-side commands such as key-save, key-switch, key-status, and key-manage. However those commands execute as prompt templates sent to the agent, not as TUI panels.
Additional Context
The installed build appears to parse tui.json but not use plugin to load external TUI modules.
From the installed sourcemap for src/tui-config.ts:
export const load = Effect.fn("TuiConfig.load")(function* () {
const fs = yield* FileSystem.FileSystem
const global = yield* Global.Service
const filepath = path.join(global.config, "tui.json")
const text = yield* fs.readFileString(filepath).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!text) return TuiConfig.resolve({}, { terminalSuspend: process.platform !== "win32" })
const errors: ParseError[] = []
const input: unknown = parse(text, errors, { allowTrailingComma: true })
if (errors.length) return TuiConfig.resolve({}, { terminalSuspend: process.platform !== "win32" })
return TuiConfig.resolve(
Option.getOrElse(Schema.decodeUnknownOption(TuiConfig.Info)(input), () => ({})),
{ terminalSuspend: process.platform !== "win32" },
)
})From the installed sourcemap for ../tui/src/config/v1/index.tsx, the schema includes TUI plugin config fields:
export const Info = Schema.Struct({
$schema: Schema.optional(Schema.String),
theme: Schema.optional(Schema.String),
keybinds: Schema.optional(TuiKeybind.KeybindOverrides),
plugin: Schema.optional(Schema.Array(PluginSpec)),
plugin_enabled: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
...
})But from the installed sourcemap for src/commands/handlers/default.ts, the TUI pluginHost only loads built-in plugins:
const config = yield* TuiConfig.load()
let disposeSlots: (() => void) | undefined
yield* run({
...
pluginHost: {
async start(pluginInput) {
disposeSlots = await loadBuiltinPlugins(pluginInput.api, pluginInput.runtime)
},
async dispose() {
disposeSlots?.()
},
},
})This makes external TUI plugins impossible to load from tui.json in this build, despite the TUI config schema accepting plugin and plugin_enabled.
A working server-side V2 plugin is not enough for this use case because the documented V2 plugin context exposes ctx.command.transform(...), which registers server-side slash command templates. It does not expose TUI APIs like api.keymap.registerLayer, api.ui.dialog, DialogSelect, DialogPrompt, routes, or slots.
Workarounds tried:
- Confirmed server-side V2 plugin loads through
/api/plugin. - Confirmed server-side
ctx.command.transformcommands appear through/api/command. - Confirmed the TUI entrypoint imports successfully under Bun.
- Confirmed
tui.jsonincludes the local TUI plugin path. - Restarted/reopened TUI and service.
Activity
github-actions commentedon Jul 12, 2026
This issue might be related to existing issues about TUI plugin loading. Please check:
tui.jsonplugins not loading)Your report is specifically about the V2 (
opencode2)pluginHostonly callingloadBuiltinPluginsand not wiring up external plugins fromtui.jsonat all, which appears to be a distinct architectural gap from the above. Flagging in case the maintainers want to track these together.opencode-agent commentedon Jul 30, 2026
Addressed by the V2 TUI plugin-runtime work now on
v2, notably commits fe91698 (initialize external plugin runtime), 068c32d (discover project plugins), and f599f8f (enable supported Bun runtime loading). Legacyplugin/plugin_enabledTUI config is migrated into the current orderedpluginsdirectives, external package and local entrypoints are resolved and activated, and the Plugin Manager is restored. Closing as resolved on the v2 branch.