@@ -735,11 +735,19 @@ impl App {
735735 buf,
736736 ) ;
737737
738- // Thinking state indicator (overlays into status area)
739- render_thinking_state ( & self . thinking , layout. status , buf) ;
740-
741- // Status line / spinner
742- Widget :: render ( & self . spinner , layout. status , buf) ;
738+ // Status line: spinner (when active) or status bar (model/tokens/cost)
739+ if self . spinner . is_active ( ) {
740+ Widget :: render ( & self . spinner , layout. status , buf) ;
741+ } else {
742+ render_status_line (
743+ & self . model_name ,
744+ self . total_input_tokens ,
745+ self . total_output_tokens ,
746+ & self . thinking ,
747+ layout. status ,
748+ buf,
749+ ) ;
750+ }
743751
744752 // Separator above input
745753 render_separator ( layout. separator_top , buf) ;
@@ -925,7 +933,7 @@ fn render_separator(area: Rect, buf: &mut Buffer) {
925933///
926934/// When thinking is active, shows `"Thinking... (Ns)"` with elapsed time.
927935/// After thinking finishes, shows `"(thought for Ns)"` for 2 seconds.
928- #[ allow( clippy:: cast_possible_truncation) ]
936+ #[ allow( dead_code , clippy:: cast_possible_truncation) ]
929937fn render_thinking_state ( thinking : & ThinkingState , area : Rect , buf : & mut Buffer ) {
930938 if area. height == 0 || area. width == 0 {
931939 return ;
@@ -1051,31 +1059,71 @@ fn render_content_scrolled(
10511059 return ;
10521060 }
10531061
1054- let lines: Vec < & str > = text. lines ( ) . collect ( ) ;
1062+ // Render lines: system prefixed lines get color-coded styles,
1063+ // everything else goes through markdown rendering.
1064+ let theme = crate :: theme:: Theme :: dark ( ) ;
1065+ let highlighter = crate :: components:: syntax:: SyntaxHighlighter :: new ( ) ;
1066+ let md_renderer = crate :: components:: markdown:: MarkdownRenderer :: new ( & theme, & highlighter) ;
1067+
1068+ let mut rendered_lines: Vec < Line < ' static > > = Vec :: new ( ) ;
1069+
1070+ // Split content into segments: system lines vs markdown blocks
1071+ let mut md_block = String :: new ( ) ;
1072+ for raw_line in text. lines ( ) {
1073+ if is_system_line ( raw_line) {
1074+ // Flush any accumulated markdown
1075+ if !md_block. is_empty ( ) {
1076+ rendered_lines. extend ( md_renderer. render ( & md_block) ) ;
1077+ md_block. clear ( ) ;
1078+ }
1079+ // Render system line with prefix-based styling
1080+ let style = classify_content_style ( raw_line, styles) ;
1081+ rendered_lines. push ( Line :: from ( Span :: styled ( raw_line. to_string ( ) , style) ) ) ;
1082+ } else {
1083+ md_block. push_str ( raw_line) ;
1084+ md_block. push ( '\n' ) ;
1085+ }
1086+ }
1087+ // Flush remaining markdown
1088+ if !md_block. is_empty ( ) {
1089+ rendered_lines. extend ( md_renderer. render ( & md_block) ) ;
1090+ }
1091+
10551092 let visible = area. height as usize ;
1056- // Show the last N lines minus scroll offset (auto-scroll to bottom)
1057- let end = lines. len ( ) . saturating_sub ( scroll_offset) ;
1093+ let end = rendered_lines. len ( ) . saturating_sub ( scroll_offset) ;
10581094 let start = end. saturating_sub ( visible) ;
10591095
1060- for ( i, line_text ) in lines
1096+ for ( i, line ) in rendered_lines
10611097 . iter ( )
10621098 . skip ( start)
1063- . take ( visible. min ( end - start) )
1099+ . take ( visible. min ( end. saturating_sub ( start) ) )
10641100 . enumerate ( )
10651101 {
10661102 let y = area. y + i as u16 ;
1067- let style = classify_content_style ( line_text, styles) ;
1068- let line_widget = Line :: from ( Span :: styled ( * line_text, style) ) ;
10691103 let line_area = Rect {
10701104 x : area. x ,
10711105 y,
10721106 width : area. width ,
10731107 height : 1 ,
10741108 } ;
1075- Widget :: render ( line_widget , line_area, buf) ;
1109+ Widget :: render ( line . clone ( ) , line_area, buf) ;
10761110 }
10771111}
10781112
1113+ /// Check if a line is a system/tool prefix line (not markdown).
1114+ fn is_system_line ( line : & str ) -> bool {
1115+ let t = line. trim_start ( ) ;
1116+ t. starts_with ( "[tool" )
1117+ || t. starts_with ( "[Error:" )
1118+ || t. starts_with ( "[warn]" )
1119+ || t. starts_with ( "[session]" )
1120+ || t. starts_with ( "[compact]" )
1121+ || t. starts_with ( "[interrupted]" )
1122+ || t. starts_with ( "❯ " )
1123+ || t. starts_with ( "────" )
1124+ || t. starts_with ( "Welcome!" )
1125+ }
1126+
10791127/// Choose a style for a content line based on its prefix/content.
10801128fn classify_content_style ( line : & str , styles : & OutputStyles ) -> Style {
10811129 let trimmed = line. trim_start ( ) ;
@@ -1180,6 +1228,73 @@ fn classify_tool_risk(tool_name: &str) -> RiskLevel {
11801228 }
11811229}
11821230
1231+ /// Render the status line: model name | token counts | thinking state.
1232+ ///
1233+ /// Matches CC's `StatusLine` component showing operational data.
1234+ fn render_status_line (
1235+ model : & str ,
1236+ input_tokens : u64 ,
1237+ output_tokens : u64 ,
1238+ thinking : & ThinkingState ,
1239+ area : Rect ,
1240+ buf : & mut Buffer ,
1241+ ) {
1242+ if area. width < 10 || area. height == 0 {
1243+ return ;
1244+ }
1245+
1246+ let mut spans = vec ! [
1247+ Span :: styled( model, Style :: default ( ) . fg( Color :: Cyan ) ) ,
1248+ Span :: styled( " │ " , Style :: default ( ) . fg( Color :: DarkGray ) ) ,
1249+ ] ;
1250+
1251+ // Token counts
1252+ let in_str = format_token_count ( input_tokens) ;
1253+ let out_str = format_token_count ( output_tokens) ;
1254+ spans. push ( Span :: styled (
1255+ format ! ( "{in_str} in" ) ,
1256+ Style :: default ( ) . fg ( Color :: DarkGray ) ,
1257+ ) ) ;
1258+ spans. push ( Span :: styled ( " · " , Style :: default ( ) . fg ( Color :: DarkGray ) ) ) ;
1259+ spans. push ( Span :: styled (
1260+ format ! ( "{out_str} out" ) ,
1261+ Style :: default ( ) . fg ( Color :: DarkGray ) ,
1262+ ) ) ;
1263+
1264+ // Thinking state
1265+ match thinking {
1266+ ThinkingState :: Thinking { started_at } => {
1267+ let elapsed = started_at. elapsed ( ) . as_secs ( ) ;
1268+ spans. push ( Span :: styled ( " │ " , Style :: default ( ) . fg ( Color :: DarkGray ) ) ) ;
1269+ spans. push ( Span :: styled (
1270+ format ! ( "thinking ({elapsed}s)" ) ,
1271+ Style :: default ( ) . fg ( Color :: Yellow ) ,
1272+ ) ) ;
1273+ }
1274+ ThinkingState :: ThoughtFor { duration, .. } => {
1275+ spans. push ( Span :: styled ( " │ " , Style :: default ( ) . fg ( Color :: DarkGray ) ) ) ;
1276+ spans. push ( Span :: styled (
1277+ format ! ( "thought for {}s" , duration. as_secs( ) ) ,
1278+ Style :: default ( ) . fg ( Color :: DarkGray ) ,
1279+ ) ) ;
1280+ }
1281+ ThinkingState :: Idle => { }
1282+ }
1283+
1284+ Widget :: render ( Line :: from ( spans) , area, buf) ;
1285+ }
1286+
1287+ /// Format token count: 1234 → "1.2k", 500 → "500"
1288+ fn format_token_count ( tokens : u64 ) -> String {
1289+ if tokens >= 1_000_000 {
1290+ format ! ( "{:.1}M" , tokens as f64 / 1_000_000.0 )
1291+ } else if tokens >= 1000 {
1292+ format ! ( "{:.1}k" , tokens as f64 / 1000.0 )
1293+ } else {
1294+ tokens. to_string ( )
1295+ }
1296+ }
1297+
11831298fn render_bottom_bar ( state : AppState , search_active : bool , area : Rect , buf : & mut Buffer ) {
11841299 let line = if search_active {
11851300 Line :: from ( Span :: styled (
0 commit comments