diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index a2b8afb7c..f0e5b3047 100644 --- a/ui-tui/src/app/useInputHandlers.ts +++ b/ui-tui/src/app/useInputHandlers.ts @@ -173,14 +173,60 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { if (isBlocked) { if (overlay.pager) { - if (key.return || ch === ' ') { - const nextOffset = overlay.pager.offset + pagerPageSize + if (key.escape || isCtrl(key, ch, 'c') || ch === 'q') { + return patchOverlayState({ pager: null }) + } - patchOverlayState({ - pager: nextOffset >= overlay.pager.lines.length ? null : { ...overlay.pager, offset: nextOffset } + const move = (delta: number | 'top' | 'bottom') => + patchOverlayState(prev => { + if (!prev.pager) { + return prev + } + + const { lines, offset } = prev.pager + const max = Math.max(0, lines.length - pagerPageSize) + const step = delta === 'top' ? -lines.length : delta === 'bottom' ? lines.length : delta + const next = Math.max(0, Math.min(offset + step, max)) + + return next === offset ? prev : { ...prev, pager: { ...prev.pager, offset: next } } + }) + + if (key.upArrow || ch === 'k') { + return move(-1) + } + + if (key.downArrow || ch === 'j') { + return move(1) + } + + if (key.pageUp || ch === 'b') { + return move(-pagerPageSize) + } + + if (ch === 'g') { + return move('top') + } + + if (ch === 'G') { + return move('bottom') + } + + if (key.return || ch === ' ' || key.pageDown) { + patchOverlayState(prev => { + if (!prev.pager) { + return prev + } + + const { lines, offset } = prev.pager + const max = Math.max(0, lines.length - pagerPageSize) + + // Auto-close only when already at the last page — otherwise clamp + // to `max` so the offset matches what the line/page-back handlers + // can reach (prevents a snap-back jump on the next ↑/↓/PgUp). + return offset >= max + ? { ...prev, pager: null } + : { ...prev, pager: { ...prev.pager, offset: Math.min(offset + pagerPageSize, max) } } }) - } else if (key.escape || isCtrl(key, ch, 'c') || ch === 'q') { - patchOverlayState({ pager: null }) } return diff --git a/ui-tui/src/components/appOverlays.tsx b/ui-tui/src/components/appOverlays.tsx index 0d08c5897..331fb5873 100644 --- a/ui-tui/src/components/appOverlays.tsx +++ b/ui-tui/src/components/appOverlays.tsx @@ -164,8 +164,8 @@ export function FloatingOverlays({ {overlay.pager.offset + pagerPageSize < overlay.pager.lines.length - ? `Enter/Space for more · q to close (${Math.min(overlay.pager.offset + pagerPageSize, overlay.pager.lines.length)}/${overlay.pager.lines.length})` - : `end · q to close (${overlay.pager.lines.length} lines)`} + ? `↑↓/jk line · Enter/Space/PgDn page · b/PgUp back · g/G top/bottom · q close (${Math.min(overlay.pager.offset + pagerPageSize, overlay.pager.lines.length)}/${overlay.pager.lines.length})` + : `end · ↑↓/jk · b/PgUp back · g top · q close (${overlay.pager.lines.length} lines)`}