- Duplicate npm plugins are deduped by package name; higher-precedence config wins.
- Duplicate file plugins are deduped by exact resolved file spec. This happens while merging config, before plugin modules are loaded.
-`plugin_enabled` is keyed by plugin id, not by plugin spec.
- For file plugins, that id must come from the plugin module's exported `id`. For npm plugins, it is the exported `id` or the package name if `id` is omitted.
- Plugins are enabled by default. `plugin_enabled` is only for explicit overrides, usually to disable a plugin with `false`.
-`plugin_enabled` is merged across config layers.
- Runtime enable/disable state is also stored in KV under `plugin_enabled`; that KV state overrides config on startup.
## Author package shape
Package entrypoint:
- Import types from `@opencode-ai/plugin/tui`.
-`@opencode-ai/plugin` exports `./tui` and declares optional peer deps on `@opentui/core` and `@opentui/solid`.
- If a package supports both server and TUI, use separate files and package `exports` (`./server` and `./tui`) so each target resolves to a target-only module.
`api.command.register` returns an unregister function. Command rows support:
-`title`, `value`
-`description`, `category`
-`keybind`
-`suggested`, `hidden`, `enabled`
-`slash: { name, aliases? }`
-`onSelect`
Command behavior:
- Registrations are reactive.
- Later registrations win for duplicate `value` and for keybind handling.
- Hidden commands are removed from the command dialog and slash list, but still respond to keybinds and `command.trigger(value)` if `enabled !== false`.
-`api.client` always reflects the current runtime client.
-`api.scopedClient(workspaceID?)` creates or reuses a client bound to a workspace.
-`api.workspace.set(...)` rebinds the active workspace; `api.client` follows that rebind.
-`api.event.on(type, handler)` subscribes to the TUI event stream and returns an unsubscribe function.
-`api.renderer` exposes the raw `CliRenderer`.
### Theme
-`api.theme.current` exposes the resolved current theme tokens.
-`api.theme.selected` is the selected theme name.
-`api.theme.has(name)` checks for an installed theme.
-`api.theme.set(name)` switches theme and returns `boolean`.
-`api.theme.mode()` returns `"dark" | "light"`.
-`api.theme.install(jsonPath)` installs a theme JSON file.
-`api.theme.ready` reports theme readiness.
Theme install behavior:
- Relative theme paths are resolved from the plugin root.
- Theme name is the JSON basename.
- Install is skipped if that theme name already exists.
- Local plugins persist installed themes under the local `.opencode/themes` area near the plugin config source.
- Global plugins persist installed themes under the global `themes` dir.
- Invalid or unreadable theme files are ignored.
### Slots
Current host slot names:
-`app`
-`home_logo`
-`home_bottom`
-`sidebar_title` with props `{ session_id, title, share_url? }`
-`sidebar_content` with props `{ session_id }`
-`sidebar_footer` with props `{ session_id }`
Slot notes:
- Slot context currently exposes only `theme`.
-`api.slots.register(plugin)` returns the host-assigned slot plugin id.
-`api.slots.register(plugin)` does not return an unregister function.
- Returned ids are `pluginId`, `pluginId:1`, `pluginId:2`, and so on.
- Plugin-provided `id` is not allowed.
- The current host renders `home_logo` with `replace`, `sidebar_title` and `sidebar_footer` with `single_winner`, and `app`, `home_bottom`, and `sidebar_content` with the slot library default mode.
- Plugins cannot define new slot names in this branch.
### Plugin control and lifecycle
-`api.plugins.list()` returns `{ id, source, spec, target, enabled, active }[]`.
-`enabled` is the persisted desired state. `active` means the plugin is currently initialized.
-`api.plugins.activate(id)` sets `enabled=true`, persists it into KV, and initializes the plugin.
-`api.plugins.deactivate(id)` sets `enabled=false`, persists it into KV, and disposes the plugin scope.
-`api.plugins.add(spec)` trims the input and returns `false` for an empty string.
-`api.plugins.add(spec)` treats the input as the runtime plugin spec and loads it without re-reading `tui.json`.
-`api.plugins.add(spec)` no-ops when that resolved spec (or resolved plugin id) is already loaded.
-`api.plugins.add(spec)` assumes enabled and always attempts initialization (it does not consult config/KV enable state).
-`api.plugins.install(spec, { global? })` runs install -> manifest read -> config patch using the same helper flow as CLI install.
-`api.plugins.install(...)` returns either `{ ok: false, message, missing? }` or `{ ok: true, dir, tui }`.
-`api.plugins.install(...)` does not load plugins into the current session. Call `api.plugins.add(spec)` to load after install.
- For packages that declare a tuple `tui` target in `oc-plugin`, `api.plugins.install(...)` stages those tuple options so a following `api.plugins.add(spec)` uses them.
- If activation fails, the plugin can remain `enabled=true` and `active=false`.
-`api.lifecycle.signal` is aborted before cleanup runs.
-`api.lifecycle.onDispose(fn)` registers cleanup and returns an unregister function.
## Plugin metadata
`meta` passed to `tui(api, options, meta)` contains:
-`state`: `first | updated | same`
-`id`, `source`, `spec`, `target`
- npm-only fields when available: `requested`, `version`