butai docs 1.3.0

The workbench#

A guide to the screens, sidebars, and menus in the terminal client. For the complete keymap, see Keyboard shortcuts; for Git operations, see Git.

Projects open in tabs. Select an item in a sidebar to show it on the center stage. Available actions appear below each list.

The frame#

Three rows of structure: a tab bar on row 0, a band of boxes between, a footer on the last row. The band is carved left to right — left rail, stage, right rail — and only the middle of it ever changes.

Agents and processes on the left, an agent conversation in the center, and Git changes on the right
The workbench with an agent conversation open on the stage. View full size

The tab bar#

Left to right: the booth chip, a rule, then one chip per workspace across every connected machine, then the spaces button, the machines button and [+ new] hard against the right edge.

The booth chip is always drawn, at every width: it is the only pointer route back to BOOTH, because the spaces menu deliberately does not carry it. It reads [ booth ] when BOOTH is up and booth otherwise, with a ! when anything anywhere is waiting.

The │ after it is doing work. BOOTH is a peer of the workspaces rather than one of them — it is every project on every machine, and they are one project each — and on a row of look-alike chips that distinction was carried by nothing but a space. It is dropped below 52 columns, where two columns of rule are two columns of project name.

A workspace chip is 1:name, prefixed with its machine (2:gpu-box:infra) only when more than one daemon is connected — with one machine every chip would carry the same word. A trailing ! means an agent in that workspace is waiting or has a question. The active chip is bracketed and bold and accent-coloured, because colour is the first thing a screenshot or a monochrome terminal loses, and it is the only chip that carries [x]. Pressing [x] opens the same confirmation alt-x does.

The machines button is [+ host] on a single-machine client and [N hosts] past one. One control, not two: there used to be a count and a [+ host] beside it and both opened the same MACHINES picker, which is where you add a machine and where you let one go. At one machine the button is an offer and 1 host would label a fact that needs no label; past one it is the roll call, which is the thing worth pressing.

When the workspaces outgrow the row#

The chips get the columns the rest of the bar does not want, and no more. The spaces button, the machines button and [+ new] are reserved first; the chips scroll inside what is left rather than pushing anything off the row. Opening a twelfth project therefore cannot take away the button that opens a thirteenth.

The strip scrolls to keep the workspace you are in whole, and only that far, so switching between neighbours does not shuffle the row. What does not fit is reached through [<] and [>] at the strip's right end: each one selects the nearest workspace the strip is not showing, which is also what brings it into view. They are the pointer's spelling of alt-< / alt->, and each is drawn only when there is something that way. A chip at the strip's edge is truncated where the drawing truncates it and is clickable across the part you can see.

Below about 52 columns the reservation is dropped instead and the chips take the whole row: on a bar that narrow the tabs are the only thing left worth drawing, and [+ new] is a second spelling of a key. The same goes for the arrows, which need a strip wide enough to still show a chip once they have taken their end of it — narrower than that, the row reads as it always did.

The spaces button#

One control on the tab bar, [agents v], that names the space you are in and opens the menu of all of them. alt-space is its key, and alt-, / alt-. still walk them without opening anything.

The menu lists Agents, Files, Git, Docker, and Docs.

The counts are in the menu and nowhere else. They used to ride the button, and before that a rail down the left edge, on the argument that a signal from a space you are not looking at needs somewhere that survives every page. It does not need the bar to do it: a waiting agent already says so on its rail row, on its workspace chip, on the booth chip, in the footer, in BOOTH's fleet and through the bell. What is genuinely given up is narrower than it sounds and worth stating — a branch that has fallen behind is invisible on pages that draw no CHANGES rail. Open the menu or the Git view itself.

space what its row says
agents n! in danger when n agents are waiting
git n! in danger when n files are conflicted, else ↓n in amber when the branch is behind
files, docker, docs nothing — "there is stuff here" is noise

The button's ink is as wide as the space it names — [git v] is seven cells and [agents v] is ten — but the columns are reserved at the widest and the ink is right-aligned inside them. So the chip strip does not reflow when you switch space, and no button is padded to make that true: the blank sits outside the brackets, where it reads as the gap before a control rather than a hole inside one. On a bar too narrow to reserve them the button is dropped whole rather than shrunk, on the same terms as the machines button and [+ new] — the chips say where you are, and this is a pointer spelling of keys that already exist.

BOOTH, SETTINGS and HELP have no row. None of them is a view of a workspace, so all three take the whole width and are left the way you arrived — and while one of them is up the button reads views rather than claiming you are in a space you are not.

The space is the project's, not the window's#

Each workspace remembers the space it was last on, and going to a workspace goes to that space. Open butai on GIT, switch to caliper — which was on FILES — and switch back, and butai is on GIT again. It survives a detach and a restart, because it is written to [views] in the client's config, keyed by the machine the project is on and the directory it is open in: two machines with the same path checked out are two workspaces and keep two answers. See Configuration for the table and what bounds it.

This follows from what a space is. Every row of the menu above is a way of looking at one project, so which way you were looking at a project is a fact about the project — it was only ever a fact about the client because there was one view.page and every tab shared it.

The three that are not views of a workspace are exactly the three left out. A workspace never remembers BOOTH, SETTINGS or HELP, and going to a workspace while one of them is up does not move the screen off it: BOOTH spans machines, SETTINGS is about this client and HELP is about the program, so none of them is answering the question "which project". Choosing a workspace chip still leaves BOOTH, as it always has — that is a choice about where to be, and the space you land on is then the one that project was left on. A project butai has not seen before keeps whatever space you arrived with.

The rails#

The left rail is 28 columns by default and holds three stacked sections — AGENTS, PROCESSES, SYSTEM. The right rail is 38 and holds CHANGES, which is the whole of its interior. Both are clamped to 12..60 and resizable; if the two together would leave the stage under 20 columns, both drop to zero rather than squeezing it.

Each left-rail section gives its last row to its verbs when it has three rows to spare. SYSTEM yields entirely below 12 rows of rail, and in zen. Left alone, PROCESSES takes two fifths of what is left after SYSTEM and AGENTS takes the rest, because the agent list grows with the work while processes is usually a shell and a server.

All three lists scroll: the cursor is kept on screen by scrolling only as far as it must, and the same arithmetic answers the hit test, so the row under the pointer is the row that gets selected.

The stage#

One pane, in the middle, whatever the cursor last staged. It is the one region the daemon renders — a pane is a program's bytes on a PTY and turning those into cells needs a terminal emulator — and everything else on screen is JSON this client drew. While the stage has the keyboard, every key is the program's.

Full-width pages (BOOTH, FILES, DOCS, GIT, DOCKER, SETTINGS, HELP) take the rails' columns and put something of their own there. DIFF does not: a diff is what is on the stage rather than somewhere you navigate to, and the CHANGES rail beside it is how you walk to the next file.

When the stage loses its machine#

The daemon going down, and a forwarded socket dying, both arrive as the pane connection ending. The last frame stays on screen, dimmed to one faint colour, under a card:

gpu-box went away — reconnecting. The card shows how long the connection has been down.

The machine is named — the daemon for the local one, which has no host to name. The age counts from when the link went, not from the last retry, because 4s and 2h10m call for completely different reactions. The third line appears only when there is something behind the card; a stage opened straight onto a machine that is already down has no photograph to point at, and says so by leaving the line out.

The dimming is the larger half of the message: a screen flattened to one colour reads as inert from across the room, without a word of the card. The connection is re-opened once a second until it answers, and the notice goes when it does.

A pane that merely exited is not this. That leaves an ordinary empty stage — there is genuinely nothing to show. The difference comes off the wire; see The butai client protocol (version 1).

Three zones, all measured before any of them is written, so they cannot overwrite each other on a narrow terminal.

  • Left — name host:/path (branch), or just butai with nothing open. The host qualifier appears only with more than one machine. The armed prefix is appended in bold, because until the next keystroke the whole keyboard means something else. In LAYOUT mode this zone becomes the layout HUD instead.
  • Middle — the transient flash if there is one, otherwise ● name is waiting for the first agent that wants you. On a page that has hidden the AGENTS rail it reads ● name is waiting · alt-w, because there the footer is the only thing on screen naming which agent is blocked. Cut down to fit, and on a phone-width screen down to a bare ●.
  • Right — [layout] [detach] [help] [settings]. [help] and [settings] name pages rather than actions, so they are lit while you are on them and a second press is the way back out.

Where focus is, and how it moves#

The keyboard is in exactly one place: Agents, Processes, Changes, AllAgents (BOOTH's fleet), Refs and History (GIT's two lists), or Stage.

It starts on Stage. That is deliberate: a workbench that opens with the keyboard pointed at a rail turns the first thing you type into commands — echo $PATH would open the agent picker on the a and spawn something on the e.

tab cycle: AGENTS → PROCESSES → CHANGES → stage → AGENTS. GIT answers tab itself; BOOTH's fleet leaves to the stage
alt-a alt-p alt-g alt-w straight to AGENTS, PROCESSES, CHANGES, the fleet
alt-esc off the stage, onto AGENTS (on BOOTH, onto the fleet)
enter on a rail row, stage it; anywhere else, move onto the stage
click anything selects; a second click on the same row stages it

Bare letters only work with the cursor off the stage. The Alt layer and the prefix always reach the workbench, from inside a running program included — that is what they are for. An Alt key the workbench does not bind falls through, so alt-b and alt-f still move by words in readline.

Spaces and workspaces#

A workspace is one project directory on one machine. It owns its agents, its processes, its git working tree, and the pane the stage defaults to. Switching workspace swaps all of it at once.

A space is a view of one workspace. There are five, and they cycle: agents, files, git, docker, docs. Each space key toggles — the key that took you there brings you back to AGENTS.

BOOTH is not in that list, and neither are SETTINGS and HELP. BOOTH spans machines, so it is a peer of the workspace chips rather than an entry in a menu of views; the other two are about the client rather than about a project. All three are entered and left rather than cycled, and SETTINGS and HELP remember the page you came from so esc puts you back there and not somewhere you never were.

alt-o alt-r alt-c alt-m files · git · docker · docs
alt-, alt-. walk the spaces
alt-0 BOOTH
alt-s SETTINGS
? · alt-1..alt-9 HELP · a workspace by number
alt-< alt-> walk the whole tab bar, across every machine
alt-n, or bare n open another workspace
alt-x, or bare X close this one — it asks first
alt-h the machines

Opening a workspace puts a folder browser on screen, starting from the directory you are already in — a sibling project is far more common than one from the filesystem root. Its rows are the directories there plus three of its own: [open this folder], [new folder] (asks for a name, makes it on that machine, steps into it) and ... With more than one machine connected and no directory to start from, a MACHINE picker asks where first — asked once, at the start, rather than discovered after you have picked a path that does not exist over there. Each press asks again rather than remembering, because a workspace silently landing on a machine you stopped thinking about is worse than one extra keystroke.

Closing asks with the workspace named — close butai and kill what is running in it — and opens with no selected.

Machines join the tab bar through alt-h. The box lists the machines already connected first, marked *, then the ~/.ssh/config aliases you could add, then a row to type a destination. Enter on a connected one drops the link: its tabs leave the bar and the ssh goes with it, and nothing on the far side is touched. The client dials directly; there is no daemon in the middle relaying another daemon's screen.

AGENTS#

An agent is a coding CLI on a PTY the daemon owns. The rail is an attention queue, not a wall of terminals — the row tells you who needs you, and the stage is where you go when it is you they need.

The box border carries [+ agent], or [+ claude] when an agent is pinned: a pinned button spawns on one click, and the label is the only place you can see what that click is about to do. It falls back to the generic word when the rail is too narrow to spell the name.

A row is the title, marquee-scrolled if it is too long, and a right-aligned status token. > marks the pane that is currently on the stage; the cursor is a background colour.

The title is the pane's own terminal title, and an agent rewrites it as it goes: Claude Code prefixes it with an animated ◐ while it works and a ✳ while it waits. A leading glyph like that is pinned between the marker and the name — it stays in its column while the name scrolls past, the same way the CHANGES rail pins a file's M. The rule is a shape and not a list of glyphs: one leading character that is neither a letter nor a digit, then a space, then the name.

status means
⠹ 1:15 working — a spinner and how long this turn has run
WAIT blocked on you: a confirmation, a question, a dialog
done it finished its turn and said something
idle up, with nothing to do
exit / exit 3 the process is gone; a non-zero code is drawn in danger

The daemon reads these off the last rows of the agent's own screen every couple of seconds; a CLI that words its prompts unusually can be taught with waiting_pattern / busy_pattern under [[agents]]. An exited agent keeps its row so its final output stays one Enter away — x is what dismisses it.

a spawn the pinned agent, or ask which when nothing is pinned
A, alt-enter always ask
j k, enter move · put it on the stage
x kill the row. It does not ask — an agent's transcript is on disk
m the row's menu: Close agent · Close others · Close all agents

m is the right button's menu opened from the keyboard, and the last two rows of it live nowhere else in the interface. It is bound and documented but not drawn: the rail's one verb row is already full at a new · A new... · x kill.

In the agent picker, d pins the highlighted agent as what a spawns; the SETTINGS page's default agent row does the same thing, and ask every time is how you unpin.

PROCESSES#

Long-running commands: dev servers, watchers, whatever a project needs up. They are panes like any other, so staging one shows its output. A workspace's .butai.toml can declare the ones it always wants running, which is why a project that has one needs no setup step.

The separator carries [+ term], which is the same verb as t.

status means
ok the readiness substring was seen
... starting
done exited zero
FAIL(2) exited non-zero, in danger — the row stays, because a build that died is the thing you most need to see
t, alt-t a new shell, staged
r restart the row. This allocates a new pane, so the stage lets go of the old one
x kill it
m the row's menu: Close · Restart

r and x do nothing when the list is empty, rather than asking the daemon about a pane that was never named.

SYSTEM#

Five kinds of gauge — CPU, RAM, one GPU per card, one NET per interface and one DSK per mount. The first four are a head row and a trace: the label on the left, what the hardware is in the middle, the reading on the right, and below it a braille trace across the full width of the rail at two samples per cell — around fifty samples, so it reads as a trend rather than a texture. DSK is the head row alone; see below.

CPU, memory, GPU, and network readings in the System panel
System readings in the terminal workbench. View full size

The middle slot is the identity: the CPU model and thread count, the GPU model, the interface name and its link speed, swap once any is in use, and a disk's mount point. It is the first thing dropped when the rail is narrow — the label says which gauge this is and the value is the reading, so an identity that pushed either off the row would cost more than it tells you. It elides whole words, never mid-token.

CPU, RAM, GPU and DSK colour by load: ok under 50%, attention from 50, danger from 85. A gauge at rest still draws a one-dot baseline, because an idle machine is not a dead feed.

NET#

A row taller than the rest, and the only gauge whose colour means direction rather than severity — a saturated link is the machine working, never a warning. ↓ (incoming) is drawn in info and ↑ (outgoing) in accent, each on its own trace row led by its arrow, so the pair still reads on a monochrome terminal.

Silence draws as silence. Throughput has a real zero, unlike a CPU, so anything under 4 KiB/s is drawn as an empty row rather than a baseline: ssh keepalives, mDNS and ARP keep a few hundred bytes a second moving on every interface that is up, and drawing that as traffic made an idle rail look like a busy one.

Both directions share one autoscale peak, floored at 64 KiB/s so a quiet minute is not amplified into a mountain range. Sharing it is deliberate — a flat ↓ under a full ↑ is the picture of an upload, and scaling each against its own peak would fill both rows and throw that comparison away. The exact rates are on the head row, so the traces carry shape and direction rather than magnitude.

Which interfaces appear is [ui] net: all by default — every link that is up and carrying something, plus the default route whether it is busy or not, capped at three — auto for a single one, or an explicit list. Loopback, bridges and veths are left out of the automatic modes because their bytes are counted again on whatever they egress from; naming one draws it anyway. The name only appears once there is more than one, the way GPUs are only numbered on a multi-GPU box.

DSK#

One row, and no trace. Every other gauge is a series; a disk is a level with no history — the daemon publishes none, because a filesystem does not visibly move across the two and a half minutes the window holds. A second row would be a flat line drawn once per disk, and three disks would spend six rows on it.

The mount is the identity, and unlike every other gauge's it is never decoration: two disks with their mounts dropped are two identical rows. So it is cut rather than abandoned, and cut from the left — /work and /media/archive agree on everything but their last segment, so …/archive is what identifies it and /media/ would not.

Capacity is used/total in one unit taken from the total, in the binary units df -h prints — 3.5/3.6T rather than four digits of gigabytes. Used is total minus available, not minus free: the blocks a filesystem reserves for root are not space a build can have, which is how df reports it too, and it is why the number can sit above df's "Used" column.

On a Mac, check it against df -h /System/Volumes/Data, not df -h /. The gauge says 73% where df -h / says 9%, and df is the one being misleading: / is the sealed system volume, a dozen gigabytes by design, while the space is spent on the data volume beside it. Both are volumes of one APFS container which they are sized and filled by, so the row is the container — the disk that can actually fill — and the data volume's own percentage is the one it matches.

A mount that did not answer in time keeps its last reading and is drawn faint rather than in the colour that reading earned. A statvfs on a filesystem whose server has gone away blocks uninterruptibly, so the daemon gives the sweep a deadline and rests a mount that misses it; 99% full and a minute out of date is news about the clock, not an alarm about the disk.

Which mounts appear is [ui] disks: all by default — every real disk, largest first, capped at three — auto for the filesystem holding / alone, or an explicit list. tmpfs, container layers and network mounts are left out of the automatic modes: a tmpfs is RAM the RAM gauge already counts, an overlay is the image under a container rather than a disk that can fill, and a network mount's capacity belongs to a machine with a rail of its own. Naming one draws it anyway.

Opening a monitor#

This is the one part of the left rail the cursor cannot walk, so it is keyed rather than entered: C-b S puts htop on the stage, C-b Y whichever GPU monitor the machine has (nvtop, then nvidia-smi, rocm-smi, radeontop, and a line saying why there is nothing if none is installed). Clicking a gauge does the same, and resolves by walking the gauge list rather than dividing the row by a constant — the gauges are no longer all the same height. NET and DSK open the system monitor rather than one of their own: there is no network or filesystem pane yet, and htop is still the honest answer to "what is using this".

CHANGES#

The workspace's git status, live, on the right. The box title carries the three facts that change what you would do next: CHANGES (6) · main ↑2↓1, or CHANGES (6) · main · REBASING while a sequence is running — you cannot push mid-rebase, so the sequence displaces the arrows.

The footer names the branch as well, but it names the workspace's, in one line shared with everything else on screen — it is the first thing cut on a narrow terminal and it is gone entirely in layout mode. The title is where "which branch am I about to commit this to" is asked, so the branch is repeated beside the file list. It is also the only part of the title with no bound, so on a narrow rail it is the part that gives way: cut to what the counts and arrows leave, and dropped rather than shown as a stub.

The list is flat with headings in it, and the headings are rows the cursor can sit on: enter on Unstaged or Staged diffs the whole section. Conflicts come first and a conflicted file is never also listed as ordinary work, because s on a conflict would commit the markers.

The Changes panel with unstaged and staged files
Unstaged files, staged files, and recent commits. View full size

The status code is pinned and only the path marquees — a row whose M slides away has stopped saying what happened to the file.

The verbs follow the selected row. That is the point of the table: the rail used to offer s stage with a commit selected, and nothing at all with a conflict.

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

p push is drawn only when the branch is actually ahead. x discard is the one verb on this rail that asks first, because it throws away the only copy of an edit; the box opens with no selected. c opens a one-line prompt with the staged summary under it (3 staged file(s), or nothing staged — this will fail, which is more useful than refusing).

The full git model — what a status code means, how the scan is refreshed, what each route can refuse — is Git.

The stage#

Whatever the cursor last staged, titled STAGE · name. Which pane that is is this client's choice: two people looking at one project can look at different panes in it. With no choice made it follows the workspace's own default.

While the stage has the keyboard every key goes to the program, which is what makes it a terminal and not a preview. Three things still reach the workbench: the Alt layer, the prefix (twice sends a literal one through), and C-b PgUp / C-b PgDn for the pane's scrollback, which only the daemon has.

Mouse. Clicks and drags are forwarded to the pane in the pane's own coordinates, whether or not the program asked for the mouse — the daemon is parsing that program's output and is the only thing that can decide, so it drops what was not wanted. The wheel over the stage is scrollback; over a rail it moves that rail's cursor without taking focus, because you are looking, not choosing.

Selection. Dragging paints a selection over the composed screen, not over a pane, so it copies whatever is under it — a rail's rows, a diff, a file. The drag is confined to the region it began in, so straying into a neighbouring column still yields a clean rectangle. Selection is linear, not rectangular: a line that runs off the right edge continues at the start of the next, and trailing blanks are trimmed. Over a file being read, the line-number gutter is skipped, so what you paste is code rather than code with a column of numbers welded to it.

Releasing copies via OSC 52 — which is what works over ssh with no display server — and flashes copied 3 lines. Inside tmux the sequence goes out twice, plainly and wrapped in tmux's DCS passthrough, because stock tmux drops a bare OSC 52 and the copy then went nowhere at all, silently.

alt-drag selects over a program that grabbed the mouse (vim, less); shift-drag bypasses butai entirely and gives your terminal emulator its own selection.

Paste. Bracketed paste is on, so a pasted run arrives as one event and is sent as a paste rather than as keystrokes — a program that asked for the markers gets them. Text goes wherever text is being typed: an open prompt first (flattened to one line, so a pasted branch name cannot silently become a different one), then a file buffer in edit mode, then the pane. With nothing staged it says nothing on the stage.

alt-v pastes the image on your clipboard: it is re-encoded to PNG, written into the workspace's scratch directory, and its path pasted where typing would have gone — which is what an agent CLI can actually open. The read happens on the machine you are sitting at, so it works over ssh. On a Linux box with no display server it refuses immediately and says so, rather than hanging for seconds on an X11 connection attempt.

Size. The client tells the daemon the pane's exact rectangle, and that is the one measurement that crosses the wire for drawing. It is page-dependent: on the DOCKER page the pane is the logs column, not the whole band.

A URL drawn anywhere on the screen is a link. Two ways to follow one, because the terminal's pointer and this client's keyboard are two different things:

The pointer, through your terminal. Every URL is marked up as an OSC 8 hyperlink as the cells are painted, so hovering underlines it and your terminal's own gesture opens it — cmd-click in iTerm2 and Ghostty, ctrl-click in GNOME Terminal and Windows Terminal, ctrl-shift-click in kitty. butai keeps mouse tracking on, so a plain click is a click in butai and the modifier is what tells the two apart; that is your terminal's rule, not butai's.

A terminal that does not implement OSC 8 discards the sequence and shows the text, which is the normal outcome for one that has never heard of it. tmux before 3.4 is the case worth knowing: it drops the sequence, so inside stock tmux the link is plain text no matter what the outer terminal can do. [ui] links = false turns the mark-up off for a terminal that gets it wrong.

The keyboard, through the picker. f — or C-b f from a focused pane — lists every URL on the screen as it stands, in reading order, each one once. enter opens it on the machine this client is running on; y copies it. With no browser here — an ssh session with no display server, which is where a TUI usually lives — the title says so and enter copies instead. The copy is OSC 52, so it lands on the clipboard of the machine you are sitting at.

What counts as a link: http, https, file, ftp, ftps, ssh, git, ws, wss, mailto, and a bare www. host, which gets https://. A trailing full stop, or a closing bracket that nothing opened, is left out — so (see https://example.com/a). links the address and not the punctuation around it. javascript: and data: are deliberately not links.

In the browser client the same map is read, and the same rules — the port in web/src/logic/links.ts is tested case for case against the Rust one, because two clients that disagreed about what a URL is would be a bug report about whichever one you happen to be using. What differs is who acts on it: a canvas has no terminal underneath to hand the address to, so the stage hit-tests the map itself. Hovering underlines the link and shows it as a tooltip; clicking opens a tab. A program that asked for the mouse keeps its clicks, and there it takes ctrl or cmd — the rule kitty and iTerm2 use, so the gesture is the same one. alt and shift still force a selection, as they do everywhere else. The whole grid is a pane there, so every row is joined; there is no picker, because the pointer and the hover are always available in a browser.

A URL too long for the row. Inside the stage the rows are joined before they are scanned, because a pane's wrapping is the program's own — so an address that runs off the right edge and continues below is one link, and opens whole. The chrome is not joined: everything the client lays out itself truncates rather than wraps, and splicing two rows there would invent an address. The signal is the last cell of the row being filled, which is what a wrap leaves behind — unless the row below begins a link of its own, which is a new address rather than the rest of one. That second rule is not a refinement, it is the common case: $ echo https://… fills a narrow pane's row exactly and the shell echoes the same URL underneath, and without it the two joined into that address written twice. What can still be got wrong is a program that ends a line exactly at the right edge and then prints unrelated prose.

BOOTH#

alt-0, or the chip at the far left of the tab bar. Every project on every connected machine, the selected one's live screen, and whether each machine is in trouble. It is the only page that spans daemons, which is why it takes the rails' columns: the left rail would list one workspace's agents beside a column listing everyone's.

BOOTH showing the project fleet, a selected agent conversation, and machine readings
BOOTH brings projects, conversations, and machine status together. View full size

On narrow terminals, meters shrink and project buttons shorten to [+]. Wider columns spell out the preferred agent, such as [+ claude].

It is a list of projects that happens to contain agents, and that is a change from what it was. The rows used to be built by walking the agent list and emitting a header whenever the workspace changed, so a project with nothing running in it produced no rows at all — the one page listing every project on every machine could not show you the ones you had not started anything in, which are exactly the ones you want to start something in. They come from the machine and project lists now, so notes above has a row and so does mini, which is connected with nothing open on it.

The NEEDS YOU tray gathers agents needing attention above the fleet, even when their projects are folded. It ranks questions first, then unread crashes, then unread completed turns. Its fixed height keeps the fleet from shifting when attention changes; when empty, it says nothing needs you.

The tray holds copies, so each agent keeps its place in the fleet. Clicking a copy selects its original row and preview; unfold its project or machine first if that row is hidden. The previewed agent stays highlighted in both lists even when the keyboard is focused on its pane.

Each chat has an [x] button in the fleet and in NEEDS YOU. It ends that chat on its own machine; the tray button also works when the project is folded. The terminal follows the existing x key behavior and closes immediately; the web client uses its existing confirmation dialog.

The fleet list is grouped by machine and then by project, and its order is a pure function of identity — daemon, then tab order, then spawn order. It reads no agent state at all, so a row is where it was an hour ago and a status change redraws a glyph in place. (Sorting by urgency was measured and rejected: rows travelled ~174 positions per ten sampler ticks at 24 agents, and hysteresis only brought that to 169.) Projects are grouped by id rather than by name: two machines routinely have a project of the same name open, and one machine may have two, so the name is left to the drawing.

Each agent has a simple status indicator in a fixed three-cell field:

indicator means
. → .. → ... working — loading dots advance every 1.2 seconds
! waiting for you
✓ a finished turn
· idle
x exited; a nonzero exit is shown in danger colour

The markers keep their meaning throughout a session; age does not change them. A faint box-drawn spine connects machines to projects and projects to agents, with branches ending at the last visible sibling after folding.

The status indicator is ours. An agent's own status glyph — Claude Code's ◐/✳ — is whatever it wrote into its terminal title, and it is pinned between the sprite and the name here exactly as the AGENTS rail pins it. Only the name marquees.

The middle column is a live pane, not a picture of one. The keyboard starts on the fleet, so j/k walk rows. Clicking an agent in the fleet or NEEDS YOU tray selects its preview and immediately hands the keyboard to that pane. Clicking a machine or project selects it and keeps the keyboard on the fleet. tab, a click on the pane, or starting an agent also hands it to the pane, and everything you type from then on is that agent's. alt-w or alt-esc takes it back — it has to be one of those, because once the pane has the keyboard esc and tab are the agent's too.

The cursor walks rows, machines and projects included, because starting a session belongs to a project and so does going somewhere. The agent under it is derived rather than tracked beside it — two indices that have to agree are two indices that eventually do not — and on a project row the pane shows the agent in it that most needs you, so walking the fleet is a fly-over of each project's screen rather than a cursor that keeps pointing the pane somewhere it has left. A project with nothing running previews nothing, and a machine row previews nothing: there is no honest answer and the stage says so.

Clicking an agent row only moves the cursor. [open], right-aligned on it, is the one thing on that row which travels: it goes to that agent's workspace on its machine, which moves the tab bar out from under you. enter is its keyboard spelling. This split exists because a click that meant "let me look at this" was throwing the whole workbench onto somebody else's project. [open] is dropped when the column is too narrow for it, and then the two-step click is the only way.

That rule is the whole list's, project rows included: text looks, buttons act. A project's name puts the cursor on the project — which points the middle column at the agent in it that most needs you — and travels nowhere. It briefly did travel, on the grounds that a project row had nothing to preview; it has one, so that was the same accidental route off the page in different clothes. enter is the keyboard route to a project or agent; both have an explicit [open] button. Nothing here takes you somewhere by accident: every route out is a field you aimed at, or a key.

a starts a session in the project the cursor is in, and A picks the type whatever the project says. They are the rails' own two verbs, bound here unchanged — what moved is only what they act on, from the tab you are looking at to the project the row names, which on this page are routinely not the same project or even the same machine. The page does not move, because a button that started something and threw the tab bar onto another machine is the bug that made agent rows two-step in the first place.

What does move is the cursor, and the keyboard with it. Once the new agent has a row in the fleet, the cursor goes to that row — which points the middle column at its screen, because the preview follows the cursor — and the focus goes to the middle column, so the next thing you type is the first thing that agent reads. Nothing else changes: same page, same tab, same machine. A project that was folded opens on the way, because a folded project has no agent rows at all and the fold would otherwise be the one thing standing between [+ claude] and the pane it promised you — an extra sprite in a strip is not an answer to "start an agent and put me in it".

That is two steps rather than one because it has to be. The client's fleet is fed by the daemon's event stream, so the row does not exist at the instant the spawn returns, and the cursor cannot be put on a row that is not there — it stayed on the project row instead, where the preview picks the agent that most needs you, which for a brand-new one (working, nothing unread, so not asking for anything) is never it: you got a fly-over of the project's first agent and no way to type into the one you had just started. So the pane id is held and the cursor lands the moment the row appears. It is held for three seconds and no longer, and only while the page is still BOOTH and the cursor has not moved: an agent that dies before it is ever listed must not leave a cursor move armed to fire at whatever you are doing a minute later.

[+ claude] on the row is the same button under the pointer, and it names what it will start for the reason the AGENTS rail's does: a button that spawns on a single click with nothing in between is the only place you can see what that click is about to do. It falls back to [+] and then to nothing as the column narrows, exactly as [open] already degrades.

What a project starts is its own [agents] autostart, then the client's default_agent pin, then the picker. Two steps and no third: a project that wants codex says so in the file it already has for exactly that, which lives with the project, travels to the machine it runs on, and is shared with whoever else opens it. A client-side pin keyed by directory would be none of those three.

z folds the machine or project the cursor is on; Z folds every project at once, leaving an index of every machine, every project, and what is running in each. They are the DIFF page's fold keys and its marks — v open, > folded — because this workbench already has a fold idiom and a second one for the same concept is drift. A folded project hides its chat rows without adding a duplicate status strip to the header. z on an agent folds the project it is in and takes the cursor up to that row, which is the only move that leaves the cursor on something you can still see.

Folding is a filter over the order and never a second ordering: a folded row is simply not emitted and the rows around it keep the positions they had.

The compute column shows one summary line per collapsed machine: its name, agent count, a meter where space allows, and the busiest resource reading.

For example, a collapsed machine can show 3 agents · RAM 42% or 4 agents · CPU 97%, depending on which resource is busiest.

Clicking the summary expands the existing SYSTEM gauge stack, including history traces, hardware details, interfaces and watched filesystems. Clicking that expanded block collapses it again. An away machine shows away and never expands stale telemetry. The wheel scrolls in machines, and j/k stay with the fleet.

Double-click a machine or project name in FLEET to fold/unfold its children. A single click selects and previews. Double-click means two nearby presses on the same machine/project within 400ms. Keyboard input, scrolling, dragging and other controls cancel the pending pair. The chevron and z retain their single-press folding behavior. Agent names give focus to the preview; [open] and enter still travel to the agent's workspace. Explicit [+], [x] and [open] buttons keep their own actions.

What the headline names: rates first, and fullness only when it is an emergency. CPU, RAM and GPU are rates — what the machine is doing this second, numbers that come back down on their own. Disk fullness is a level: the same number all day, moved by nobody but you. The column took the plain worst of all four for a while, and the level won permanently — the shipping disks = "all" watches the three largest local filesystems, so one 3.6 TB media drive that had been 90% full for a year held every row at DSK 90% in danger red while the CPUs idled. A column whose only job is answering which of these machines is busy answered "the disk", forever. Reported as the compute column reading high, and it was.

The original insight is kept rather than reversed — a box at 30% CPU with a full root filesystem is in trouble and its CPU number says it is fine — it just has to be full enough to outrank what the machine is actually doing, which is 95%. That is well clear of the 85% where the colour ramp starts painting red, because 85–95 is exactly the band a well-used drive lives in permanently; and it is where ext4's 5% root reserve runs out, so it is where ordinary writes start failing rather than where they are getting close. Below it, expand the machine to inspect each watched filesystem and its mount.

A stale mount is out of the headline entirely. A filesystem nobody has heard from is not news about how full it is, and a hung NFS export reporting 99% from an hour ago must not paint a working machine as an emergency. The expanded stack still prints the number it last saw, faint — the same judgement the SYSTEM rail already makes.

x ends the thing the row is. On an agent that is the session, wherever it lives, and it does not ask, for the reason the rail's x does not: an agent is a process whose transcript is on disk. On a project row it is the workspace and everything running in it, so it asks — in the tab bar's own box and its own words, because that is the same act reached from somewhere else.

[x] is that press under the pointer, right of [+], and it is drawn on the cursor's row and nowhere else — and only while the fleet has the keyboard. That is the tab bar's rule for its own [x] and it has the same reason: a button that ends a workspace has to be one you aimed at, not one sitting under a row you were passing, and a cursor belongs to the column being steered. It costs four cells, which on this column is a sprite or half a name — worth spending on the row you are looking at and not on the ten you are not.

Because those four cells are spent at the right end, whether the row is the cursor's decides where every control left of it sits. The drawing and the hit-test therefore ask one function whether a row is the cursor's, rather than each spelling the condition out. They did each spell it out, and drifted by exactly those four cells: with the cursor on a project row and the keyboard on the middle column, [+ claude] was drawn flush right with no [x] beside it, while a press on it resolved four cells to the left — so clicking the button you could see opened the close-workspace confirm, or folded the project.

Every control on the row keeps its place before any of them is spelled out, so a narrow column draws [+] [x] rather than [+ claude] and no way to close: [+] starts exactly the agent [+ claude] would, and [x] has no shorter form. What neither of them ever costs is a sprite — a folded project that cannot say what is in it is a row you have to unfold to read.

m or the right button opens that row's menu — Close agent, Close others, Close all agents, the same three the AGENTS rail offers, acting on the row's own project rather than on the tab you are looking at. Neither asks first, for the reason the rail's x does not: an agent is a process whose transcript is on disk. On a project row the menu is the tab bar's own, against its chip; a machine row has none, because there is nothing generic to offer about a host here that the tab bar does not already offer about its tabs.

Pasting with the cursor still on the fleet says click the preview or Tab to it to type there rather than silently landing in an agent on another machine.

The fleet's bare keys are j, k, enter, tab, x, m, a, A, z and Z, and no more. It used to be the first six, and the reason given was that the rest of the lettered rail verbs are about lists this page does not draw — a new agent belongs to a project, and BOOTH is not in one. That sentence stopped being true when the rows became projects. It survives in a better form: a new agent belongs to a project, and here the cursor is always in one.

FILES and DOCS#

alt-o (and alt-e, which names the page rather than the space and does not toggle back) for FILES; alt-m for DOCS, which is the same widget over a listing filtered to markdown, READMEs, and every directory except target and node_modules. They keep separate cursors and separate open buffers, so switching between them does not lose your place in the other.

The browser is a trail of columns#

Every directory on the path from the workspace root to where you are is a column of its own, side by side, with the row you came through still marked in each — the Finder's column view, in a terminal.

The file browser with a Rust source file open in the editor
FILES shows the directory trail beside the selected file. View full size

It was one directory and one cursor, and descending replaced both. That made every folder a one-way trip you could only reverse by remembering you had: nothing on screen said where you were or how you got there. A trail says both at once, and where you are is the shape of the whole thing rather than a line of text you have to read.

A column is 20 cells. The browser grows one at a time and stops at half the band, so a trail walked six deep still leaves the file the room — and on a terminal too narrow for a browser and a file at once, the browser goes to nothing rather than squeezing the thing you came to read. The trail scrolls left as it grows, so the column you are working in is the one that stays on screen; ← past the left edge pans the other way. [find] is on the top border and each column wears its own directory's name (docs for the root on DOCS).

A row is ● in amber when git sees a change in that file, or in something under that directory that this page shows — so on DOCS a folder holding nothing but changed code stays unmarked, and the dot always leads somewhere. Then the name, then ▸ if it is a folder, on the edge the next column opens from.

There is no .. row. There used to be, because descending read as a one-way trip and something had to say up existed. The trail says it — the directory you came from is the column to the left, still listed — and ← walks back to it, so a row whose only meaning is "the column immediately left of this one" is a row you have to learn not to click. backspace still works, and so does ← when the trail does not reach where you are.

The columns to the right of the cursor are kept, not dropped, so ← then → is two local moves and no round trip — over ssh that is the difference between browsing and waiting. Moving the cursor is what drops them, and that is the point rather than a side effect: those columns are what the old selection contained.

The filter is the daemon's (?filter=docs), not each client's: the marker and the rows are one decision, and splitting them is what used to let a trail of dots end in an empty box.

The file, and the minimap beside it#

The right column is the open file with a line-number gutter and syntax colours, titled with its path and a * when it has unsaved changes — the asterisk goes where the eye already is rather than into a status line further away.

Down its right edge is a minimap: the whole file as sixteen cells of texture, with the rows you are looking at marked on it. Sixteen cells cannot hold a line of code and it does not try — each cell stands for a rectangle of the file, drawn as one shaded block whose density is how much ink is in that rectangle and whose colour is what that ink mostly was. A comment block is a muted slab, a run of strings is green, an indent is the blank left edge. At that size indentation is the signal, which is exactly what makes a file recognisable from across the room. Click anywhere on it to jump, and what you clicked lands in the middle of the window rather than on its top row — you aimed at a shape in order to read what is around it.

It takes all sixteen cells or none: below a floor the scale stops meaning anything, and a minimap you cannot read is sixteen cells of code you no longer have. On a narrow terminal, or with the trail walked several deep, the file column keeps the file.

The bottom row is either a notice, … truncated; download to see the rest when the daemon stopped at its cap, or the keys:

File mode Footer actions
Read-only ← / → navigate, space preview, q close
Viewing e edit, j / k scroll, q close
Editing Ctrl-s save, esc stop editing
j k walk the column; once the cursor is on the file, scroll it
← h up a level — the column to the left, or out of the file back into the browser
→ l into the selected directory, or open the selected file
space peek: read the file the cursor is on without handing it the keyboard, so the next j walks to the next name
enter open — the same read, and the keyboard goes to the file
backspace up a directory
/, [find] search the workspace
e (or i) edit the open file
C-s save
esc stop editing
x delete the file the cursor is on, after a confirm box. Refused on a directory
q esc close the page. A changed buffer refuses once; the second press discards

space and enter both put the file in the viewer and differ only in where they leave the keyboard. That is what makes one of them a peek: a way to read down a directory a file at a time without committing to any of them.

x is the only key on this page that destroys something, and it is the only one whose damage git cannot undo — the CHANGES rail's x puts a file back to what the index holds, and this one leaves nothing to put back. So it asks: the box names the path and opens on "no", the same shape the discard box has. Deleting the file that is open in the right column closes it too, and the listing is read again afterwards, which is what repaints the ● markers around the gap.

The buffer lives in the client. That is a deliberate trade: the daemon used to hold it so unsaved edits survived a detach, and what replaces the guarantee is the refusal above.

Searching is the daemon's — it walks the workspace and returns hits, so it is as fast over ssh as locally, and a hit opens the file at its line.

GIT#

alt-r. The repository over time and across branches, which is a different question from the CHANGES rail's "what did I change just now". Three columns: REFS above, the commit graph below it, and whatever the cursor names drawn as a diff on the right.

Git refs and commit history on the left with the selected commit diff on the right
GIT shows refs, commit history, and the selected diff. View full size

REFS lists the working tree first — working tree clean, or working tree · 6 changed in amber, whose enter takes you back to the CHANGES rail where staging lives. Then branches, remotes, tags, stashes and worktrees. A branch carries ↑2↓1 drift, > and bold when it is the one you are on, and ⇢name when it is checked out in another worktree. A worktree row is tagged here for the checkout you are looking at and open when it is already a workspace.

HISTORY is one page of the log, newest first, in topological order, with real parent edges drawn in up to six lanes. Lanes are computed over the whole page rather than the visible slice, so a lane a merge opened above the fold still passes through the rows below it, and the shape does not change as you scroll. Below 30 columns the lane column is dropped and the list draws plain. Each row is the sha, any ref chips (tags in accent, remotes faint, branches in ok, bold when HEAD is here), then the summary. The box title names the scope: HISTORY · all refs or the ref you narrowed to.

Nothing here mutates on enter. enter reads — it scopes the history to a ref, opens a commit, or shows a stash. The verbs that act are lettered and each row offers only the ones that would work on it.

tab REFS → HISTORY → the commit body, which joins the cycle only once it holds one
j k, PgUp PgDn, home end move
enter scope, or read
c m d checkout · merge · delete a branch
x drop a stash, remove a worktree, delete a tag
y v p copy the sha · revert · cherry-pick
f fetch, on a remote row
esc widen the scope back to every ref; in the body, close it
g r ? the git menu · refresh · keys

A branch checked out elsewhere offers no c, and neither does a remote branch — checking one out properly means creating a local branch that tracks it, and a verb that would only ever fail is not advertised. g is the menu here, which is why this page has no g-for-top.

Empty states are honest about which one they are: REFS says not a git repository once it has loaded and loading… before, and the body says Enter on a commit to read it, loading…, or no workspace open.

DOCKER#

alt-c — containers take c because alt-d is detach. The list column holds this project's compose stacks and their containers; the logs column beside it follows whichever the cursor is on.

A stack whose compose working directory is at, under or over the workspace's cwd is "mine". Stopped stacks are dropped; when anything belongs to this project, everything else is dropped too, because a page listing every container on the machine is a machine inspector and this is a workbench. When nothing matches it falls back to showing them all, so the page is never mysteriously empty — and with nothing running at all it says no running containers.

A multi-container stack is a ▾ header with its containers under it. A one-container stack is its container, so its header wears the container's own dot — ● running, ○ not — rather than being listed twice. The right of a header carries up or 3/5.

j k move
enter follow the logs
r x restart · stop — these stay in the rail rather than taking the stage, because docker restart exits in a second
s a shell in the container, which goes on the stage with the agents
q esc close the page

The logs are an ordinary process pane running docker logs -f. There is no docker client in the client and no docker message on the wire — which is exactly why this page works unchanged against a daemon on another machine.

The diff#

Reached from the CHANGES rail (d, or enter), and left by staging anything else. It sits on the stage with the rails still beside it, so j/k on the rail walks files and the diff follows.

] [ next · previous hunk
space stage the hunk — or unstage it, on a staged diff
v line-select; space picks a line, enter applies the picked ones, v or esc backs out
x discard the hunk
j k, PgUp PgDn, g G scroll
r refresh
q esc close the page — not the session

A commit's diff is history: it offers navigation and nothing else, rather than verbs that would fail. enter does nothing in read mode rather than quietly meaning "the whole hunk", which is the one mistake a partial-staging tool must not make.

SETTINGS#

alt-s, or [settings] in the footer. This client's own configuration, as a page you enter, change and leave. A groups column on the left, the settings themselves on the right.

Every row names the TOML key it writes, drawn faint beside the label, so the page and the file are never two vocabularies for one setting. There is no Save button: a change applies and is written when you make it, and toml_edit rewrites one key and leaves every comment and unrelated table alone.

group rows
APPEARANCE [theme] name, and the themes directory as a fact
AGENTS [general] default_agent, and the daemon's configured agent types
WORKBENCH [ui] left_rail, right_rail, procs_height, system_height
MACHINES [general] remote_auto_attach, then a block per machine, then add a machine
KEYS the prefix, and how many keys are bound and how many came from your config
ABOUT version, the config path, the socket path

Moving the cursor onto a theme in the open list applies it to the whole workbench, live, and leaving without choosing puts the old one back. That is the feature a modal cannot have, because a modal covers the thing you are trying to look at — and it is why this is a page. The fifteen colour roles are drawn as swatches under the APPEARANCE rows and nowhere else.

j k rows, or options inside an open list
tab S-tab groups — only while nothing is expanded
enter open a list, or choose from it — or, on one of MACHINES' action rows, do what it says
space toggle
- + (and h l, ← →) adjust a size
0 back to automatic
r in MACHINES: re-read the blocks, and ask every connected daemon its version again
esc q close the list first, the page second

A size row cannot be typed past the floor a drag stops at: both gestures go through the same clamp, so a rail you can type is a rail you could have dragged to.

MACHINES#

The one group that is not only a view of the file. Its first row is [general] remote_auto_attach and its last is add a machine, which opens the same machines picker alt-h does; between them is a block per machine this client knows of — the ones in the tab bar and the [[remote]] blocks that are not, which is the list nothing else in the client puts in one place.

A machine's name heads its block, and its value is what that machine is doing and what it is carrying while it does it: connected — 3 agents, 2 workspaces, away — last seen with 3 agents, 2 workspaces, connecting…, or not connected — <the last dial failure>. The counts stay when a machine goes away, and say what they are: it is still running everything it was running, and the last numbers it sent are a better answer than nothing at all.

Under the name, indented two columns, are version and where. version is the build the daemon named at its handshake — 1.3.0, or 1.3.0 — 1.3.1 available when this client's own update check has seen a newer one, or 1.3.0 → 1.3.1, restarting once it has accepted an update and is going down. "Newer" there is this client's answer, and a daemon follows the release channel configured where it runs, so a machine on the dev track can be ahead of a stable client that thinks it is behind. where is the ssh destination, or the socket already forwarded here. A machine that is not answering has no handshake to read, and its version says unknown rather than dressing that up.

Then the two or three things you can do about it. These are not settings and have no value to report — an ssh takes several seconds and a toggle that flips back when it fails is worse than a row that plainly says what it will do — so their right-hand column is the sentence describing the act, and the footer offers the row's own verb rather than the word "act".

row offered on what enter does
update any machine that is answering On another machine, POST /v1/update: that daemon fetches its own build, replaces its binary and restarts, and a confirm box names the machine first. On this one it is the client's own update — the same question :update asks — because a local daemon is spawned from the binary you are running.
disconnect a machine in the tab bar, on an ssh this client opened Kills the forward, drops the machine out of the tab bar, and removes its [[remote]] block. Both halves, in that order: a disconnect that left the block behind came back on the next attach and read as having quietly undone itself. The far daemon keeps running.
connect a configured machine that is not here, with a destination to dial The same dial the machines picker runs, arguments and remembering and all. The row goes to connecting… on the press rather than waiting for the ssh to land.
forget a configured machine that is not here Removes its [[remote]] block and nothing else, so it stops being dialled every morning. Nothing on the machine is touched.

The rows a machine does not get are the point of the shape. An unreachable machine gets no update and no disconnect, because both are requests to a daemon that has to answer them and a row that can only ever report a refusal reads as broken. A machine reached over somebody else's ssh -L gets a link row saying on a forward of its own instead of a disconnect, because there is no ssh of ours to kill — the same rule the machines picker follows. The daemon on this machine gets update alone: it is never dialled and never dropped.

update on another machine is refused unless that machine opted in. [update] allow_remote lives in the daemon's own config.toml and is off by default; the refusal is the ordinary answer rather than a fault, so the flash names the machine and the key instead of reporting a 400. See [update] and Remote machines for why the default is what it is.

What keeps up on its own, and what r is for. Which machines are connected and what each is carrying is rebuilt whenever the fleet moves, so the head rows follow a machine going away or coming back without being asked. The two things that do not are the [[remote]] blocks and the versions. The blocks are read off disk, and each connected daemon is asked for its build by opening a control connection and taking the version off its handshake — there is no REST route that reports one — and both happen once, the first time you open the page, and after that only on r. A daemon's version changes when it restarts, and half a dozen socket round trips a frame to watch for something that happens twice a week is not a trade worth making.

disconnect and forget re-read the blocks themselves, so a section still listing a machine whose block you just removed cannot happen. r is bound in this group and nowhere else on the page: every other group is a view of a file this page has just written, so a refresh key on one would be a key that does nothing.

HELP#

?, C-b ?, or [help]. butai's own reference, as a page. A contents column of twelve topics, the topic beside it, page 4 of 12 at the bottom of the contents and more below at the right of the verb row when there is more to read.

Nothing here is a file. The topics are compiled into the binary, so the page opens with no daemon in the loop, reads the same over ssh as locally, and has no path, no save and no editor. The prefix key is substituted at draw time, so the page never prints a key that is a lie on a changed config.

j k scroll
space f · b, PgDn PgUp a screen at a time, with two rows of overlap
home end (g G) top · bottom
tab n l → · S-tab p h ← next · previous topic
esc q, or [help] again close, back to the page you came from

Clicking a topic in the contents does what tab would; clicking a tab, a space or [help] on the two bars is the other way out — both bars stay the workbench's on this page and on SETTINGS.

Overlays#

Exactly one at a time, by construction: an overlay is a question the interface is asking, and two questions at once has no sensible answer. It takes the keyboard and the pointer — a click on one of its rows picks it, and a click anywhere outside dismisses. There is one renderer and one hit test for all of them, so a click lands on the line it looks like it lands on.

Lists — j/k, enter to choose, esc/q to dismiss, wheel to move.

list opened by notes
agents to spawn A, alt-enter, a with nothing pinned d pins the highlighted one
branches b, C-b b the current one is marked
machines alt-h, the machines button connected ones first, marked *; then ssh aliases; then a row to type one
which machine opening a workspace with more than one connected
a folder n, alt-n, [+ new] plus [open this folder], [new folder], ..
the git menu g, C-b g two levels: groups, then a group's rows with .. back
a row's menu right-click, or m agent, process, or workspace tab

The git menu is seven groups — Branch, Remote, Stash, Integrate, Fixup, Worktree, Tag — each reachable by a letter. Mid-sequence it offers only the way out: continue, abort, skip, and nothing that would tangle a stuck repository further. push --force-with-lease, reset --hard and abort confirm before their picker; deleting a branch, a tag, a stash, a remote or a worktree confirms after it, because only then is there a name to put in the question.

Prompts are a single line with a caret, a title, and a subtitle under it: a commit message, a branch name, a tag, a worktree branch, an ssh destination, a new folder's name, or the : command prompt. They claim every printable key, q included, so nothing can steal a character out of a commit message.

Confirmations spell out what is about to happen, then no and yes, with no selected — the keystroke that throws work away is never the one that opened the box. y answers directly, n dismisses, tab/j/k toggles, enter takes the selected answer.

Find (/, alt-/, [find]) is a prompt and a list in one box, because they are one action: every keystroke narrows the list. It says searching… while a query is in flight — a grep over a large tree is not instant, and a list that has not caught up otherwise looks like a list with no matches — and (nothing found) when there is genuinely nothing. A hit reads path:line preview and opens the file there.

LAYOUT and zen#

alt-l turns the arrows into rail resizing and says so in the footer:

The footer shows the current rail width and section height. Press esc to save.

←/→ move the focused rail's width by two cells, ↑/↓ the focused section's height. Which rail the arrows widen follows from which section you are in, so the two questions have one answer. AGENTS grows by taking from PROCESSES; PROCESSES takes from SYSTEM, so AGENTS stays where you last put it; CHANGES is the whole right rail, so only its width moves. Rail growth is capped so the stage keeps its minimum — otherwise growing a rail on a narrow terminal trips the fallback that collapses both, and the key appears to do the opposite of what it says. esc or enter leaves and writes [ui], but only if something actually moved.

Layout is workbench-wide, not per project: one gesture moves every tab, which is why the HUD says so.

alt-z is zen — both rails collapse to four-column status strips. The tab bar is untouched, so the spaces button is still there to leave by; it used to take the view rail with it, which meant zen changed the layout twice over. The left strip is one marker per agent then per process:

A! waiting
A~ working
A* finished
A idle
Ax exited
P✓ P✗ P· a process ok or done · failed · anything else

The right strip is the change count, C6. There is no spinner in zen: the strip is a glance, not a display, and animating a four-column strip would repaint the screen for something nobody is reading.

States you will hit#

Empty rails. The cursor stays at zero and the verbs that act on a row do nothing rather than reporting a failure about a pane that was never named. An empty CHANGES rail draws its box and no verb row at all, so there is nothing to click.

No repository. The CHANGES box draws with its plain CHANGES title and no rows. On the GIT page, REFS says not a git repository — once it has loaded; before that it says loading…, because "this repository has no branches" and "the answer has not arrived" are different facts.

No daemon. Starting with nothing to connect to fails with no daemon to connect to, or no daemon answered (…) naming each socket it tried. One unreachable machine among several is not fatal — a forwarded socket whose tunnel is down is the ordinary case — so the others come up and the failures are flashed. If the connection drops later the session ends with daemon connection lost, and a lost event on one of several daemons flashes daemon: … and leaves the rest running. A machine reached over ssh goes further: its forward is rebuilt and it comes back in the tab it had, saying <host> went away — reconnecting and then <host> is back. See Remote machines.

A daemon of a different build. The handshake carries the server's version; a mismatch flashes both numbers. Not fatal — the wire is additive — but it is the whole difference between "butai is broken" and "restart the daemon".

An exited pane. An agent keeps its row, greyed, with exit or exit 3 in danger, until x dismisses it — its final output is still one enter away. A process that exits non-zero keeps FAIL(2) for the same reason. If the pane the stage was streaming goes away, the stage empties rather than the client wedging on a dead connection. Restarting a process allocates a new pane id, and the stage lets go of the old one rather than showing an empty box until the next tab switch.

An agent needing attention. Four places say so at once, at four altitudes: WAIT on its rail row, n! on the AGENTS row of the spaces menu, ! on its workspace chip and on the booth chip, and ● codex is waiting in the footer — with · alt-w appended on a page that has hidden the AGENTS rail, because there the footer is the only thing naming which agent it is. On BOOTH it also appears on its fleet row. The daemon rings a bell through to your terminal too.

A workspace whose directory disappeared — an unmounted share, a dropped VPN, a hung NFS mount. Reads, stats and git calls on it block in the kernel and cannot be interrupted, so the daemon runs all of them off its actor thread; the rest of the workbench keeps repainting and every other workspace stays usable. The calls that do touch it fail or hang, and the footer names them (tree: …, diff: …).

A key that is bound to nothing here. A prefix followed by an unbound key says M-y is not bound rather than being silently swallowed — an unbound key after a prefix is a typo, and saying so is how you find out the binding you thought you had is not there. A key bound to something from the free-pane model butai dropped says why: not in this workbench: the stage holds one pane.

A mistyped [keys] entry is a warning flashed at startup and listed on the SETTINGS page, not a refusal to start.

The terminal client and the browser client#

Both clients use the daemon's REST API for workspace data and its framed protocol for a live pane. The terminal client draws terminal cells; the browser client draws its own React interface and mounts a canvas-based screen for the pane. Its HELP page describes the browser's available keys.

    ↑↓ move ↵ open esc close