Keys#
Every action in the workbench has a keyboard shortcut. This page is the whole
list; the in-app reference (?, or [help] in the footer) is the same material
split by subject, and the footer under each list is the short version that fits.
The rule#
Nothing is reachable by pointer alone, and nothing is bound that cannot be found.
Two halves, and both are enforced rather than intended:
- Every click target has a key.
every_click_target_has_a_keyincrates/butai-client/src/workbench.rsis amatchoverhit::Targetwith no catch-all, so a new clickable thing does not compile until someone has said which key reaches it. The assertions read the real tables, not a list of letters, so a key that moves fails the test rather than leaving a stale comment behind. - Every key is in a table.
crates/butai-client/src/verbs.rsholds one table per surface, and it drives four things at once: the footer text, the click hit-test, the key dispatch and the?reference. Binding a key without listing it is not possible — dispatch readsVerbId.
A verb that does not fit the footer is marked quiet: still bound, still in the
reference, just not competing for a column. That is the difference between not
shown here and undiscoverable, and it is the answer to "why isn't m written
under the rail?" — the PROCESSES footer is t new · r restart · x kill, 26
columns in 26.
Two gestures are the pointer's alone, deliberately: dragging to select text and the wheel. Neither stands for a verb, so neither has a key.
The two layers#
The same set on the same letters, reaching the workbench from anywhere.
- Alt works from inside a running program — that is what it is for. An Alt
key the workbench does not bind falls through, so
alt-bandalt-fstill move by words in readline. - The prefix (
C-bby default,prefixunder[general]) is for terminals that eat Alt. Press it twice to send a literal one through.
Spaces and workspaces#
| Alt | prefix | ||
|---|---|---|---|
| files | alt-o, alt-e |
C-b o, C-b e |
alt-e does not toggle back |
| docs | alt-m |
C-b m |
a project's own markdown |
| docker | alt-c |
C-b c |
alt-d is detach, so containers take c |
| git | alt-r |
C-b r |
the repository: its refs, its history, its working tree |
| work | the space key again | C-b w |
each space key toggles back |
| walk the spaces | alt-, alt-. |
C-b , C-b . |
|
| the spaces menu | alt-space |
C-b space |
every space with its badge — what the tab bar's own control opens |
| BOOTH | alt-0 |
— | a peer of the workspaces, not a space |
| settings | alt-s |
— (C-b S in the browser) |
entered and left, not cycled |
| workspace by number | alt-1..alt-9 |
C-b 1..C-b 9 |
|
| walk the tab bar | alt-< alt-> |
C-b [ C-b ] |
spans every machine |
| open a workspace | alt-n |
C-b n |
bare n too, off the stage |
| close this one | alt-x |
C-b X |
asks first |
| machines | alt-h |
C-b H |
connect one, or disconnect one |
The rails#
| Alt | prefix | bare | |
|---|---|---|---|
| AGENTS | alt-a |
C-b A |
|
| PROCESSES | alt-p |
C-b P |
|
| CHANGES | alt-g |
C-b G |
|
| the fleet (BOOTH) | alt-w |
C-b W |
x and m too — see below |
| the stage | — | C-b s |
enter |
| off the stage | alt-esc |
||
| cycle | tab |
||
| move, open | j k, enter |
alt-g is the CHANGES rail, not the git space — alt-r is. They are the
two most easily confused things here, so they do not share a letter. Both show
the working tree's files and both stage them with the same letters; the rail is
the one that sits beside the agents doing the changing and owns the commit box,
and the space is the one with the history and the diff beside it.
What fills them#
| Alt | prefix | bare | |
|---|---|---|---|
| spawn the pinned agent | a |
||
| choose an agent | alt-enter |
C-b a |
A |
| a new shell | alt-t |
C-b t |
t (PROCESSES) |
| restart | r (PROCESSES) |
||
| kill the row | x |
||
| kill what is staged | C-b x |
||
| the row's menu | m |
||
| the system monitor | C-b S |
click a gauge | |
| the gpu monitor | C-b Y |
click a gauge | |
| scroll the stage | C-b PgUp/PgDn |
the wheel |
m is the right-click menu opened from the keyboard: on a rail it is that
row's, and off one it is the workspace's own. It is the only route to close
others and close all agents — two actions that live nowhere else in the
interface. It also carries disconnect host on a remote tab, which is the one
row of it that has a second way in: alt-h lists the machines you are connected
to, and choosing one drops it.
x, m, a and A also answer on BOOTH's fleet, and there they act on the
row's own machine and project rather than on the tab you are looking at — the one
list in the workbench where those are routinely not the same thing.
The SYSTEM gauges are the one part of the left rail the cursor cannot walk, so
their monitor is keyed rather than entered: C-b S is htop, C-b Y is
whichever GPU monitor the machine has (nvtop, nvidia-smi, rocm-smi,
radeontop).
The rest of the workbench#
| Alt | prefix | bare | |
|---|---|---|---|
| zen — collapse the rails | alt-z |
C-b z |
|
| layout — resize them | alt-l |
C-b l |
|
| find | alt-/ |
C-b / |
/ |
| links — the URLs on screen | C-b f |
f |
|
| the git menu | C-b g |
g |
|
| branches | C-b b |
b |
|
| paste an image | alt-v |
C-b v |
|
| this reference | C-b ? |
? |
|
| the command prompt | C-b : |
||
| detach | alt-d |
C-b d |
q |
Bare keys need the cursor off the stage. On it, every key is the program's — which is what makes it a terminal and not a preview.
Keys that belong to one surface#
Each of these comes from that surface's verb table, and the footer under it names the ones that fit.
CHANGES — the verbs follow the selected row:
| row | keys |
|---|---|
| unstaged file | s stage · x discard · d diff |
| staged file | u unstage · d diff |
| conflicted file | o ours · t theirs · a resolved · d diff |
| a commit | d show |
| always | c commit · C stage all and commit · p push · g git menu · r refresh · ? keys |
The diff — ] [ hunks · space stage the hunk · v line-select
(space picks, enter applies) · x discard · z fold the file · Z fold
them all · r refresh · q close. On a staged diff the same keys unstage; a
commit's diff is read-only and offers none of them.
The same widget draws every diff — the DIFF space, the GIT page's body, a
commit, a stash — so these keys mean the same thing wherever you meet one. It
draws one card per file (its path, whether it was added, deleted or renamed, and
its +n -m) over the lines, each numbered on the side it exists in: a removed
line has no number on the new side, an added one none on the old. On a body too
narrow for the numbers they are dropped and the @@ headers come back, which is
the same bargain the commit graph makes with its lanes.
GIT — tab walks the three columns. What acts depends on the row.
| row | keys |
|---|---|
| working tree | enter diff the whole worktree · C go to the CHANGES rail |
| unstaged file | enter diff · s stage · x discard |
| staged file | enter diff · u unstage |
| conflicted file | o ours · t theirs · a resolved · enter diff |
| a branch | enter scope · c checkout · m merge · d delete |
| a tag | enter scope · x delete |
| a stash | enter show · p pop · x drop |
| a remote | f fetch |
| a worktree | enter open · x remove |
| a commit | enter diff · y copy the sha · v revert · p cherry-pick |
| always | esc widen the scope back · g the menu · r refresh · ? keys |
The file rows are the CHANGES rail's rows, under the rail's headings, answering
the rail's letters — one gesture for "stage this file" wherever you meet it.
enter opens the file's diff in the body beside the lists rather than taking
over the DIFF space, so you keep the refs and the history you opened it from,
and the diff keys above work there. The rail is unchanged and still owns the
commit box and the sync buttons, which is what C goes to.
BOOTH — j k walk the rows · enter go there: an agent's workspace on its
machine, or the project the cursor names · a start that project's agent · A
pick which · x end what the row is: the session on an agent, the workspace and
everything in it on a project (which asks) · z fold this machine or project ·
Z fold every project · tab the preview · m the row's menu.
Ten keys, and every one of them is a key this workbench already had: a and A
are the AGENTS rail's, and z/Z are the DIFF page's, marks (v open, >
folded) and all. It used to be six, and the reason given was that the rest of the
rails' table is about a project and this page is not in one — which stopped being
true when its rows became projects.
Clicking an agent in FLEET selects its preview and gives the stage
keyboard focus. Clicking a machine/project keeps focus on the fleet; double-click
its name within 400ms to fold/unfold it. The chevron and z also fold/unfold.
There is no separate NEEDS YOU tray; each chat carries its status in FLEET.
FILES / DOCS — j k move · ← h up a level · → l into · space
peek · enter open · backspace up · / find · e edit · C-s save · esc
stop editing · x delete · q close.
The browser is a trail of columns — every directory on the way to where you are,
side by side — so ← and → walk it and the columns to the right of the cursor
are kept: ← then → asks the daemon nothing. From inside the file, ← hands
the keyboard back to the browser.
space and enter both read the file the cursor is on, and differ only in where
they leave the keyboard: enter gives it to the file, space keeps it in the
browser so the next j walks to the next name and shows you that one. That is
what makes space a peek rather than a second enter.
x deletes the file the cursor is on, and it asks first — the box names the
path and opens on "no", as discarding does. It is the one key here that git
cannot undo: an unstaged file is not in the index and a never-committed one is
not anywhere, so there is nothing to restore from. Directories are refused; the
key does nothing on one.
DOCKER — enter follow the logs · r restart · x stop · s a shell in
the container · q close.
SETTINGS — j k rows · tab groups · enter open a list, choose, or
carry out an action row · space toggle · - + resize a rail · 0 back to
automatic · r re-read MACHINES · esc leave. enter does more here than
anywhere else because the MACHINES section holds rows that act rather than
hold a value — connect, disconnect, forget, update — and an action has
no value a toggle could report while an ssh is still landing.
Overlays — j k move · enter choose · esc or q dismiss · y / n
answer a confirmation. In the agent picker, d pins the highlighted agent as
what a spawns. In the link picker, y copies the highlighted URL instead of
opening it — which is the one that works from an ssh session with no browser on
it, and what enter falls back to there.
Links have no Alt key, unlike everything else in the table above. alt-f is
readline's forward-word and butai leaves it — with alt-b and alt-y — to the
pane, so a shell inside butai edits its line the way it does everywhere else.
f bare and C-b f reach the picker instead, which is the pairing g (the git
menu) and b (branches) already have.
Changing them#
Every key is a name in one mini-language, shared by [keys], the : prompt and
the command palette — so anything the prompt can say can go on a key, including
the things the shipped table does not bind.
[keys] is the prefix layer. An entry names the key you press after the
prefix, and it overrides that one row of the table above — the Alt layer is
built in and not reconfigurable. So o = "space files" binds C-b o, and
M-y binds C-b alt-y, which is a real binding but rarely the one you meant.
[general]
prefix = "C-a"
[keys]
o = "space files" # C-a o
F5 = "process build cargo build" # C-a F5
The vocabulary:
space work|files|docker|docs|git|booth|next|prev|menu
workspace 1..9|next|prev|new|close
focus agents|processes|changes|fleet|stage
agent [NAME] spawn one, or pick from the list
agent-default [NAME] pin what `a` spawns; bare unpins
process NAME COMMAND start a process
terminal a new shell
monitor [gpu] the machine's own monitor
host add a machine
branch the branch picker
update check for a newer butai, and offer it
find search the workspace
layout resize the rails
zoom zen
git-menu
close-pane kill what is staged
paste-image
help
detach
reload-config
kill-server [clear]
An entry that does not parse is a warning on the SETTINGS page, not a refusal to start — and that page also reports how many keys are bound and how many of them came from your config, which is the question you have when a key does something you did not expect.
In the browser#
The browser client keeps its verb tables in web/src/logic/verbs.ts and
keyboard dispatch in web/src/logic/keys.ts. Its HELP page renders the browser's
own reference; use that page for the actions available on the current surface.
The C-b prefix offers an alternative when a browser reserves an Alt shortcut,
such as Alt plus a digit for switching browser tabs. On a Mac the browser can
read KeyboardEvent.code, so Option combinations that compose characters in a
terminal can still be recognized.
When the live pane has focus, ordinary typing goes to that pane. Use alt-w
or alt-esc to return to the fleet or rail. The browser's page components live
under web/src/pages/; the terminal and browser share the daemon API but have
separate renderers and keyboard handlers.
On a Mac#
Option is a compose key by default: pressing Option-o types ø, and no terminal
reports Alt at all. butai reads those characters back, so Option-o is alt-o
and nothing needs configuring. Only the keys the Alt layer binds are read this
way — ∫ (Option-b) is still a character you can type.
Option-e and Option-n are dead keys and emit nothing until the next keystroke,
so they cannot be recovered: use C-b n to open a workspace, and alt-o for
files. To type the punctuation instead, set option_as_alt = false under
[general] and use the prefix layer — or set the terminal to send a real Alt,
which is better than either:
Terminal.app Settings › Profiles › Keyboard › Use Option as Meta Key
iTerm2 Profiles › Keys › Left Option Key › Esc+
Ghostty macos-option-as-alt = true
kitty macos_option_as_alt = yes
Inside tmux, keep xterm-keys on so it passes Alt through rather than eating it.