diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 34d8d85a9..9989644ed 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -918,8 +918,10 @@ impl App { history_cell::SessionHeaderHistoryCell::new( self.chat_widget.current_model().to_string(), self.chat_widget.current_reasoning_effort(), - self.chat_widget - .should_show_fast_status(self.chat_widget.current_service_tier()), + self.chat_widget.should_show_fast_status( + self.chat_widget.current_model(), + self.chat_widget.current_service_tier(), + ), self.config.cwd.clone(), version, ) @@ -3790,6 +3792,7 @@ mod tests { use crate::app_backtrack::BacktrackState; use crate::app_backtrack::user_count; use crate::chatwidget::tests::make_chatwidget_manual_with_sender; + use crate::chatwidget::tests::set_chatgpt_auth; use crate::file_search::FileSearchManager; use crate::history_cell::AgentMessageCell; use crate::history_cell::HistoryCell; @@ -5463,6 +5466,32 @@ mod tests { assert_snapshot!("clear_ui_after_long_transcript_fresh_header_only", rendered); } + #[tokio::test] + async fn clear_ui_header_shows_fast_status_only_for_gpt54() { + let mut app = make_test_app().await; + app.config.cwd = PathBuf::from("/tmp/project"); + app.chat_widget.set_model("gpt-5.4"); + app.chat_widget + .set_reasoning_effort(Some(ReasoningEffortConfig::XHigh)); + app.chat_widget + .set_service_tier(Some(codex_protocol::config_types::ServiceTier::Fast)); + set_chatgpt_auth(&mut app.chat_widget); + + let rendered = app + .clear_ui_header_lines_with_version(80, "") + .iter() + .map(|line| { + line.spans + .iter() + .map(|span| span.content.as_ref()) + .collect::() + }) + .collect::>() + .join("\n"); + + assert_snapshot!("clear_ui_header_fast_status_gpt54_only", rendered); + } + async fn make_test_app() -> App { let (chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender().await; let config = chat_widget.config_ref().clone(); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 1d850de26..b6c329788 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -306,6 +306,7 @@ use strum::IntoEnumIterator; const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally"; const USER_SHELL_COMMAND_HELP_HINT: &str = "Example: !ls"; const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1"; +const FAST_STATUS_MODEL: &str = "gpt-5.4"; const DEFAULT_STATUS_LINE_ITEMS: [&str; 3] = ["model-with-reasoning", "context-remaining", "current-dir"]; // Track information about an in-flight exec command. @@ -1270,7 +1271,7 @@ impl ChatWidget { self.sync_personality_command_enabled(); self.refresh_plugin_mentions(); let startup_tooltip_override = self.startup_tooltip_override.take(); - let show_fast_status = self.should_show_fast_status(event.service_tier); + let show_fast_status = self.should_show_fast_status(&model_for_header, event.service_tier); let session_info_cell = history_cell::new_session_info( &self.config, &model_for_header, @@ -5424,7 +5425,14 @@ impl ChatWidget { StatusLineItem::ModelWithReasoning => { let label = Self::status_line_reasoning_effort_label(self.effective_reasoning_effort()); - Some(format!("{} {label}", self.model_display_name())) + 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::CurrentDir => { Some(format_directory_display(self.status_line_cwd(), None)) @@ -7413,8 +7421,13 @@ impl ChatWidget { self.config.service_tier } - pub(crate) fn should_show_fast_status(&self, service_tier: Option) -> bool { - matches!(service_tier, Some(ServiceTier::Fast)) + pub(crate) fn should_show_fast_status( + &self, + model: &str, + service_tier: Option, + ) -> bool { + model == FAST_STATUS_MODEL + && matches!(service_tier, Some(ServiceTier::Fast)) && self .auth_manager .auth_cached() diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_fast_footer.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_fast_footer.snap new file mode 100644 index 000000000..6355decd6 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_fast_footer.snap @@ -0,0 +1,9 @@ +--- +source: tui/src/chatwidget/tests.rs +expression: terminal.backend() +--- +" " +" " +"› Ask Codex to do anything " +" " +" gpt-5.4 xhigh fast · 100% left · /tmp/project " diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 080f141fe..85f4d051d 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1934,7 +1934,7 @@ fn assert_no_submit_op(op_rx: &mut tokio::sync::mpsc::UnboundedReceiver) { } } -fn set_chatgpt_auth(chat: &mut ChatWidget) { +pub(crate) fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.auth_manager = codex_core::test_support::auth_manager_from_auth( CodexAuth::create_dummy_chatgpt_auth_for_testing(), ); @@ -7787,22 +7787,31 @@ async fn user_turn_carries_service_tier_after_fast_toggle() { #[tokio::test] async fn fast_status_indicator_requires_chatgpt_auth() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await; + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; chat.set_service_tier(Some(ServiceTier::Fast)); - assert!(!chat.should_show_fast_status(chat.current_service_tier())); + assert!(!chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),)); set_chatgpt_auth(&mut chat); - assert!(chat.should_show_fast_status(chat.current_service_tier())); + assert!(chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),)); +} + +#[tokio::test] +async fn fast_status_indicator_is_hidden_for_non_gpt54_model() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await; + chat.set_service_tier(Some(ServiceTier::Fast)); + set_chatgpt_auth(&mut chat); + + assert!(!chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),)); } #[tokio::test] async fn fast_status_indicator_is_hidden_when_fast_mode_is_off() { - let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await; + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; set_chatgpt_auth(&mut chat); - assert!(!chat.should_show_fast_status(chat.current_service_tier())); + assert!(!chat.should_show_fast_status(chat.current_model(), chat.current_service_tier(),)); } #[tokio::test] @@ -9552,6 +9561,64 @@ async fn status_line_fast_mode_footer_snapshot() { assert_snapshot!("status_line_fast_mode_footer", terminal.backend()); } +#[tokio::test] +async fn status_line_model_with_reasoning_includes_fast_for_gpt54_only() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; + chat.config.cwd = PathBuf::from("/tmp/project"); + chat.config.tui_status_line = Some(vec![ + "model-with-reasoning".to_string(), + "context-remaining".to_string(), + "current-dir".to_string(), + ]); + chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh)); + chat.set_service_tier(Some(ServiceTier::Fast)); + set_chatgpt_auth(&mut chat); + chat.refresh_status_line(); + + assert_eq!( + status_line_text(&chat), + Some("gpt-5.4 xhigh fast · 100% left · /tmp/project".to_string()) + ); + + chat.set_model("gpt-5.3-codex"); + chat.refresh_status_line(); + + assert_eq!( + status_line_text(&chat), + Some("gpt-5.3-codex xhigh · 100% left · /tmp/project".to_string()) + ); +} + +#[tokio::test] +async fn status_line_model_with_reasoning_fast_footer_snapshot() { + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await; + chat.show_welcome_banner = false; + chat.config.cwd = PathBuf::from("/tmp/project"); + chat.config.tui_status_line = Some(vec![ + "model-with-reasoning".to_string(), + "context-remaining".to_string(), + "current-dir".to_string(), + ]); + chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh)); + chat.set_service_tier(Some(ServiceTier::Fast)); + set_chatgpt_auth(&mut chat); + chat.refresh_status_line(); + + let width = 80; + let height = chat.desired_height(width); + let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("create terminal"); + terminal + .draw(|f| chat.render(f.area(), f.buffer_mut())) + .expect("draw model-with-reasoning footer"); + assert_snapshot!( + "status_line_model_with_reasoning_fast_footer", + terminal.backend() + ); +} + #[tokio::test] async fn stream_recovery_restores_previous_status_header() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; diff --git a/codex-rs/tui/src/snapshots/codex_tui__app__tests__clear_ui_header_fast_status_gpt54_only.snap b/codex-rs/tui/src/snapshots/codex_tui__app__tests__clear_ui_header_fast_status_gpt54_only.snap new file mode 100644 index 000000000..08afd027a --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__app__tests__clear_ui_header_fast_status_gpt54_only.snap @@ -0,0 +1,10 @@ +--- +source: tui/src/app.rs +expression: rendered +--- +╭────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v) │ +│ │ +│ model: gpt-5.4 xhigh fast /model to change │ +│ directory: /tmp/project │ +╰────────────────────────────────────────────────────╯