@@ -75,9 +75,9 @@ struct AppState {
7575 /// When true, auto-scroll to bottom on new content.
7676 auto_scroll : bool ,
7777 /// When true, the next stream chunk must start on a fresh agent line
78- /// (set after a TOOL_RESULT so text doesn't concatenate onto the result).
78+ /// (set after a ` TOOL_RESULT` so text doesn't concatenate onto the result).
7979 pending_stream_newline : bool ,
80- /// True between AGENT_CYCLE_START and AGENT_CYCLE_END.
80+ /// True between ` AGENT_CYCLE_START` and ` AGENT_CYCLE_END` .
8181 /// Stream chunks outside a cycle are dropped (late-arriving bus events).
8282 in_cycle : bool ,
8383}
@@ -236,6 +236,7 @@ fn word_wrap_rows(text: &str, width: usize) -> usize {
236236/// The wrap decision for each word is made **before** checking the cursor so
237237/// that a cursor at the start of a wrapped word lands on the new row, not the
238238/// old one.
239+ #[ allow( clippy:: cast_possible_truncation) ]
239240fn word_wrap_cursor ( text : & str , cursor_char_abs : usize , width : usize ) -> ( u16 , u16 ) {
240241 if width == 0 {
241242 return ( 0 , 0 ) ;
@@ -293,51 +294,9 @@ fn word_wrap_cursor(text: &str, cursor_char_abs: usize, width: usize) -> (u16, u
293294 ( row, col as u16 )
294295}
295296
296- #[ cfg( test) ]
297- mod wrap_tests {
298- use super :: * ;
299-
300- // Prefix used by the input bar.
301- const P : & str = " > " ;
302-
303- #[ test]
304- fn empty_input_cursor_after_prefix ( ) {
305- // " > " — cursor at position 3 (just after the prefix), width=80
306- assert_eq ! ( word_wrap_cursor( P , 3 , 80 ) , ( 0 , 3 ) ) ;
307- assert_eq ! ( word_wrap_rows( P , 80 ) , 1 ) ;
308- }
309-
310- #[ test]
311- fn one_char_cursor_at_end ( ) {
312- // " > a" — cursor at 4
313- assert_eq ! ( word_wrap_cursor( " > a" , 4 , 80 ) , ( 0 , 4 ) ) ;
314- assert_eq ! ( word_wrap_rows( " > a" , 80 ) , 1 ) ;
315- }
316-
317- #[ test]
318- fn word_wraps_to_new_row ( ) {
319- // " > hello world" at width=10: "hello" fits row 0, "world" wraps to row 1.
320- // Cursor at 9 ('w') should be (1, 0) after the wrap decision.
321- assert_eq ! ( word_wrap_cursor( " > hello world" , 9 , 10 ) , ( 1 , 0 ) ) ;
322- assert_eq ! ( word_wrap_rows( " > hello world" , 10 ) , 2 ) ;
323- }
324-
325- #[ test]
326- fn cursor_at_very_start ( ) {
327- assert_eq ! ( word_wrap_cursor( "hello" , 0 , 80 ) , ( 0 , 0 ) ) ;
328- }
329-
330- #[ test]
331- fn hard_wrap_long_word ( ) {
332- // "aaaaaaaaaaa" (11 a's) at width=10: spills onto row 2
333- assert_eq ! ( word_wrap_rows( "aaaaaaaaaaa" , 10 ) , 2 ) ;
334- // Cursor at position 10 (start of row 2)
335- assert_eq ! ( word_wrap_cursor( "aaaaaaaaaaa" , 10 , 10 ) , ( 1 , 0 ) ) ;
336- }
337- }
338-
339297// ── Rendering ─────────────────────────────────────────────────────────────────
340298
299+ #[ allow( clippy:: cast_possible_truncation, clippy:: too_many_lines) ]
341300fn render ( frame : & mut Frame , state : & AppState ) {
342301 let area = frame. area ( ) ;
343302 let prefix = " > " ;
@@ -354,8 +313,8 @@ fn render(frame: &mut Frame, state: &AppState) {
354313 let chunks = Layout :: default ( )
355314 . direction ( Direction :: Vertical )
356315 . constraints ( [
357- Constraint :: Length ( 2 ) , // status bar
358- Constraint :: Min ( 0 ) , // conversation
316+ Constraint :: Length ( 2 ) , // status bar
317+ Constraint :: Min ( 0 ) , // conversation
359318 Constraint :: Length ( input_height) , // input bar (dynamic)
360319 ] )
361320 . split ( area) ;
@@ -421,12 +380,13 @@ fn render(frame: &mut Frame, state: &AppState) {
421380
422381 // Scroll indicator at the bottom of the log area
423382 if effective_scroll > 0 {
424- let indicator_text = format ! (
425- " ▼ {} more below [↓/PgDn to scroll, G to jump to bottom] " ,
426- effective_scroll
383+ let indicator_text =
384+ format ! ( " ▼ {effective_scroll} more below [↓/PgDn to scroll, G to jump to bottom] " ) ;
385+ let indicator = Paragraph :: new ( indicator_text) . style (
386+ Style :: default ( )
387+ . fg ( Color :: DarkGray )
388+ . add_modifier ( Modifier :: DIM ) ,
427389 ) ;
428- let indicator = Paragraph :: new ( indicator_text)
429- . style ( Style :: default ( ) . fg ( Color :: DarkGray ) . add_modifier ( Modifier :: DIM ) ) ;
430390 let indicator_area = Rect {
431391 x : log_area. x ,
432392 y : log_area. y + log_area. height . saturating_sub ( 1 ) ,
@@ -559,12 +519,18 @@ fn centred_rect_fixed(width: u16, height: u16, area: Rect) -> Rect {
559519 let h = height. min ( area. height ) ;
560520 let x = area. x + ( area. width . saturating_sub ( w) ) / 2 ;
561521 let y = area. y + ( area. height . saturating_sub ( h) ) / 2 ;
562- Rect { x, y, width : w, height : h }
522+ Rect {
523+ x,
524+ y,
525+ width : w,
526+ height : h,
527+ }
563528}
564529
565530// ── Bus event handling ────────────────────────────────────────────────────────
566531
567- fn handle_bus_event ( state : & mut AppState , event : Event ) {
532+ #[ allow( clippy:: too_many_lines) ]
533+ fn handle_bus_event ( state : & mut AppState , event : & Event ) {
568534 match event. kind . as_str ( ) {
569535 k if k == core_kinds:: USER_MESSAGE => {
570536 let text = event. payload [ "text" ] . as_str ( ) . unwrap_or ( "" ) . to_string ( ) ;
@@ -614,7 +580,9 @@ fn handle_bus_event(state: &mut AppState, event: Event) {
614580 state. push ( Line :: from ( "" ) ) ;
615581 state. push ( Line :: from ( vec ! [ Span :: styled(
616582 " agent " ,
617- Style :: default ( ) . fg( Color :: Green ) . add_modifier( Modifier :: BOLD ) ,
583+ Style :: default ( )
584+ . fg( Color :: Green )
585+ . add_modifier( Modifier :: BOLD ) ,
618586 ) ] ) ) ;
619587 }
620588
@@ -723,21 +691,27 @@ fn handle_bus_event(state: &mut AppState, event: Event) {
723691 let args_preview = event
724692 . payload
725693 . get ( "args" )
726- . map ( |v| v . to_string ( ) )
694+ . map ( ToString :: to_string)
727695 . unwrap_or_default ( ) ;
728696 // Push a visible pause line so the conversation log doesn't look
729697 // abruptly truncated — the stream stopped here on purpose.
730698 state. push ( Line :: from ( vec ! [ Span :: styled(
731699 format!( " ⏸ approval required for {tool} (Y/N)" ) ,
732- Style :: default ( ) . fg( Color :: Yellow ) . add_modifier( Modifier :: DIM ) ,
700+ Style :: default ( )
701+ . fg( Color :: Yellow )
702+ . add_modifier( Modifier :: DIM ) ,
733703 ) ] ) ) ;
734704 state. mode = Mode :: AwaitingApproval { tool, args_preview } ;
735705 }
736706
737707 k if k == CODING_APPROVAL_GRANTED || k == CODING_APPROVAL_DENIED => {
738708 let granted = k == CODING_APPROVAL_GRANTED ;
739709 state. push ( Line :: from ( vec ! [ Span :: styled(
740- if granted { " ▶ approved" } else { " ✗ denied" } ,
710+ if granted {
711+ " ▶ approved"
712+ } else {
713+ " ✗ denied"
714+ } ,
741715 Style :: default ( )
742716 . fg( if granted { Color :: Green } else { Color :: Red } )
743717 . add_modifier( Modifier :: DIM ) ,
@@ -753,7 +727,7 @@ fn handle_bus_event(state: &mut AppState, event: Event) {
753727 let deleted = event. payload [ "deleted_files" ] . as_u64 ( ) . unwrap_or ( 0 ) ;
754728 if changed + new + deleted > 0 {
755729 state. push ( Line :: from ( vec ! [ Span :: styled(
756- format!( " ∆ {} changed, {} new, {} deleted" , changed , new , deleted ) ,
730+ format!( " ∆ {changed } changed, {new } new, {deleted } deleted" ) ,
757731 Style :: default ( ) . fg( Color :: Blue ) ,
758732 ) ] ) ) ;
759733 }
@@ -790,7 +764,9 @@ fn handle_bus_event(state: &mut AppState, event: Event) {
790764 state. push ( Line :: from ( vec ! [
791765 Span :: styled(
792766 format!( " ✓ sub-agent #{id_short} " ) ,
793- Style :: default ( ) . fg( Color :: Green ) . add_modifier( Modifier :: BOLD ) ,
767+ Style :: default ( )
768+ . fg( Color :: Green )
769+ . add_modifier( Modifier :: BOLD ) ,
794770 ) ,
795771 Span :: styled( preview, Style :: default ( ) . fg( Color :: Green ) ) ,
796772 ] ) ) ;
@@ -831,7 +807,7 @@ async fn handle_key(
831807 // ── Global shortcuts ──────────────────────────────────────────────────────
832808 if key. modifiers . contains ( KeyModifiers :: CONTROL ) {
833809 match key. code {
834- KeyCode :: Char ( 'c' ) | KeyCode :: Char ( 'q' ) => {
810+ KeyCode :: Char ( 'c' | 'q' ) => {
835811 return KeyAction :: Quit ;
836812 }
837813 KeyCode :: Char ( 'x' ) => {
@@ -900,14 +876,14 @@ async fn handle_key(
900876
901877 // ── AwaitingApproval: y/n + scroll ────────────────────────────────────
902878 Mode :: AwaitingApproval { .. } => match key. code {
903- KeyCode :: Char ( 'y' ) | KeyCode :: Char ( 'Y' ) => {
879+ KeyCode :: Char ( 'y' | 'Y' ) => {
904880 debug ! ( "user granted tool approval" ) ;
905881 let _ = bus
906882 . publish ( Event :: new ( CODING_APPROVAL_GRANTED , json ! ( { } ) ) )
907883 . await ;
908884 state. mode = Mode :: Working ;
909885 }
910- KeyCode :: Char ( 'n' ) | KeyCode :: Char ( 'N' ) => {
886+ KeyCode :: Char ( 'n' | 'N' ) => {
911887 debug ! ( "user denied tool approval" ) ;
912888 let _ = bus
913889 . publish ( Event :: new ( CODING_APPROVAL_DENIED , json ! ( { } ) ) )
@@ -994,7 +970,7 @@ pub async fn run_tui(
994970 maybe_event = bus_rx. recv( ) => {
995971 match maybe_event {
996972 Some ( event) => {
997- handle_bus_event( & mut state, event) ;
973+ handle_bus_event( & mut state, & event) ;
998974 needs_redraw = true ;
999975 }
1000976 None => break ,
@@ -1032,3 +1008,46 @@ pub async fn run_tui(
10321008
10331009 result
10341010}
1011+
1012+ #[ cfg( test) ]
1013+ mod wrap_tests {
1014+ use super :: * ;
1015+
1016+ // Prefix used by the input bar.
1017+ const P : & str = " > " ;
1018+
1019+ #[ test]
1020+ fn empty_input_cursor_after_prefix ( ) {
1021+ // " > " — cursor at position 3 (just after the prefix), width=80
1022+ assert_eq ! ( word_wrap_cursor( P , 3 , 80 ) , ( 0 , 3 ) ) ;
1023+ assert_eq ! ( word_wrap_rows( P , 80 ) , 1 ) ;
1024+ }
1025+
1026+ #[ test]
1027+ fn one_char_cursor_at_end ( ) {
1028+ // " > a" — cursor at 4
1029+ assert_eq ! ( word_wrap_cursor( " > a" , 4 , 80 ) , ( 0 , 4 ) ) ;
1030+ assert_eq ! ( word_wrap_rows( " > a" , 80 ) , 1 ) ;
1031+ }
1032+
1033+ #[ test]
1034+ fn word_wraps_to_new_row ( ) {
1035+ // " > hello world" at width=10: "hello" fits row 0, "world" wraps to row 1.
1036+ // Cursor at 9 ('w') should be (1, 0) after the wrap decision.
1037+ assert_eq ! ( word_wrap_cursor( " > hello world" , 9 , 10 ) , ( 1 , 0 ) ) ;
1038+ assert_eq ! ( word_wrap_rows( " > hello world" , 10 ) , 2 ) ;
1039+ }
1040+
1041+ #[ test]
1042+ fn cursor_at_very_start ( ) {
1043+ assert_eq ! ( word_wrap_cursor( "hello" , 0 , 80 ) , ( 0 , 0 ) ) ;
1044+ }
1045+
1046+ #[ test]
1047+ fn hard_wrap_long_word ( ) {
1048+ // "aaaaaaaaaaa" (11 a's) at width=10: spills onto row 2
1049+ assert_eq ! ( word_wrap_rows( "aaaaaaaaaaa" , 10 ) , 2 ) ;
1050+ // Cursor at position 10 (start of row 2)
1051+ assert_eq ! ( word_wrap_cursor( "aaaaaaaaaaa" , 10 , 10 ) , ( 1 , 0 ) ) ;
1052+ }
1053+ }
0 commit comments