Add opt-out for rate limit model nudge (#6433)
## Summary - add a `hide_rate_limit_model_nudge` notice flag plus config edit plumbing so the rate limit reminder preference is persisted and documented - extend the chat widget prompt with a "never show again" option, and wire new app events so selecting it hides future nudges immediately and writes the config - add unit coverage and refresh the snapshot for the three-option prompt ## Testing - `just fmt` - `just fix -p codex-tui` - `just fix -p codex-core` - `cargo test -p codex-tui` - `cargo test -p codex-core` *(fails at `exec::tests::kill_child_process_group_kills_grandchildren_on_timeout`: grandchild process still alive)* ------ [Codex Task](https://chatgpt.com/codex/tasks/task_i_6910d7f407748321b2661fc355416994)
This commit is contained in:
@@ -25,6 +25,8 @@ pub enum ConfigEdit {
|
|||||||
SetNoticeHideFullAccessWarning(bool),
|
SetNoticeHideFullAccessWarning(bool),
|
||||||
/// Toggle the Windows world-writable directories warning acknowledgement flag.
|
/// Toggle the Windows world-writable directories warning acknowledgement flag.
|
||||||
SetNoticeHideWorldWritableWarning(bool),
|
SetNoticeHideWorldWritableWarning(bool),
|
||||||
|
/// Toggle the rate limit model nudge acknowledgement flag.
|
||||||
|
SetNoticeHideRateLimitModelNudge(bool),
|
||||||
/// Toggle the Windows onboarding acknowledgement flag.
|
/// Toggle the Windows onboarding acknowledgement flag.
|
||||||
SetWindowsWslSetupAcknowledged(bool),
|
SetWindowsWslSetupAcknowledged(bool),
|
||||||
/// Replace the entire `[mcp_servers]` table.
|
/// Replace the entire `[mcp_servers]` table.
|
||||||
@@ -246,6 +248,11 @@ impl ConfigDocument {
|
|||||||
&[Notice::TABLE_KEY, "hide_world_writable_warning"],
|
&[Notice::TABLE_KEY, "hide_world_writable_warning"],
|
||||||
value(*acknowledged),
|
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(
|
ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged) => Ok(self.write_value(
|
||||||
Scope::Global,
|
Scope::Global,
|
||||||
&["windows_wsl_setup_acknowledged"],
|
&["windows_wsl_setup_acknowledged"],
|
||||||
@@ -486,6 +493,12 @@ impl ConfigEditsBuilder {
|
|||||||
self
|
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 {
|
pub fn set_windows_wsl_setup_acknowledged(mut self, acknowledged: bool) -> Self {
|
||||||
self.edits
|
self.edits
|
||||||
.push(ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged));
|
.push(ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged));
|
||||||
@@ -733,6 +746,34 @@ hide_full_access_warning = true
|
|||||||
assert_eq!(contents, expected);
|
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]
|
#[test]
|
||||||
fn blocking_replace_mcp_servers_round_trips() {
|
fn blocking_replace_mcp_servers_round_trips() {
|
||||||
let tmp = tempdir().expect("tmpdir");
|
let tmp = tempdir().expect("tmpdir");
|
||||||
|
|||||||
@@ -360,6 +360,8 @@ pub struct Notice {
|
|||||||
pub hide_full_access_warning: Option<bool>,
|
pub hide_full_access_warning: Option<bool>,
|
||||||
/// Tracks whether the user has acknowledged the Windows world-writable directories warning.
|
/// Tracks whether the user has acknowledged the Windows world-writable directories warning.
|
||||||
pub hide_world_writable_warning: Option<bool>,
|
pub hide_world_writable_warning: Option<bool>,
|
||||||
|
/// Tracks whether the user opted out of the rate limit model switch reminder.
|
||||||
|
pub hide_rate_limit_model_nudge: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Notice {
|
impl Notice {
|
||||||
|
|||||||
@@ -499,6 +499,9 @@ impl App {
|
|||||||
self.chat_widget
|
self.chat_widget
|
||||||
.set_world_writable_warning_acknowledged(ack);
|
.set_world_writable_warning_acknowledged(ack);
|
||||||
}
|
}
|
||||||
|
AppEvent::UpdateRateLimitSwitchPromptHidden(hidden) => {
|
||||||
|
self.chat_widget.set_rate_limit_switch_prompt_hidden(hidden);
|
||||||
|
}
|
||||||
AppEvent::PersistFullAccessWarningAcknowledged => {
|
AppEvent::PersistFullAccessWarningAcknowledged => {
|
||||||
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
if let Err(err) = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||||
.set_hide_full_access_warning(true)
|
.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 => {
|
AppEvent::OpenApprovalsPopup => {
|
||||||
self.chat_widget.open_approvals_popup();
|
self.chat_widget.open_approvals_popup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ pub(crate) enum AppEvent {
|
|||||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||||
UpdateWorldWritableWarningAcknowledged(bool),
|
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.
|
/// Persist the acknowledgement flag for the full access warning prompt.
|
||||||
PersistFullAccessWarningAcknowledged,
|
PersistFullAccessWarningAcknowledged,
|
||||||
|
|
||||||
@@ -111,6 +114,9 @@ pub(crate) enum AppEvent {
|
|||||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||||
PersistWorldWritableWarningAcknowledged,
|
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.
|
/// Skip the next world-writable scan (one-shot) after a user-confirmed continue.
|
||||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||||
SkipNextWorldWritableScan,
|
SkipNextWorldWritableScan,
|
||||||
|
|||||||
@@ -522,6 +522,7 @@ impl ChatWidget {
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if high_usage
|
if high_usage
|
||||||
|
&& !self.rate_limit_switch_prompt_hidden()
|
||||||
&& self.config.model != NUDGE_MODEL_SLUG
|
&& self.config.model != NUDGE_MODEL_SLUG
|
||||||
&& !matches!(
|
&& !matches!(
|
||||||
self.rate_limit_switch_prompt,
|
self.rate_limit_switch_prompt,
|
||||||
@@ -1710,7 +1711,18 @@ impl ChatWidget {
|
|||||||
.find(|preset| preset.model == NUDGE_MODEL_SLUG)
|
.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) {
|
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!(
|
if !matches!(
|
||||||
self.rate_limit_switch_prompt,
|
self.rate_limit_switch_prompt,
|
||||||
RateLimitSwitchPromptState::Pending
|
RateLimitSwitchPromptState::Pending
|
||||||
@@ -1744,6 +1756,10 @@ impl ChatWidget {
|
|||||||
})];
|
})];
|
||||||
|
|
||||||
let keep_actions: Vec<SelectionAction> = Vec::new();
|
let keep_actions: Vec<SelectionAction> = Vec::new();
|
||||||
|
let never_actions: Vec<SelectionAction> = vec![Box::new(|tx| {
|
||||||
|
tx.send(AppEvent::UpdateRateLimitSwitchPromptHidden(true));
|
||||||
|
tx.send(AppEvent::PersistRateLimitSwitchPromptHidden);
|
||||||
|
})];
|
||||||
let description = if preset.description.is_empty() {
|
let description = if preset.description.is_empty() {
|
||||||
Some("Uses fewer credits for upcoming turns.".to_string())
|
Some("Uses fewer credits for upcoming turns.".to_string())
|
||||||
} else {
|
} else {
|
||||||
@@ -1769,6 +1785,17 @@ impl ChatWidget {
|
|||||||
dismiss_on_select: true,
|
dismiss_on_select: true,
|
||||||
..Default::default()
|
..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 {
|
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||||
@@ -2386,6 +2413,13 @@ impl ChatWidget {
|
|||||||
self.config.notices.hide_world_writable_warning = Some(acknowledged);
|
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))]
|
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||||
pub(crate) fn world_writable_warning_hidden(&self) -> bool {
|
pub(crate) fn world_writable_warning_hidden(&self) -> bool {
|
||||||
self.config
|
self.config
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
---
|
---
|
||||||
source: tui/src/chatwidget/tests.rs
|
source: tui/src/chatwidget/tests.rs
|
||||||
|
assertion_line: 500
|
||||||
expression: popup
|
expression: popup
|
||||||
---
|
---
|
||||||
Approaching rate limits
|
Approaching rate limits
|
||||||
Switch to gpt-5-codex-mini for lower credit usage?
|
Switch to gpt-5-codex-mini for lower credit usage?
|
||||||
|
|
||||||
› 1. Switch to gpt-5-codex-mini Optimized for codex. Cheaper, faster, but
|
› 1. Switch to gpt-5-codex-mini Optimized for codex. Cheaper,
|
||||||
less capable.
|
faster, but less capable.
|
||||||
2. Keep current model
|
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
|
Press enter to confirm or esc to go back
|
||||||
|
|||||||
@@ -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]
|
#[test]
|
||||||
fn rate_limit_switch_prompt_defers_until_task_complete() {
|
fn rate_limit_switch_prompt_defers_until_task_complete() {
|
||||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ windows_wsl_setup_acknowledged = false
|
|||||||
# In-product notices (mostly set automatically by Codex).
|
# In-product notices (mostly set automatically by Codex).
|
||||||
[notice]
|
[notice]
|
||||||
# hide_full_access_warning = true
|
# hide_full_access_warning = true
|
||||||
|
# hide_rate_limit_model_nudge = true
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Authentication & Login
|
# Authentication & Login
|
||||||
|
|||||||
Reference in New Issue
Block a user