The rail showed a frozen attention count over a worktree list backed by a crate that does not exist. What replaces it starts from a question rather than from a list of things we happened to know: what needs me, what am I running, what did I set aside, where does it run and what will stop me. Layout is data. sidebar/model.rs holds no renderer types, so which sections exist, what collapsing hides, what the filter keeps, and where the keyboard cursor lands are ordinary tests in CI; sidebar/view.rs renders and decides nothing. Eleven model tests, none of which need a window. The cursor is a RowKey rather than an index, because an index is wrong the moment a row above it disappears and silently pointing at a different row is worse than losing the cursor. Every header renders even when its section is empty, so positions never move under the pointer. The filter's empty state does not quote what was typed — the sidebar is the part of the window people screenshot. One selection language everywhere: before this, attention cards darkened on hover while worktree rows lightened, so the same gesture meant two different things a hundred pixels apart. Two defects the screenshots caught that review had not. Flexbox shrinks proportionally, so the longer string wins: the attention row rendered as "Te… Exited with code 7 · observed", having discarded the one word that says which pane to look at. And three quota rows all read "CLAUDE CODE" with the scope truncated away, naming the same thing three times and identifying none of them. Titles now have a floor and the harness name prints once per group. WORKSPACE is deliberately flat: a Repository → Worktree → Pane tree would need lumbridge-git, and every level above Pane would be a second fixture. The depth field and disclosure column are reserved for when it is real. HOSTS has two states, live or not — connecting and unreachable are unbuildable until lumbridge-remote exists, and shipping them would be the Buzz card again in a Rust enum. The rail drags between 200 and 480 px, applied live so the workspace reflows under the pointer; decision 0009 measures pane thresholds after the sidebar, so widening really can drop three panes to one. PTYs are resized on release only, or every mouse-move is a SIGWINCH storm through the runtime's bounded queues. While the sidebar owns the keyboard, on_key_down returns before encoding anything. Without that guard a bare `j` would be written into whatever pane happened to be selected while the user believed they were walking a list. Not persisted yet, not virtualised, and describe() has nothing to attach to until the accessibility adapter from decision 0017 lands. Recorded in 0021. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.8 KiB
0021: The sidebar answers four questions, and its layout is data
Status: accepted; rebuilt and live.
The old rail showed a frozen ATTENTION · 1 over a five-entry worktree list
naming a developer's machines, and a Buzz card asserting a connection to a crate
the binary did not depend on. All of that is gone (decision 0018's commit and the
truth pass before it). What replaces it starts from a question rather than from a
list of things we happened to know.
The four questions
A workspace multiplexer's rail exists to answer, in this order:
- What needs me? —
NEEDS YOU - What am I running? —
WORKSPACE - What did I set aside? —
DETACHED - Where does it run, and what will stop me? —
HOSTS,QUOTA
Anything that answers none of those does not belong in the rail. That is the test the worktree list failed, and it is the test any future addition has to pass.
WORKSPACE is deliberately flat. A Repository → Worktree → Pane tree is the
obvious shape and every level above Pane would be a second fixture, because
lumbridge-git does not exist. The depth field and the disclosure column are
reserved so the tree can arrive without moving every row.
Layout is data
sidebar/model.rs holds no renderer types. flatten(&SidebarInput, &SidebarState) -> Vec<SidebarRow> is a pure function, so which sections exist,
what collapsing hides, what the filter keeps and where the keyboard cursor lands
are ordinary tests in CI. sidebar/view.rs renders and decides nothing.
Two rules that came out of writing the tests rather than the view:
- The cursor is a
RowKey, not an index. An index is wrong the moment a row above it disappears — a pane exits, a filter narrows — and quietly pointing at a different row than the one the user was looking at is worse than losing the cursor. An unknown key restarts at the top. - Every header renders, even for an empty section, and every empty section says what empty means. Positions then never move under the pointer because something appeared above.
The empty state under a filter is "No match in this section" and does not quote what was typed. The sidebar is the part of the window that gets screenshotted and shared.
One selection language
Rest transparent, hover surface_raised, selected surface_active, keyboard
cursor a one-pixel border — accent when the rail has the keyboard, border when
it does not. Before this the attention cards darkened on hover while the
worktree rows lightened, so the same gesture meant two different things a
hundred pixels apart.
Every row has the same slots — disclosure, indicator, kind, label, meta — always reserved. A row that omitted its indicator column pulled its label left and broke the vertical line the eye follows.
One indicator per row, by precedence: faulted, then waiting, then quota nearly spent, then finished, then working. A row showing two of those tells you to look, which you already knew, and hides which one to act on.
What the layout work actually caught
Flexbox shrinks proportionally, so the longest string wins. The attention row's
reason is longer than its title, and the row rendered as Te… Exited with code 7 · observed — it had thrown away the one word that says which pane to look at.
Titles now carry a minimum width and the meta yields first. Three quota rows all
read CLAUDE CODE with the scope truncated off the end, naming the same thing
three times and identifying none of them; the harness name is now printed once
per group, as the footer already did.
Both were found by screenshot, not by review.
The keyboard
secondary-alt-b toggles, secondary-alt-s moves the keyboard into the rail,
secondary-alt-a jumps to the next pane that is certainly waiting. Inside the
rail, arrows and j/k move, space peeks and stays, enter activates and
hands the keyboard to what it opened, escape leaves.
The critical part is a guard, not a feature: on_key_down returns before it
encodes anything for a terminal while the sidebar owns the keyboard. Without it a
bare j would be written into whatever pane happened to be selected while the
user believed they were walking a list.
Resizing
The rail drags between 200 and 480 pixels, applied live. Widening it really can drop the workspace from three panes to one, because decision 0009 measures its thresholds after the sidebar — that is correct, and hiding it until release would make the result look like a bug. The PTYs are told once, on release: a resize per mouse-move would be a SIGWINCH storm through the runtime's bounded queues.
The drag is owned by the root element rather than by the handle. A child that only sees events inside its own five pixels cannot follow a pointer across the window.
Not in this pass
- Persistence. Width, collapsed sections and visibility return to defaults on restart. They belong in their own snapshot, separate from the workspace blob so the two fail closed independently.
- Virtualisation. The list renders every row. At the number of panes a person has open this is not measurable; it becomes real when a host has fifty.
HOSTSbeyond the local machine. Two states only, live or not. Connecting, retrying and unreachable are unbuildable untillumbridge-remoteexists, and shipping them would be the Buzz card again in a Rust enum.- Accessibility.
describeproduces the sentence for each row and is tested; there is nothing to attach it to until the adapter in decision 0017 lands.
Behaviour studied
Zed's project panel (GPL — behaviour only, no code read for reuse), Zellij's cursor clamping after deletion (MIT), bb's thread-list indicator precedence (MIT), Orca's host list (MIT). Nothing was copied; the row anatomy, the precedence chain and the key-cursor rule are convergent answers to the same problems.