@@ -1070,10 +1070,104 @@ mod tests {
10701070 . iter ( )
10711071 . map ( |placement| placement. area . y )
10721072 . collect :: < Vec < _ > > ( ) ;
1073- assert_eq ! ( rows, vec![ 3 , 2 , 1 , 0 ] ) ;
1073+ assert_eq ! ( rows, vec![ 0 , 1 , 2 , 3 ] ) ;
10741074 assert ! ( placements. iter( ) . all( |placement| placement. area. y != 4 ) ) ;
10751075 }
10761076
1077+ #[ test]
1078+ fn right_fact_stack_never_leaves_an_occupied_row_between_facts ( ) {
1079+ let area = Rect :: new ( 0 , 0 , 40 , 7 ) ;
1080+ let mut buffer = ratatui:: buffer:: Buffer :: empty ( area) ;
1081+ buffer[ ( 39 , 4 ) ] . set_symbol ( "x" ) ;
1082+ let lines = [ "oauth" , "model" , "dir" , "context" ]
1083+ . into_iter ( )
1084+ . map ( |text| RightFactLine :: new ( vec ! [ Span :: raw( text) ] ) . expect ( "fact line" ) )
1085+ . collect ( ) ;
1086+
1087+ let placements = right_fact_placements (
1088+ & buffer,
1089+ lines,
1090+ 0 ,
1091+ 7 ,
1092+ 0 ,
1093+ 40 ,
1094+ Rect :: new ( 0 , 0 , 40 , 5 ) ,
1095+ false ,
1096+ None ,
1097+ ) ;
1098+ let rows = placements
1099+ . iter ( )
1100+ . map ( |placement| placement. area . y )
1101+ . collect :: < Vec < _ > > ( ) ;
1102+ assert_eq ! ( rows, vec![ 0 , 1 , 2 , 3 ] ) ;
1103+ assert_eq ! (
1104+ placements
1105+ . iter( )
1106+ . map( |placement| placement. line. spans[ 0 ] . content. as_ref( ) )
1107+ . collect:: <Vec <_>>( ) ,
1108+ vec![ "oauth" , "model" , "dir" , "context" ]
1109+ ) ;
1110+ }
1111+
1112+ #[ test]
1113+ fn right_fact_stack_collision_state_space_is_contiguous_or_hidden ( ) {
1114+ const HEIGHT : u16 = 8 ;
1115+ const STACK_HEIGHT : u16 = 4 ;
1116+
1117+ for occupied_mask in 0_u16 ..( 1 << HEIGHT ) {
1118+ let area = Rect :: new ( 0 , 0 , 40 , HEIGHT ) ;
1119+ let mut buffer = ratatui:: buffer:: Buffer :: empty ( area) ;
1120+ for row in 0 ..HEIGHT {
1121+ if occupied_mask & ( 1 << row) != 0 {
1122+ buffer[ ( 39 , row) ] . set_symbol ( "x" ) ;
1123+ }
1124+ }
1125+ let lines = [ "oauth" , "model" , "dir" , "context" ]
1126+ . into_iter ( )
1127+ . map ( |text| RightFactLine :: new ( vec ! [ Span :: raw( text) ] ) . expect ( "fact line" ) )
1128+ . collect ( ) ;
1129+
1130+ let placements = right_fact_placements (
1131+ & buffer,
1132+ lines,
1133+ 0 ,
1134+ HEIGHT ,
1135+ 0 ,
1136+ 40 ,
1137+ Rect :: new ( 0 , 0 , 40 , HEIGHT ) ,
1138+ false ,
1139+ None ,
1140+ ) ;
1141+ let expected_top = ( 0 ..=HEIGHT - STACK_HEIGHT ) . rev ( ) . find ( |& start| {
1142+ ( start..start + STACK_HEIGHT )
1143+ . all ( |row| occupied_mask & ( 1 << row) == 0 )
1144+ } ) ;
1145+
1146+ match expected_top {
1147+ Some ( start) => {
1148+ assert_eq ! ( placements. len( ) , STACK_HEIGHT as usize , "mask {occupied_mask:08b}" ) ;
1149+ assert_eq ! (
1150+ placements
1151+ . iter( )
1152+ . map( |placement| placement. area. y)
1153+ . collect:: <Vec <_>>( ) ,
1154+ ( start..start + STACK_HEIGHT ) . collect:: <Vec <_>>( ) ,
1155+ "mask {occupied_mask:08b}"
1156+ ) ;
1157+ assert_eq ! (
1158+ placements
1159+ . iter( )
1160+ . map( |placement| placement. line. spans[ 0 ] . content. as_ref( ) )
1161+ . collect:: <Vec <_>>( ) ,
1162+ vec![ "oauth" , "model" , "dir" , "context" ] ,
1163+ "mask {occupied_mask:08b}"
1164+ ) ;
1165+ }
1166+ None => assert ! ( placements. is_empty( ) , "mask {occupied_mask:08b}" ) ,
1167+ }
1168+ }
1169+ }
1170+
10771171 #[ test]
10781172 fn right_fact_stack_treats_styled_blank_cells_as_occupied ( ) {
10791173 let area = Rect :: new ( 0 , 0 , 32 , 2 ) ;
@@ -2200,10 +2294,7 @@ fn right_fact_lines(app: &dyn TuiState) -> Vec<RightFactLine> {
22002294 if let Some ( dir) = app
22012295 . working_dir ( )
22022296 . and_then ( |path| overscroll_dir_label ( & path) )
2203- && let Some ( line) = RightFactLine :: new ( vec ! [ Span :: styled(
2204- dir,
2205- right_fact_neutral_style( ) ,
2206- ) ] )
2297+ && let Some ( line) = RightFactLine :: new ( vec ! [ Span :: styled( dir, right_fact_neutral_style( ) ) ] )
22072298 {
22082299 lines. push ( line) ;
22092300 }
@@ -2241,64 +2332,101 @@ fn right_fact_placements(
22412332 transcript_scrollbar_visible : bool ,
22422333 protected_position : Option < Position > ,
22432334) -> Vec < RightFactPlacement > {
2244- if top >= bottom || left >= right {
2335+ if top >= bottom || left >= right || lines . is_empty ( ) {
22452336 return Vec :: new ( ) ;
22462337 }
22472338
2248- let mut placements = Vec :: with_capacity ( lines. len ( ) ) ;
2249- let mut next_row = bottom. checked_sub ( 1 ) ;
2339+ let Ok ( block_height) = u16:: try_from ( lines. len ( ) ) else {
2340+ return Vec :: new ( ) ;
2341+ } ;
2342+ if bottom. saturating_sub ( top) < block_height {
2343+ return Vec :: new ( ) ;
2344+ }
22502345
2251- for line in lines. into_iter ( ) . rev ( ) {
2252- let Some ( mut row) = next_row else {
2253- break ;
2346+ // Facts are one visual object. Probe complete consecutive blocks from the
2347+ // bottom upward; if any row collides, move the entire stack rather than
2348+ // skipping that row and letting unrelated content split the facts apart.
2349+ let mut block_bottom = bottom;
2350+ loop {
2351+ let Some ( block_top) = block_bottom. checked_sub ( block_height) else {
2352+ return Vec :: new ( ) ;
22542353 } ;
2255- let mut placed = None ;
2256-
2257- loop {
2258- if row < top {
2259- break ;
2260- }
2261- let row_right = if transcript_scrollbar_visible
2262- && row >= messages_area. y
2263- && row < messages_area. bottom ( )
2264- {
2265- right. saturating_sub ( 1 )
2266- } else {
2267- right
2268- } ;
2269-
2270- let required = line
2271- . width
2272- . saturating_add ( RIGHT_FACT_GAP )
2273- . saturating_add ( RIGHT_FACT_PAD ) ;
2274- if row_right. saturating_sub ( left) >= required {
2275- let fact_right = row_right. saturating_sub ( RIGHT_FACT_PAD ) ;
2276- let fact_left = fact_right. saturating_sub ( line. width ) ;
2277- let probe_left = fact_left. saturating_sub ( RIGHT_FACT_GAP ) ;
2278- if probe_left >= left
2279- && ( probe_left..row_right) . all ( |x| {
2280- protected_position != Some ( Position :: new ( x, row) )
2281- && right_fact_cell_is_blank ( & buffer[ ( x, row) ] )
2282- } )
2283- {
2284- placed = Some ( Rect :: new ( fact_left, row, line. width , 1 ) ) ;
2285- break ;
2286- }
2287- }
2354+ if block_top < top {
2355+ return Vec :: new ( ) ;
2356+ }
22882357
2289- let Some ( previous) = row. checked_sub ( 1 ) else {
2290- break ;
2291- } ;
2292- row = previous;
2358+ let areas = lines
2359+ . iter ( )
2360+ . enumerate ( )
2361+ . map ( |( index, line) | {
2362+ let row = block_top + index as u16 ;
2363+ right_fact_area_on_row (
2364+ buffer,
2365+ line,
2366+ row,
2367+ left,
2368+ right,
2369+ messages_area,
2370+ transcript_scrollbar_visible,
2371+ protected_position,
2372+ )
2373+ } )
2374+ . collect :: < Option < Vec < _ > > > ( ) ;
2375+ if let Some ( areas) = areas {
2376+ return lines
2377+ . into_iter ( )
2378+ . zip ( areas)
2379+ . map ( |( line, area) | RightFactPlacement { line, area } )
2380+ . collect ( ) ;
22932381 }
22942382
2295- if let Some ( area) = placed {
2296- next_row = area. y . checked_sub ( 1 ) ;
2297- placements. push ( RightFactPlacement { line, area } ) ;
2383+ if block_top == top {
2384+ return Vec :: new ( ) ;
22982385 }
2386+ block_bottom = block_bottom. saturating_sub ( 1 ) ;
2387+ }
2388+ }
2389+
2390+ fn right_fact_area_on_row (
2391+ buffer : & ratatui:: buffer:: Buffer ,
2392+ line : & RightFactLine ,
2393+ row : u16 ,
2394+ left : u16 ,
2395+ right : u16 ,
2396+ messages_area : Rect ,
2397+ transcript_scrollbar_visible : bool ,
2398+ protected_position : Option < Position > ,
2399+ ) -> Option < Rect > {
2400+ let row_right = if transcript_scrollbar_visible
2401+ && row >= messages_area. y
2402+ && row < messages_area. bottom ( )
2403+ {
2404+ right. saturating_sub ( 1 )
2405+ } else {
2406+ right
2407+ } ;
2408+
2409+ let required = line
2410+ . width
2411+ . saturating_add ( RIGHT_FACT_GAP )
2412+ . saturating_add ( RIGHT_FACT_PAD ) ;
2413+ if row_right. saturating_sub ( left) < required {
2414+ return None ;
2415+ }
2416+
2417+ let fact_right = row_right. saturating_sub ( RIGHT_FACT_PAD ) ;
2418+ let fact_left = fact_right. saturating_sub ( line. width ) ;
2419+ let probe_left = fact_left. saturating_sub ( RIGHT_FACT_GAP ) ;
2420+ if probe_left < left
2421+ || !( probe_left..row_right) . all ( |x| {
2422+ protected_position != Some ( Position :: new ( x, row) )
2423+ && right_fact_cell_is_blank ( & buffer[ ( x, row) ] )
2424+ } )
2425+ {
2426+ return None ;
22992427 }
23002428
2301- placements
2429+ Some ( Rect :: new ( fact_left , row , line . width , 1 ) )
23022430}
23032431
23042432fn right_fact_cell_is_blank ( cell : & ratatui:: buffer:: Cell ) -> bool {
0 commit comments