feat(claudbg-nypt): Space/PageDown page down, Shift+Space/PageUp page up in TUI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
Elijah Voigt 2 months ago
parent 9be62abb78
commit f4f341d133

@ -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

@ -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\

@ -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))? {

@ -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();

@ -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(),

Loading…
Cancel
Save