mirror of
https://github.com/openai/codex.git
synced 2026-11-14 06:48:26 +00:00
[codex] Fix usage-limit reset copy and state (#28793)
## Why The reset flow introduced in #28154 still describes earned reset credits as "rate-limit resets" and uses generic reset-scope copy. It can also retain a stale available-credit count after redemption or an account change, leaving the reset action enabled after the last credit is used. This follow-up updates terminology only within that reset feature. Existing rate-limit wording elsewhere in the CLI and TUI is unchanged. ## What changed - Rename reset-specific `/usage` menu items, startup hints, and reset dialogs to "usage limit reset." - Describe monthly resets for Free, Go, and accounts that report a monthly usage window; otherwise describe the current 5-hour and weekly limits. - Recheck a cached zero balance when `/usage` is reopened, and refresh the balance after redemption so the final reset immediately disables the action. - Correlate async refresh results before updating snapshots and clear account-derived reset state, warnings, prompts, and status surfaces when the account changes. ## Validation - `just test -p codex-tui chatwidget::tests::usage` — 29 passed. - `just test -p codex-tui chatwidget::tests::status_command_tests` — 7 passed. - Account-boundary prompt and plan-mode prompt regression tests passed. - `cargo insta pending-snapshots` from `codex-rs/tui` — no pending snapshots.\ <img width="814" height="318" alt="image" src="https://github.com/user-attachments/assets/2a460e96-458b-4805-8d9f-c759382d21a4" /> view for monthly <img width="905" height="243" alt="image" src="https://github.com/user-attachments/assets/179f88e3-08fb-4af5-8dc6-ce6a944ed681" />
This commit is contained in:
20 files changed
+496
-112
No files matched your search
@@ -3,6 +3,7 @@ use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse;
|
||||
use codex_app_server_protocol::RateLimitResetCreditsSummary;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::rate_limits::get_limits_duration;
|
||||
use super::*;
|
||||
|
||||
const USAGE_MENU_VIEW_ID: &str = "usage-menu";
|
||||
@@ -11,8 +12,21 @@ const RATE_LIMIT_RESET_VIEW_ID: &str = "rate-limit-reset";
|
||||
impl ChatWidget {
|
||||
pub(super) fn open_usage_menu(&mut self) {
|
||||
self.clear_pending_rate_limit_reset_hint();
|
||||
let reset_eligible =
|
||||
self.has_chatgpt_account && !self.plan_type.is_some_and(PlanType::is_workspace_account);
|
||||
let should_refresh_reset_availability = self.available_rate_limit_reset_credits == Some(0);
|
||||
self.bottom_pane
|
||||
.show_selection_view(self.usage_menu_params());
|
||||
if should_refresh_reset_availability {
|
||||
let request_id = self.take_next_rate_limit_reset_request_id();
|
||||
self.pending_usage_menu_rate_limit_request_id = Some(request_id);
|
||||
self.app_event_tx.send(AppEvent::RefreshRateLimits {
|
||||
origin: RateLimitRefreshOrigin::UsageMenu { request_id },
|
||||
});
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn usage_menu_params(&self) -> SelectionViewParams {
|
||||
let reset_eligible = self.has_chatgpt_account;
|
||||
let (reset_action_enabled, reset_description) =
|
||||
match (reset_eligible, self.available_rate_limit_reset_credits) {
|
||||
(true, Some(available_count)) if available_count > 0 => (
|
||||
@@ -22,10 +36,12 @@ impl ChatWidget {
|
||||
reset_label(available_count)
|
||||
),
|
||||
),
|
||||
(true, _) => (true, "Check reset availability.".to_string()),
|
||||
(false, _) => (false, "No rate-limit resets available.".to_string()),
|
||||
(true, None) => (true, "Check reset availability.".to_string()),
|
||||
(true, Some(_)) | (false, _) => {
|
||||
(false, "No usage limit resets available.".to_string())
|
||||
}
|
||||
};
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
SelectionViewParams {
|
||||
view_id: Some(USAGE_MENU_VIEW_ID),
|
||||
title: Some("Usage".to_string()),
|
||||
subtitle: Some("View account usage or redeem an earned reset.".to_string()),
|
||||
@@ -41,7 +57,7 @@ impl ChatWidget {
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Redeem rate limit reset".to_string(),
|
||||
name: "Redeem usage limit reset".to_string(),
|
||||
description: Some(reset_description),
|
||||
is_disabled: !reset_action_enabled,
|
||||
actions: vec![Box::new(|tx| {
|
||||
@@ -52,8 +68,32 @@ impl ChatWidget {
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
});
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish_usage_menu_rate_limit_refresh(
|
||||
&mut self,
|
||||
request_id: u64,
|
||||
snapshots: Vec<RateLimitSnapshot>,
|
||||
result: Result<RateLimitResetCreditsSummary, String>,
|
||||
) {
|
||||
if self.pending_usage_menu_rate_limit_request_id != Some(request_id) {
|
||||
return;
|
||||
}
|
||||
self.pending_usage_menu_rate_limit_request_id = None;
|
||||
for snapshot in snapshots {
|
||||
self.on_rate_limit_snapshot(Some(snapshot));
|
||||
}
|
||||
if let Ok(response) = result {
|
||||
self.available_rate_limit_reset_credits = Some(response.available_count);
|
||||
}
|
||||
let params = self.usage_menu_params();
|
||||
if self
|
||||
.bottom_pane
|
||||
.replace_selection_view_if_present(USAGE_MENU_VIEW_ID, params)
|
||||
{
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn show_rate_limit_reset_loading_popup(&mut self) -> u64 {
|
||||
@@ -62,7 +102,7 @@ impl ChatWidget {
|
||||
self.pending_rate_limit_reset_request_id = Some(request_id);
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
title: Some("Rate-limit resets".to_string()),
|
||||
title: Some("Usage limit resets".to_string()),
|
||||
subtitle: Some("Checking your available resets...".to_string()),
|
||||
items: vec![SelectionItem {
|
||||
name: "Loading...".to_string(),
|
||||
@@ -78,26 +118,30 @@ impl ChatWidget {
|
||||
pub(crate) fn finish_rate_limit_reset_credits_refresh(
|
||||
&mut self,
|
||||
request_id: u64,
|
||||
snapshots: Vec<RateLimitSnapshot>,
|
||||
result: Result<RateLimitResetCreditsSummary, String>,
|
||||
) -> bool {
|
||||
if self.pending_rate_limit_reset_request_id != Some(request_id) {
|
||||
return false;
|
||||
}
|
||||
self.pending_rate_limit_reset_request_id = None;
|
||||
for snapshot in snapshots {
|
||||
self.on_rate_limit_snapshot(Some(snapshot));
|
||||
}
|
||||
|
||||
let params = match result {
|
||||
Ok(response) => {
|
||||
self.available_rate_limit_reset_credits = Some(response.available_count);
|
||||
if response.available_count > 0 {
|
||||
Self::rate_limit_reset_confirmation_params(response.available_count)
|
||||
self.rate_limit_reset_confirmation_params(response.available_count)
|
||||
} else {
|
||||
Self::rate_limit_reset_message_params(
|
||||
"You don't have any rate-limit resets available.",
|
||||
"You don't have any usage limit resets available.",
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(_) => Self::rate_limit_reset_message_params(
|
||||
"Couldn't load rate-limit resets. Please try again.",
|
||||
"Couldn't load usage limit resets. Please try again.",
|
||||
),
|
||||
};
|
||||
let replaced = self
|
||||
@@ -109,11 +153,32 @@ impl ChatWidget {
|
||||
replaced
|
||||
}
|
||||
|
||||
fn rate_limit_reset_confirmation_params(available_count: i64) -> SelectionViewParams {
|
||||
fn rate_limit_reset_confirmation_params(&self, available_count: i64) -> SelectionViewParams {
|
||||
let idempotency_key = Uuid::new_v4().to_string();
|
||||
let has_monthly_window = self
|
||||
.rate_limit_snapshots_by_limit_id
|
||||
.iter()
|
||||
.find(|(limit_id, _)| limit_id.eq_ignore_ascii_case("codex"))
|
||||
.into_iter()
|
||||
.flat_map(|(_, snapshot)| [snapshot.primary.as_ref(), snapshot.secondary.as_ref()])
|
||||
.flatten()
|
||||
.any(|window| {
|
||||
window
|
||||
.window_minutes
|
||||
.and_then(get_limits_duration)
|
||||
.as_deref()
|
||||
== Some("monthly")
|
||||
});
|
||||
let reset_description = if has_monthly_window
|
||||
|| matches!(self.plan_type, Some(PlanType::Free | PlanType::Go))
|
||||
{
|
||||
"Reset your current monthly usage limit."
|
||||
} else {
|
||||
"Reset your current 5-hour and weekly usage limits."
|
||||
};
|
||||
SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
title: Some("Rate-limit resets".to_string()),
|
||||
title: Some("Usage limit resets".to_string()),
|
||||
subtitle: Some(format!(
|
||||
"You have {available_count} {} available.",
|
||||
reset_label(available_count)
|
||||
@@ -122,7 +187,7 @@ impl ChatWidget {
|
||||
items: vec![
|
||||
SelectionItem {
|
||||
name: "Use a reset".to_string(),
|
||||
description: Some("Reset your current Codex usage windows.".to_string()),
|
||||
description: Some(reset_description.to_string()),
|
||||
actions: vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::ConsumeRateLimitResetCredit {
|
||||
idempotency_key: idempotency_key.clone(),
|
||||
@@ -145,7 +210,7 @@ impl ChatWidget {
|
||||
fn rate_limit_reset_message_params(message: &str) -> SelectionViewParams {
|
||||
SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
title: Some("Rate-limit resets".to_string()),
|
||||
title: Some("Usage limit resets".to_string()),
|
||||
subtitle: Some(message.to_string()),
|
||||
items: vec![SelectionItem {
|
||||
name: "Close".to_string(),
|
||||
@@ -162,7 +227,7 @@ impl ChatWidget {
|
||||
self.pending_rate_limit_reset_request_id = Some(request_id);
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
title: Some("Rate-limit resets".to_string()),
|
||||
title: Some("Usage limit resets".to_string()),
|
||||
subtitle: Some("Resetting your usage...".to_string()),
|
||||
items: vec![SelectionItem {
|
||||
name: "Using a reset...".to_string(),
|
||||
@@ -206,7 +271,7 @@ impl ChatWidget {
|
||||
}
|
||||
ConsumeAccountRateLimitResetCreditOutcome::NoCredit => {
|
||||
self.available_rate_limit_reset_credits = Some(0);
|
||||
"No rate-limit resets are available."
|
||||
"No usage limit resets are available."
|
||||
}
|
||||
ConsumeAccountRateLimitResetCreditOutcome::Reset
|
||||
| ConsumeAccountRateLimitResetCreditOutcome::AlreadyRedeemed => unreachable!(),
|
||||
@@ -218,7 +283,7 @@ impl ChatWidget {
|
||||
self.pending_rate_limit_reset_request_id = None;
|
||||
self.replace_rate_limit_reset_popup(SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
title: Some("Rate-limit resets".to_string()),
|
||||
title: Some("Usage limit resets".to_string()),
|
||||
subtitle: Some("Couldn't reset usage. Please try again.".to_string()),
|
||||
items: vec![
|
||||
SelectionItem {
|
||||
@@ -247,12 +312,16 @@ impl ChatWidget {
|
||||
pub(crate) fn finish_post_consume_reset_credits_refresh(
|
||||
&mut self,
|
||||
request_id: u64,
|
||||
snapshots: Vec<RateLimitSnapshot>,
|
||||
result: Result<RateLimitResetCreditsSummary, String>,
|
||||
) -> bool {
|
||||
if self.pending_rate_limit_reset_request_id != Some(request_id) {
|
||||
return false;
|
||||
}
|
||||
self.pending_rate_limit_reset_request_id = None;
|
||||
for snapshot in snapshots {
|
||||
self.on_rate_limit_snapshot(Some(snapshot));
|
||||
}
|
||||
|
||||
let message = match result {
|
||||
Ok(response) => {
|
||||
@@ -272,7 +341,7 @@ impl ChatWidget {
|
||||
fn rate_limit_reset_success_loading_params() -> SelectionViewParams {
|
||||
SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
title: Some("Rate-limit resets".to_string()),
|
||||
title: Some("Usage limit resets".to_string()),
|
||||
subtitle: Some("Usage reset. Checking your remaining resets...".to_string()),
|
||||
items: vec![SelectionItem {
|
||||
name: "Refreshing...".to_string(),
|
||||
@@ -303,18 +372,19 @@ impl ChatWidget {
|
||||
pub(crate) fn finish_rate_limit_reset_hint_refresh(
|
||||
&mut self,
|
||||
request_id: u64,
|
||||
snapshots: Vec<RateLimitSnapshot>,
|
||||
result: Result<RateLimitResetCreditsSummary, String>,
|
||||
) -> bool {
|
||||
if self.pending_rate_limit_reset_hint_request_id != Some(request_id) {
|
||||
return false;
|
||||
}
|
||||
self.pending_rate_limit_reset_hint_request_id = None;
|
||||
for snapshot in snapshots {
|
||||
self.on_rate_limit_snapshot(Some(snapshot));
|
||||
}
|
||||
if !self.has_codex_backend_auth {
|
||||
return false;
|
||||
}
|
||||
if self.plan_type.is_some_and(PlanType::is_workspace_account) {
|
||||
return true;
|
||||
}
|
||||
if let Ok(response) = result {
|
||||
self.available_rate_limit_reset_credits = Some(response.available_count);
|
||||
self.set_rate_limit_reset_available_hint(response.available_count);
|
||||
@@ -324,8 +394,11 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn clear_pending_rate_limit_reset_requests(&mut self) {
|
||||
self.pending_rate_limit_reset_request_id = None;
|
||||
self.pending_usage_menu_rate_limit_request_id = None;
|
||||
self.available_rate_limit_reset_credits = None;
|
||||
self.rate_limit_snapshots_by_limit_id.clear();
|
||||
self.clear_pending_rate_limit_reset_hint();
|
||||
self.bottom_pane.dismiss_view_by_id(USAGE_MENU_VIEW_ID);
|
||||
self.bottom_pane
|
||||
.dismiss_view_by_id(RATE_LIMIT_RESET_VIEW_ID);
|
||||
}
|
||||
@@ -375,8 +448,8 @@ impl ChatWidget {
|
||||
|
||||
fn reset_label(count: i64) -> &'static str {
|
||||
if count == 1 {
|
||||
"rate-limit reset"
|
||||
"usage limit reset"
|
||||
} else {
|
||||
"rate-limit resets"
|
||||
"usage limit resets"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user