opencode TUI plugin that shows session state in the terminal title, which Ghostty and most terminals display in the tab. Two animated modes:
- Working: a braille spinner (
⠋⠙⠹⠸…, 100ms — the same frames Codex CLI uses) while any session is busy or retrying. - Waiting for you: a slow-blinking
?(500ms) while the agent is blocked on user input — a pending permission request or question. Visually distinct from the spinner so a background tab that needs attention is obvious at a glance.
Titles are written through opencode's own renderer title API and restored on idle.
Desktop notifications and sounds for these same events (permission, question, done, error) are built into opencode itself — enable them in
tui.jsonwith"attention": { "enabled": true }. opencode emitsOSC 777;notify, which Ghostty turns into a native macOS notification when the terminal is unfocused. This plugin only handles the in-tab indicator.
| opencode state | Tab title |
|---|---|
Any session busy or retry |
⠹ OC | <session title> (spinning, 100ms) |
| Any pending permission or question | ? OC | <session title> (blinking, 500ms) — wins over working |
| All sessions idle | Restored (OC | <session title>) |
| TUI exit | Restored |
Sessions are tracked individually, so one session going idle does not clear the indicator while a subagent or another session is still working or waiting. Pending requests are tracked per session and cleared defensively when a session goes idle or is deleted (an aborted run can end without a reply event). Title writes can never throw into the TUI's event dispatch.
The title mirrors the TUI's own format (OC | <title>, truncated at 40 chars, OpenCode on home/placeholder titles) and respects the TUI's title controls: the "Disable terminal title" toggle (terminal_title_enabled kv) and OPENCODE_DISABLE_TERMINAL_TITLE.
Events used: session.status, session.deleted, permission.asked/permission.replied, question.asked/question.replied/question.rejected.
This is a TUI plugin, so it is registered in tui.json, not in opencode.json.
Add it to one of:
~/.config/opencode/tui.json(global)tui.json/tui.jsoncin your project, or.opencode/tui.json
{
"plugin": ["/absolute/path/to/op-loading/src/index.ts"]
}Relative paths also work and resolve relative to the tui.json that declares them.
{
"plugin": ["opencode-op-loading"]
}Restart opencode after editing the config — it is read once at startup.
opencode's TUI has a built-in plugin installer that handles registration for npm packages automatically: open the plugins dialog (command palette → plugins → install), enter the package name, and pick global or project scope. This works because the package declares the exports["./tui"] entry the installer looks for.
Set OPENCODE_OP_LOADING_DISABLE=1 to turn the plugin off without unregistering it.
Set OPENCODE_OP_LOADING_DEBUG=1 to append diagnostics to /tmp/op-loading.log.
- The indicator is cleared on idle, plugin dispose, and normal TUI exit. A hard crash (SIGKILL) can leave it set until the terminal's next title write resets it.
- If the plugin is loaded while a session is already busy or has a pending request (e.g.
opencode attachto a working server), the indicator appears on the next event rather than immediately.
v0.1 (opencode-osc94-progress) also emitted the ConEmu OSC 9;4 progress sequence (Ghostty surface bar / Windows Terminal taskbar). Removed in v0.2: the bar is invisible when the tab is in the background (where the indicator matters), and on macOS Ghostty expires it after 15s without a refresh. The title glyph plus opencode's native attention notifications cover the same need with less escape-sequence traffic.
bun install
bun test # behavior + entry-contract tests
bun run typecheckNo build step: opencode runs on Bun and loads the TypeScript entry directly (src/index.ts is both the dev and published entry; exports["./tui"] points at it because the TUI plugin loader resolves npm packages only via that subpath).
Three ways to load the plugin from source — restart opencode to pick up changes, there is no hot reload:
- In this repo: the root
tui.jsonregisters./src/index.ts, so any opencode session started here loads it. - Machine-wide: add
"file:///absolute/path/to/op-loading/src/index.ts"to thepluginarray in~/.config/opencode/tui.json. Path specs are normalized to absolutefile://URLs before merging, so the global entry and this repo'stui.jsondedupe instead of double-loading. - Isolated sandbox:
bun run devcreates a scratch project under.sandbox/with its owntui.jsonand launches opencode there (global models/auth still apply, only plugin registration is project-scoped).--resetwipes the sandbox,--printjust shows the paths without launching.
Manual verification in Ghostty: submit a prompt and confirm the spinner appears while busy; trigger a permission prompt (e.g. a bash command needing approval) and confirm the tab switches to the blinking ?; answer it and confirm the spinner returns; confirm the title restores on idle, Ctrl+C, and exit.