feat(tui): auto-fit the to-do strip to its step count

The top work strip rendered at a fixed persisted height regardless of
content: a two-step plan floated in a band of blank water, and a long
plan scrolled inside a short window (user request, 2026-07-23 session).

height() now fits the content — the selectable rows plus the pinned
progress receipt and the divider — and treats the persisted
work_surface_top_height as a cap rather than a fixed size, together
with the existing half-terminal cap. Drag-resizing the divider and the
settings key keep working; they now bound how tall the strip may grow.
The default cap rises from 3 to 8 so a typical plan is visible in full,
matching the requested ceiling, while short terminals still clamp to
half their height.

Verified: cargo test -p codewhale-tui --bin codewhale-tui —
work_surface 57/57 (new top_strip_auto_fits_step_count_up_to_caps
pins content-fit, cap-wins, short-terminal, and empty cases), tui::ui
625/625, settings 104/104.
This commit is contained in:
Hunter B
2026-07-23 18:15:58 -07:00
parent 2011b9b11c
commit 1211f8b563
3 changed files with 44 additions and 2 deletions
+3 -1
View File
@@ -448,7 +448,9 @@ impl Default for Settings {
fancy_animations: true,
ocean_treatment: "ombre".to_string(),
work_surface_placement: "top".to_string(),
work_surface_top_height: 3,
// Cap, not fixed height: the top strip auto-fits its rows and
// only grows to this many lines (user request, 2026-07-23).
work_surface_top_height: 8,
work_surface_side_width: 30,
constrained_frame_rate: false,
bracketed_paste: true,
+28
View File
@@ -493,6 +493,34 @@ mod tests {
assert_eq!(app.work_surface.hitboxes[0].row_y, 1);
}
#[test]
fn top_strip_auto_fits_step_count_up_to_caps() {
// Two steps: divider + progress receipt + 2 rows = 4 lines, not a
// fixed-height band of blank water.
let mut two_steps = app();
two_steps.work_surface.top_height = 8;
add_todos(&mut two_steps, 2);
assert_eq!(super::height(&mut two_steps, 100, 40, false), 4);
// Ten steps: content wants 12 lines, the default 8-line cap wins.
let mut ten_steps = app();
ten_steps.work_surface.top_height = 8;
add_todos(&mut ten_steps, 10);
assert_eq!(super::height(&mut ten_steps, 100, 40, false), 8);
// Short terminal: the half-terminal cap beats both content and the
// configured cap.
let mut short_terminal = app();
short_terminal.work_surface.top_height = 8;
add_todos(&mut short_terminal, 10);
assert_eq!(super::height(&mut short_terminal, 100, 12, false), 6);
// Nothing to show: no strip at all.
let mut empty = app();
empty.work_surface.top_height = 8;
assert_eq!(super::height(&mut empty, 100, 40, false), 0);
}
#[test]
fn minimum_top_surface_keeps_a_numbered_todo_selectable() {
let mut app = app();
+13 -1
View File
@@ -55,10 +55,22 @@ pub fn height(app: &mut App, width: u16, terminal_height: u16, classic_shell: bo
if app.work_surface.effective_placement != WorkSurfacePlacement::Top {
return 0;
}
// The strip auto-fits its content: the literal selectable list plus the
// pinned progress receipt and the divider row. `top_height` (drag-resize
// / settings) and half the terminal act as caps, so a two-step plan
// takes two rows while an eight-step plan grows to show all eight —
// never a fixed-height band of blank water.
let terminal_cap = terminal_height
.saturating_div(2)
.clamp(super::model::TOP_HEIGHT_MIN, super::model::TOP_HEIGHT_MAX);
app.work_surface.top_height.min(terminal_cap)
let cap = app.work_surface.top_height.min(terminal_cap);
let selectable = rows.iter().filter(|row| row.selectable).count();
let progress = u16::from(top_todo_progress(app, &rows).is_some());
let desired = u16::try_from(selectable)
.unwrap_or(u16::MAX)
.saturating_add(progress)
.saturating_add(1);
desired.clamp(super::model::TOP_HEIGHT_MIN, cap)
}
/// Split the transcript slot for a side rail. Top placement consumes its own