Skip to content

tui: external TUI plugins from tui.json are parsed but not loaded #36505

Closed

Description

@dmmop

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

  1. Create or use a TUI plugin module exporting a default object with { id, tui }.
  2. Add it to ~/.config/opencode/tui.json:
    {
      "plugin": [
        "/path/to/opencode-key-rotator/dist/tui.js"
      ],
      "plugin_enabled": {
        "opencode-key-rotator": true
      }
    }
  3. Verify the TUI plugin module can be imported by Bun:
    bun -e "import('./dist/tui.js').then(m=>console.log({id:m.default?.id,tui:typeof m.default?.tui}))"
    Output:
    {"id":"opencode-key-rotator","tui":"function"}
  4. Start a fresh TUI:
    opencode2
  5. 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.transform commands appear through /api/command.
  • Confirmed the TUI entrypoint imports successfully under Bun.
  • Confirmed tui.json includes the local TUI plugin path.
  • Restarted/reopened TUI and service.

Activity

  1. github-actions commented on Jul 12, 2026

    @github-actions
    Contributor

    This issue might be related to existing issues about TUI plugin loading. Please check:

    Your report is specifically about the V2 (opencode2) pluginHost only calling loadBuiltinPlugins and not wiring up external plugins from tui.json at all, which appears to be a distinct architectural gap from the above. Flagging in case the maintainers want to track these together.

  2. opencode-agent commented on Jul 30, 2026

    @opencode-agent
    Contributor

    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). Legacy plugin / plugin_enabled TUI config is migrated into the current ordered plugins directives, external package and local entrypoints are resolved and activated, and the Plugin Manager is restored. Closing as resolved on the v2 branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions