@@ -49,6 +49,7 @@ use crate::render::renderable::Renderable;
4949#[ strum( serialize_all = "kebab_case" ) ]
5050pub ( crate ) enum StatusLineItem {
5151 /// The current model name.
52+ #[ strum( to_string = "model" , serialize = "model-name" ) ]
5253 ModelName ,
5354
5455 /// Model name with reasoning level suffix.
@@ -58,11 +59,20 @@ pub(crate) enum StatusLineItem {
5859 CurrentDir ,
5960
6061 /// Project root directory (if detected).
62+ #[ strum(
63+ to_string = "project-name" ,
64+ serialize = "project" ,
65+ serialize = "project-root"
66+ ) ]
6167 ProjectRoot ,
6268
6369 /// Current git branch name (if in a repository).
6470 GitBranch ,
6571
72+ /// Compact runtime run-state text.
73+ #[ strum( to_string = "run-state" , serialize = "status" ) ]
74+ Status ,
75+
6676 /// Percentage of context window remaining.
6777 ContextRemaining ,
6878
@@ -101,6 +111,9 @@ pub(crate) enum StatusLineItem {
101111
102112 /// Current thread title (if set by user).
103113 ThreadTitle ,
114+
115+ /// Latest checklist task progress from `update_plan` (if available).
116+ TaskProgress ,
104117}
105118
106119impl StatusLineItem {
@@ -110,8 +123,9 @@ impl StatusLineItem {
110123 StatusLineItem :: ModelName => "Current model name" ,
111124 StatusLineItem :: ModelWithReasoning => "Current model name with reasoning level" ,
112125 StatusLineItem :: CurrentDir => "Current working directory" ,
113- StatusLineItem :: ProjectRoot => "Project root directory (omitted when unavailable)" ,
126+ StatusLineItem :: ProjectRoot => "Project name (omitted when unavailable)" ,
114127 StatusLineItem :: GitBranch => "Current Git branch (omitted when unavailable)" ,
128+ StatusLineItem :: Status => "Compact session run-state text (Ready, Working, Thinking)" ,
115129 StatusLineItem :: ContextRemaining => {
116130 "Percentage of context window remaining (omitted when unknown)"
117131 }
@@ -135,7 +149,10 @@ impl StatusLineItem {
135149 "Current session identifier (omitted until session starts)"
136150 }
137151 StatusLineItem :: FastMode => "Whether Fast mode is currently active" ,
138- StatusLineItem :: ThreadTitle => "Current thread title (omitted unless changed by user)" ,
152+ StatusLineItem :: ThreadTitle => "Current thread title (omitted when unavailable)" ,
153+ StatusLineItem :: TaskProgress => {
154+ "Latest task progress from update_plan (omitted until available)"
155+ }
139156 }
140157 }
141158
@@ -146,6 +163,7 @@ impl StatusLineItem {
146163 StatusLineItem :: CurrentDir => StatusSurfacePreviewItem :: CurrentDir ,
147164 StatusLineItem :: ProjectRoot => StatusSurfacePreviewItem :: ProjectRoot ,
148165 StatusLineItem :: GitBranch => StatusSurfacePreviewItem :: GitBranch ,
166+ StatusLineItem :: Status => StatusSurfacePreviewItem :: Status ,
149167 StatusLineItem :: ContextRemaining => StatusSurfacePreviewItem :: ContextRemaining ,
150168 StatusLineItem :: ContextUsed => StatusSurfacePreviewItem :: ContextUsed ,
151169 StatusLineItem :: FiveHourLimit => StatusSurfacePreviewItem :: FiveHourLimit ,
@@ -158,6 +176,7 @@ impl StatusLineItem {
158176 StatusLineItem :: SessionId => StatusSurfacePreviewItem :: SessionId ,
159177 StatusLineItem :: FastMode => StatusSurfacePreviewItem :: FastMode ,
160178 StatusLineItem :: ThreadTitle => StatusSurfacePreviewItem :: ThreadTitle ,
179+ StatusLineItem :: TaskProgress => StatusSurfacePreviewItem :: TaskProgress ,
161180 }
162181 }
163182}
@@ -289,7 +308,15 @@ impl Renderable for StatusLineSetupView {
289308#[ cfg( test) ]
290309mod tests {
291310 use super :: * ;
311+ use crate :: app_event_sender:: AppEventSender ;
312+ use insta:: assert_snapshot;
292313 use pretty_assertions:: assert_eq;
314+ use ratatui:: buffer:: Buffer ;
315+ use ratatui:: layout:: Rect ;
316+ use ratatui:: text:: Line ;
317+ use tokio:: sync:: mpsc:: unbounded_channel;
318+
319+ use crate :: app_event:: AppEvent ;
293320
294321 #[ test]
295322 fn context_used_accepts_context_usage_legacy_id ( ) {
@@ -315,4 +342,222 @@ mod tests {
315342 "context-remaining"
316343 ) ;
317344 }
345+ #[ test]
346+ fn project_name_is_canonical_and_accepts_legacy_ids ( ) {
347+ assert_eq ! ( StatusLineItem :: ProjectRoot . to_string( ) , "project-name" ) ;
348+ assert_eq ! (
349+ "project-name" . parse:: <StatusLineItem >( ) ,
350+ Ok ( StatusLineItem :: ProjectRoot )
351+ ) ;
352+ assert_eq ! (
353+ "project" . parse:: <StatusLineItem >( ) ,
354+ Ok ( StatusLineItem :: ProjectRoot )
355+ ) ;
356+ assert_eq ! (
357+ "project-root" . parse:: <StatusLineItem >( ) ,
358+ Ok ( StatusLineItem :: ProjectRoot )
359+ ) ;
360+ }
361+
362+ #[ test]
363+ fn model_is_canonical_and_accepts_model_name_legacy_id ( ) {
364+ assert_eq ! ( StatusLineItem :: ModelName . to_string( ) , "model" ) ;
365+ assert_eq ! (
366+ "model" . parse:: <StatusLineItem >( ) ,
367+ Ok ( StatusLineItem :: ModelName )
368+ ) ;
369+ assert_eq ! (
370+ "model-name" . parse:: <StatusLineItem >( ) ,
371+ Ok ( StatusLineItem :: ModelName )
372+ ) ;
373+ }
374+
375+ #[ test]
376+ fn run_state_is_canonical_and_accepts_status_legacy_id ( ) {
377+ assert_eq ! ( StatusLineItem :: Status . to_string( ) , "run-state" ) ;
378+ assert_eq ! (
379+ "run-state" . parse:: <StatusLineItem >( ) ,
380+ Ok ( StatusLineItem :: Status )
381+ ) ;
382+ assert_eq ! (
383+ "status" . parse:: <StatusLineItem >( ) ,
384+ Ok ( StatusLineItem :: Status )
385+ ) ;
386+ }
387+
388+ #[ test]
389+ fn parse_status_line_items_accepts_title_only_variants ( ) {
390+ let items = [ "run-state" , "task-progress" ]
391+ . into_iter ( )
392+ . map ( str:: parse :: < StatusLineItem > )
393+ . collect :: < Result < Vec < _ > , _ > > ( ) ;
394+ assert_eq ! (
395+ items,
396+ Ok ( vec![ StatusLineItem :: Status , StatusLineItem :: TaskProgress , ] )
397+ ) ;
398+ }
399+
400+ #[ test]
401+ fn preview_uses_runtime_values ( ) {
402+ let preview_data = StatusSurfacePreviewData :: from_iter ( [
403+ (
404+ StatusLineItem :: ModelName . preview_item ( ) ,
405+ "gpt-5" . to_string ( ) ,
406+ ) ,
407+ (
408+ StatusLineItem :: CurrentDir . preview_item ( ) ,
409+ "/repo" . to_string ( ) ,
410+ ) ,
411+ ] ) ;
412+ let items = [
413+ MultiSelectItem {
414+ id : StatusLineItem :: ModelName . to_string ( ) ,
415+ name : String :: new ( ) ,
416+ description : None ,
417+ enabled : true ,
418+ } ,
419+ MultiSelectItem {
420+ id : StatusLineItem :: CurrentDir . to_string ( ) ,
421+ name : String :: new ( ) ,
422+ description : None ,
423+ enabled : true ,
424+ } ,
425+ ] ;
426+
427+ assert_eq ! (
428+ preview_data. line_for_items(
429+ items
430+ . iter( )
431+ . filter_map( |item| item. id. parse:: <StatusLineItem >( ) . ok( ) )
432+ . map( StatusLineItem :: preview_item) ,
433+ ) ,
434+ Some ( Line :: from( "gpt-5 · /repo" ) )
435+ ) ;
436+ }
437+
438+ #[ test]
439+ fn preview_uses_placeholders_when_runtime_values_are_missing ( ) {
440+ let preview_data = StatusSurfacePreviewData :: from_iter ( [ (
441+ StatusSurfacePreviewItem :: Model ,
442+ "gpt-5" . to_string ( ) ,
443+ ) ] ) ;
444+ let items = [
445+ MultiSelectItem {
446+ id : StatusLineItem :: ModelName . to_string ( ) ,
447+ name : String :: new ( ) ,
448+ description : None ,
449+ enabled : true ,
450+ } ,
451+ MultiSelectItem {
452+ id : StatusLineItem :: GitBranch . to_string ( ) ,
453+ name : String :: new ( ) ,
454+ description : None ,
455+ enabled : true ,
456+ } ,
457+ ] ;
458+
459+ assert_eq ! (
460+ preview_data. line_for_items(
461+ items
462+ . iter( )
463+ . filter_map( |item| item. id. parse:: <StatusLineItem >( ) . ok( ) )
464+ . map( StatusLineItem :: preview_item) ,
465+ ) ,
466+ Some ( Line :: from( "gpt-5 · feat/awesome-feature" ) )
467+ ) ;
468+ }
469+
470+ #[ test]
471+ fn preview_includes_thread_title ( ) {
472+ let preview_data = StatusSurfacePreviewData :: from_iter ( [
473+ (
474+ StatusLineItem :: ModelName . preview_item ( ) ,
475+ "gpt-5" . to_string ( ) ,
476+ ) ,
477+ (
478+ StatusLineItem :: ThreadTitle . preview_item ( ) ,
479+ "Roadmap cleanup" . to_string ( ) ,
480+ ) ,
481+ ] ) ;
482+ let items = [
483+ MultiSelectItem {
484+ id : StatusLineItem :: ModelName . to_string ( ) ,
485+ name : String :: new ( ) ,
486+ description : None ,
487+ enabled : true ,
488+ } ,
489+ MultiSelectItem {
490+ id : StatusLineItem :: ThreadTitle . to_string ( ) ,
491+ name : String :: new ( ) ,
492+ description : None ,
493+ enabled : true ,
494+ } ,
495+ ] ;
496+
497+ assert_eq ! (
498+ preview_data. line_for_items(
499+ items
500+ . iter( )
501+ . filter_map( |item| item. id. parse:: <StatusLineItem >( ) . ok( ) )
502+ . map( StatusLineItem :: preview_item) ,
503+ ) ,
504+ Some ( Line :: from( "gpt-5 · Roadmap cleanup" ) )
505+ ) ;
506+ }
507+
508+ #[ test]
509+ fn setup_view_snapshot_uses_runtime_preview_values ( ) {
510+ let ( tx_raw, _rx) = unbounded_channel :: < AppEvent > ( ) ;
511+ let view = StatusLineSetupView :: new (
512+ Some ( & [
513+ StatusLineItem :: ModelName . to_string ( ) ,
514+ StatusLineItem :: CurrentDir . to_string ( ) ,
515+ StatusLineItem :: GitBranch . to_string ( ) ,
516+ ] ) ,
517+ StatusSurfacePreviewData :: from_iter ( [
518+ (
519+ StatusLineItem :: ModelName . preview_item ( ) ,
520+ "gpt-5-codex" . to_string ( ) ,
521+ ) ,
522+ (
523+ StatusLineItem :: CurrentDir . preview_item ( ) ,
524+ "~/codex-rs" . to_string ( ) ,
525+ ) ,
526+ (
527+ StatusLineItem :: GitBranch . preview_item ( ) ,
528+ "jif/statusline-preview" . to_string ( ) ,
529+ ) ,
530+ (
531+ StatusLineItem :: WeeklyLimit . preview_item ( ) ,
532+ "weekly 82%" . to_string ( ) ,
533+ ) ,
534+ ] ) ,
535+ AppEventSender :: new ( tx_raw) ,
536+ ) ;
537+
538+ assert_snapshot ! ( render_lines( & view, /*width*/ 72 ) ) ;
539+ }
540+
541+ fn render_lines ( view : & StatusLineSetupView , width : u16 ) -> String {
542+ let height = view. desired_height ( width) ;
543+ let area = Rect :: new ( 0 , 0 , width, height) ;
544+ let mut buf = Buffer :: empty ( area) ;
545+ view. render ( area, & mut buf) ;
546+
547+ ( 0 ..area. height )
548+ . map ( |row| {
549+ let mut line = String :: new ( ) ;
550+ for col in 0 ..area. width {
551+ let symbol = buf[ ( area. x + col, area. y + row) ] . symbol ( ) ;
552+ if symbol. is_empty ( ) {
553+ line. push ( ' ' ) ;
554+ } else {
555+ line. push_str ( symbol) ;
556+ }
557+ }
558+ line
559+ } )
560+ . collect :: < Vec < _ > > ( )
561+ . join ( "\n " )
562+ }
318563}
0 commit comments