Skip to content

Course · OpenCode · Living in the TUI

Make the TUI yours

Live in the TUI for a while - on feedmill, the course’s illustrative feed aggregator, say - and the rough edges stop being abstract. There’s one action you reach for constantly: for you it’s the session switcher, because juggling several parser threads means you’re forever jumping between the one where you’re fixing the JSON feed parser and the one where you’re auditing dedupe - and it’s still ctrl+x then l, a leader chord, every single time. Meanwhile half the default keymap is bound to things you’ve never once wanted, and a couple of those chords are close enough to the ones you do use that you keep firing them by accident. And the theme that shipped is fine at your desk and unreadable on the couch at night.

None of that is a flaw in OpenCode. It’s just that the defaults are defaults - a reasonable starting point, not a fit to your hands. The leader key lesson shows the two facts that make fixing this cheap: the keymap lives in tui.json, and keybinds is merged over the built-ins rather than replacing them. This page is about actually using that - turning the keymap from someone else’s into yours, and settling on a theme you can live in.

First, learn the name of the thing you want to bind

Section titled “First, learn the name of the thing you want to bind”

Every action in the TUI has an internal name - session_list, theme_list, command_list, session_new, and so on. Rebinding is just pointing one of those names at a different key. So before you edit anything, the move is to find the name of the action you keep reaching for, and you already have the tool for that: the command palette (ctrl+p) tells you the human label and the current chord, which pins down which action you mean - and the exact internal name to write into config is in the keybinds documentation, which lists every action with its default (as of October 2026).

You can also ask the agent, as in the illustrative exchange below. Treat its answer as a lead, not the source: a model can produce a plausible name that doesn’t exist, and a misspelled name won’t bind the action you meant. Check the name against the keybinds documentation, or let the $schema line in tui.json give your editor autocomplete for the valid names:

> what are the keybind action names in tui.json, and which one opens the session list?
The TUI keymap is keyed by action name in tui.json under "keybinds".
The session switcher is "session_list" (default <leader>l). A few others
you'll likely touch: "session_new" (<leader>n), "command_list" (ctrl+p),
"theme_list" (<leader>t), "model_list" (<leader>m).

Now you’re not guessing at key combos in the dark. You know that the thing you fire forty times a day is called session_list, and rebinding it is one line.

Here’s the change that pays for the whole lesson. A leader chord is the right default for most actions - it keeps OpenCode’s keymap out of your terminal’s way - but the one or two actions you trigger constantly don’t deserve to sit behind a prefix. If you’re jumping sessions all day, make it a single keystroke.

You only write the bindings you’re changing - everything you leave out keeps its default, because the merge is additive (“keybinds is merged with built-in defaults, so you only need to configure the shortcuts you want to change” - TUI documentation, as of October 2026). And tui.json lives where opencode.json lives: ~/.config/opencode/tui.json to remap the keymap everywhere on your machine, or tui.json alongside opencode.json at a project’s root to scope it to that repo (config documentation, as of October 2026). When both exist, OpenCode merges them, and the project file wins where both set the same key (as of October 2026; the loader in OpenCode’s TUI config source applies the global file first and the project file after it). A project file is usually committed, so it sets keys for everyone on the repo; keep a personal keymap in the global file:

{
"$schema": "https://opencode.ai/tui.json",
"keybinds": {
"session_list": "ctrl+s"
}
}

That’s it. session_list is now a bare ctrl+s - one stroke, no leader - and every other binding in the TUI is unchanged. Save the file and the change takes effect; if the running TUI doesn’t reflect it, a restart will pick it up. (One caveat on this particular key: ctrl+s is the classic terminal flow-control stop - XOFF - so on some setups it may get swallowed before OpenCode sees it; if it feels dead, pick a different free chord. The mechanism is the point, not the exact key.) The discipline here is restraint: don’t sit down and remap the whole keymap to your imagined ideal in one sitting. You’ll get it wrong, because you don’t yet know which actions you actually reach for. Promote a chord to a single key only after you’ve felt yourself wanting it - let real friction, not a planning session, drive what you bind.

You can also bind through the leader explicitly with the <leader> token, which is how you move an action onto a different leader chord rather than off the leader entirely:

{
"keybinds": {
"session_new": "<leader>k"
}
}

The <leader> token resolves to whatever your leader key is, so if you later move the leader off ctrl+x, this binding follows it instead of breaking.

Unbind what you never use - and what bites you

Section titled “Unbind what you never use - and what bites you”

The other half of making the keymap yours is subtraction. Some default chord sits one key away from one you use constantly, and every so often your fingers miss and fire it. The clean fix isn’t to be more careful - it’s to take the binding away. Set it to "none" (or false) and it stops existing:

{
"keybinds": {
"session_new": "<leader>k",
"messages_half_page_up": "none"
}
}

messages_half_page_up scrolls the transcript up half a page and defaults to ctrl+alt+u, one key over from ctrl+alt+y, which scrolls up a single line (as of October 2026; keybinds documentation). Now the chord you kept tripping over is inert. This reads as a small thing and isn’t - on a long feedmill session, an accidental action at the wrong moment is exactly the kind of papercut that erodes your trust in driving by keyboard. Unbinding the two or three chords you fat-finger is how the TUI stops surprising you.

If ctrl+x itself is the thing that clashes - it’s a common terminal binding, and you may have trained your fingers on it elsewhere - move the leader once and every <leader> chord moves with it:

{
"keybinds": {
"leader": "ctrl+o"
}
}

The leader lives inside the keybinds object, alongside the action names - not at the top level. (leader_timeout, confusingly, is a top-level key; leader is not. Easy to conflate.) That single change re-homes the entire leader keymap. You don’t touch the individual bindings; they’re all expressed relative to leader, so they ride along.

Keys are the half of “make it yours” that’s about speed. The theme is the half that’s about not hating your screen by the end of a long session. The picker - /themes, or its leader chord <leader>t - is the right tool for browsing: it opens a list you can arrow through with a live preview, so you can audition tokyonight against your room’s lighting before you commit. An illustrative rendering:

> /themes
┌─ theme ─────────────────────────────────┐
│ opencode (current) │
│ tokyonight │
│ catppuccin │
│ gruvbox │
└─────────────────────────────────────────┘

Picking from that list saves your choice in OpenCode’s local state on this machine, so it survives a restart (as of October 2026; the theme code in OpenCode’s TUI source). Set it in config when you want the choice written down: a theme in tui.json wins over whatever you last picked when the TUI starts, and a project tui.json carries it with the repo. This is where the file boundary matters. The theme lives in tui.json under the theme key, the same file as your keybinds:

{
"$schema": "https://opencode.ai/tui.json",
"theme": "tokyonight",
"keybinds": {
"session_list": "ctrl+s"
}
}

The default theme is opencode, and the built-in set is broad enough that you’ll usually just be choosing among it. If none of the built-ins fit, OpenCode also reads custom themes from JSON files in its theme directories - ~/.config/opencode/themes/ for a user-wide theme, .opencode/themes/ for one scoped to the project - with later sources overriding earlier ones (as of October 2026; themes documentation). That’s a deeper cut than most people need.

One thing worth keeping straight, because it’s a recurring source of confusion: tui.json is not opencode.json. Your model and provider config lives in opencode.json. The TUI’s keys, leader, timing, and theme live in tui.json (as of October 2026; TUI documentation). Two files, two jobs. Older setups put theme and keybinds in opencode.json; those keys are deprecated there, and OpenCode migrates them to the new home only when it can (config documentation, as of October 2026). So a key-or-theme change written into opencode.json may not take; put it in tui.json.

A configurable keyboard-first TUI lets the interface disappear, so your attention stays on the feed parsers, not the keymap.

The next chapter moves from the interface to what the agent knows: rules you write once in AGENTS.md, which OpenCode loads at the start of every session, so it stops reintroducing the same feedmill bugs you keep fixing. Next: Rules (AGENTS.md).