feat(tui-go): page-scroll the diff/preview/command fullscreen viewer

The ^x fullscreen viewer (OverlayDiff) backs every tool preview — split diff
for write/edit, the command card for shell, plain lines otherwise, plus any
transcript tool-output — but only scrolled one line per keypress, making a
long diff tedious to read.

- Add paging to the viewer, matching the OUTPUT free-scroll contract: ↑↓/jk
  by line, PgUp/PgDn by a full body, ^u/^d by half, g/G to the ends. New
  scrollDiff() clamps to [0, diffMaxScroll] so paging past either edge lands
  exactly (no dead presses). diff_scroll_test.go (2): clamp + no-op-when-fits.
- Update the modal hint to advertise paging (line / page / ends / close) and
  add a "diff / preview viewer (^x)" section to the ? help overlay. Also note
  the T3+ confirm-again behaviour in the approval-band help.

Approval ergonomics re-audit (BACKLOG §E): the spec'd band still holds —
single-key y/n/e for T0-T2, mandatory second-key confirm for T3+ (HighTier),
navigable queue with an i/n indicator, docked band never modal-stacked; all
covered green by approval_test.go. The scroll granularity above was the only
gap, now closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 09:09:54 +00:00
parent 08b5c63a0f
commit db27b3436b
3 changed files with 88 additions and 9 deletions
+24 -3
View File
@@ -353,6 +353,20 @@ func (m Model) diffMaxScroll() int {
return max
}
// scrollDiff moves the diff/preview/command modal offset by delta rows, clamped to
// [0, diffMaxScroll] so paging past either end lands exactly on the edge (no dead
// presses unwinding an overshoot — same contract as the OUTPUT free-scroll).
func (m *Model) scrollDiff(delta int) {
off := m.diffScrollOffset + delta
if mx := m.diffMaxScroll(); off > mx {
off = mx
}
if off < 0 {
off = 0
}
m.diffScrollOffset = off
}
func (m Model) diffModal() string {
if cmd := m.pendingCommand(); cmd != nil {
return m.commandModal(cmd)
@@ -390,7 +404,7 @@ func (m Model) diffModal() string {
for _, ln := range lines {
b.WriteString(ln + "\n")
}
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "scroll"}, {"^x/esc", "close"}}))
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "line"}, {"PgUp/Dn", "page"}, {"g/G", "ends"}, {"^x/esc", "close"}}))
modal := t.Overlay.Width(w).Render(b.String())
return m.center(modal)
@@ -424,7 +438,7 @@ func (m Model) commandModal(a *Approval) string {
for i := off; i < end; i++ {
b.WriteString(lines[i] + "\n")
}
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "scroll"}, {"^x/esc", "close"}}))
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "line"}, {"PgUp/Dn", "page"}, {"g/G", "ends"}, {"^x/esc", "close"}}))
return m.center(t.Overlay.Width(w).Render(b.String()))
}
@@ -742,11 +756,18 @@ func (m Model) helpModal() string {
{"G / I", "grants / idea board"},
})
section(&b, "approval band", []kb{
{"y / a / enter", "approve once"},
{"y / a / enter", "approve once (T3+ asks again to confirm)"},
{"n / r", "reject"},
{"e / s", "steer (add a note)"},
{"A", "approve-always — choose scope"},
{"↑ / ↓", "walk the pending queue"},
{"^x", "fullscreen the diff / command preview"},
})
section(&b, "diff / preview viewer (^x)", []kb{
{"↑ / ↓", "scroll a line"},
{"PgUp / PgDn", "scroll a page (^u / ^d half)"},
{"g / G", "jump to top / end"},
{"^x / esc", "close"},
})
b.WriteString(modalHints(t, [][2]string{{"any key", "close"}}))
modal := t.Overlay.Width(w).Render(b.String())