diff --git a/codex-rs/tui/src/chatwidget/rate_limits.rs b/codex-rs/tui/src/chatwidget/rate_limits.rs index 77a0c5c7e..498f43263 100644 --- a/codex-rs/tui/src/chatwidget/rate_limits.rs +++ b/codex-rs/tui/src/chatwidget/rate_limits.rs @@ -210,7 +210,19 @@ impl ChatWidget { { self.codex_rate_limit_reached_type = Some(rate_limit_reached_type); } - let warnings = if is_codex_limit { + + let has_workspace_credits = snapshot.credits.as_ref().is_some_and(|credits| { + credits.has_credits + && (credits.unlimited + || credits.balance.as_deref().is_some_and(|balance| { + balance + .trim() + .parse::() + .is_ok_and(|balance| balance > 0.0) + })) + }); + let should_warn_about_rate_limit_usage = is_codex_limit && !has_workspace_credits; + let warnings = if should_warn_about_rate_limit_usage { self.rate_limit_warnings.take_warnings( snapshot .secondary @@ -245,12 +257,6 @@ impl ChatWidget { .map(|w| f64::from(w.used_percent) >= RATE_LIMIT_SWITCH_PROMPT_THRESHOLD) .unwrap_or(false)); - let has_workspace_credits = snapshot - .credits - .as_ref() - .map(|credits| credits.has_credits) - .unwrap_or(false); - if high_usage && !has_workspace_credits && !self.rate_limit_switch_prompt_hidden() diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 57de08cee..329d24934 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1009,6 +1009,82 @@ async fn rate_limit_switch_prompt_skips_non_codex_limit() { )); } +#[tokio::test] +async fn rate_limit_usage_warnings_show_when_workspace_credits_zero_balance() { + let (mut chat, mut rx, _) = make_chatwidget_manual(Some("gpt-5")).await; + chat.has_chatgpt_account = true; + + let mut rate_limit_snapshot = snapshot(/*percent*/ 95.0); + rate_limit_snapshot.credits = Some(CreditsSnapshot { + has_credits: true, + unlimited: false, + balance: Some("0".to_string()), + }); + + chat.on_rate_limit_snapshot(Some(rate_limit_snapshot)); + + assert!( + !drain_insert_history(&mut rx).is_empty(), + "zero-balance workspace credits should not suppress proactive usage warnings" + ); + assert!(matches!( + chat.rate_limit_switch_prompt, + RateLimitSwitchPromptState::Pending + )); +} + +#[tokio::test] +async fn rate_limit_usage_warnings_skip_when_workspace_credits_are_available() { + let (mut chat, mut rx, _) = make_chatwidget_manual(Some("gpt-5")).await; + chat.has_chatgpt_account = true; + + let mut rate_limit_snapshot = snapshot(/*percent*/ 95.0); + rate_limit_snapshot.credits = Some(CreditsSnapshot { + has_credits: true, + unlimited: false, + balance: Some("25.00".to_string()), + }); + + chat.on_rate_limit_snapshot(Some(rate_limit_snapshot)); + + assert!( + drain_insert_history(&mut rx).is_empty(), + "workspace credits should suppress proactive usage warnings" + ); + assert!(matches!( + chat.rate_limit_switch_prompt, + RateLimitSwitchPromptState::Idle + )); + assert_eq!( + chat.rate_limit_warnings.primary_index, 0, + "suppressed warnings should not consume warning thresholds" + ); +} + +#[tokio::test] +async fn rate_limit_usage_warnings_skip_with_unlimited_workspace_credits() { + let (mut chat, mut rx, _) = make_chatwidget_manual(Some("gpt-5")).await; + chat.has_chatgpt_account = true; + + let mut rate_limit_snapshot = snapshot(/*percent*/ 95.0); + rate_limit_snapshot.credits = Some(CreditsSnapshot { + has_credits: true, + unlimited: true, + balance: None, + }); + + chat.on_rate_limit_snapshot(Some(rate_limit_snapshot)); + + assert!( + drain_insert_history(&mut rx).is_empty(), + "unlimited workspace credits should suppress proactive usage warnings" + ); + assert!(matches!( + chat.rate_limit_switch_prompt, + RateLimitSwitchPromptState::Idle + )); +} + #[tokio::test] async fn rate_limit_switch_prompt_shows_once_per_session() { let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await;