TUI Navigation
Coco's interactive TUI is keyboard-driven and chord-based. Whether you launch it as coco ui or coco log -i, every git surface — history, working tree, diffs, commit compose, branches, tags, stash — is reachable from any other surface through a unified navigation model.
This page is the source of truth for that model. The companion pages Coco UI and Interactive Log TUI cover surface-specific actions and command-line flags; everything navigation-related lives here.
Mental model
- Views are top-level destinations. There are sixteen:
history,status,diff,compose,branches,tags,stash,worktrees,pull-request,pull-request-triage,issues,conflicts,reflog,bisect,submodules, andchangelog. - Chords jump you between views. They start with
g, followed by a single key (g h,g s,g d,g c,g b,g t,g z,g w,g p,g P,g i,g x,g r,g B,g M). - The navigation stack remembers where you came from. Pressing
<orEscpops back. Goinghistory → diff → compose → backreturns to diff, then back again returns to history. - The repo stack (#931) nests the navigation stack one level deeper. Drilling into a submodule pushes a frame; every view inside that frame is scoped to the submodule's working directory, as if you had launched
coco uifrom there. Esc /<pops the frame when you're at the root of its view stack — and the header shows acoco › vendor/lib ← escbreadcrumb so the level you're on is always visible. - Selection state survives navigation. The selected commit, selected branch, selected stash, and current compose draft are all preserved when you jump away and come back.
- The chrome adapts. A
›-separated breadcrumb in the header reflects the navigation stack; the footer's right-side global slot stays the same across views; help and palette read the active view to show the right scope.
Views and chords
| View | Chord | What it shows |
|---|---|---|
| History | g h | Commit graph/list with refs and metadata. gh also clears the navigation stack. |
| Status | g s | Working-tree files (staged/unstaged/untracked) with stage/unstage/revert. |
| Diff | g d | Hunks of the currently-selected commit (from history) or worktree file (from status). |
| Compose | g c | Full-screen commit draft editor with summary/body cursors, AI draft, hook feedback. |
| Branches | g b | Local branches with divergence info; checkout/delete/create-PR via workflow keys. |
| Tags | g t | Tag list. |
| Stash | g z | Stash list with rich rows (branch · files · age); a/A apply, p pop, R rename, b branch-from-stash, X drop, u undo-drop. (g s is reserved for status, hence g z. gZ creates a stash from any view.) |
| Worktrees | g w | Linked worktrees with current/dirty markers; remove via W. |
| Pull request | g p | Dedicated PR action panel for the current branch (header, checks, reviews, body) with m merge / x close / a approve / R request changes / c comment / O open in browser. |
| PR triage | g P | Multi-PR triage list with filter cycling and per-row actions. Capital P disambiguates from g p (single, current-branch panel). See Issue & PR Triage. |
| Issues | g i | Issue triage list with filter cycling and per-row actions: comment, label, assign, close, reopen. See Issue & PR Triage. |
| Conflicts | g x | Conflict resolution helper view, available during merge / rebase / cherry-pick / revert. Per-row keys s stage / u theirs / U ours / o edit / C continue. |
| Reflog | g r | Chronological recovery log — every HEAD movement (commit / checkout / merge / reset / etc.) with relative time, action, hash, and message. Enter drills into the diff for the entry's hash. |
| Bisect | g B | Bisect workflow surface (#784). Capital B disambiguates from g b (branches). Shows the current candidate, the parsed decision log, and the four action keys: g good / b bad / s skip / x reset. |
| Submodules | g M | Registered submodules (#932) with name, pinned sha, tracking branch, and clean/modified/uninitialized/conflicted state. Enter drills into the cursored submodule. Capital M disambiguates from g m (compare-base mark). |
| Changelog | L | Full-screen AI-generated changelog for the current branch (#914). Reached via L from history or branches rather than a g-prefixed chord. Per-branch cache; r regenerates, y yanks, E opens in $EDITOR, c kicks off create-PR seeded with the content. |
g g is a separate chord that jumps to the first commit in the active history list — it pre-dates the view chords. g H (uppercase) on a diff view applies the cursored hunk to the index (companion to bare H which applies to the worktree). g T (uppercase) on the history view opens a new-tag prompt rooted at the cursored commit.
One-keystroke workflows (not view-jumps)
A few uppercase keys kick off workflows that span multiple views. These aren't chords; press them once and the workflow takes over.
| Key | Where | Action |
|---|---|---|
C | history / branches | Create a pull request for the current branch. Seeds the title + body from a generated changelog and opens a multi-line prompt for review. |
L | history / branches | Generate a changelog for the current branch in a full-screen surface. Per-branch cache; r regenerates, y yanks to clipboard, E opens in $EDITOR, c kicks off create-PR seeded with this content. |
S | compose | Split the staged set into multiple commits. Opens an overlay with the LLM-generated plan. y apply / r regenerate / < cancel. |
E | compose / status / diff | Open the current commit draft in $EDITOR. Round-trips through a temp file; on save the content is re-split into summary + body. Companion to lowercase e (inline edit). |
I | compose / status / diff | Generate an AI commit draft from the staged set. Press Esc while the draft is loading to cancel; press R afterwards to accept a pending draft when one is staged over user-typed content. See Coco UI → Compose surface for the streaming preview and pending-draft details. |
B | history (on a commit) | Create a branch rooted at the cursored commit (git switch -c). |
+ | branches view / sidebar branches tab | Create a branch from HEAD (git switch -c). |
Going back
| Key | Action |
|---|---|
< | Pop the navigation stack — and once that's drained inside a nested submodule frame (#931), pop the repo frame itself |
Esc | Same as < when in a normal view; also closes filter/help/palette/confirmation modes |
The stack always has at least one frame (the root view), so pressing < from the root is a no-op rather than an exit. Use q or Ctrl+C to quit.
Contextual transitions
Some keys perform navigation based on what's selected:
| Trigger | Effect |
|---|---|
Enter on a commit (history view) | Push diff scoped to that commit. The diff view inherits the selection so < returns to the same row. |
Enter on a file (status view) | Push diff scoped to that file. < returns to status. |
Enter on a reflog row | Push diff scoped to that entry's hash (#781). |
Enter on a submodule row (submodules view, g M) | Drill into the submodule — every view re-scopes to the submodule's working directory (#931). |
Enter on a submodule file (commit diff view) | Drill into the submodule with the diff's (oldPin, newPin) range captured as the entry hint (#931). |
e from status or diff | Push compose and start editing. From inside compose, e toggles edit mode without re-pushing. |
c from status or diff | Push compose and run createManualCommit — the result (success or hook output) lands in compose so you can see it. |
Cross-view workflows
Some flows span multiple views — you mark a state on one view and then act on it from a different one. The footer hints adapt to the flow state so the override is always discoverable.
Compare two refs (#779)
Diff any two refs (branches, tags, or commits) without leaving the workstation:
| Step | Trigger | Effect |
|---|---|---|
| 1 | m on a row in branches / tags / history | Mark the cursored ref as the compare base. Status banner sticks: "Compare base: <label> — press enter on another ref to diff." |
| 2 | m on the same ref again | Toggle the base off (no Enter needed). |
| 3 | Navigate to a second ref on any of branches / tags / history | (no key required — just move) |
| 4 | Enter on that second ref | Push the diff view in compare mode, showing git diff <base>..<head>. |
| 5 | < / Esc | Pop the diff. The compare base clears automatically when the diff is popped. |
The compare diff is read-only — no per-file cherry-pick or hunk-apply across arbitrary refs (those don't have a sensible mutate-from-here flow). Just scroll with j/k, toggle split mode with d, and back out with <.
While a base is set, the footer adapts on every compare-flow target view:
Branches: ↑/↓ branches · enter compare · m clear · esc back
Tags: ↑/↓ tags · enter compare · m clear · esc back
History: ↑/↓ move · enter compare · m clear · esc backOutside the compare flow, m is unbound on those views except the dedicated PR view (where it triggers merge).
Bisect (#784)
Bisect support pairs the g B view with a top-bar BISECTING badge so you can't lose the workflow. The TUI picks up an active bisect on its next refresh — start it from your shell with git bisect start <bad-ref> <good-ref> and re-open coco ui.
| Trigger | Effect |
|---|---|
g B | Push the bisect view. Shows current candidate (HEAD), parsed decision log, and the action keys. The view is reachable even when bisect is inactive — the empty-state hint tells you how to start one. |
g (on the bisect view) | Mark the current candidate as good (bug not yet present). Advances to the next candidate. |
b (on the bisect view) | Mark the current candidate as bad (bug present). Advances to the next candidate. |
s (on the bisect view) | Skip the current candidate (e.g. it doesn't build). Advances to the next candidate. |
x (on the bisect view) | Reset the bisect — discards in-progress state. Routed through the y-confirm path. |
Inside the bisect view, g and b bypass the chord prefix — pressing g marks good rather than entering chord mode. The path back out is < / Esc (never a chord). Outside the view, g/b/s/x keep their existing semantics.
The status line surfaces git's own "Bisecting: N revisions left to test after this (roughly K steps)" line after each decision so you always know how far you have to go.
Submodule drill-in (#931)
Drilling into a submodule pushes a repo frame that re-scopes every view — history, branches, status, diff, even the inspector — to the submodule's working directory. It's the mental equivalent of running coco ui from inside the submodule, but without leaving the parent session.
| Trigger | Effect |
|---|---|
Enter on a submodule row (submodules view, g M) | Push a frame against that submodule. Lands on its history view. |
Enter on a submodule file in a commit diff | Push a frame with the diff's (oldPin, newPin) range captured. Useful for seeing what changed between the two pinned commits. |
Esc / < at the root of a frame's view stack | Pop the frame. Returns the user to the parent's exact view position (selected commit, filter, etc.). |
Esc / < deeper in a frame's view stack | Pop a view first (drains the frame's own view stack), then pop the frame on the next press. |
The header shows a breadcrumb whenever you're inside a frame:
coco › vendor/lib ← esc
Frames stack. Drilling further (a submodule of the submodule) produces coco › vendor/lib › vendor/lib/inner ← esc, and each Esc walks back one level.
Cached state survives the round trip — pop back to the parent and you land on the same row, with the same filter, with cached context (no refetch flicker).
Mutations (stage, commit, checkout, …) inside a nested frame run against the submodule's working tree, since the active frame's SimpleGit instance is bound to its workdir. Two known polish items are tracked as follow-ups to #931: per-frame sidebar tab / sort mode persistence (today these carry over root → submodule), and frame-tagging on in-flight refreshes (rare race where a parent refresh that's mid-await when a push fires writes to the submodule frame).
Command palette (:)
The palette is an interactive launcher, not a static reference. Press : to open it; type to filter, press Enter to run.
| Key | Action |
|---|---|
| Printable keys | Append to the fuzzy filter |
Backspace / Delete | Remove the last filter character |
Ctrl+U | Clear the filter |
↑ / ↓, Ctrl+P / Ctrl+N | Move the selection cursor (clamped to filtered count) |
Enter | Run the selected command (records as recent, then closes) |
Esc | Close without running |
The palette enumerates every keybinding plus every workflow action (commit, delete-branch, ai-commit-summary, etc.). Recently-used items float to the top when the filter is empty; once a query is set, relevance ranking takes over and recent ordering is ignored.
Behind the scenes, palette execution maps each command id to the same events the keystroke would dispatch — palette and keymap stay in sync.
Search (/)
/ opens filter mode in the active view:
- History: ranked fuzzy matching across hash, date, author, message, and refs.
- Branches: substring match on branch names and upstreams.
- Tags: substring match on tag names and subjects.
- Stash: substring match on stash refs and messages.
While filtering, the active view's header shows N/M totals plus the filter text (5/12 local | filter: feat). Ctrl+U clears, Esc or Enter exits filter mode.
In other views (diff, compose, status), / opens filter mode but the surface doesn't currently apply it — typing closes the filter without effect. That's a polish opportunity for a follow-up; track it via the wiki/issue tracker.
Help overlay (?)
Help is grouped into two sections so the right scope is always obvious:
- Global — bindings that work from any view or focus (
?,:,q,r, focus nav, workflow actions, the navigation chords). - This view (
<active>) — bindings filtered to what makes sense in the current view + focus, with the active view named in the section title so you always know which scope applies.
Esc or ? closes the overlay.
Footer
The footer has two slots that don't overlap:
- Contextual (left, dimmed) — what changes by mode/view/focus, like
↑/↓ files · enter diff · space stage · z revert · e/c composein status. - Global (right edge, dimmed) — persistent affordances anchored to the right:
g jump · < back · ? help · : cmds · q quit.
In special modes (filter / help open / palette open), the global slot trims down (q quit always present) since : and ? mean close in those contexts.
A few keys are context-routed across surfaces:
[/]— diff view: jump previous/next hunk or file. Sidebar focused: cycle sidebar tabs. Inspector focused (on short terminals where the inspector is tabbed): cycle inspector tabs. Each context owns its meaning of the key; the footer hints surface which one applies.←/→— sidebar focused: switch between Status / Branches / Tags / Stashes / Worktrees tabs. Vertical axis (↑/↓) navigates items within the active tab.v— narrow / single-pane terminals only (under ~100 cols): momentary sidebar peek from the main or inspector pane.vagain orEscsnaps you back to where you were. A no-op in the full three-pane layout where everything's already visible.
Header breadcrumb
The header shows where you are in the navigation stack as a ›-separated trail:
| Stack | Breadcrumb |
|---|---|
[history] | (empty — no breadcrumb shown at root) |
[history, diff] | history › diff |
[status, diff] | status › diff |
[history, status, diff] | history › status › diff |
The breadcrumb is purely informational — there's no clickable navigation through it (the TUI is keyboard-only). Use < / Esc to walk it back.
Themes and accessibility
- 49 built-in color themes / 50 selectable presets (17 light) —
default,monochrome,catppuccin,gruvbox,dracula,nord,tokyo-night, and many more (full gallery). Set via--theme <preset>orlogTui.theme.presetin.coco.config.json.NO_COLOR=1honored;monochromefor grayscale. The active/selected row renders readably on every theme (light + dark, truecolor + downgraded). NO_COLOR=1is honored end-to-end. Borders fall back to the terminal default and color emphasis is dropped without changing layout.- The chrome uses a small set of unicode glyphs (
›,↑/↓,·); layout is ASCII-only so a missing glyph never breaks columns. - Empty / loading copy is unified across views and points users at the next sensible action — no blank screens.
- The TUI is keyboard-only by design. Every action is reachable from the keymap or
:palette. Mouse input is not consumed.
What's new since v0.34.0
The shell architecture landed in v0.34.0 (TUI shell epic, #747). The releases since have layered on a navigable, action-rich workstation. Highlights worth knowing if you came from an earlier version:
v0.60.0 — stash workflow, narrow terminals, honest chrome
- Stash workflow overhaul. The stash list went from
ref + messageto richer rows:stash@{0} on main · 3 files · 2w <message>(origin branch · file count · relative age; refs are the compactstash@{N}form again). New keys on the stash view / sidebar tab:Rrename a stash ·bcreate a branch from it (git stash branch) ·Aapply restoring the staged/unstaged split (git stash apply --index, vs plaina) ·uundo the last drop.gZstashes all changes from any view (including status / diff / compose, where bareSis the commit-split key) — mnemonic pair withgz(jump to the stash view). Submitting an empty message makes a quick WIP stash. The:palette adds Stash staged only (--staged) and Stash keeping index (--keep-index).
- Narrow terminals are first-class. Below 100 columns the three-pane layout folds to a single full-width pane (the old 8-cell icon rails are retired).
Tab/Shift+Tabcycles which pane is visible (sidebar → main → inspector); the footer prepends atab: [sidebar] main inspectorswitcher.vmomentarily peeks the sidebar from the main/inspector pane —vorEscsnaps you straight back (a no-op in the full three-pane layout). - Honest, trimmed chrome. The commit-diff footer was relabeled
j/k lines · [/] hunk(the keys did the opposite of what it said), and each footer row was trimmed to its highest-value actions (the rest live in?). The header breadcrumb dropped its trailing← <hint (it's pure location now), and the⊘ no PRchip is gone — the PR chip shows only when a PR actually exists.
v0.40.0 — unified three-tier navigation
Wherever a list of items lives inside a named group, the group's title is now a first-class cursor target with its own canonical action. The same mental model carries across all four surfaces it applies to:
- Sidebar.
↑at items index 0 promotes the cursor onto the active tab's header (Branches / Tags / Stashes / Worktrees).←/→scans neighboring tab headers;Enteron the header drills into the dedicated view. - Status view. Files render under
▾ Staged (n)/▾ Unstaged (n)/▾ Untracked (n).←/→jumps between groups;↑at a group's first file promotes to the header;Enteron a header fires the batch action (Stage all unstaged,Unstage all staged,Stage all untrackedwith y-confirm). - Inspector Actions.
[/]toggles to the Actions tab;↑/↓moves the cursor;Enterfires the cursored action (cherry-pick, revert, reset, yank, open in browser). - Stash diffs.
diff --gitrows render as compact▾ <path>headers; the file the cursor is scrolled inside gets selection styling so the active context is always visible.
Header focus persists across ← / → switches in the sidebar so the user can scan tab → tab → drill in one fluid motion. All four surfaces use the same selection styling for their headers, so the visual cue is the same wherever you are.
v0.39.0 — inspector and cross-list cursor sync
- The right-hand inspector dropped its duplicative repo / branch / status trailer and added an
Actions:section listing the keystrokes available on the cursored entity. Destructive actions render with a[!]marker. - Inspector at rest is narrow (~22% of width); focusing it via
tabexpands to ~40% so the metadata, body preview, and action panel get the room they need. Mirrors the existing sidebar focus-expand pattern. - On short terminals (rows < 28), the inspector collapses into a tabbed
[Inspector] Actionslayout.[/]while the inspector is focused cycles tabs. - Cursoring a branch in the sidebar / branches view auto-jumps the history panel cursor to that branch's tip commit (debounced 150ms). Same for tags. A
Synced history to <ref>status confirms the jump even when the dedicated branches view obscures the history graph. - Current branch always pins to position 0 of the branches list regardless of sort mode. After a checkout, the cursor snaps back to row 0 so it lands on the just-checked-out branch.
- New keys on the history view:
Bcreate branch from cursored commit,gTcreate tag from cursored commit. The prompt itself is the affirmative gate. - Header label is
coco(wascoco ui).
v0.38.0 — action surface
- Pull-request panel via
g p: header / checks / reviews / body withm/x/a/R/c/Oaction keys. - History-view mutations:
Rrevert,Zreset (soft / mixed / hard mode prompt),iinteractive rebase. All routed through y-confirm or mode prompt. don a commit-diff or stash-diff toggles between unified and side-by-side rendering. Persists per-repo. Falls back to unified on terminals narrower than 120 cols.Hon a diff view applies the cursored hunk to the worktree (git apply).gHapplies to the index (git apply --cached).- Higher-fidelity commit graph: pattern junctions (
├╮/├╯), per-lane coloring, distinct merge / HEAD glyphs (◆/◉). - In-sidebar selection: when sidebar is focused,
←/→switch tabs and↑/↓(orj/k) navigate items. Per-entity ops (Enter checkout /aapply /Ddelete / etc.) fire from the sidebar without drilling into the dedicated view. - Multi-line input prompt for PR comments + review bodies. Enter inserts a newline;
Ctrl+Dsubmits. Single-line prompts (branch names, merge strategies) keep Enter as submit. - Branches list polish: dropped "even with X" noise, added relative timestamps (
today/Nd/Nw), four-state remote indicator (*/◌/≡/↕). - Stash diff header surfaces
@{N} <message> on <branch>instead of the raw timestamp ref.
v0.37.0 — quick-win affordances
y/Yyank the cursored identifier (commit hash, branch, tag, stash ref, file path) to the system clipboard from every promoted view.1/2/3on the status view toggle staged / unstaged / untracked visibility./path:src/fooand/author:alicere-rungit logwith the matching flags so commits touching a path or by a given author show up without dropping to a shell.
v0.34.0 — chord prefix
gis now a chord prefix. Pressinggalone no longer toggles the graph immediately — it waits for the second key. The graph toggle moved to\. PreviouslygdidtoggleGraphas a side effect of starting aggchord, which caused a brief flicker on everyg-prefixed sequence.
If you had muscle memory for g, you can either retrain to \ or use : and search for "graph" to invoke it from the palette.
Quick reference card
1Navigation Within a view Modes
2 g h history j/k, ↑/↓ move ? help (toggles)
3 g s status gg / G top / bottom : palette (toggles)
4 g d diff n / N next / prev / search (toggles)
5 g c compose Tab focus next Esc close mode / back
6 g b branches ←/→ sidebar tab < back (nav stack)
7 g t tags [/] sidebar tab / Quit
8 g z stash inspector tab q, Ctrl+C
9 g w worktrees 1-5 sidebar jump
10 g p pull request \ toggle graph
11 g x conflicts r refresh
12 g r reflog d unified ↔ split (diff view)
13 g B bisect m mark compare base (branches/tags/history)
14 g/b/s/x bisect: good/bad/skip/reset (bisect view)
15
16History view ops Diff view ops PR view ops
17 c cherry-pick H apply hunk m merge
18 R revert gH apply hunk to x close
19 Z reset (mode prompt) index a approve
20 i interactive rebase [/] file/hunk nav R request changes
21 B create branch here space stage/unstage c comment
22 gT create tag here z revert O open in browser
23 y/Y yank hash c cherry-pick file
24 O open in browserFor surface-specific actions (staging, hunks, commit compose, workflow keys like D/X/I), see the per-view sections in Coco UI.