# 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: 1. **What needs me?** — `NEEDS YOU` 2. **What am I running?** — `WORKSPACE` 3. **What did I set aside?** — `DETACHED` 4. **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` 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. - **`HOSTS` beyond the local machine.** Two states only, live or not. Connecting, retrying and unreachable are unbuildable until `lumbridge-remote` exists, and shipping them would be the Buzz card again in a Rust enum. - **Accessibility.** `describe` produces 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.