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, and changelog.
  • 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 < or Esc pops back. Going history → diff → compose → back returns 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 ui from there. Esc / < pops the frame when you're at the root of its view stack — and the header shows a coco › vendor/lib ← esc breadcrumb 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

ViewChordWhat it shows
Historyg hCommit graph/list with refs and metadata. gh also clears the navigation stack.
Statusg sWorking-tree files (staged/unstaged/untracked) with stage/unstage/revert.
Diffg dHunks of the currently-selected commit (from history) or worktree file (from status).
Composeg cFull-screen commit draft editor with summary/body cursors, AI draft, hook feedback.
Branchesg bLocal branches with divergence info; checkout/delete/create-PR via workflow keys.
Tagsg tTag list.
Stashg zStash 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.)
Worktreesg wLinked worktrees with current/dirty markers; remove via W.
Pull requestg pDedicated 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 triageg PMulti-PR triage list with filter cycling and per-row actions. Capital P disambiguates from g p (single, current-branch panel). See Issue & PR Triage.
Issuesg iIssue triage list with filter cycling and per-row actions: comment, label, assign, close, reopen. See Issue & PR Triage.
Conflictsg xConflict resolution helper view, available during merge / rebase / cherry-pick / revert. Per-row keys s stage / u theirs / U ours / o edit / C continue.
Reflogg rChronological 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.
Bisectg BBisect 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.
Submodulesg MRegistered 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).
ChangelogLFull-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.

KeyWhereAction
Chistory / branchesCreate a pull request for the current branch. Seeds the title + body from a generated changelog and opens a multi-line prompt for review.
Lhistory / branchesGenerate 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.
ScomposeSplit the staged set into multiple commits. Opens an overlay with the LLM-generated plan. y apply / r regenerate / < cancel.
Ecompose / status / diffOpen 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).
Icompose / status / diffGenerate 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.
Bhistory (on a commit)Create a branch rooted at the cursored commit (git switch -c).
+branches view / sidebar branches tabCreate a branch from HEAD (git switch -c).

Going back

KeyAction
<Pop the navigation stack — and once that's drained inside a nested submodule frame (#931), pop the repo frame itself
EscSame 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:

TriggerEffect
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 rowPush 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 diffPush compose and start editing. From inside compose, e toggles edit mode without re-pushing.
c from status or diffPush 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:

StepTriggerEffect
1m on a row in branches / tags / historyMark the cursored ref as the compare base. Status banner sticks: "Compare base: <label> — press enter on another ref to diff."
2m on the same ref againToggle the base off (no Enter needed).
3Navigate to a second ref on any of branches / tags / history(no key required — just move)
4Enter on that second refPush the diff view in compare mode, showing git diff <base>..<head>.
5< / EscPop 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:

text
Branches:  ↑/↓ branches · enter compare · m clear · esc back
Tags:      ↑/↓ tags     · enter compare · m clear · esc back
History:   ↑/↓ move     · enter compare · m clear · esc back

Outside 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.

TriggerEffect
g BPush 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.

TriggerEffect
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 diffPush 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 stackPop the frame. Returns the user to the parent's exact view position (selected commit, filter, etc.).
Esc / < deeper in a frame's view stackPop 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.

KeyAction
Printable keysAppend to the fuzzy filter
Backspace / DeleteRemove the last filter character
Ctrl+UClear the filter
/ , Ctrl+P / Ctrl+NMove the selection cursor (clamped to filtered count)
EnterRun the selected command (records as recent, then closes)
EscClose 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 compose in 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. v again or Esc snaps 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:

StackBreadcrumb
[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> or logTui.theme.preset in .coco.config.json. NO_COLOR=1 honored; monochrome for grayscale. The active/selected row renders readably on every theme (light + dark, truecolor + downgraded).
  • NO_COLOR=1 is 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 + message to richer rows: stash@{0} on main · 3 files · 2w <message> (origin branch · file count · relative age; refs are the compact stash@{N} form again). New keys on the stash view / sidebar tab:
    • R rename a stash · b create a branch from it (git stash branch) · A apply restoring the staged/unstaged split (git stash apply --index, vs plain a) · u undo the last drop.
    • gZ stashes all changes from any view (including status / diff / compose, where bare S is the commit-split key) — mnemonic pair with gz (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+Tab cycles which pane is visible (sidebar → main → inspector); the footer prepends a tab: [sidebar] main inspector switcher. v momentarily peeks the sidebar from the main/inspector pane — v or Esc snaps 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 PR chip 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; Enter on 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; Enter on a header fires the batch action (Stage all unstaged, Unstage all staged, Stage all untracked with y-confirm).
  • Inspector Actions. [ / ] toggles to the Actions tab; / moves the cursor; Enter fires the cursored action (cherry-pick, revert, reset, yank, open in browser).
  • Stash diffs. diff --git rows 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 tab expands 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] Actions layout. [/] 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: B create branch from cursored commit, gT create tag from cursored commit. The prompt itself is the affirmative gate.
  • Header label is coco (was coco ui).

v0.38.0 — action surface

  • Pull-request panel via g p: header / checks / reviews / body with m / x / a / R / c / O action keys.
  • History-view mutations: R revert, Z reset (soft / mixed / hard mode prompt), i interactive rebase. All routed through y-confirm or mode prompt.
  • d on 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.
  • H on a diff view applies the cursored hunk to the worktree (git apply). gH applies 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 ↑/↓ (or j/k) navigate items. Per-entity ops (Enter checkout / a apply / D delete / 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+D submits. 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 / Y yank the cursored identifier (commit hash, branch, tag, stash ref, file path) to the system clipboard from every promoted view.
  • 1 / 2 / 3 on the status view toggle staged / unstaged / untracked visibility.
  • /path:src/foo and /author:alice re-run git log with 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

g is now a chord prefix. Pressing g alone no longer toggles the graph immediately — it waits for the second key. The graph toggle moved to \. Previously g did toggleGraph as a side effect of starting a gg chord, which caused a brief flicker on every g-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

text
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 browser

For surface-specific actions (staging, hunks, commit compose, workflow keys like D/X/I), see the per-view sections in Coco UI.