fix: Menu Viewport Scrolling#28
Conversation
Menus taller than the terminal broke arrow-key navigation and leaked scrollback on every repaint. The shared renderer now windows rows to terminal height (term.GetSize, 24-row fallback), tracks a scroll offset in menuState, and shows up/down overflow indicators — fixing all 11 call sites through the single ShowMenu/MenuSession primitive.
There was a problem hiding this comment.
Pull request overview
This PR fixes interactive menus that are taller than the terminal by introducing a terminal-height-aware scrolling viewport inside the shared menu renderer, ensuring arrow-key navigation remains usable without repainting into scrollback.
Changes:
- Added a pure
menuLayoutviewport calculator andmenuState.topwindow offset, with overflow indicators and a “drop chrome on cramped heights” rule to maintain a safe repaint footprint. - Threaded an injectable
heightFn(production usesterm.GetSizewith a 24-row fallback) through the interactive render path so windowing is PTY-free testable. - Expanded unit tests to cover layout invariants, byte-equality between paint/redraw, shrink hardening, and the
GetSizefallback; updated CLI memory docs to reflect the new viewport contract.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/internal/worktree/menu.go | Implements viewport windowing (layout + rendering) and injects terminal height into the interactive menu core. |
| src/internal/worktree/menu_test.go | Adds comprehensive PTY-free tests for windowing invariants, rendering, shrink hardening, and height fallback. |
| fab/changes/260705-3wr8-menu-viewport-scrolling/plan.md | Captures requirements, non-goals, and acceptance criteria for the viewport change. |
| fab/changes/260705-3wr8-menu-viewport-scrolling/intake.md | Records the diagnosed bug, chosen approach, and explicit rejections/out-of-scope items. |
| fab/changes/260705-3wr8-menu-viewport-scrolling/.status.yaml | Tracks fab pipeline status/metrics for this change. |
| fab/changes/260705-3wr8-menu-viewport-scrolling/.history.jsonl | Records fab stage transition history for auditability. |
| docs/memory/wt-cli/menu-navigation-contract.md | Updates the documented menu navigation and redraw/viewport invariants (including PageUp/PageDown staying ignored). |
| docs/memory/wt-cli/index.md | Updates the memory index entry to reflect the viewport-enhanced contract description. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // rowsRendered tracks how many lines the menu currently occupies on | ||
| // screen so the next redraw knows how far up to move the cursor. | ||
| // The menu region is: 1 prompt line + len(options) option rows + | ||
| // 1 Cancel row = len(options) + 2 lines. | ||
| rowsRendered := paintMenu(w, prompt, options, state) | ||
| // screen so the next redraw knows how far up to move the cursor. It is the | ||
| // *windowed* row count (1 prompt + indicators + visible options + 1 Cancel) | ||
| // and never exceeds the terminal height — this is what keeps the cursor-up | ||
| // in-place redraw sound for lists taller than the terminal. |
There was a problem hiding this comment.
Fixed — aligned the rowsRendered doc comment with menuLayout's invariant: the footprint stays within height-1 (one reserved row), overshooting only at degenerate heights 1–3 via the ≥1-option escape hatch. (dbe1100)
| // paintMenu writes the (windowed) menu region for the first time and returns | ||
| // the number of lines written (so redraw / finalize know how far to move up). | ||
| // height drives the scrolling viewport; the returned count is the windowed | ||
| // total (prompt + indicators + visible options + Cancel) and never exceeds | ||
| // height, keeping the cursor-up in-place redraw sound for over-tall lists. |
There was a problem hiding this comment.
Fixed — corrected paintMenu's doc comment to state the returned count stays within menuLayout's budget of height-1 (one reserved row), except at degenerate heights 1–3 where the ≥1-option escape hatch overshoots. (dbe1100)
Meta
3wr8excludes
fab/,docs/· generated by fab-kit v2.13.4Pipeline: intake ✓ → apply ✓ → review ✓ → hydrate ✓ → ship → review-pr
Summary
The interactive arrow-key menu renderer had no concept of terminal height: menus taller than the terminal scrolled into scrollback on first paint, and the cursor-up in-place redraw couldn't reach rows that had scrolled off, so repaints re-emitted the whole menu on every keystroke and earlier rows became permanently unreachable with arrow keys. This adds an fzf/gum-style scrolling viewport to the shared renderer so
rowsRenderednever exceeds terminal height, restoring full arrow-key reachability across all menus.Changes
menuStaterenderRows+ overflow indicators