diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index 0901e426..0446c24f 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -253,7 +253,7 @@ impl std::fmt::Display for UsageLimitReachedError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let message = match self.plan_type.as_ref() { Some(PlanType::Known(KnownPlan::Plus)) => format!( - "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing){}", + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit chatgpt.com/codex/settings/usage to purchase more credits{}", retry_suffix_after_or(self.resets_at.as_ref()) ), Some(PlanType::Known(KnownPlan::Team)) | Some(PlanType::Known(KnownPlan::Business)) => { @@ -266,8 +266,11 @@ impl std::fmt::Display for UsageLimitReachedError { "You've hit your usage limit. Upgrade to Plus to continue using Codex (https://openai.com/chatgpt/pricing)." .to_string() } - Some(PlanType::Known(KnownPlan::Pro)) - | Some(PlanType::Known(KnownPlan::Enterprise)) + Some(PlanType::Known(KnownPlan::Pro)) => format!( + "You've hit your usage limit. Visit chatgpt.com/codex/settings/usage to purchase more credits{}", + retry_suffix_after_or(self.resets_at.as_ref()) + ), + Some(PlanType::Known(KnownPlan::Enterprise)) | Some(PlanType::Known(KnownPlan::Edu)) => format!( "You've hit your usage limit.{}", retry_suffix(self.resets_at.as_ref()) @@ -467,7 +470,7 @@ mod tests { }; assert_eq!( err.to_string(), - "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing) or try again later." + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit chatgpt.com/codex/settings/usage to purchase more credits or try again later." ); } @@ -597,7 +600,7 @@ mod tests { #[test] fn usage_limit_reached_error_formats_default_for_other_plans() { let err = UsageLimitReachedError { - plan_type: Some(PlanType::Known(KnownPlan::Pro)), + plan_type: Some(PlanType::Known(KnownPlan::Enterprise)), resets_at: None, rate_limits: Some(rate_limit_snapshot()), }; @@ -607,6 +610,23 @@ mod tests { ); } + #[test] + fn usage_limit_reached_error_formats_pro_plan_with_reset() { + let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); + let resets_at = base + ChronoDuration::hours(1); + with_now_override(base, move || { + let err = UsageLimitReachedError { + plan_type: Some(PlanType::Known(KnownPlan::Pro)), + resets_at: Some(resets_at), + rate_limits: Some(rate_limit_snapshot()), + }; + assert_eq!( + err.to_string(), + "You've hit your usage limit. Visit chatgpt.com/codex/settings/usage to purchase more credits or try again in 1 hour." + ); + }); + } + #[test] fn usage_limit_reached_includes_minutes_when_available() { let base = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); @@ -636,7 +656,7 @@ mod tests { }; assert_eq!( err.to_string(), - "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing) or try again in 3 hours 32 minutes." + "You've hit your usage limit. Upgrade to Pro (https://openai.com/chatgpt/pricing), visit chatgpt.com/codex/settings/usage to purchase more credits or try again in 3 hours 32 minutes." ); }); } diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 93335097..fdf6629a 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -31,6 +31,8 @@ use super::rate_limits::StatusRateLimitRow; use super::rate_limits::compose_rate_limit_data; use super::rate_limits::format_status_limit_summary; use super::rate_limits::render_status_limit_progress_bar; +use crate::wrapping::RtOptions; +use crate::wrapping::word_wrap_lines; #[derive(Debug, Clone)] struct StatusContextWindowData { @@ -195,13 +197,7 @@ impl StatusHistoryCell { lines } StatusRateLimitData::Missing => { - vec![formatter.line( - "Limits", - vec![ - Span::from("visit ").dim(), - "chatgpt.com/codex/settings/usage".cyan().underlined(), - ], - )] + vec![formatter.line("Limits", vec![Span::from("data not available yet").dim()])] } } } @@ -315,6 +311,21 @@ impl HistoryCell for StatusHistoryCell { let formatter = FieldFormatter::from_labels(labels.iter().map(String::as_str)); let value_width = formatter.value_width(available_inner_width); + let note_first_line = Line::from(vec![ + Span::from("Visit ").cyan(), + "chatgpt.com/codex/settings/usage".cyan().underlined(), + Span::from(" for up-to-date").cyan(), + ]); + let note_second_line = Line::from(vec![ + Span::from("information on rate limits and credits").cyan(), + ]); + let note_lines = word_wrap_lines( + [note_first_line, note_second_line], + RtOptions::new(available_inner_width), + ); + lines.extend(note_lines); + lines.push(Line::from(Vec::>::new())); + let mut model_spans = vec![Span::from(self.model_name.clone())]; if !self.model_details.is_empty() { model_spans.push(Span::from(" (").dim()); diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap index cf3f492e..cf45f3f8 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap @@ -7,6 +7,9 @@ expression: sanitized ╭────────────────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ +│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ │ Model: gpt-5-codex (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ │ Approval: on-request │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap index 505375ea..e38a1c7c 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap @@ -7,6 +7,9 @@ expression: sanitized ╭─────────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ +│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ │ Model: gpt-5-codex (reasoning high, summaries detailed) │ │ Directory: [[workspace]] │ │ Approval: on-request │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap index 472df543..82ed8fc0 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap @@ -7,6 +7,9 @@ expression: sanitized ╭─────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ +│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ │ Model: gpt-5-codex (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ │ Approval: on-request │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap index 99970063..82ed8fc0 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap @@ -7,6 +7,9 @@ expression: sanitized ╭─────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ +│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ │ Model: gpt-5-codex (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ │ Approval: on-request │ @@ -15,5 +18,5 @@ expression: sanitized │ │ │ Token usage: 750 total (500 input + 250 output) │ │ Context window: 100% left (750 used / 272K) │ -│ Limits: visit chatgpt.com/codex/settings/usage │ +│ Limits: data not available yet │ ╰─────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap index bd073352..bd187197 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_stale_limits_message.snap @@ -7,6 +7,9 @@ expression: sanitized ╭─────────────────────────────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ +│ Visit chatgpt.com/codex/settings/usage for up-to-date │ +│ information on rate limits and credits │ +│ │ │ Model: gpt-5-codex (reasoning none, summaries auto) │ │ Directory: [[workspace]] │ │ Approval: on-request │ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap index b7c59534..64e9271d 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap @@ -7,6 +7,10 @@ expression: sanitized ╭────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ +│ Visit chatgpt.com/codex/settings/usage for │ +│ up-to-date │ +│ information on rate limits and credits │ +│ │ │ Model: gpt-5-codex (reasoning │ │ Directory: [[workspace]] │ │ Approval: on-request │