@aexol/opencode-tui
v0.3.6
Published
OpenCode TUI plugin bundle with a SQLite-backed server recall target
Keywords
Readme
@aexol/opencode-tui
@aexol/opencode-tui is an OpenCode plugin package that ships explicit target-only plugin entrypoints: a TUI bundle for local sidebar and prompt utilities plus a small server target for compaction continuation control and local session recall.
The TUI bundle entrypoint is src/tui.ts, exposed as ./tui under plugin id @aexol/opencode-tui. The package also exposes src/server.ts as ./server, a separate target-only server module that keeps the compaction auto-continue hook and adds direct readonly SQLite session recall tools. There is no root . runtime export. TUI plugins are registered through the static registry in src/plugins/registry.ts, and the server target delegates through src/plugins/server/index.ts.
What this repo ships
Package-level plugin targets:
./tui→src/tui.ts→ the real TUI bundle./server→src/server.ts→ server hooks for compaction auto-continue gating plus readonly session recall tools
TUI registry plugins only
This list is the TUI bundle registry from src/plugins/registry.ts only.
- It is the ordered list of plugins mounted by
./tui. - It is not the shared config-key list.
- It does not include server-only gates or tools.
databasesessionsis intentionally absent here because it is a shared config key for the separate./servertarget, not a TUI registry plugin.
The TUI bundle currently wires these plugins, in registry order:
tpsautoapproveusagenotificationssessionsenchanterautopilotqueue
That 8-item order is the source of truth for TUI registry docs and contributor guidance.
Repo map
src/tui.ts— TUI plugin entry; loops over the static registrysrc/server.ts— target-only server plugin entry that delegates tosrc/plugins/server/index.tssrc/plugins/registry.ts— canonical plugin registration order and config-key wiringsrc/plugins/server/index.ts— canonical server export surface for compaction gating and session recall toolssrc/config.ts— config loader for plugin toggles and plugin settingssrc/constants.ts— plugin id and config filename constantsscripts/generate-screenshots.tsx— scripted screenshot regeneration entrypointscripts/screenshot-*.ts*— OpenTUI test-render fixtures and local-environment rasterization helpers for plugin PNGssrc/plugins/shared/session-prompt.tsx— shared wrapper that renders the live prompt and publishes session-scoped prompt refs for prompt-mutating featuressrc/plugins/**— TUI plugin implementations plus the server exportsrc/plugins/server/**— server-target implementation filessrc/plugins/*/README.md— per-plugin behavior and file-layout notesdocs/test-scenarios/**— manual local runtime smoke scenarios for plugin behavior that needs a real OpenCode host
Plugin docs
| Plugin | README | Screenshot assets |
| --- | --- | --- |
| TPS | src/plugins/tps/README.md | tps.png |
| Auto-approve | src/plugins/autoapprove/README.md | autoapprove.png |
| Usage | src/plugins/usage/README.md | sidebar.png, copilot-usage.png, codex-usage.png |
| Notifications | src/plugins/notifications/README.md | notifications.png |
| Sessions | src/plugins/sessions/README.md | sessions.png |
| Enchanter | src/plugins/enchanter/README.md | enchanter.png |
| Goal (autopilot key) | src/plugins/autopilot/README.md | autopilot.png |
| Queue | src/plugins/queue/README.md | queue.png, queue-dialog.png |
Installation
Package metadata exposes explicit target-specific modules only:
@aexol/opencode-tui/tuiresolves through the package./tuiexport@aexol/opencode-tui/serverresolves through the package./serverexport
Host config is split across OpenCode's two host layers:
~/.config/opencode/opencode.jsoncis the server/plugin host config~/.config/opencode/tui.jsoncis the TUI host config
If you want both host layers to load this package in a normal local setup, add @aexol/opencode-tui@latest to both hosts' plugin arrays. The package metadata (oc-plugin: ["server", "tui"]) still matters, but it does not collapse the two host config files into one.
There is still no root package runtime export. The server target stays separate from the TUI registry, while both targets share the plugin-owned config file documented below.
Server target behavior
The separate ./server target currently exposes:
experimental.compaction.autocontinuegating that respects the persisted automation continuation policy already used by local queue/Goal behaviorsession-listfor local SQLite-backed session discovery by metadata/time filters, with recent-session ordering and user/assistant message countssession-searchfor substring search across local OpenCode user/assistant text parts, with simple scoping filters for session/project/worktree/role/timesession-messagesfor browsing one local session's user/assistant messages with offset/limit/order/role controlssession-messagefor reading one local user/assistant message with its ordered text partssession-contextfor expanding a small user/assistant message window around one local message idsession-transcriptfor reading one local session's user/assistant text transcript in ascending or descending order
Those server features are separate from the TUI registry list above. They are not TUI plugins and should not be added to the 8-item registry-order section.
The recall tools read the local OpenCode SQLite database in readonly mode. Database path resolution prefers OPENCODE_DB_PATH, then opencode db path, then ~/.local/share/opencode/opencode.db. They stay deliberately local and lightweight: no remote services, embeddings, large fuzzy-ranking stacks, or SDK-internal coupling.
For local development overrides, point each host at its own target artifact instead of assuming one host entry covers both:
// ~/.config/opencode/opencode.jsonc
{
"plugin": ["/home/alek/projects/opencode-tui/dist/server.js"]
}// ~/.config/opencode/tui.jsonc
{
"plugin": ["/home/alek/projects/opencode-tui/dist/tui.js"]
}You can reference just one direct target during development, but dist/tui.js and dist/server.js remain separate entrypoints and belong in different host config files.
Configuration
The package-owned shared plugin config lives at:
~/.config/opencode/aexol-opencode-tui.jsonc
Config rules in this repo:
- the file uses a top-level
pluginsobject - shared plugin-owned config keys currently include the 8 TUI registry keys
tps,autoapprove,usage,notifications,sessions,enchanter,autopilot,queue, plusdatabasesessions databasesessionsis a shared config key, not a TUI registry plugin- each plugin should use an explicit object shape
- missing plugin flags default to enabled
- this file is shared by the TUI and server targets
plugins.databasesessions.enabledcontrols whether the server target registers the readonlysession-list,session-search,session-messages,session-message,session-context, andsession-transcripttools- config examples are the canonical source of truth; there is no shipped JSON schema
Canonical example:
{
"plugins": {
"tps": {
"enabled": true
},
"autoapprove": {
"enabled": true
},
"usage": {
"enabled": true,
"settings": {
"copilot": {
"enabled": true,
"collect": true
},
"codex": {
"enabled": true,
"collect": true
}
}
},
"notifications": {
"enabled": true,
"settings": {
"volume": 40,
"eventFiles": {
"question.asked": "/home/alek/.config/opencode/sounds/staplebops-01.aac",
"permission.asked": "/home/alek/.config/opencode/sounds/staplebops-02.aac",
"session.error": "/home/alek/.config/opencode/sounds/nope-03.aac",
"session.completed": "/home/alek/.config/opencode/sounds/completed.mp3"
}
}
},
"sessions": {
"enabled": true,
"settings": {
"recentSessions": 5
}
},
"enchanter": {
"enabled": true,
"settings": {
"model": "github-copilot/gpt-5.4"
}
},
"autopilot": {
"enabled": true,
"settings": {
"model": "github-copilot/gpt-5.4",
"maxTurns": 6,
"minConfidenceToContinue": 0.8,
"cooldownMs": 5000,
"askPolicy": "auto"
}
},
"queue": {
"enabled": true
},
"databasesessions": {
"enabled": true
}
}
}Plugin behavior reference
TPS
- token-rate display plugin registered first in the prompt-right strip
- when live samples go stale after a completed response, the fallback completed-message rate is shown and labeled
avg
Autoapprove
- state is root-session-family scoped, not exact-session scoped
- a root session and its child/subagent sessions share one auto-approve toggle and permission-drain scope
- it should only drain permission asks inside the active root session family
- runtime listens only to confirmed permission lifecycle events plus route/session family refreshes
- active-family
permission.askeddrains/replies without reloading unrelated families permission.repliedupdates handled-request state- newly observed permission asks refresh the active root-session family before draining when needed
- route changes reload the active root-session family
- speculative
permission.askhandling is intentionally not part of the contract - the
appslot remains the primary runtime owner, while prompt-right proactively self-bootstraps via an idempotent fallbackensureReady(...)path when needed - if family-scope loading fails, the runtime degrades to current-session-only fallback, shows a warning toast, and exposes that degraded scope in the prompt-right control via a
solobadge
Usage
- settings live under
plugins.usage.settings.{copilot,codex} - each provider uses
{ enabled, collect } - history files are written only when
collect: true - history files live at:
~/.local/share/opencode/copilot-usage-history.json~/.local/share/opencode/codex-usage-history.json
- retained history is roughly 90 days for compact detail-dialog context
- the plugin loads usage data on mount and manual refresh only
- Copilot and Codex load independently so one provider can remain visible if the other errors
- percentages, timestamps, and day labels use the runtime locale consistently
Notifications
- event notifications route through host
api.attention.notifywhen available - sound settings live in
plugins.notifications.settings.{volume,eventFiles} - sound files are discovered from
~/.config/opencode/sounds - the sidebar keeps volume controls plus a runtime-only inline playback enable/disable action that does not persist to config, and still surfaces invalid override warnings
- configured or discovered local sound files are preserved as a compatibility path: host notification is still sent, host sound is disabled for that event, and the plugin plays the local file through its serialized runtime queue
- if host attention does not play a sound, the plugin falls back to local playback; manual/test playback remains local
- completion sounds are limited to the active root session and suppressed after interrupt or error events
- docs and examples should continue to show all four event file keys
Sessions
- settings live under
plugins.sessions.settings.{recentSessions} - the sessions sidebar no longer refreshes on global
message.updated - live
session.statusupdates drive busy/idle cues without forcing a full reload - load failures render a distinct error state with retry instead of collapsing into the empty state
Enchanter
- separate plugin/config flag from queue
- renders as a compact icon-only wand action in prompt-right UI, with hover styling
- rewrites only the current session draft and does not submit it
- preserves the user's language and only enriches or clarifies the existing draft unless the user explicitly asks for translation or broader changes
- mixed prompts keep attached files and agent mentions unchanged while only the text portion is rewritten
- requires
plugins.enchanter.settings.model - that model must be set as
provider/model - there is no global
small_modelfallback
Goal (autopilot config key)
- separate plugin/config flag from queue and enchanter
- runtime is app-mounted so idle/status listeners remain active outside sidebar remounts
- current goal objective and state persist only for the exact currently routed session; child/subagent sessions do not inherit it
- inspired by Codex CLI
/goal, but implemented locally in this TUI plugin rather than integrated with Codex CLI internals - supported TUI actions are Set/Replace, Pause, Resume, Clear, and Audit log; command-like
/goal ...prompt interception is not available through the current plugin API, so the sidebar is the supported control surface - goal states are
active,paused,budgetLimited, andcomplete; objectives are bounded to 4000 characters and persist across turns/compaction - Set/Replace immediately submits the objective as the first live prompt turn when the session is idle, the prompt ref is available, the model config is valid, current-session asks are resolved according to policy, and the current-session queue length is zero; if a gate is unsafe, the active goal persists and starts later when the gate clears
- requires
plugins.autopilot.settings.modelfor the hidden goal audit session - that model must be set as
provider/model plugins.autopilot.settings.askPolicydefaults to"auto", which replies to current-session permissions withreply: "always"but leaves question asks for the user; set"manual"(or legacy alias"block") to leave all asks pending, or opt in to"aggressive"to also auto-reject current-session questions- main UI is sidebar-only as a compact status card with state, gate summary, elapsed time, ask policy, turn counts, latest audit summary, and audit-log access
- uses a hidden temporary child session as a sidecar audit and expects structured decisions with
status,confidence,reason,nextPrompt, andevidence - the audit dialog shows the goal objective, latest audit prompt, and latest parsed decision details in bounded readable sections when available
- local gates stop autonomous continuation on max-turn/budget cap, cooldown, and queue collisions
- queue coordination is strict and unconditional: Goal must not audit or continue unless the current session queue length is exactly zero
- while Goal is active for the routed session under the default
autoask policy, current-session permissions are auto-replied withreply: "always"and current-session questions remain pending for the user;"aggressive"additionally auto-rejects current-session questions, and unresolved, manually blocked, still-pending, or failed asks still block continuation before auditing or first-turn submission - missing or malformed
plugins.autopilot.settings.modelconfig is surfaced immediately in sidebar/runtime state so invalid config blocks continuation before the first judge run - on route load/remount, Goal hydrates persisted current-session queue length before acting so queued drafts still block continuation even before the queue UI remounts
- continuations replay through
promptRef.set(...)+promptRef.submit()so submission uses the live selected model, agent, and variant
Queue
- queue state is exact-session scoped: queued drafts, badge count, and pause/resume state are keyed by the exact current session id
- the queue badge reflects only the current session's queued draft count
- queue rows expose mixed prompt parts with compact text/file/agent counts so queued items with attachments stay distinguishable
- inline editing is intentionally limited to text-only queued drafts; mixed items should be re-queued from the live prompt if their non-text parts need changes
- queued drafts replay through
promptRef.set(...)+promptRef.submit()so submission uses the live selected model, agent, and variant - prompt reconstruction preserves real pasted text content instead of OpenCode's visual
[Pasted ~X lines]placeholder - queue status uses
session.statusas the primary gating and cycle-tracking surface - there is still a compatibility
session.idlefallback - queue auto-drain does not poll prompt state; append validates on click, and auto-drain is interaction/submission/status-driven
- the implementation keeps a small persisted queued-send-cycle remount snapshot for continuity across remounts
- Goal/autopilot coordination stays indirect through a shared internal bridge; queue does not depend on Goal internals
Prompt-right integration contract
Prompt-mutating features in this repo must use src/plugins/shared/session-prompt.tsx. Plain session_prompt_right slots do not get the live prompt ref, so the shared wrapper publishes session-scoped prompt refs for consumers like queue, enchanter, and Goal/autopilot.
The shared wrapper renders the prompt-right strip in this order:
- token rate
- queue
- enchanter
- other prompt-right extensions wired through the shared wrapper
Local development
Install dependencies:
bun installTypical verification flow:
bun run screenshots
bun test src/plugins/tps/metrics.test.ts
bun run typecheck
bun run lint
bun run build
bun testManual runtime smoke checklists live under docs/test-scenarios/ for behavior that needs a rebuilt local plugin target plus an OpenCode host restart.
Screenshot regeneration
Plugin PNGs under src/plugins/** are generated assets, not hand-captured screenshots.
- Command:
bun run screenshots - Fixture source:
scripts/screenshot-fixtures.tsx - Entry script:
scripts/generate-screenshots.tsx - Rendering source: OpenTUI test rendering via
@opentui/solid/@opentui/coretest helpers - Rasterization: headless Chrome/Chromium against a generated SVG snapshot with framed presentation around the captured TUI output
The command refreshes the checked-in plugin assets in place and requires a local Chrome/Chromium binary (google-chrome, chromium, or CHROME_BIN). Final PNGs depend on the local browser and available fonts, so treat them as generated repo assets rather than byte-for-byte portable output across every machine.
Published output includes separate target-only entry builds:
- TUI runtime/types:
dist/tui.js,dist/tui.d.ts - server runtime/types:
dist/server.js,dist/server.d.ts
Published package metadata exposes those builds only through ./tui and ./server; it does not alias . to the TUI build.
bun run build emits both runtime .js files and declaration .d.ts files into dist/.
Contributor orientation
- Start at
src/tui.tsfor the active registration flow. - Read
src/plugins/registry.tsbefore changing docs that mention plugin order or plugin names. - Read
src/config.tsbefore changing config docs or examples. - Read the target
src/plugins/*/README.mdbefore editing a plugin implementation. - Reuse
src/plugins/shared/**only when duplication is real across multiple plugins.
Useful folder split notes:
src/plugins/queue/includesindex.tsx,prompt.ts,state.tsx,ui.tsx,types.ts,automation-policy.ts,prompt.test.ts, andstate.test.tssrc/plugins/notifications/includesindex.tsx,sidebar.tsx,pending.ts,types.ts,runtime.ts,attention.ts,settings-config.ts, andsound-playback.tssrc/plugins/sessions/includesindex.tsx,sidebar.tsx,helpers.ts, andtypes.ts
