From 4fbb9ddf878abd22b5e194b4bc138628d8d8212e Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Fri, 17 Apr 2026 16:42:44 -0700 Subject: [PATCH 1/3] Normalize status and title items --- ..._snapshot_uses_runtime_preview_values.snap | 19 ++ ...up__tests__terminal_title_setup_basic.snap | 23 ++ .../tui/src/bottom_pane/status_line_setup.rs | 235 +++++++++++++++++- codex-rs/tui/src/bottom_pane/title_setup.rs | 205 ++++++++++++++- codex-rs/tui/src/chatwidget.rs | 9 +- .../tui/src/chatwidget/status_surfaces.rs | 66 ++++- 6 files changed, 530 insertions(+), 27 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap new file mode 100644 index 000000000000..9a79ebaa1160 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap @@ -0,0 +1,19 @@ +--- +source: tui/src/bottom_pane/status_line_setup.rs +expression: "render_lines(&view, 72)" +--- + + Configure Status Line + Select which items to display in the status line. + + Type to search + > +› [x] model Current model name + [x] current-dir Current working directory + [x] git-branch Current Git branch (omitted when unavailable) + [ ] model-with-reasoning Current model name with reasoning level + [ ] project-name Project name (omitted when unavailable) + [ ] status Compact session status text (Ready, Working, … + + gpt-5-codex · ~/codex-rs · jif/statusline-preview + Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap new file mode 100644 index 000000000000..63de8efc8b50 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap @@ -0,0 +1,23 @@ +--- +source: tui/src/bottom_pane/title_setup.rs +expression: "render_lines(&view, 84)" +--- + + Configure Terminal Title + Select which items to display in the terminal title. + + Type to search + > +› [x] project-name Project name (falls back to current directory name) + [x] spinner Animated task spinner (omitted while idle or when animations… + [x] status Compact session status text (Ready, Working, Thinking) + [x] thread-title Current thread title (omitted when unavailable) + [ ] app-name Codex app name + [ ] current-dir Current working directory + [ ] git-branch Current Git branch (omitted when unavailable) + [ ] context-remaining Percentage of context window remaining (omitted when unk… + [ ] context-used Percentage of context window used (omitted when unknown) + [ ] five-hour-limit Remaining usage on 5-hour usage limit (omitted when unava… + + my-project ⠋ Working | thread title + Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel. diff --git a/codex-rs/tui/src/bottom_pane/status_line_setup.rs b/codex-rs/tui/src/bottom_pane/status_line_setup.rs index 0ae5ddd9c141..d683eea1fbb4 100644 --- a/codex-rs/tui/src/bottom_pane/status_line_setup.rs +++ b/codex-rs/tui/src/bottom_pane/status_line_setup.rs @@ -49,6 +49,7 @@ use crate::render::renderable::Renderable; #[strum(serialize_all = "kebab_case")] pub(crate) enum StatusLineItem { /// The current model name. + #[strum(to_string = "model", serialize = "model-name")] ModelName, /// Model name with reasoning level suffix. @@ -58,11 +59,19 @@ pub(crate) enum StatusLineItem { CurrentDir, /// Project root directory (if detected). + #[strum( + to_string = "project-name", + serialize = "project", + serialize = "project-root" + )] ProjectRoot, /// Current git branch name (if in a repository). GitBranch, + /// Compact runtime status text. + Status, + /// Percentage of context window remaining. ContextRemaining, @@ -101,6 +110,9 @@ pub(crate) enum StatusLineItem { /// Current thread title (if set by user). ThreadTitle, + + /// Latest checklist task progress from `update_plan` (if available). + TaskProgress, } impl StatusLineItem { @@ -110,8 +122,9 @@ impl StatusLineItem { StatusLineItem::ModelName => "Current model name", StatusLineItem::ModelWithReasoning => "Current model name with reasoning level", StatusLineItem::CurrentDir => "Current working directory", - StatusLineItem::ProjectRoot => "Project root directory (omitted when unavailable)", + StatusLineItem::ProjectRoot => "Project name (omitted when unavailable)", StatusLineItem::GitBranch => "Current Git branch (omitted when unavailable)", + StatusLineItem::Status => "Compact session status text (Ready, Working, Thinking)", StatusLineItem::ContextRemaining => { "Percentage of context window remaining (omitted when unknown)" } @@ -135,7 +148,10 @@ impl StatusLineItem { "Current session identifier (omitted until session starts)" } StatusLineItem::FastMode => "Whether Fast mode is currently active", - StatusLineItem::ThreadTitle => "Current thread title (omitted unless changed by user)", + StatusLineItem::ThreadTitle => "Current thread title (omitted when unavailable)", + StatusLineItem::TaskProgress => { + "Latest task progress from update_plan (omitted until available)" + } } } @@ -146,6 +162,7 @@ impl StatusLineItem { StatusLineItem::CurrentDir => StatusSurfacePreviewItem::CurrentDir, StatusLineItem::ProjectRoot => StatusSurfacePreviewItem::ProjectRoot, StatusLineItem::GitBranch => StatusSurfacePreviewItem::GitBranch, + StatusLineItem::Status => StatusSurfacePreviewItem::Status, StatusLineItem::ContextRemaining => StatusSurfacePreviewItem::ContextRemaining, StatusLineItem::ContextUsed => StatusSurfacePreviewItem::ContextUsed, StatusLineItem::FiveHourLimit => StatusSurfacePreviewItem::FiveHourLimit, @@ -158,6 +175,7 @@ impl StatusLineItem { StatusLineItem::SessionId => StatusSurfacePreviewItem::SessionId, StatusLineItem::FastMode => StatusSurfacePreviewItem::FastMode, StatusLineItem::ThreadTitle => StatusSurfacePreviewItem::ThreadTitle, + StatusLineItem::TaskProgress => StatusSurfacePreviewItem::TaskProgress, } } } @@ -289,7 +307,15 @@ impl Renderable for StatusLineSetupView { #[cfg(test)] mod tests { use super::*; + use crate::app_event_sender::AppEventSender; + use insta::assert_snapshot; use pretty_assertions::assert_eq; + use ratatui::buffer::Buffer; + use ratatui::layout::Rect; + use ratatui::text::Line; + use tokio::sync::mpsc::unbounded_channel; + + use crate::app_event::AppEvent; #[test] fn context_used_accepts_context_usage_legacy_id() { @@ -315,4 +341,209 @@ mod tests { "context-remaining" ); } + #[test] + fn project_name_is_canonical_and_accepts_legacy_ids() { + assert_eq!(StatusLineItem::ProjectRoot.to_string(), "project-name"); + assert_eq!( + "project-name".parse::(), + Ok(StatusLineItem::ProjectRoot) + ); + assert_eq!( + "project".parse::(), + Ok(StatusLineItem::ProjectRoot) + ); + assert_eq!( + "project-root".parse::(), + Ok(StatusLineItem::ProjectRoot) + ); + } + + #[test] + fn model_is_canonical_and_accepts_model_name_legacy_id() { + assert_eq!(StatusLineItem::ModelName.to_string(), "model"); + assert_eq!( + "model".parse::(), + Ok(StatusLineItem::ModelName) + ); + assert_eq!( + "model-name".parse::(), + Ok(StatusLineItem::ModelName) + ); + } + + #[test] + fn parse_status_line_items_accepts_title_only_variants() { + let items = ["status", "task-progress"] + .into_iter() + .map(|id| id.parse::()) + .collect::, _>>(); + assert_eq!( + items, + Ok(vec![StatusLineItem::Status, StatusLineItem::TaskProgress,]) + ); + } + + #[test] + fn preview_uses_runtime_values() { + let preview_data = StatusSurfacePreviewData::from_iter([ + ( + StatusLineItem::ModelName.preview_item(), + "gpt-5".to_string(), + ), + ( + StatusLineItem::CurrentDir.preview_item(), + "/repo".to_string(), + ), + ]); + let items = [ + MultiSelectItem { + id: StatusLineItem::ModelName.to_string(), + name: String::new(), + description: None, + enabled: true, + }, + MultiSelectItem { + id: StatusLineItem::CurrentDir.to_string(), + name: String::new(), + description: None, + enabled: true, + }, + ]; + + assert_eq!( + preview_data.line_for_items( + items + .iter() + .filter_map(|item| item.id.parse::().ok()) + .map(StatusLineItem::preview_item), + ), + Some(Line::from("gpt-5 · /repo")) + ); + } + + #[test] + fn preview_uses_placeholders_when_runtime_values_are_missing() { + let preview_data = StatusSurfacePreviewData::from_iter([( + StatusSurfacePreviewItem::Model, + "gpt-5".to_string(), + )]); + let items = [ + MultiSelectItem { + id: StatusLineItem::ModelName.to_string(), + name: String::new(), + description: None, + enabled: true, + }, + MultiSelectItem { + id: StatusLineItem::GitBranch.to_string(), + name: String::new(), + description: None, + enabled: true, + }, + ]; + + assert_eq!( + preview_data.line_for_items( + items + .iter() + .filter_map(|item| item.id.parse::().ok()) + .map(StatusLineItem::preview_item), + ), + Some(Line::from("gpt-5 · feat/awesome-feature")) + ); + } + + #[test] + fn preview_includes_thread_title() { + let preview_data = StatusSurfacePreviewData::from_iter([ + ( + StatusLineItem::ModelName.preview_item(), + "gpt-5".to_string(), + ), + ( + StatusLineItem::ThreadTitle.preview_item(), + "Roadmap cleanup".to_string(), + ), + ]); + let items = [ + MultiSelectItem { + id: StatusLineItem::ModelName.to_string(), + name: String::new(), + description: None, + enabled: true, + }, + MultiSelectItem { + id: StatusLineItem::ThreadTitle.to_string(), + name: String::new(), + description: None, + enabled: true, + }, + ]; + + assert_eq!( + preview_data.line_for_items( + items + .iter() + .filter_map(|item| item.id.parse::().ok()) + .map(StatusLineItem::preview_item), + ), + Some(Line::from("gpt-5 · Roadmap cleanup")) + ); + } + + #[test] + fn setup_view_snapshot_uses_runtime_preview_values() { + let (tx_raw, _rx) = unbounded_channel::(); + let view = StatusLineSetupView::new( + Some(&[ + StatusLineItem::ModelName.to_string(), + StatusLineItem::CurrentDir.to_string(), + StatusLineItem::GitBranch.to_string(), + ]), + StatusSurfacePreviewData::from_iter([ + ( + StatusLineItem::ModelName.preview_item(), + "gpt-5-codex".to_string(), + ), + ( + StatusLineItem::CurrentDir.preview_item(), + "~/codex-rs".to_string(), + ), + ( + StatusLineItem::GitBranch.preview_item(), + "jif/statusline-preview".to_string(), + ), + ( + StatusLineItem::WeeklyLimit.preview_item(), + "weekly 82%".to_string(), + ), + ]), + AppEventSender::new(tx_raw), + ); + + assert_snapshot!(render_lines(&view, /*width*/ 72)); + } + + fn render_lines(view: &StatusLineSetupView, width: u16) -> String { + let height = view.desired_height(width); + let area = Rect::new(0, 0, width, height); + let mut buf = Buffer::empty(area); + view.render(area, &mut buf); + + (0..area.height) + .map(|row| { + let mut line = String::new(); + for col in 0..area.width { + let symbol = buf[(area.x + col, area.y + row)].symbol(); + if symbol.is_empty() { + line.push(' '); + } else { + line.push_str(symbol); + } + } + line + }) + .collect::>() + .join("\n") + } } diff --git a/codex-rs/tui/src/bottom_pane/title_setup.rs b/codex-rs/tui/src/bottom_pane/title_setup.rs index 0595a17974d9..c1eec1d88fef 100644 --- a/codex-rs/tui/src/bottom_pane/title_setup.rs +++ b/codex-rs/tui/src/bottom_pane/title_setup.rs @@ -37,17 +37,45 @@ pub(crate) enum TerminalTitleItem { /// Codex app name. AppName, /// Project root name, or a compact cwd fallback. + #[strum(to_string = "project-name", serialize = "project")] Project, + /// Current working directory path. + CurrentDir, /// Animated task spinner while active. Spinner, /// Compact runtime status text. Status, /// Current thread title (if available). + #[strum(to_string = "thread-title", serialize = "thread")] Thread, /// Current git branch (if available). GitBranch, + /// Percentage of context window remaining. + ContextRemaining, + /// Percentage of context window used. + #[strum(to_string = "context-used", serialize = "context-usage")] + ContextUsed, + /// Remaining usage on the 5-hour rate limit. + FiveHourLimit, + /// Remaining usage on the weekly rate limit. + WeeklyLimit, + /// Codex application version. + CodexVersion, + /// Total tokens used in the current session. + UsedTokens, + /// Total input tokens consumed. + TotalInputTokens, + /// Total output tokens generated. + TotalOutputTokens, + /// Full session UUID. + SessionId, + /// Whether Fast mode is currently active. + FastMode, /// Current model name. + #[strum(to_string = "model", serialize = "model-name")] Model, + /// Current model name with reasoning level. + ModelWithReasoning, /// Latest checklist task progress from `update_plan` (if available). TaskProgress, } @@ -57,13 +85,35 @@ impl TerminalTitleItem { match self { TerminalTitleItem::AppName => "Codex app name", TerminalTitleItem::Project => "Project name (falls back to current directory name)", + TerminalTitleItem::CurrentDir => "Current working directory", TerminalTitleItem::Spinner => { "Animated task spinner (omitted while idle or when animations are off)" } TerminalTitleItem::Status => "Compact session status text (Ready, Working, Thinking)", - TerminalTitleItem::Thread => "Current thread title (omitted until available)", + TerminalTitleItem::Thread => "Current thread title (omitted when unavailable)", TerminalTitleItem::GitBranch => "Current Git branch (omitted when unavailable)", + TerminalTitleItem::ContextRemaining => { + "Percentage of context window remaining (omitted when unknown)" + } + TerminalTitleItem::ContextUsed => { + "Percentage of context window used (omitted when unknown)" + } + TerminalTitleItem::FiveHourLimit => { + "Remaining usage on 5-hour usage limit (omitted when unavailable)" + } + TerminalTitleItem::WeeklyLimit => { + "Remaining usage on weekly usage limit (omitted when unavailable)" + } + TerminalTitleItem::CodexVersion => "Codex application version", + TerminalTitleItem::UsedTokens => "Total tokens used in session (omitted when zero)", + TerminalTitleItem::TotalInputTokens => "Total input tokens used in session", + TerminalTitleItem::TotalOutputTokens => "Total output tokens used in session", + TerminalTitleItem::SessionId => { + "Current session identifier (omitted until session starts)" + } + TerminalTitleItem::FastMode => "Whether Fast mode is currently active", TerminalTitleItem::Model => "Current model name", + TerminalTitleItem::ModelWithReasoning => "Current model name with reasoning level", TerminalTitleItem::TaskProgress => { "Latest task progress from update_plan (omitted until available)" } @@ -74,11 +124,27 @@ impl TerminalTitleItem { match self { TerminalTitleItem::AppName => Some(StatusSurfacePreviewItem::AppName), TerminalTitleItem::Project => Some(StatusSurfacePreviewItem::ProjectName), + TerminalTitleItem::CurrentDir => Some(StatusSurfacePreviewItem::CurrentDir), TerminalTitleItem::Spinner => None, TerminalTitleItem::Status => Some(StatusSurfacePreviewItem::Status), TerminalTitleItem::Thread => Some(StatusSurfacePreviewItem::ThreadTitle), TerminalTitleItem::GitBranch => Some(StatusSurfacePreviewItem::GitBranch), + TerminalTitleItem::ContextRemaining => Some(StatusSurfacePreviewItem::ContextRemaining), + TerminalTitleItem::ContextUsed => Some(StatusSurfacePreviewItem::ContextUsed), + TerminalTitleItem::FiveHourLimit => Some(StatusSurfacePreviewItem::FiveHourLimit), + TerminalTitleItem::WeeklyLimit => Some(StatusSurfacePreviewItem::WeeklyLimit), + TerminalTitleItem::CodexVersion => Some(StatusSurfacePreviewItem::CodexVersion), + TerminalTitleItem::UsedTokens => Some(StatusSurfacePreviewItem::UsedTokens), + TerminalTitleItem::TotalInputTokens => Some(StatusSurfacePreviewItem::TotalInputTokens), + TerminalTitleItem::TotalOutputTokens => { + Some(StatusSurfacePreviewItem::TotalOutputTokens) + } + TerminalTitleItem::SessionId => Some(StatusSurfacePreviewItem::SessionId), + TerminalTitleItem::FastMode => Some(StatusSurfacePreviewItem::FastMode), TerminalTitleItem::Model => Some(StatusSurfacePreviewItem::Model), + TerminalTitleItem::ModelWithReasoning => { + Some(StatusSurfacePreviewItem::ModelWithReasoning) + } TerminalTitleItem::TaskProgress => Some(StatusSurfacePreviewItem::TaskProgress), } } @@ -267,12 +333,56 @@ impl Renderable for TerminalTitleSetupView { #[cfg(test)] mod tests { use super::*; + use insta::assert_snapshot; use pretty_assertions::assert_eq; + use tokio::sync::mpsc::unbounded_channel; + + fn render_lines(view: &TerminalTitleSetupView, width: u16) -> String { + let height = view.desired_height(width); + let area = Rect::new(0, 0, width, height); + let mut buf = Buffer::empty(area); + view.render(area, &mut buf); + + let lines: Vec = (0..area.height) + .map(|row| { + let mut line = String::new(); + for col in 0..area.width { + let symbol = buf[(area.x + col, area.y + row)].symbol(); + if symbol.is_empty() { + line.push(' '); + } else { + line.push_str(symbol); + } + } + line + }) + .collect(); + lines.join("\n") + } + + #[test] + fn renders_title_setup_popup() { + let (tx_raw, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let selected = [ + "project-name".to_string(), + "spinner".to_string(), + "status".to_string(), + "thread-title".to_string(), + ]; + let view = + TerminalTitleSetupView::new(Some(&selected), StatusSurfacePreviewData::default(), tx); + assert_snapshot!( + "terminal_title_setup_basic", + render_lines(&view, /*width*/ 84) + ); + } #[test] fn parse_terminal_title_items_preserves_order() { - let items = - parse_terminal_title_items(["project", "spinner", "status", "thread"].into_iter()); + let items = parse_terminal_title_items( + ["project-name", "spinner", "status", "thread-title"].into_iter(), + ); assert_eq!( items, Some(vec![ @@ -290,14 +400,101 @@ mod tests { assert_eq!(items, None); } + #[test] + fn project_name_is_canonical_and_accepts_project_legacy_id() { + assert_eq!(TerminalTitleItem::Project.to_string(), "project-name"); + assert_eq!( + "project-name".parse::(), + Ok(TerminalTitleItem::Project) + ); + assert_eq!( + "project".parse::(), + Ok(TerminalTitleItem::Project) + ); + } + + #[test] + fn thread_title_is_canonical_and_accepts_thread_legacy_id() { + assert_eq!(TerminalTitleItem::Thread.to_string(), "thread-title"); + assert_eq!( + "thread-title".parse::(), + Ok(TerminalTitleItem::Thread) + ); + assert_eq!( + "thread".parse::(), + Ok(TerminalTitleItem::Thread) + ); + } + + #[test] + fn model_is_canonical_and_accepts_model_name_legacy_id() { + assert_eq!(TerminalTitleItem::Model.to_string(), "model"); + assert_eq!( + "model".parse::(), + Ok(TerminalTitleItem::Model) + ); + assert_eq!( + "model-name".parse::(), + Ok(TerminalTitleItem::Model) + ); + } + + #[test] + fn model_with_reasoning_has_distinct_id() { + assert_eq!( + TerminalTitleItem::ModelWithReasoning.to_string(), + "model-with-reasoning" + ); + assert_eq!( + "model-with-reasoning".parse::(), + Ok(TerminalTitleItem::ModelWithReasoning) + ); + } + #[test] fn parse_terminal_title_items_accepts_kebab_case_variants() { - let items = parse_terminal_title_items(["app-name", "git-branch"].into_iter()); + let items = parse_terminal_title_items( + [ + "app-name", + "context-remaining", + "context-used", + "five-hour-limit", + "git-branch", + "spinner", + "current-dir", + "project-name", + "model", + "model-with-reasoning", + "weekly-limit", + "codex-version", + "used-tokens", + "total-input-tokens", + "total-output-tokens", + "session-id", + "fast-mode", + ] + .into_iter(), + ); assert_eq!( items, Some(vec![ TerminalTitleItem::AppName, + TerminalTitleItem::ContextRemaining, + TerminalTitleItem::ContextUsed, + TerminalTitleItem::FiveHourLimit, TerminalTitleItem::GitBranch, + TerminalTitleItem::Spinner, + TerminalTitleItem::CurrentDir, + TerminalTitleItem::Project, + TerminalTitleItem::Model, + TerminalTitleItem::ModelWithReasoning, + TerminalTitleItem::WeeklyLimit, + TerminalTitleItem::CodexVersion, + TerminalTitleItem::UsedTokens, + TerminalTitleItem::TotalInputTokens, + TerminalTitleItem::TotalOutputTokens, + TerminalTitleItem::SessionId, + TerminalTitleItem::FastMode, ]) ); } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index be258fae4a8d..7af70e441501 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -7662,13 +7662,7 @@ impl ChatWidget { fn terminal_title_preview_data(&mut self) -> StatusSurfacePreviewData { let mut preview_data = self.status_surface_preview_data(); let now = Instant::now(); - for item in [ - TerminalTitleItem::Project, - TerminalTitleItem::Thread, - TerminalTitleItem::GitBranch, - TerminalTitleItem::Model, - TerminalTitleItem::TaskProgress, - ] { + for item in TerminalTitleItem::iter() { let Some(preview_item) = item.preview_item() else { continue; }; @@ -7679,7 +7673,6 @@ impl ChatWidget { } preview_data } - fn open_theme_picker(&mut self) { let codex_home = crate::legacy_core::config::find_codex_home().ok(); let terminal_width = self diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index 7045bbecfdc4..a6a10495d3b3 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -7,7 +7,7 @@ use super::*; /// Items shown in the terminal title when the user has not configured a /// custom selection. Intentionally minimal: spinner + project name. -pub(super) const DEFAULT_TERMINAL_TITLE_ITEMS: [&str; 2] = ["spinner", "project"]; +pub(super) const DEFAULT_TERMINAL_TITLE_ITEMS: [&str; 2] = ["spinner", "project-name"]; /// Braille-pattern dot-spinner frames for the terminal title animation. pub(super) const TERMINAL_TITLE_SPINNER_FRAMES: [&str; 10] = @@ -421,18 +421,7 @@ impl ChatWidget { pub(super) fn status_line_value_for_item(&mut self, item: &StatusLineItem) -> Option { match item { StatusLineItem::ModelName => Some(self.model_display_name().to_string()), - StatusLineItem::ModelWithReasoning => { - let label = - Self::status_line_reasoning_effort_label(self.effective_reasoning_effort()); - let fast_label = if self - .should_show_fast_status(self.current_model(), self.config.service_tier) - { - " fast" - } else { - "" - }; - Some(format!("{} {label}{fast_label}", self.model_display_name())) - } + StatusLineItem::ModelWithReasoning => Some(self.model_with_reasoning_display_name()), StatusLineItem::CurrentDir => { Some(format_directory_display( self.status_line_cwd(), @@ -441,6 +430,7 @@ impl ChatWidget { } StatusLineItem::ProjectRoot => self.status_line_project_root_name(), StatusLineItem::GitBranch => self.status_line_branch.clone(), + StatusLineItem::Status => Some(self.terminal_title_status_text()), StatusLineItem::UsedTokens => { let usage = self.status_line_total_usage(); let total = usage.tokens_in_context_window(); @@ -502,6 +492,7 @@ impl ChatWidget { let trimmed = name.trim(); (!trimmed.is_empty()).then(|| trimmed.to_string()) }), + StatusLineItem::TaskProgress => self.terminal_title_task_progress(), } } @@ -547,6 +538,10 @@ impl ChatWidget { match item { TerminalTitleItem::AppName => Some("codex".to_string()), TerminalTitleItem::Project => self.terminal_title_project_name(), + TerminalTitleItem::CurrentDir => Some(Self::truncate_terminal_title_part( + format_directory_display(self.status_line_cwd(), /*max_width*/ None), + /*max_chars*/ 32, + )), TerminalTitleItem::Spinner => self.terminal_title_spinner_text_at(now), TerminalTitleItem::Status => Some(self.terminal_title_status_text()), TerminalTitleItem::Thread => self.thread_name.as_ref().and_then(|name| { @@ -563,14 +558,59 @@ impl ChatWidget { TerminalTitleItem::GitBranch => self.status_line_branch.as_ref().map(|branch| { Self::truncate_terminal_title_part(branch.clone(), /*max_chars*/ 32) }), + TerminalTitleItem::ContextRemaining => self + .status_line_value_for_item(&StatusLineItem::ContextRemaining) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), + TerminalTitleItem::ContextUsed => self + .status_line_value_for_item(&StatusLineItem::ContextUsed) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), + TerminalTitleItem::FiveHourLimit => self + .status_line_value_for_item(&StatusLineItem::FiveHourLimit) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), + TerminalTitleItem::WeeklyLimit => self + .status_line_value_for_item(&StatusLineItem::WeeklyLimit) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), + TerminalTitleItem::CodexVersion => self + .status_line_value_for_item(&StatusLineItem::CodexVersion) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), + TerminalTitleItem::UsedTokens => self + .status_line_value_for_item(&StatusLineItem::UsedTokens) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), + TerminalTitleItem::TotalInputTokens => self + .status_line_value_for_item(&StatusLineItem::TotalInputTokens) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), + TerminalTitleItem::TotalOutputTokens => self + .status_line_value_for_item(&StatusLineItem::TotalOutputTokens) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), + TerminalTitleItem::SessionId => self + .status_line_value_for_item(&StatusLineItem::SessionId) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), + TerminalTitleItem::FastMode => self + .status_line_value_for_item(&StatusLineItem::FastMode) + .map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)), TerminalTitleItem::Model => Some(Self::truncate_terminal_title_part( self.model_display_name().to_string(), /*max_chars*/ 32, )), + TerminalTitleItem::ModelWithReasoning => Some(Self::truncate_terminal_title_part( + self.model_with_reasoning_display_name(), + /*max_chars*/ 32, + )), TerminalTitleItem::TaskProgress => self.terminal_title_task_progress(), } } + fn model_with_reasoning_display_name(&self) -> String { + let label = Self::status_line_reasoning_effort_label(self.effective_reasoning_effort()); + let fast_label = + if self.should_show_fast_status(self.current_model(), self.config.service_tier) { + " fast" + } else { + "" + }; + format!("{} {label}{fast_label}", self.model_display_name()) + } + /// Computes the compact runtime status label used by the terminal title. /// /// Startup takes precedence over normal task states, and idle state renders From a466deff89b78e7552a3259fc0e599f0a87085ba Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Tue, 21 Apr 2026 14:14:49 -0700 Subject: [PATCH 2/3] Remove redundant closure, update snapshots --- ...iew_snapshot_uses_runtime_preview_values.snap | 12 +++++++----- ...setup__tests__terminal_title_setup_basic.snap | 16 +++++++--------- .../tui/src/bottom_pane/status_line_setup.rs | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap index 9a79ebaa1160..bd6fcc227ebf 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap @@ -8,12 +8,14 @@ expression: "render_lines(&view, 72)" Type to search > -› [x] model Current model name - [x] current-dir Current working directory - [x] git-branch Current Git branch (omitted when unavailable) +› [x] model Current model name + [x] current-dir Current working directory + [x] git-branch Current Git branch (omitted when unavaila… [ ] model-with-reasoning Current model name with reasoning level - [ ] project-name Project name (omitted when unavailable) - [ ] status Compact session status text (Ready, Working, … + [ ] project-name Project name (omitted when unavailable) + [ ] status Compact session status text (Ready, Worki… + [ ] context-remaining Percentage of context window remaining (o… + [ ] context-used Percentage of context window used (omitte… gpt-5-codex · ~/codex-rs · jif/statusline-preview Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap index 63de8efc8b50..034b537d07cc 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap @@ -8,16 +8,14 @@ expression: "render_lines(&view, 84)" Type to search > -› [x] project-name Project name (falls back to current directory name) - [x] spinner Animated task spinner (omitted while idle or when animations… - [x] status Compact session status text (Ready, Working, Thinking) - [x] thread-title Current thread title (omitted when unavailable) - [ ] app-name Codex app name - [ ] current-dir Current working directory - [ ] git-branch Current Git branch (omitted when unavailable) +› [x] project-name Project name (falls back to current directory name) + [x] spinner Animated task spinner (omitted while idle or when animat… + [x] status Compact session status text (Ready, Working, Thinking) + [x] thread-title Current thread title (omitted when unavailable) + [ ] app-name Codex app name + [ ] current-dir Current working directory + [ ] git-branch Current Git branch (omitted when unavailable) [ ] context-remaining Percentage of context window remaining (omitted when unk… - [ ] context-used Percentage of context window used (omitted when unknown) - [ ] five-hour-limit Remaining usage on 5-hour usage limit (omitted when unava… my-project ⠋ Working | thread title Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc to cancel. diff --git a/codex-rs/tui/src/bottom_pane/status_line_setup.rs b/codex-rs/tui/src/bottom_pane/status_line_setup.rs index d683eea1fbb4..74e443465378 100644 --- a/codex-rs/tui/src/bottom_pane/status_line_setup.rs +++ b/codex-rs/tui/src/bottom_pane/status_line_setup.rs @@ -375,7 +375,7 @@ mod tests { fn parse_status_line_items_accepts_title_only_variants() { let items = ["status", "task-progress"] .into_iter() - .map(|id| id.parse::()) + .map(str::parse::) .collect::, _>>(); assert_eq!( items, From 1a41c216fa4c27f2b23e6d647c235c61fb340a2d Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Tue, 21 Apr 2026 15:51:12 -0700 Subject: [PATCH 3/3] Rename status to run-state, retain legacy config support --- ..._snapshot_uses_runtime_preview_values.snap | 2 +- ...up__tests__terminal_title_setup_basic.snap | 2 +- .../tui/src/bottom_pane/status_line_setup.rs | 20 +++++++++++++--- codex-rs/tui/src/bottom_pane/title_setup.rs | 24 +++++++++++++++---- codex-rs/tui/src/chatwidget.rs | 6 ++++- ...inal_title_setup_popup_hardcoded_only.snap | 2 +- ..._terminal_title_setup_popup_live_only.snap | 2 +- ...sts__terminal_title_setup_popup_mixed.snap | 2 +- 8 files changed, 47 insertions(+), 13 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap index bd6fcc227ebf..ff93e8374865 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap @@ -13,7 +13,7 @@ expression: "render_lines(&view, 72)" [x] git-branch Current Git branch (omitted when unavaila… [ ] model-with-reasoning Current model name with reasoning level [ ] project-name Project name (omitted when unavailable) - [ ] status Compact session status text (Ready, Worki… + [ ] run-state Compact session run-state text (Ready, Wo… [ ] context-remaining Percentage of context window remaining (o… [ ] context-used Percentage of context window used (omitte… diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap index 034b537d07cc..c8cdb76be027 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__title_setup__tests__terminal_title_setup_basic.snap @@ -10,7 +10,7 @@ expression: "render_lines(&view, 84)" > › [x] project-name Project name (falls back to current directory name) [x] spinner Animated task spinner (omitted while idle or when animat… - [x] status Compact session status text (Ready, Working, Thinking) + [x] run-state Compact session run-state text (Ready, Working, Thinking) [x] thread-title Current thread title (omitted when unavailable) [ ] app-name Codex app name [ ] current-dir Current working directory diff --git a/codex-rs/tui/src/bottom_pane/status_line_setup.rs b/codex-rs/tui/src/bottom_pane/status_line_setup.rs index 74e443465378..ea522f3bf051 100644 --- a/codex-rs/tui/src/bottom_pane/status_line_setup.rs +++ b/codex-rs/tui/src/bottom_pane/status_line_setup.rs @@ -69,7 +69,8 @@ pub(crate) enum StatusLineItem { /// Current git branch name (if in a repository). GitBranch, - /// Compact runtime status text. + /// Compact runtime run-state text. + #[strum(to_string = "run-state", serialize = "status")] Status, /// Percentage of context window remaining. @@ -124,7 +125,7 @@ impl StatusLineItem { StatusLineItem::CurrentDir => "Current working directory", StatusLineItem::ProjectRoot => "Project name (omitted when unavailable)", StatusLineItem::GitBranch => "Current Git branch (omitted when unavailable)", - StatusLineItem::Status => "Compact session status text (Ready, Working, Thinking)", + StatusLineItem::Status => "Compact session run-state text (Ready, Working, Thinking)", StatusLineItem::ContextRemaining => { "Percentage of context window remaining (omitted when unknown)" } @@ -371,9 +372,22 @@ mod tests { ); } + #[test] + fn run_state_is_canonical_and_accepts_status_legacy_id() { + assert_eq!(StatusLineItem::Status.to_string(), "run-state"); + assert_eq!( + "run-state".parse::(), + Ok(StatusLineItem::Status) + ); + assert_eq!( + "status".parse::(), + Ok(StatusLineItem::Status) + ); + } + #[test] fn parse_status_line_items_accepts_title_only_variants() { - let items = ["status", "task-progress"] + let items = ["run-state", "task-progress"] .into_iter() .map(str::parse::) .collect::, _>>(); diff --git a/codex-rs/tui/src/bottom_pane/title_setup.rs b/codex-rs/tui/src/bottom_pane/title_setup.rs index c1eec1d88fef..51b14976f0bf 100644 --- a/codex-rs/tui/src/bottom_pane/title_setup.rs +++ b/codex-rs/tui/src/bottom_pane/title_setup.rs @@ -43,7 +43,8 @@ pub(crate) enum TerminalTitleItem { CurrentDir, /// Animated task spinner while active. Spinner, - /// Compact runtime status text. + /// Compact runtime run-state text. + #[strum(to_string = "run-state", serialize = "status")] Status, /// Current thread title (if available). #[strum(to_string = "thread-title", serialize = "thread")] @@ -89,7 +90,9 @@ impl TerminalTitleItem { TerminalTitleItem::Spinner => { "Animated task spinner (omitted while idle or when animations are off)" } - TerminalTitleItem::Status => "Compact session status text (Ready, Working, Thinking)", + TerminalTitleItem::Status => { + "Compact session run-state text (Ready, Working, Thinking)" + } TerminalTitleItem::Thread => "Current thread title (omitted when unavailable)", TerminalTitleItem::GitBranch => "Current Git branch (omitted when unavailable)", TerminalTitleItem::ContextRemaining => { @@ -367,7 +370,7 @@ mod tests { let selected = [ "project-name".to_string(), "spinner".to_string(), - "status".to_string(), + "run-state".to_string(), "thread-title".to_string(), ]; let view = @@ -381,7 +384,7 @@ mod tests { #[test] fn parse_terminal_title_items_preserves_order() { let items = parse_terminal_title_items( - ["project-name", "spinner", "status", "thread-title"].into_iter(), + ["project-name", "spinner", "run-state", "thread-title"].into_iter(), ); assert_eq!( items, @@ -439,6 +442,19 @@ mod tests { ); } + #[test] + fn run_state_is_canonical_and_accepts_status_legacy_id() { + assert_eq!(TerminalTitleItem::Status.to_string(), "run-state"); + assert_eq!( + "run-state".parse::(), + Ok(TerminalTitleItem::Status) + ); + assert_eq!( + "status".parse::(), + Ok(TerminalTitleItem::Status) + ); + } + #[test] fn model_with_reasoning_has_distinct_id() { assert_eq!( diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 7af70e441501..61e96d137b5e 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1896,7 +1896,11 @@ impl ChatWidget { .config .tui_terminal_title .as_ref() - .is_some_and(|items| items.iter().any(|item| item == "status")); + .is_some_and(|items| { + items + .iter() + .any(|item| item == "run-state" || item == "status") + }); let title_uses_spinner = self .config .tui_terminal_title diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_hardcoded_only.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_hardcoded_only.snap index eb27cdd01bb3..b1c1cf5aed2f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_hardcoded_only.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_hardcoded_only.snap @@ -13,7 +13,7 @@ expression: terminal_title_popup_snapshot(&mut chat) [ ] app-name Codex app name [ ] project Project name (falls back to current directory name) [ ] spinner Animated task spinner (omitted while idle or when animations are off) - [ ] status Compact session status text (Ready, Working, Thinking) + [ ] run-state Compact session run-state text (Ready, Working, Thinking) [ ] model Current model name thread title | feat/awesome-feature | Tasks 0/0 diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_live_only.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_live_only.snap index bc2bf2502a7e..9842d8eca8f5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_live_only.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_live_only.snap @@ -13,7 +13,7 @@ expression: terminal_title_popup_snapshot(&mut chat) [x] task-progress Latest task progress from update_plan (omitted until available) [ ] app-name Codex app name [ ] spinner Animated task spinner (omitted while idle or when animations are off) - [ ] status Compact session status text (Ready, Working, Thinking) + [ ] run-state Compact session run-state text (Ready, Working, Thinking) [ ] model Current model name preview-live-root | Live preview thread | feature/live-preview-branch | Tasks 2/5 diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_mixed.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_mixed.snap index 4d2f3128d526..d75306617cef 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_mixed.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__terminal_title_setup_popup_mixed.snap @@ -12,7 +12,7 @@ expression: terminal_title_popup_snapshot(&mut chat) [x] task-progress Latest task progress from update_plan (omitted until available) [ ] app-name Codex app name [ ] spinner Animated task spinner (omitted while idle or when animations are off) - [ ] status Compact session status text (Ready, Working, Thinking) + [ ] run-state Compact session run-state text (Ready, Working, Thinking) [ ] git-branch Current Git branch (omitted when unavailable) [ ] model Current model name