@@ -495,6 +495,8 @@ pub struct App {
495495 pub sandbox_providers_list : Vec < String > ,
496496 pub policy_lines : Vec < ratatui:: text:: Line < ' static > > ,
497497 pub policy_scroll : usize ,
498+ /// Visible line count in the policy pane, set during draw for PageUp/PageDown.
499+ pub policy_viewport_height : usize ,
498500
499501 // Create sandbox modal
500502 pub create_form : Option < CreateSandboxForm > ,
@@ -656,6 +658,7 @@ impl App {
656658 sandbox_providers_list : Vec :: new ( ) ,
657659 policy_lines : Vec :: new ( ) ,
658660 policy_scroll : 0 ,
661+ policy_viewport_height : 0 ,
659662 create_form : None ,
660663 pending_create_sandbox : false ,
661664 pending_forward_ports : Vec :: new ( ) ,
@@ -1155,9 +1158,21 @@ impl App {
11551158 KeyCode :: Char ( 'k' ) | KeyCode :: Up => {
11561159 self . scroll_policy ( -1 ) ;
11571160 }
1161+ // Page-scroll by one viewport height.
1162+ KeyCode :: PageDown => {
1163+ let delta = self . policy_viewport_height . max ( 1 ) . cast_signed ( ) ;
1164+ self . scroll_policy ( delta) ;
1165+ }
1166+ KeyCode :: PageUp => {
1167+ let delta = self . policy_viewport_height . max ( 1 ) . cast_signed ( ) ;
1168+ self . scroll_policy ( -delta) ;
1169+ }
11581170 KeyCode :: Char ( 'G' ) => {
1159- // Scroll to bottom.
1160- self . policy_scroll = self . policy_lines . len ( ) . saturating_sub ( 1 ) ;
1171+ // Scroll to bottom, keeping a full viewport visible.
1172+ self . policy_scroll = self
1173+ . policy_lines
1174+ . len ( )
1175+ . saturating_sub ( self . policy_viewport_height . max ( 1 ) ) ;
11611176 }
11621177 KeyCode :: Char ( 'g' ) => {
11631178 self . policy_scroll = 0 ;
@@ -1426,6 +1441,21 @@ impl App {
14261441 self . draft_scroll -= 1 ;
14271442 }
14281443 }
1444+ // Page-scroll by one viewport height, clamping the cursor to
1445+ // stay within the visible range.
1446+ KeyCode :: PageDown if total > 0 => {
1447+ let page = vh. max ( 1 ) ;
1448+ let max_scroll = total. saturating_sub ( vh. min ( total) ) ;
1449+ self . draft_scroll = ( self . draft_scroll + page) . min ( max_scroll) ;
1450+ let visible = total. saturating_sub ( self . draft_scroll ) . min ( vh) ;
1451+ self . draft_selected = self . draft_selected . min ( visible. saturating_sub ( 1 ) ) ;
1452+ }
1453+ KeyCode :: PageUp => {
1454+ let page = vh. max ( 1 ) ;
1455+ self . draft_scroll = self . draft_scroll . saturating_sub ( page) ;
1456+ let visible = total. saturating_sub ( self . draft_scroll ) . min ( vh) ;
1457+ self . draft_selected = self . draft_selected . min ( visible. saturating_sub ( 1 ) ) ;
1458+ }
14291459 KeyCode :: Char ( 'g' ) => {
14301460 self . draft_scroll = 0 ;
14311461 self . draft_selected = 0 ;
@@ -1490,8 +1520,11 @@ impl App {
14901520 }
14911521
14921522 /// Scroll policy pane by a delta (positive = down, negative = up).
1523+ ///
1524+ /// Clamps so at least one viewport of content remains visible.
14931525 pub fn scroll_policy ( & mut self , delta : isize ) {
1494- let max = self . policy_lines . len ( ) . saturating_sub ( 1 ) ;
1526+ let total = self . policy_lines . len ( ) ;
1527+ let max = total. saturating_sub ( self . policy_viewport_height . max ( 1 ) ) ;
14951528 if delta < 0 {
14961529 self . policy_scroll = self . policy_scroll . saturating_sub ( delta. unsigned_abs ( ) ) ;
14971530 } else {
@@ -1608,6 +1641,15 @@ impl App {
16081641 }
16091642 self . log_autoscroll = false ;
16101643 }
1644+ // Page-scroll by one viewport height.
1645+ KeyCode :: PageDown => {
1646+ let delta = vh. max ( 1 ) . cast_signed ( ) ;
1647+ self . scroll_logs ( delta) ;
1648+ }
1649+ KeyCode :: PageUp => {
1650+ let delta = vh. max ( 1 ) . cast_signed ( ) ;
1651+ self . scroll_logs ( -delta) ;
1652+ }
16111653 KeyCode :: Char ( 'G' | 'f' ) => {
16121654 self . log_selection_anchor = None ;
16131655 self . sandbox_log_scroll = self . log_autoscroll_offset ( ) ;
0 commit comments