Skip to content

tui: give a project header its own structure in full mode - #1165

Merged
edwin-zvs merged 6 commits into
mainfrom
list-project-row-spacing
Aug 2, 2026
Merged

tui: give a project header its own structure in full mode#1165
edwin-zvs merged 6 commits into
mainfrom
list-project-row-spacing

Conversation

@edwin-zvs

@edwin-zvs edwin-zvs commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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_height is 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

  • Before/after screenshots for each commit are posted as PR comments, recorded on isolated daemons with the build id visible in the modeline.
  • Spec 0106-session-list-view-modes.md updated: 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.
  • Regression tests: 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's first_line lands on the title row, not the gap).
  • cargo test --workspace green.

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.
@edwin-zvs

Copy link
Copy Markdown
Contributor Author

Before / after — sidebar session list, full mode

Demo Project with two members, plus one ungrouped session above it.

Before — the header sits flush against api-server, and the ├─ rail under it starts in mid-air:

After — the header gets the same closing row a session card ends with, carrying the stem down into its first member:

Recorded with vhs against isolated daemons: before from 8b586d4 (.claude/worktrees/list-project-row-before), after from 52930e8. The media commit is removed in the next push; the raw links stay live via refs/pull.

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.
@edwin-zvs

Copy link
Copy Markdown
Contributor Author

Second commit: a normalized two-row margin above a project (full mode)

Same fixture both sides: an ungrouped scratch card, an 1 archived disclosure row, then two projects — so one header follows an archived row (which ends flush, no gap at all) and the other follows a session card (which already ends in one blank row). Recorded on isolated daemons; the modeline build id identifies each side.

Before52930e8 (this PR's first commit): Second Project sits flush under 1 archived, Demo Project one row under docs-site. Two projects, two different gaps, neither reading as a section break.

before

After8c0fe2c: both open on two rows of air, however each one was paid for.

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 .pr-media/ commit on this branch, pinned by SHA above; the next commit removes the directory.)

@edwin-zvs edwin-zvs changed the title tui: cap a project header's member rails in full mode tui: give a project header its own structure in full mode Aug 2, 2026
@edwin-zvs
edwin-zvs merged commit 67b2cf4 into main Aug 2, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the list-project-row-spacing branch August 2, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant