SIGN IN SIGN UP

feat: btop-style multi-row history graphs in the local Activity panel (#277)

## Summary

Part 3 of the sparkline readability chain. On tall terminals (35+ rows) the local Activity panel now renders btop-style multi-row history graphs for the main utilization time series, using the multi-row braille API (#272) and the soft auto-range / scale badges (#273). On shorter terminals both halves keep the current compact single-row layout so the process list is not starved, and remote mode is untouched.

## What changed

- `src/ui/activity_panel.rs`: added the shared `use_multirow_graphs(terminal_rows)` predicate (single source of truth for the mode decision), `GRAPH_ROWS`/`MULTIROW_MIN_TERMINAL_ROWS` constants, and a reusable `multirow_graph_lines` helper. `panel_height` now takes `rows` and, in multirow mode, reserves a 3-row CPU total-utilization history graph (12 dot levels, fixed 0-100 axis) above the core bars, with the current value at the top-right and a 0-100 axis label at the bottom-right. `render_activity_panel` takes the CPU history as a `&[f64]` slice (kept decoupled from `AppState`) plus `terminal_rows`.
- `src/ui/gpu_sparkline_panel.rs`: `gpu_content_rows` and `render_gpu_lines` take `terminal_rows`; in multirow mode GPU Util becomes a matching 3-row graph and the remaining metrics collapse to compact two-per-line rows (Mem + Temp, then ANE + Pkg Power on Apple Silicon, Pkg Power alone without ANE, or NPU + Pkg Power when the NPU scaffolding row is active). Each compact half-cell keeps a single-row sparkline with the #273 soft range and its `scale_badge`; all width math is saturating and panic-free. `render_combined_activity_panel` threads `terminal_rows` and hands the CPU history down to the left half.
- `src/ui/layout.rs`: `calculate_content_area` passes `rows` into both height functions so the reserved height uses the same mode decision as the renderer.
- `src/view/frame_renderer.rs`: passes `rows` into `render_combined_activity_panel`.

## Critical invariant

The height functions (`panel_height`, `gpu_content_rows`) and the actual rendered line counts derive from the same `use_multirow_graphs` predicate and the same `terminal_rows`, so the reserved height can never disagree with the emitted line count. This is what prevents the documented "header disappearing / content scroll" regression. On Apple Silicon with the P/E cluster strategy both halves land at 7 rows (border + 3 graph + 2 content rows + border), so the combined panel does not grow versus today.

## Follow-up fix: narrow-width compact cell overflow

Review found a MEDIUM issue in the initial implementation: at terminal widths 81-82 the combined panel's right half is 41 columns wide, and `draw_compact_cell`'s budget could not hold the minimum field layout (label + sparkline + value + badge). The old code kept emitting all four fixed-width fields regardless of the budget, which leaked one column past the pair's half-width split, pushed the compact row past the panel border, and wrapped the terminal line. `draw_compact_cell` now walks its fields in a loop, clamps each one to the columns actually remaining in the budget, and stops once the budget is exhausted instead of overflowing it, so the emitted width never exceeds `budget`. A new regression test, `test_multirow_lines_fit_panel_width_at_narrow_widths`, renders every line at widths 40-60 (spanning the 81-82 narrow-width case at the half-panel level) across both compact and NVIDIA/Apple Silicon states and asserts every line's visible display width (ANSI-stripped) equals the requested panel width exactly.

## Test plan

- [x] `cargo test --lib ui::activity_panel` (23 passed)
- [x] `cargo test --lib ui::gpu_sparkline_panel` (28 passed)
- [x] `cargo test --lib ui::layout` (8 passed)
- [x] `cargo check --lib --tests`
- [x] `cargo clippy --lib --tests -- -D warnings`
- [x] `cargo fmt --check`

New tests cover: height functions for both modes across all three collapse strategies (Individual / PECluster / SocketGroup), the 7-row Apple Silicon multirow target and the unchanged fallback values; rendered-line-count == height-function output for both halves and the combined panel; no-panic rendering at narrow (81 cols), short (24 rows) and tall (50 rows) sizes with empty histories and a missing GPU; the compact pairing for Apple (ANE), non-Apple (no ANE) and the NPU scaffolding; and the narrow-width display-width regression above.

Closes #274
J
Jeongkyu Shin committed
a61386e86d0c1e975204e83604aacab7bee25240
Parent: e27cd09
Committed by GitHub <noreply@github.com> on 7/20/2026, 2:15:58 AM