diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index 64974c24..df0b1273 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -25,6 +25,8 @@ pub enum ConfigEdit { SetNoticeHideFullAccessWarning(bool), /// Toggle the Windows world-writable directories warning acknowledgement flag. SetNoticeHideWorldWritableWarning(bool), + /// Toggle the rate limit model nudge acknowledgement flag. + SetNoticeHideRateLimitModelNudge(bool), /// Toggle the Windows onboarding acknowledgement flag. SetWindowsWslSetupAcknowledged(bool), /// Replace the entire `[mcp_servers]` table. @@ -246,6 +248,11 @@ impl ConfigDocument { &[Notice::TABLE_KEY, "hide_world_writable_warning"], value(*acknowledged), )), + ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value( + Scope::Global, + &[Notice::TABLE_KEY, "hide_rate_limit_model_nudge"], + value(*acknowledged), + )), ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged) => Ok(self.write_value( Scope::Global, &["windows_wsl_setup_acknowledged"], @@ -486,6 +493,12 @@ impl ConfigEditsBuilder { self } + pub fn set_hide_rate_limit_model_nudge(mut self, acknowledged: bool) -> Self { + self.edits + .push(ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged)); + self + } + pub fn set_windows_wsl_setup_acknowledged(mut self, acknowledged: bool) -> Self { self.edits .push(ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged)); @@ -733,6 +746,34 @@ hide_full_access_warning = true assert_eq!(contents, expected); } + #[test] + fn blocking_set_hide_rate_limit_model_nudge_preserves_table() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#"[notice] +existing = "value" +"#, + ) + .expect("seed"); + + apply_blocking( + codex_home, + None, + &[ConfigEdit::SetNoticeHideRateLimitModelNudge(true)], + ) + .expect("persist"); + + let contents = + std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let expected = r#"[notice] +existing = "value" +hide_rate_limit_model_nudge = true +"#; + assert_eq!(contents, expected); + } + #[test] fn blocking_replace_mcp_servers_round_trips() { let tmp = tempdir().expect("tmpdir"); diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index a5fd71d1..c1dae19f 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -360,6 +360,8 @@ pub struct Notice { pub hide_full_access_warning: Option, /// Tracks whether the user has acknowledged the Windows world-writable directories warning. pub hide_world_writable_warning: Option, + /// Tracks whether the user opted out of the rate limit model switch reminder. + pub hide_rate_limit_model_nudge: Option, } impl Notice { diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 188e8137..ed5a2415 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -499,6 +499,9 @@ impl App { self.chat_widget .set_world_writable_warning_acknowledged(ack); } + AppEvent::UpdateRateLimitSwitchPromptHidden(hidden) => { + self.chat_widget.set_rate_limit_switch_prompt_hidden(hidden); + } AppEvent::PersistFullAccessWarningAcknowledged => { if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home) .set_hide_full_access_warning(true) @@ -529,6 +532,21 @@ impl App { )); } } + AppEvent::PersistRateLimitSwitchPromptHidden => { + if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home) + .set_hide_rate_limit_model_nudge(true) + .apply() + .await + { + tracing::error!( + error = %err, + "failed to persist rate limit switch prompt preference" + ); + self.chat_widget.add_error_message(format!( + "Failed to save rate limit reminder preference: {err}" + )); + } + } AppEvent::OpenApprovalsPopup => { self.chat_widget.open_approvals_popup(); } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 7068cb79..54850d69 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -104,6 +104,9 @@ pub(crate) enum AppEvent { #[cfg_attr(not(target_os = "windows"), allow(dead_code))] UpdateWorldWritableWarningAcknowledged(bool), + /// Update whether the rate limit switch prompt has been acknowledged for the session. + UpdateRateLimitSwitchPromptHidden(bool), + /// Persist the acknowledgement flag for the full access warning prompt. PersistFullAccessWarningAcknowledged, @@ -111,6 +114,9 @@ pub(crate) enum AppEvent { #[cfg_attr(not(target_os = "windows"), allow(dead_code))] PersistWorldWritableWarningAcknowledged, + /// Persist the acknowledgement flag for the rate limit switch prompt. + PersistRateLimitSwitchPromptHidden, + /// Skip the next world-writable scan (one-shot) after a user-confirmed continue. #[cfg_attr(not(target_os = "windows"), allow(dead_code))] SkipNextWorldWritableScan, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2a5eb276..0665709d 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -522,6 +522,7 @@ impl ChatWidget { .unwrap_or(false); if high_usage + && !self.rate_limit_switch_prompt_hidden() && self.config.model != NUDGE_MODEL_SLUG && !matches!( self.rate_limit_switch_prompt, @@ -1710,7 +1711,18 @@ impl ChatWidget { .find(|preset| preset.model == NUDGE_MODEL_SLUG) } + fn rate_limit_switch_prompt_hidden(&self) -> bool { + self.config + .notices + .hide_rate_limit_model_nudge + .unwrap_or(false) + } + fn maybe_show_pending_rate_limit_prompt(&mut self) { + if self.rate_limit_switch_prompt_hidden() { + self.rate_limit_switch_prompt = RateLimitSwitchPromptState::Idle; + return; + } if !matches!( self.rate_limit_switch_prompt, RateLimitSwitchPromptState::Pending @@ -1744,6 +1756,10 @@ impl ChatWidget { })]; let keep_actions: Vec = Vec::new(); + let never_actions: Vec = vec![Box::new(|tx| { + tx.send(AppEvent::UpdateRateLimitSwitchPromptHidden(true)); + tx.send(AppEvent::PersistRateLimitSwitchPromptHidden); + })]; let description = if preset.description.is_empty() { Some("Uses fewer credits for upcoming turns.".to_string()) } else { @@ -1769,6 +1785,17 @@ impl ChatWidget { dismiss_on_select: true, ..Default::default() }, + SelectionItem { + name: "Keep current model (never show again)".to_string(), + description: Some( + "Hide future rate limit reminders about switching models.".to_string(), + ), + selected_description: None, + is_current: false, + actions: never_actions, + dismiss_on_select: true, + ..Default::default() + }, ]; self.bottom_pane.show_selection_view(SelectionViewParams { @@ -2386,6 +2413,13 @@ impl ChatWidget { self.config.notices.hide_world_writable_warning = Some(acknowledged); } + pub(crate) fn set_rate_limit_switch_prompt_hidden(&mut self, hidden: bool) { + self.config.notices.hide_rate_limit_model_nudge = Some(hidden); + if hidden { + self.rate_limit_switch_prompt = RateLimitSwitchPromptState::Idle; + } + } + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] pub(crate) fn world_writable_warning_hidden(&self) -> bool { self.config diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_switch_prompt_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_switch_prompt_popup.snap index f21400e1..c89d65a5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_switch_prompt_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__rate_limit_switch_prompt_popup.snap @@ -1,12 +1,15 @@ --- source: tui/src/chatwidget/tests.rs +assertion_line: 500 expression: popup --- Approaching rate limits Switch to gpt-5-codex-mini for lower credit usage? -› 1. Switch to gpt-5-codex-mini Optimized for codex. Cheaper, faster, but - less capable. +› 1. Switch to gpt-5-codex-mini Optimized for codex. Cheaper, + faster, but less capable. 2. Keep current model + 3. Keep current model (never show again) Hide future rate limit reminders + about switching models. Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 5bb89a32..f3c13c0f 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -448,6 +448,22 @@ fn rate_limit_switch_prompt_shows_once_per_session() { )); } +#[test] +fn rate_limit_switch_prompt_respects_hidden_notice() { + let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); + let (mut chat, _, _) = make_chatwidget_manual(); + chat.config.model = "gpt-5".to_string(); + chat.auth_manager = AuthManager::from_auth_for_testing(auth); + chat.config.notices.hide_rate_limit_model_nudge = Some(true); + + chat.on_rate_limit_snapshot(Some(snapshot(95.0))); + + assert!(matches!( + chat.rate_limit_switch_prompt, + RateLimitSwitchPromptState::Idle + )); +} + #[test] fn rate_limit_switch_prompt_defers_until_task_complete() { let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); diff --git a/docs/example-config.md b/docs/example-config.md index 524cbbe0..573e3ed9 100644 --- a/docs/example-config.md +++ b/docs/example-config.md @@ -159,6 +159,7 @@ windows_wsl_setup_acknowledged = false # In-product notices (mostly set automatically by Codex). [notice] # hide_full_access_warning = true +# hide_rate_limit_model_nudge = true ################################################################################ # Authentication & Login