tui: give a project header its own structure in full mode - #1165
Conversation
Full mode gives every session card a closing rail/breathing row, but a project header — and a service, which heads a subtree the same way — rendered as a single line in both modes. So in full mode a header sat flush against its first member while every card around it floated, and its members' `├─` rails began in mid-air with nothing above to hang from: switching to full mode changed nothing about how a project reads. Give both subtree-heading rows the same closing row a session card ends with, carrying the stem down into the first member. No new data — the header still shows name, attention rollup, and member count. When the header is collapsed or empty the next row isn't a descendant, so the row is a plain breathing row that still separates it from what follows. `list_item_display_height` is the authority for both the display-row → item hit-test map and scroll geometry, so it moves to 2 for these rows in lockstep with the rendered line count.
Before / after — sidebar session list, full mode
Before — the header sits flush against After — the header gets the same closing row a session card ends with, carrying the Recorded with vhs against isolated daemons: |
In full mode a project header sat one blank row below whatever came before it — the same gap two sibling session cards get — so a project read as another item in the stream rather than the start of a section. After an archived-disclosure row, which ends flush, it had no gap at all. Give a full-mode project header a leading margin normalized to two blank rows: count whatever the item above already leaves behind (a card, a service, or another header leaves one; an archived row leaves none) and supply only the difference. Every project then opens on the same amount of air regardless of its neighbor, and one at the top of the list — with nothing to be set apart from — takes none. Compact mode stays packed. The margin counts toward the header's measured height, so the click/hover hit map and scroll geometry account for it and clicking the gap selects the project it belongs to. It renders as its own widget row rather than as part of the header, because ratatui highlights every line of the selected item: folded in, selecting a project would paint two highlighted blank rows above its title. The header's first line — where its gutter affordances live — therefore stays its title row.
Second commit: a normalized two-row margin above a project (full mode)Same fixture both sides: an ungrouped Before — After — The margin counts toward the header's measured height (hit map + scroll geometry stay in sync, and clicking the gap selects the project) but renders as its own widget row — folded into the header, ratatui's whole-item highlight would paint two lit blank rows above a selected project's title. Compact mode is untouched, and a project at the top of the list takes no margin. (The images live in a temporary |




Problem
In the sidebar session list, a project header row renders identically in full mode and compact mode — toggling the view mode does nothing for it.
It's worse than a no-op. Full mode draws tree rails, and a project's members hang off
├─/└─at depth 1 — but the header itself draws nothing in that gutter, so its members' rails begin in mid-air with nothing above them to hang from. And because every session card ends with a breathing row, the header alone sits flush against its first member while everything around it floats.The same is true of a service row, which also parents its children at depth 1.
Above the header, the list was inconsistent in the other direction: a project sat one blank row below a session card — the same gap two sibling cards get, so it read as one more item in the stream rather than the start of a section — and no gap at all below an archived-disclosure row, which ends flush.
Change
Two commits, both full-mode-only; compact mode stays packed by design.
1. Cap the header's member rails. Give both subtree-heading rows the closing row a session card already ends with, carrying the stem (
│) down into the first member. Collapsed or empty headers get a blank row instead — still separating them from what follows. No new data — structure only. No header gains a detail line in either mode; archived-disclosure rows head nothing and stay one line in both modes.2. Open a project on a normalized two-row margin. A project header now carries a leading margin of blank rows, wider than the single row between sibling cards. The gap is normalized, not additive: it counts whatever the item above already leaves behind (a card, a service, or another header leaves one; an archived row leaves none) and supplies only the difference — so every project opens on the same amount of air regardless of its neighbor. A project at the top of the list has nothing to be set apart from and takes none; services head a subtree but not a section, so they don't take one either.
list_item_display_heightis the single authority for both the click/hover row map and scroll geometry, so both changes move in step with the rendered line count — the margin included, which is why clicking the gap selects the project it belongs to. The margin renders as its own widget row rather than as part of the header: folded in, ratatui's whole-item highlight would paint two lit blank rows above a selected project's title. The header's first line — where its gutter affordances live — therefore stays its title row.Notes
0106-session-list-view-modes.mdupdated: the one-line-header rule is replaced by the subtree-heading rule, the margin rule is recorded alongside it, and the mixed-height consequence now notes that an item's height can depend on its neighbor — so any new spacing rule must live in the same measurement the hit map and scroll geometry read.full_mode_project_header_caps_its_member_rails(display height in both modes, project and service, plus the rendered│row);project_margin_normalizes_the_gap_above_a_header(every predecessor kind, both modes);project_margin_renders_above_the_header_without_moving_its_first_line(blank rows really appear, and the hit map'sfirst_linelands on the title row, not the gap).cargo test --workspacegreen.