From f4f341d13394a98414e69308a58a52a8c0baeb22 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Tue, 31 Mar 2026 15:51:49 -0700 Subject: [PATCH] feat(claudbg-nypt): Space/PageDown page down, Shift+Space/PageUp page up in TUI Co-Authored-By: Claude Sonnet 4.6 --- ...wn-scrolls-down-shiftspacepageup-scroll.md | 13 +++ src/tui/modals/help_modal.rs | 4 +- src/tui/run.rs | 8 ++ src/tui/screens/transcript.rs | 84 ++++++++++++++++++- src/tui/state.rs | 6 ++ 5 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 .beans/claudbg-nypt--spacepagedown-scrolls-down-shiftspacepageup-scroll.md diff --git a/.beans/claudbg-nypt--spacepagedown-scrolls-down-shiftspacepageup-scroll.md b/.beans/claudbg-nypt--spacepagedown-scrolls-down-shiftspacepageup-scroll.md new file mode 100644 index 0000000..83847f9 --- /dev/null +++ b/.beans/claudbg-nypt--spacepagedown-scrolls-down-shiftspacepageup-scroll.md @@ -0,0 +1,13 @@ +--- +# claudbg-nypt +title: Space/PageDown scrolls down, Shift+Space/PageUp scrolls up in TUI +status: completed +type: feature +priority: normal +created_at: 2026-03-31T22:44:51Z +updated_at: 2026-03-31T22:51:43Z +--- + +Pressing Space should scroll down one page in TUI (like less/more). Shift+Space scrolls up one page. PageDown/PageUp should also scroll by page. + +## Summary\n\n- Added to , set each frame via \n- / scroll down one page; / scroll up one page\n- Updated block title hint and help modal (⇧Spc/PgUp entry)\n- 6 new tests diff --git a/src/tui/modals/help_modal.rs b/src/tui/modals/help_modal.rs index 96f762b..631b6ad 100644 --- a/src/tui/modals/help_modal.rs +++ b/src/tui/modals/help_modal.rs @@ -16,7 +16,7 @@ use crate::tui::state::AppState; /// Dialog dimensions. const DIALOG_WIDTH: u16 = 32; -const DIALOG_HEIGHT: u16 = 15; +const DIALOG_HEIGHT: u16 = 17; /// Compute a centered [`Rect`] of the given size within `area`. fn centered_rect(width: u16, height: u16, area: Rect) -> Rect { @@ -37,6 +37,8 @@ fn centered_rect(width: u16, height: u16, area: Rect) -> Rect { const HELP_TEXT: &str = "\ Navigation\n\ \u{2191}/\u{2193} k/j scroll up/dn\n\ + Spc/PgDn page down\n\ + \u{21e7}Spc/PgUp page up\n\ \u{2190}/\u{2192} h/l scroll lr\n\ Tab cycle panes\n\ Enter open/select\n\ diff --git a/src/tui/run.rs b/src/tui/run.rs index 1a64343..ed02c7e 100644 --- a/src/tui/run.rs +++ b/src/tui/run.rs @@ -226,6 +226,14 @@ pub fn run_tui() -> Result<()> { // Load transcript data lazily when entering a transcript screen. maybe_load_transcript(&mut state); + // Record the visible chat-log height before rendering so that + // Space / PageDown / PageUp can scroll by exactly one page. + // Layout: 4-row stats header + 2 border rows = 6 fixed rows. + state.transcript_page_height = guard + .terminal + .size() + .map(|s| s.height.saturating_sub(6)) + .unwrap_or(0); guard.terminal.draw(|f| render(f, &state))?; if event::poll(Duration::from_millis(50))? { diff --git a/src/tui/screens/transcript.rs b/src/tui/screens/transcript.rs index 1a308b2..2d763b1 100644 --- a/src/tui/screens/transcript.rs +++ b/src/tui/screens/transcript.rs @@ -5,7 +5,7 @@ //! a scrollable chat log below. use ratatui::Frame; -use ratatui::crossterm::event::{Event, KeyCode, KeyEventKind}; +use ratatui::crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span, Text}; @@ -301,7 +301,7 @@ pub fn render_transcript(f: &mut Frame, area: Rect, state: &AppState) { }; let chat_block = Block::default() - .title(" Transcript [↑/↓ scroll · ←/→ h-scroll · Tab focus · Esc back · ? help] ") + .title(" Transcript [↑/↓ scroll · Spc/PgDn page · ←/→ h-scroll · Tab · Esc · ?] ") .borders(Borders::ALL) .border_style(chat_border_style); @@ -423,6 +423,23 @@ pub fn handle_transcript_event(event: Event, state: &mut AppState) -> bool { true } }, + // Page scrolling — always acts on the chat log, matching less/more. + // Shift+Space must come before the plain-Space arm so the guard fires first. + KeyCode::Char(' ') if key.modifiers.contains(KeyModifiers::SHIFT) => { + let page = (state.transcript_page_height as usize).max(1); + state.transcript_scroll = state.transcript_scroll.saturating_sub(page); + true + } + KeyCode::PageUp => { + let page = (state.transcript_page_height as usize).max(1); + state.transcript_scroll = state.transcript_scroll.saturating_sub(page); + true + } + KeyCode::PageDown | KeyCode::Char(' ') => { + let page = (state.transcript_page_height as usize).max(1); + state.transcript_scroll = state.transcript_scroll.saturating_add(page); + true + } // Horizontal scroll — only meaningful in ChatLog. KeyCode::Left | KeyCode::Char('h') => { state.transcript_h_scroll = state.transcript_h_scroll.saturating_sub(1); @@ -947,6 +964,69 @@ mod tests { assert!(state.show_help); } + fn press_with_mod(code: KeyCode, mods: KeyModifiers) -> Event { + Event::Key(KeyEvent { + code, + modifiers: mods, + kind: KeyEventKind::Press, + state: KeyEventState::NONE, + }) + } + + #[test] + fn space_scrolls_down_by_page() { + let mut state = transcript_state(); + state.transcript_page_height = 20; + handle_transcript_event(press(KeyCode::Char(' ')), &mut state); + assert_eq!(state.transcript_scroll, 20); + } + + #[test] + fn pagedown_scrolls_down_by_page() { + let mut state = transcript_state(); + state.transcript_page_height = 15; + handle_transcript_event(press(KeyCode::PageDown), &mut state); + assert_eq!(state.transcript_scroll, 15); + } + + #[test] + fn shift_space_scrolls_up_by_page() { + let mut state = transcript_state(); + state.transcript_page_height = 10; + state.transcript_scroll = 25; + handle_transcript_event( + press_with_mod(KeyCode::Char(' '), KeyModifiers::SHIFT), + &mut state, + ); + assert_eq!(state.transcript_scroll, 15); + } + + #[test] + fn pageup_scrolls_up_by_page() { + let mut state = transcript_state(); + state.transcript_page_height = 10; + state.transcript_scroll = 25; + handle_transcript_event(press(KeyCode::PageUp), &mut state); + assert_eq!(state.transcript_scroll, 15); + } + + #[test] + fn pageup_clamps_at_zero() { + let mut state = transcript_state(); + state.transcript_page_height = 20; + state.transcript_scroll = 5; + handle_transcript_event(press(KeyCode::PageUp), &mut state); + assert_eq!(state.transcript_scroll, 0); + } + + #[test] + fn page_scroll_uses_one_when_height_is_zero() { + let mut state = transcript_state(); + state.transcript_page_height = 0; + handle_transcript_event(press(KeyCode::Char(' ')), &mut state); + assert_eq!(state.transcript_scroll, 1); + } + #[test] fn unhandled_key_not_consumed() { let mut state = transcript_state(); diff --git a/src/tui/state.rs b/src/tui/state.rs index de6503f..f75dc95 100644 --- a/src/tui/state.rs +++ b/src/tui/state.rs @@ -109,6 +109,11 @@ pub struct AppState { pub transcript_scroll: usize, /// Horizontal scroll offset (columns from the left edge of the chat log). pub transcript_h_scroll: usize, + /// Height of the visible chat-log content area in rows (set each frame). + /// + /// Used by Space / PageDown / PageUp to scroll by exactly one page. + /// Zero until the first frame is drawn. + pub transcript_page_height: u16, // ── Sub-agents panel ──────────────────────────────────────────────────── /// Sub-agent references for the currently viewed session. @@ -207,6 +212,7 @@ impl AppState { transcript_entries: Vec::new(), transcript_scroll: 0, transcript_h_scroll: 0, + transcript_page_height: 0, subagents: Vec::new(), subagent_selected: 0, focus: Focus::default(),