-
-
Notifications
You must be signed in to change notification settings - Fork 0
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.
-
Views are top-level destinations. There are eighteen:
history,status,diff,compose,branches,tags,stash,worktrees,pull-request,pull-request-triage,issues,conflicts,reflog,bisect,submodules,remotes,blame, 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,g n). Pressgfor a live which-key menu of these, org?for the single-key actions in the view you're already in (see View-keys strip). -
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.
| 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; r rebases the current branch onto the selected ref (#0.71 — non-interactive, confirms, guards self-rebase / detached HEAD). |
| Tags | g t |
Tag list. |
| Stash | g z |
Stash list as an aligned table (ref · age · branch · files · message) with a column header; 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; c checks out the entry (detaches HEAD), Z resets HEAD to it (soft / mixed / hard prompt), B creates a branch at it — reset and branch reuse the history-view workflows. |
| 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. i init / u update (--init) / s sync the cursored submodule (#0.71). Capital M disambiguates from g m (compare-base mark). |
| Remotes | g n |
Configured remotes with their fetch / push URLs (#0.71). a add (prompts for name url), e set-url for the cursored remote, x remove*, p prune*, y yank URL. (* remove / prune confirm first; n for network, since g r is reflog.) |
| Blame |
b (from status) |
Per-file git blame drill-down (#0.71). Press b on a file in the status view; shows a dimmed <shorthash> <author> gutter per line, fetched on demand and windowed so large files stay responsive. j/k move, Esc back. |
| 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.
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). |
| 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.
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. |
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.
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 back
Outside the compare flow, m is unbound on those views except the dedicated PR view (where it triggers merge).
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.
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).
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.
/ 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 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.
The g which-key overlay solves discovery for the chords (g h, g s, …). g? is its counterpart for the single keys — the per-view overloads where the same letter means different things in different views (c cherry-picks on history but cherry-picks a file in a commit diff; R reverts on history; m marks a compare base; [/] jump hunks or sidebar tabs depending on focus).
Press g? in any view to pop a compact strip listing exactly the single-key actions available right there, with their labels. It's sourced live from the same binding table the ? help uses, filtered to the active view + focus — so as you move between views the list changes with you, and it can never drift out of sync with what the keys actually do.
-
g?— open the strip for the current view. -
?— step up from the strip to the full categorized help (progressive disclosure). -
Esc— close.
It changes nothing about what any key does; it's a pure discoverability surface. Bare ? still opens the full help directly, so existing muscle memory is untouched. The strip is also reachable from the command palette (:) as keys.
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.
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.
- 108 built-in color themes / 109 selectable presets (29 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.
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:
-
Worktree-aware checkout conflicts. Checking out a branch that's already checked out in another worktree no longer dead-ends on git's raw
already checked out at <path>. Coco names the branch + worktree and offersy(switch to that worktree — opens it as a nested repo frame),r(remove that worktree and check out here — refused/warned if it's dirty), orx(remove the worktree and delete the branch). (0.62.4) -
Stash view is an aligned table. The stash list is now a real table — a dim column header over aligned columns (
ref · age · branch · files · message), metadata dim, the message filling the remainder and tail-truncated, numeric columns right-aligned. Rows fit the panel exactly (no wrapping), and on narrow terminals the columns the inspector preview already shows are shed in priority order (branch → age → file count) before the message is squeezed. (0.62.3) -
The checked-out branch is highlighted in green. Its name renders in the theme's
successcolour (matching the green*HEAD marker) in both the Branches view and the sidebar Branches tab; the colour yields to the selection highlight on the focused row, andNO_COLORthemes fall back to the*glyph. (0.62.3) - Inline spinner on branch checkout. Checking out a branch now shows the same per-row pending spinner as a delete, instead of blanking the list behind a "loading branches…" placeholder. Deleting a branch that's checked out in another worktree now explains which worktree to free up rather than dead-ending on git's raw error. (0.62.2)
-
History opens on the full multi-ref graph again on a bare
coco. A barecocolaunch was dropping the--alldefault and booting the history view in compact mode (fewer commits, branches-ahead hidden, a graph that could loop back to the initial commit); it now opens on the all-refs graph as intended. (0.62.1)
-
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.
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.
- 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).
- 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.
-
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.
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.
Navigation Within a view Modes
g h history j/k, ↑/↓ move ? help (toggles)
g s status gg / G top / bottom : palette (toggles)
g d diff n / N next / prev / search (toggles)
g c compose Tab focus next Esc close mode / back
g b branches ←/→ sidebar tab < back (nav stack)
g t tags [/] sidebar tab / Quit
g z stash inspector tab q, Ctrl+C
g w worktrees 1-5 sidebar jump
g p pull request \ toggle graph
g x conflicts r refresh
g r reflog d unified ↔ split (diff view)
g B bisect m mark compare base (branches/tags/history)
g/b/s/x bisect: good/bad/skip/reset (bisect view)
History view ops Diff view ops PR view ops
c cherry-pick H apply hunk m merge
R revert gH apply hunk to x close
Z reset (mode prompt) index a approve
i interactive rebase [/] file/hunk nav R request changes
B create branch here space stage/unstage c comment
gT create tag here z revert O open in browser
y/Y yank hash c cherry-pick file
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.