From 88d090a2b7ebc5ee7f7db9e84c65a992cae0d0ff Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 5 Jan 2026 10:02:44 -0800 Subject: [PATCH] models --- codex-rs/tui/src/app.rs | 5 +++ codex-rs/tui/src/chatwidget.rs | 12 ++---- codex-rs/tui/src/model_migration.rs | 62 +++++++++++++++++++++++++--- codex-rs/tui2/src/app.rs | 5 +++ codex-rs/tui2/src/chatwidget.rs | 12 ++---- codex-rs/tui2/src/model_migration.rs | 62 +++++++++++++++++++++++++--- 6 files changed, 130 insertions(+), 28 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 0ecad59308..6f99145774 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -238,6 +238,11 @@ impl App { } }; + // Startup-only: show any previously scheduled model migration notice, then refresh the + // schedule to reflect the current config + model presets (at most one scheduled notice). + chat_widget.maybe_show_pending_model_migration_notice(); + chat_widget.refresh_pending_model_migration_notice(); + chat_widget.maybe_prompt_windows_sandbox_enable(); let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 1a04dea63c..87c834b491 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -453,7 +453,6 @@ impl ChatWidget { event, self.show_welcome_banner, )); - self.maybe_show_pending_model_migration_notice(); if let Some(messages) = initial_messages { self.replay_initial_messages(messages); } @@ -470,7 +469,6 @@ impl ChatWidget { // turn now that we know the session is configured. self.maybe_send_next_queued_input(); } - self.maybe_schedule_model_migration_notice(&model_for_header); if !self.suppress_session_configured_redraw { self.request_redraw(); } @@ -808,7 +806,7 @@ impl ChatWidget { self.request_redraw(); } - fn maybe_show_pending_model_migration_notice(&mut self) { + pub(crate) fn maybe_show_pending_model_migration_notice(&mut self) { let Some(notice) = model_migration::take_pending_model_migration_notice(&mut self.config) else { return; @@ -828,17 +826,13 @@ impl ChatWidget { }); } - fn maybe_schedule_model_migration_notice(&self, model: &str) { + pub(crate) fn refresh_pending_model_migration_notice(&self) { let available_models = match self.models_manager.try_list_models(&self.config) { Ok(models) => models, Err(_) => return, }; - model_migration::maybe_schedule_model_migration_notice( - &self.config, - model, - &available_models, - ); + model_migration::refresh_pending_model_migration_notice(&self.config, &available_models); } /// Handle a turn aborted due to user interrupt (Esc). diff --git a/codex-rs/tui/src/model_migration.rs b/codex-rs/tui/src/model_migration.rs index 67ccf08dd1..8f7848b9f8 100644 --- a/codex-rs/tui/src/model_migration.rs +++ b/codex-rs/tui/src/model_migration.rs @@ -15,6 +15,9 @@ pub(crate) struct PendingModelMigrationNotice { // Used to respect hide flags even if config changes between scheduling and display. #[serde(default)] pub(crate) migration_config_key: Option, + /// Unix timestamp (seconds) when this notice was scheduled. Used to expire stale notices. + #[serde(default)] + pub(crate) scheduled_at_unix_seconds: Option, } /// Read and clear the one-shot migration notice file, returning the notice if it should be shown. @@ -50,6 +53,11 @@ pub(crate) fn take_pending_model_migration_notice( } }; + if notice_expired(¬ice) { + let _ = std::fs::remove_file(¬ice_path); + return None; + } + if let Some(migration_config_key) = notice.migration_config_key.as_deref() && migration_prompt_hidden(config, migration_config_key) { @@ -75,11 +83,30 @@ pub(crate) fn take_pending_model_migration_notice( Some(notice) } -pub(crate) fn maybe_schedule_model_migration_notice( +/// Persist the migration notice for the next startup, replacing any existing scheduled notice. +/// +/// Scheduling is intentionally independent of session configuration: it uses the user's config +/// (or the default model preset) to determine what to schedule. +pub(crate) fn refresh_pending_model_migration_notice( config: &Config, - current_model: &str, available_models: &[ModelPreset], ) { + let current_model = config + .model + .as_deref() + .filter(|model| !model.is_empty()) + .or_else(|| { + available_models + .iter() + .find(|preset| preset.is_default) + .map(|preset| preset.model.as_str()) + }); + + let Some(current_model) = current_model else { + clear_pending_model_migration_notice(config); + return; + }; + let Some(ModelUpgrade { id: target_model, migration_config_key, @@ -89,10 +116,12 @@ pub(crate) fn maybe_schedule_model_migration_notice( .find(|preset| preset.model == current_model) .and_then(|preset| preset.upgrade.as_ref()) else { + clear_pending_model_migration_notice(config); return; }; if migration_prompt_hidden(config, migration_config_key.as_str()) { + clear_pending_model_migration_notice(config); return; } @@ -100,6 +129,7 @@ pub(crate) fn maybe_schedule_model_migration_notice( .iter() .all(|preset| preset.model != target_model.as_str()) { + clear_pending_model_migration_notice(config); return; } @@ -109,18 +139,17 @@ pub(crate) fn maybe_schedule_model_migration_notice( available_models, config, ) { + clear_pending_model_migration_notice(config); return; } let notice_path = pending_model_migration_notice_path(config); - if notice_path.exists() { - return; - } let notice = PendingModelMigrationNotice { from_model: current_model.to_string(), to_model: target_model.to_string(), migration_config_key: Some(migration_config_key.to_string()), + scheduled_at_unix_seconds: now_unix_seconds(), }; let Ok(json_line) = serde_json::to_string(¬ice).map(|json| format!("{json}\n")) else { return; @@ -167,6 +196,29 @@ fn migration_prompt_hidden(config: &Config, migration_config_key: &str) -> bool } } +fn clear_pending_model_migration_notice(config: &Config) { + let _ = std::fs::remove_file(pending_model_migration_notice_path(config)); +} + +fn now_unix_seconds() -> Option { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()) +} + +fn notice_expired(notice: &PendingModelMigrationNotice) -> bool { + let Some(scheduled_at) = notice.scheduled_at_unix_seconds else { + return false; + }; + let Some(now) = now_unix_seconds() else { + return false; + }; + + const WEEK_SECONDS: u64 = 7 * 24 * 60 * 60; + now.saturating_sub(scheduled_at) > WEEK_SECONDS +} + fn should_show_model_migration_notice( current_model: &str, target_model: &str, diff --git a/codex-rs/tui2/src/app.rs b/codex-rs/tui2/src/app.rs index b94dfe3f4c..76e8bc46a4 100644 --- a/codex-rs/tui2/src/app.rs +++ b/codex-rs/tui2/src/app.rs @@ -279,6 +279,11 @@ impl App { } }; + // Startup-only: show any previously scheduled model migration notice, then refresh the + // schedule to reflect the current config + model presets (at most one scheduled notice). + chat_widget.maybe_show_pending_model_migration_notice(); + chat_widget.refresh_pending_model_migration_notice(); + chat_widget.maybe_prompt_windows_sandbox_enable(); let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone()); diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index 9ede42cb6e..dce2c5b2f8 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -418,7 +418,6 @@ impl ChatWidget { event, self.show_welcome_banner, )); - self.maybe_show_pending_model_migration_notice(); if let Some(messages) = initial_messages { self.replay_initial_messages(messages); } @@ -435,7 +434,6 @@ impl ChatWidget { // turn now that we know the session is configured. self.maybe_send_next_queued_input(); } - self.maybe_schedule_model_migration_notice(&model_for_header); if !self.suppress_session_configured_redraw { self.request_redraw(); } @@ -750,7 +748,7 @@ impl ChatWidget { self.request_redraw(); } - fn maybe_show_pending_model_migration_notice(&mut self) { + pub(crate) fn maybe_show_pending_model_migration_notice(&mut self) { let Some(notice) = model_migration::take_pending_model_migration_notice(&mut self.config) else { return; @@ -770,17 +768,13 @@ impl ChatWidget { }); } - fn maybe_schedule_model_migration_notice(&self, model: &str) { + pub(crate) fn refresh_pending_model_migration_notice(&self) { let available_models = match self.models_manager.try_list_models(&self.config) { Ok(models) => models, Err(_) => return, }; - model_migration::maybe_schedule_model_migration_notice( - &self.config, - model, - &available_models, - ); + model_migration::refresh_pending_model_migration_notice(&self.config, &available_models); } fn on_mcp_startup_complete(&mut self, ev: McpStartupCompleteEvent) { diff --git a/codex-rs/tui2/src/model_migration.rs b/codex-rs/tui2/src/model_migration.rs index 6f1914537c..927c9ceecd 100644 --- a/codex-rs/tui2/src/model_migration.rs +++ b/codex-rs/tui2/src/model_migration.rs @@ -15,6 +15,9 @@ pub(crate) struct PendingModelMigrationNotice { // Used to respect hide flags even if config changes between scheduling and display. #[serde(default)] pub(crate) migration_config_key: Option, + /// Unix timestamp (seconds) when this notice was scheduled. Used to expire stale notices. + #[serde(default)] + pub(crate) scheduled_at_unix_seconds: Option, } /// Read and clear the one-shot migration notice file, returning the notice if it should be shown. @@ -50,6 +53,11 @@ pub(crate) fn take_pending_model_migration_notice( } }; + if notice_expired(¬ice) { + let _ = std::fs::remove_file(¬ice_path); + return None; + } + if let Some(migration_config_key) = notice.migration_config_key.as_deref() && migration_prompt_hidden(config, migration_config_key) { @@ -75,11 +83,30 @@ pub(crate) fn take_pending_model_migration_notice( Some(notice) } -pub(crate) fn maybe_schedule_model_migration_notice( +/// Persist the migration notice for the next startup, replacing any existing scheduled notice. +/// +/// Scheduling is intentionally independent of session configuration: it uses the user's config +/// (or the default model preset) to determine what to schedule. +pub(crate) fn refresh_pending_model_migration_notice( config: &Config, - current_model: &str, available_models: &[ModelPreset], ) { + let current_model = config + .model + .as_deref() + .filter(|model| !model.is_empty()) + .or_else(|| { + available_models + .iter() + .find(|preset| preset.is_default) + .map(|preset| preset.model.as_str()) + }); + + let Some(current_model) = current_model else { + clear_pending_model_migration_notice(config); + return; + }; + let Some(ModelUpgrade { id: target_model, migration_config_key, @@ -89,10 +116,12 @@ pub(crate) fn maybe_schedule_model_migration_notice( .find(|preset| preset.model == current_model) .and_then(|preset| preset.upgrade.as_ref()) else { + clear_pending_model_migration_notice(config); return; }; if migration_prompt_hidden(config, migration_config_key.as_str()) { + clear_pending_model_migration_notice(config); return; } @@ -100,6 +129,7 @@ pub(crate) fn maybe_schedule_model_migration_notice( .iter() .all(|preset| preset.model != target_model.as_str()) { + clear_pending_model_migration_notice(config); return; } @@ -109,18 +139,17 @@ pub(crate) fn maybe_schedule_model_migration_notice( available_models, config, ) { + clear_pending_model_migration_notice(config); return; } let notice_path = pending_model_migration_notice_path(config); - if notice_path.exists() { - return; - } let notice = PendingModelMigrationNotice { from_model: current_model.to_string(), to_model: target_model.to_string(), migration_config_key: Some(migration_config_key.to_string()), + scheduled_at_unix_seconds: now_unix_seconds(), }; let Ok(json_line) = serde_json::to_string(¬ice).map(|json| format!("{json}\n")) else { return; @@ -167,6 +196,29 @@ fn migration_prompt_hidden(config: &Config, migration_config_key: &str) -> bool } } +fn clear_pending_model_migration_notice(config: &Config) { + let _ = std::fs::remove_file(pending_model_migration_notice_path(config)); +} + +fn now_unix_seconds() -> Option { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()) +} + +fn notice_expired(notice: &PendingModelMigrationNotice) -> bool { + let Some(scheduled_at) = notice.scheduled_at_unix_seconds else { + return false; + }; + let Some(now) = now_unix_seconds() else { + return false; + }; + + const WEEK_SECONDS: u64 = 7 * 24 * 60 * 60; + now.saturating_sub(scheduled_at) > WEEK_SECONDS +} + fn should_show_model_migration_notice( current_model: &str, target_model: &str,