diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index c1d5539f2e..1ab678f622 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -16,6 +16,7 @@ use crate::app_server_session::source_agent_path; use crate::app_server_session::thread_blocks_direct_input; use crate::chatwidget::ThreadInputStateRestoreMode; use crate::streaming::commit_tick::CommitTickScope; +use codex_model_provider_info::OPENAI_PROVIDER_ID; use std::collections::HashSet; #[derive(Clone, Copy)] @@ -1108,6 +1109,20 @@ impl App { self.enqueue_primary_thread_session(started.session, started.turns) .await?; self.apply_backend_banner_fallback(app_server).await; + if let Some(model) = self.harness_overrides.model.as_deref() + && model == self.chat_widget.current_model() + && self.chat_widget.has_chatgpt_account() + && self.chat_widget.config_ref().model_provider_id == OPENAI_PROVIDER_ID + && !self + .model_catalog + .models + .iter() + .any(|preset| preset.model == model) + { + self.chat_widget.add_to_history(history_cell::new_visible_warning_event(format!( + "Your ChatGPT model list doesn't include `{model}`. Check the spelling or use /model to choose another model." + ))); + } if let Some(notice) = self.pending_server_version_notice.take() { self.chat_widget.add_server_version_warning(notice); } diff --git a/codex-rs/tui/src/app/tests/startup.rs b/codex-rs/tui/src/app/tests/startup.rs index 04d2c4ea26..7a91a3a2fb 100644 --- a/codex-rs/tui/src/app/tests/startup.rs +++ b/codex-rs/tui/src/app/tests/startup.rs @@ -1365,7 +1365,10 @@ async fn known_thread_started_preserves_session_without_reading_unmaterialized_r #[tokio::test] async fn startup_thread_started_submits_queued_startup_input() { - let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await; + let (mut app, mut events, mut op_rx) = make_test_app_with_channels().await; + set_chatgpt_auth(&mut app.chat_widget); + let model = "unlisted-model"; + app.harness_overrides.model = Some(model.into()); app.pending_startup_thread_start = true; app.chat_widget .set_queue_submissions_until_session_configured(/*queue*/ true); @@ -1384,11 +1387,13 @@ async fn startup_thread_started_submits_queued_startup_input() { )) .await .expect("embedded app server"); - let thread_id = ThreadId::new(); + let mut session = test_thread_session(ThreadId::new(), test_path_buf("/tmp/project")); + session.model = model.into(); + session.model_provider_id = "openai".into(); app.handle_startup_thread_started( &mut app_server, Ok(AppServerStartedThread { - session: test_thread_session(thread_id, test_path_buf("/tmp/project")), + session, turns: Vec::new(), blocks_direct_input: false, task_tools_available: false, @@ -1397,6 +1402,19 @@ async fn startup_thread_started_submits_queued_startup_input() { .await .expect("startup thread should attach"); + let warnings: Vec<_> = std::iter::from_fn(|| events.try_recv().ok()) + .filter_map(|event| match event { + AppEvent::InsertHistoryCell(cell) if !cell.warning_entries().is_empty() => { + Some(lines_to_single_string(&cell.display_lines(/*width*/ 80))) + } + _ => None, + }) + .collect(); + insta::assert_snapshot!(warnings.join("\n"), @r" + ⚠ Your ChatGPT model list doesn't include `unlisted-model`. Check the spelling + or use /model to choose another model. + "); + assert_eq!(app.chat_widget.current_model(), model); match next_user_turn_op(&mut op_rx) { Op::UserTurn { items, .. } => assert_eq!( items, diff --git a/codex-rs/tui/src/chatwidget/rate_limits.rs b/codex-rs/tui/src/chatwidget/rate_limits.rs index a7162b98b9..60c2470d1a 100644 --- a/codex-rs/tui/src/chatwidget/rate_limits.rs +++ b/codex-rs/tui/src/chatwidget/rate_limits.rs @@ -393,7 +393,7 @@ impl ChatWidget { if !warnings.is_empty() { for warning in warnings { - self.add_to_history(history_cell::new_usage_warning_event(warning)); + self.add_to_history(history_cell::new_visible_warning_event(warning)); } self.request_redraw(); } diff --git a/codex-rs/tui/src/history_cell/notices.rs b/codex-rs/tui/src/history_cell/notices.rs index 9f5e2137f0..7308338aa8 100644 --- a/codex-rs/tui/src/history_cell/notices.rs +++ b/codex-rs/tui/src/history_cell/notices.rs @@ -108,7 +108,7 @@ pub(crate) fn new_warning_event(message: String) -> WarningHistoryCell { } } -pub(crate) fn new_usage_warning_event(message: String) -> WarningHistoryCell { +pub(crate) fn new_visible_warning_event(message: String) -> WarningHistoryCell { WarningHistoryCell { visible_in_transcript: true, ..new_warning_event(message) diff --git a/codex-rs/tui/src/history_cell/warnings_tests.rs b/codex-rs/tui/src/history_cell/warnings_tests.rs index 42cbb002e9..020b3cf981 100644 --- a/codex-rs/tui/src/history_cell/warnings_tests.rs +++ b/codex-rs/tui/src/history_cell/warnings_tests.rs @@ -6,12 +6,12 @@ use pretty_assertions::assert_eq; #[test] fn usage_warnings_render_in_both_transcripts_including_composites() { let message = "Only 8% of your 5h limit remains. Run /status for details."; - let usage = new_usage_warning_event(message.into()); + let usage = new_visible_warning_event(message.into()); let diagnostic = new_warning_event(message.into()); let composite = CompositeHistoryCell::new(vec![ Box::new(PlainHistoryCell::new(vec!["Earlier output".into()])), Box::new(new_warning_event("Hidden diagnostic".into())), - Box::new(new_usage_warning_event(message.into())), + Box::new(new_visible_warning_event(message.into())), ]); assert_eq!(usage.warning_entries(), diagnostic.warning_entries()); for mode in [HistoryRenderMode::Rich, HistoryRenderMode::Raw] {