[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:
jay
2026-06-22 11:45:41 -07:00
committed by GitHub
Unverified
parent 20431d49a0
commit 5c0fbf3492
20 changed files with 496 additions and 112 deletions
+3 -21
View File
@@ -19,7 +19,6 @@ use codex_app_server_protocol::MarketplaceRemoveResponse;
use codex_app_server_protocol::MarketplaceUpgradeParams;
use codex_app_server_protocol::MarketplaceUpgradeResponse;
use codex_app_server_protocol::RateLimitResetCreditsSummary;
use codex_app_server_protocol::RequestId;
use crate::hooks_rpc::fetch_hooks_list;
@@ -90,7 +89,8 @@ impl App {
.and_then(|result| result.map_err(|err| err.to_string()))
}
RateLimitRefreshOrigin::StartupPrefetch { .. }
| RateLimitRefreshOrigin::StatusCommand { .. } => {
| RateLimitRefreshOrigin::StatusCommand { .. }
| RateLimitRefreshOrigin::UsageMenu { .. } => {
request.await.map_err(|err| err.to_string())
}
};
@@ -127,7 +127,7 @@ impl App {
tokio::spawn(async move {
let result = tokio::time::timeout(
RATE_LIMIT_RESET_REQUEST_TIMEOUT,
fetch_rate_limit_reset_credits(request_handle),
fetch_account_rate_limits(request_handle),
)
.await
.map_err(|_| "account/rateLimits/read timed out in TUI".to_string())
@@ -789,24 +789,6 @@ pub(super) async fn fetch_account_token_activity(
.wrap_err("account/usage/read failed in TUI")
}
pub(super) async fn fetch_rate_limit_reset_credits(
request_handle: AppServerRequestHandle,
) -> Result<RateLimitResetCreditsSummary> {
let request_id = RequestId::String(format!("account-rate-limit-resets-{}", Uuid::new_v4()));
let response: GetAccountRateLimitsResponse = request_handle
.request_typed(ClientRequest::GetAccountRateLimits {
request_id,
params: None,
})
.await
.wrap_err("account/rateLimits/read failed in TUI")?;
response.rate_limit_reset_credits.ok_or_else(|| {
color_eyre::eyre::eyre!(
"account/rateLimits/read response did not include rateLimitResetCredits"
)
})
}
pub(super) async fn consume_rate_limit_reset_credit_request(
request_handle: AppServerRequestHandle,
idempotency_key: String,
+47 -12
View File
@@ -758,15 +758,14 @@ impl App {
AppEvent::RateLimitsLoaded { origin, result } => match result {
Ok(response) => {
let rate_limit_reset_credits = response.rate_limit_reset_credits.clone();
for snapshot in app_server_rate_limit_snapshots(response) {
self.chat_widget.on_rate_limit_snapshot(Some(snapshot));
}
let snapshots = app_server_rate_limit_snapshots(response);
match origin {
RateLimitRefreshOrigin::StartupPrefetch {
reset_hint_request_id,
} => {
if self.chat_widget.finish_rate_limit_reset_hint_refresh(
reset_hint_request_id,
snapshots,
rate_limit_reset_credits.ok_or_else(|| {
"account/rateLimits/read response did not include rateLimitResetCredits"
.to_string()
@@ -779,6 +778,7 @@ impl App {
RateLimitRefreshOrigin::ResetConsume { request_id } => {
self.chat_widget.finish_post_consume_reset_credits_refresh(
request_id,
snapshots,
rate_limit_reset_credits.ok_or_else(|| {
"account/rateLimits/read response did not include rateLimitResetCredits"
.to_string()
@@ -788,7 +788,17 @@ impl App {
}
RateLimitRefreshOrigin::StatusCommand { request_id } => {
self.chat_widget
.finish_status_rate_limit_refresh(request_id);
.finish_status_rate_limit_refresh(request_id, snapshots);
}
RateLimitRefreshOrigin::UsageMenu { request_id } => {
self.chat_widget.finish_usage_menu_rate_limit_refresh(
request_id,
snapshots,
rate_limit_reset_credits.ok_or_else(|| {
"account/rateLimits/read response did not include rateLimitResetCredits"
.to_string()
}),
);
}
}
}
@@ -800,16 +810,27 @@ impl App {
} => {
self.chat_widget.finish_rate_limit_reset_hint_refresh(
reset_hint_request_id,
Vec::new(),
Err(err),
);
}
RateLimitRefreshOrigin::ResetConsume { request_id } => {
self.chat_widget
.finish_post_consume_reset_credits_refresh(request_id, Err(err));
self.chat_widget.finish_post_consume_reset_credits_refresh(
request_id,
Vec::new(),
Err(err),
);
}
RateLimitRefreshOrigin::StatusCommand { request_id } => {
self.chat_widget
.finish_status_rate_limit_refresh(request_id);
.finish_status_rate_limit_refresh(request_id, Vec::new());
}
RateLimitRefreshOrigin::UsageMenu { request_id } => {
self.chat_widget.finish_usage_menu_rate_limit_refresh(
request_id,
Vec::new(),
Err(err),
);
}
}
}
@@ -822,15 +843,29 @@ impl App {
let request_id = self.chat_widget.show_rate_limit_reset_loading_popup();
self.refresh_rate_limit_reset_credits(app_server, request_id);
}
AppEvent::RateLimitResetCreditsLoaded { request_id, result } => {
if let Err(err) = &result {
AppEvent::RateLimitResetCreditsLoaded { request_id, result } => match result {
Ok(response) => {
let rate_limit_reset_credits = response.rate_limit_reset_credits.clone();
self.chat_widget.finish_rate_limit_reset_credits_refresh(
request_id,
app_server_rate_limit_snapshots(response),
rate_limit_reset_credits.ok_or_else(|| {
"account/rateLimits/read response did not include rateLimitResetCredits"
.to_string()
}),
);
}
Err(err) => {
tracing::warn!(
"account/rateLimits/read failed during reset-credit refresh: {err}"
);
self.chat_widget.finish_rate_limit_reset_credits_refresh(
request_id,
Vec::new(),
Err(err),
);
}
self.chat_widget
.finish_rate_limit_reset_credits_refresh(request_id, result);
}
},
AppEvent::ConsumeRateLimitResetCredit { idempotency_key } => {
let request_id = self.chat_widget.show_rate_limit_reset_consuming_popup();
self.consume_rate_limit_reset_credit(app_server, request_id, idempotency_key);
+6 -3
View File
@@ -27,7 +27,6 @@ use codex_app_server_protocol::PluginMarketplaceEntry;
use codex_app_server_protocol::PluginReadParams;
use codex_app_server_protocol::PluginReadResponse;
use codex_app_server_protocol::PluginUninstallResponse;
use codex_app_server_protocol::RateLimitResetCreditsSummary;
use codex_app_server_protocol::SkillsListResponse;
use codex_app_server_protocol::ThreadGoalStatus;
use codex_file_search::FileMatch;
@@ -119,7 +118,9 @@ pub(crate) struct PluginRemoteSectionError {
/// updates the cached snapshots and any available reset-credit notice (no
/// status card to finalize). A `StatusCommand` is tied to a specific `/status`
/// invocation and must call `finish_status_rate_limit_refresh` when done so the
/// card stops showing a "refreshing" state.
/// card stops showing a "refreshing" state. A `UsageMenu` refreshes a cached
/// zero reset count so the disabled menu entry can become available without a
/// restart.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RateLimitRefreshOrigin {
/// Eagerly fetched after bootstrap for `/status` data and reset availability.
@@ -127,6 +128,8 @@ pub(crate) enum RateLimitRefreshOrigin {
/// User-initiated via `/status`; the `request_id` correlates with the
/// status card that should be updated when the fetch completes.
StatusCommand { request_id: u64 },
/// User reopened `/usage` while the cached reset-credit count was zero.
UsageMenu { request_id: u64 },
/// Refresh requested after a reset credit was successfully consumed.
ResetConsume { request_id: u64 },
}
@@ -315,7 +318,7 @@ pub(crate) enum AppEvent {
/// Result of reading the current reset-credit balance.
RateLimitResetCreditsLoaded {
request_id: u64,
result: Result<RateLimitResetCreditsSummary, String>,
result: Result<GetAccountRateLimitsResponse, String>,
},
/// Consume one reset credit using a stable idempotency key.
+1
View File
@@ -557,6 +557,7 @@ pub(crate) struct ChatWidget {
next_token_activity_request_id: u64,
pending_rate_limit_reset_request_id: Option<u64>,
pending_rate_limit_reset_hint_request_id: Option<u64>,
pending_usage_menu_rate_limit_request_id: Option<u64>,
pending_rate_limit_reset_hint: Option<PlainHistoryCell>,
available_rate_limit_reset_credits: Option<i64>,
next_rate_limit_reset_request_id: u64,
@@ -131,6 +131,7 @@ impl ChatWidget {
next_token_activity_request_id: 0,
pending_rate_limit_reset_request_id: None,
pending_rate_limit_reset_hint_request_id: None,
pending_usage_menu_rate_limit_request_id: None,
pending_rate_limit_reset_hint: None,
available_rate_limit_reset_credits: None,
next_rate_limit_reset_request_id: 0,
@@ -5,6 +5,7 @@ use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
pub(super) const NUDGE_MODEL_SLUG: &str = "gpt-5.4-mini";
pub(super) const RATE_LIMIT_SWITCH_PROMPT_THRESHOLD: f64 = 90.0;
pub(super) const RATE_LIMIT_SWITCH_PROMPT_VIEW_ID: &str = "rate-limit-switch-prompt";
const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [75.0, 90.0, 95.0];
const PRIMARY_LIMIT_FALLBACK_LABEL: &str = "usage";
@@ -399,6 +400,7 @@ impl ChatWidget {
];
self.bottom_pane.show_selection_view(SelectionViewParams {
view_id: Some(RATE_LIMIT_SWITCH_PROMPT_VIEW_ID),
title: Some("Approaching rate limits".to_string()),
subtitle: Some(format!("Switch to {switch_model} for lower credit usage?")),
footer_hint: Some(standard_popup_hint_line()),
+18 -7
View File
@@ -2,6 +2,7 @@
use super::*;
use crate::app_event::AppEvent;
use crate::chatwidget::rate_limits::RATE_LIMIT_SWITCH_PROMPT_VIEW_ID;
impl ChatWidget {
/// Set the approval policy in the widget's config copy.
@@ -217,12 +218,22 @@ impl ChatWidget {
has_chatgpt_account: bool,
has_codex_backend_auth: bool,
) {
let account_state_changed = self.status_account_display != status_account_display
|| self.has_chatgpt_account != has_chatgpt_account
|| self.has_codex_backend_auth != has_codex_backend_auth;
if account_state_changed {
self.clear_pending_token_activity_refreshes();
self.clear_pending_rate_limit_reset_requests();
// Account-update notifications are the identity boundary. The visible account fields can
// be identical across two accounts, so always invalidate account-scoped requests and data.
self.clear_pending_token_activity_refreshes();
self.clear_pending_rate_limit_reset_requests();
self.codex_rate_limit_reached_type = None;
self.rate_limit_warnings = RateLimitWarningState::default();
self.rate_limit_switch_prompt = RateLimitSwitchPromptState::Idle;
self.bottom_pane
.dismiss_view_by_id(RATE_LIMIT_SWITCH_PROMPT_VIEW_ID);
let had_refreshing_status_outputs = !self.refreshing_status_outputs.is_empty();
let now = Local::now();
for (_, handle) in self.refreshing_status_outputs.drain(..) {
handle.finish_rate_limit_refresh(&[], now);
}
if had_refreshing_status_outputs {
self.request_redraw();
}
self.status_line_workspace_headline = None;
self.status_line_workspace_headline_pending_request_id = None;
@@ -236,7 +247,7 @@ impl ChatWidget {
.set_connectors_enabled(self.connectors_enabled());
self.bottom_pane
.set_token_activity_command_enabled(has_codex_backend_auth);
self.refresh_status_line();
self.refresh_status_surfaces();
}
/// Set the syntax theme override in the widget's config copy.
@@ -2,4 +2,4 @@
source: tui/src/chatwidget/tests/usage.rs
expression: rendered
---
• You have 2 rate-limit resets available. Run /usage to use one.
• You have 2 usage limit resets available. Run /usage to use one.
@@ -4,4 +4,4 @@ expression: "lines_to_single_string(&chat.active_cell_transcript_lines(80).expec
---
active tool
• You have 2 rate-limit resets available. Run /usage to use one.
• You have 2 usage limit resets available. Run /usage to use one.
@@ -2,41 +2,41 @@
source: tui/src/chatwidget/tests/usage.rs
expression: "states.join(\"\\n---\\n\")"
---
Rate-limit resets
Usage limit resets
Checking your available resets...
Loading...
Press enter to confirm or esc to go back
---
Rate-limit resets
You have 2 rate-limit resets available.
Usage limit resets
You have 2 usage limit resets available.
1. Use a reset Reset your current Codex usage windows.
1. Use a reset Reset your current 5-hour and weekly usage limits.
2. Cancel
Press enter to confirm or esc to go back
---
Rate-limit resets
You don't have any rate-limit resets available.
Usage limit resets
You don't have any usage limit resets available.
1. Close
Press enter to confirm or esc to go back
---
Rate-limit resets
Couldn't load rate-limit resets. Please try again.
Usage limit resets
Couldn't load usage limit resets. Please try again.
1. Close
Press enter to confirm or esc to go back
---
Rate-limit resets
Usage limit resets
Resetting your usage...
Using a reset...
---
Rate-limit resets
Usage limit resets
Couldn't reset usage. Please try again.
1. Try again
@@ -44,27 +44,27 @@ expression: "states.join(\"\\n---\\n\")"
Press enter to confirm or esc to go back
---
Rate-limit resets
Usage limit resets
Your usage does not need a reset right now.
1. Close
Press enter to confirm or esc to go back
---
Rate-limit resets
No rate-limit resets are available.
Usage limit resets
No usage limit resets are available.
1. Close
Press enter to confirm or esc to go back
---
Rate-limit resets
Usage limit resets
Usage reset. Checking your remaining resets...
Refreshing...
---
Rate-limit resets
Usage reset. You have 1 rate-limit reset left.
Usage limit resets
Usage reset. You have 1 usage limit reset left.
1. Close
@@ -5,7 +5,7 @@ expression: "render_bottom_popup(&chat, 80)"
Usage
View account usage or redeem an earned reset.
1. Show usage View recent account token usage.
2. Redeem rate limit reset You have 2 rate-limit resets available.
1. Show usage View recent account token usage.
2. Redeem usage limit reset You have 2 usage limit resets available.
Press enter to confirm or esc to go back
@@ -5,7 +5,7 @@ expression: "render_bottom_popup(&chat, 80)"
Usage
View account usage or redeem an earned reset.
1. Show usage View recent account token usage.
2. Redeem rate limit reset Check reset availability.
1. Show usage View recent account token usage.
2. Redeem usage limit reset Check reset availability.
Press enter to confirm or esc to go back
@@ -5,7 +5,7 @@ expression: "render_bottom_popup(&chat, 80)"
Usage
View account usage or redeem an earned reset.
1. Show usage View recent account token usage.
2. Redeem rate limit reset Check reset availability.
1. Show usage View recent account token usage.
Redeem usage limit reset No usage limit resets available.
Press enter to confirm or esc to go back
@@ -0,0 +1,30 @@
---
source: tui/src/chatwidget/tests/usage.rs
expression: "states.join(\"\\n---\\n\")"
---
Free:
Usage limit resets
You have 1 usage limit reset available.
1. Use a reset Reset your current monthly usage limit.
2. Cancel
Press enter to confirm or esc to go back
---
Go:
Usage limit resets
You have 1 usage limit reset available.
1. Use a reset Reset your current monthly usage limit.
2. Cancel
Press enter to confirm or esc to go back
---
Business with monthly window:
Usage limit resets
You have 1 usage limit reset available.
1. Use a reset Reset your current monthly usage limit.
2. Cancel
Press enter to confirm or esc to go back
+14 -2
View File
@@ -247,11 +247,23 @@ impl ChatWidget {
self.add_to_history(cell);
}
pub(crate) fn finish_status_rate_limit_refresh(&mut self, request_id: u64) {
if self.refreshing_status_outputs.is_empty() {
pub(crate) fn finish_status_rate_limit_refresh(
&mut self,
request_id: u64,
snapshots: Vec<RateLimitSnapshot>,
) {
if !self
.refreshing_status_outputs
.iter()
.any(|(pending_request_id, _)| *pending_request_id == request_id)
{
return;
}
for snapshot in snapshots {
self.on_rate_limit_snapshot(Some(snapshot));
}
let rate_limit_snapshots: Vec<RateLimitSnapshotDisplay> = self
.rate_limit_snapshots_by_limit_id
.values()
@@ -1032,6 +1032,39 @@ async fn rate_limit_switch_prompt_shows_once_per_session() {
));
}
#[tokio::test]
async fn account_update_clears_derived_usage_limit_state_and_prompt() {
let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await;
set_chatgpt_auth(&mut chat);
let mut limits = snapshot(/*percent*/ 95.0);
limits.rate_limit_reached_type = Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached);
chat.on_rate_limit_snapshot(Some(limits));
chat.maybe_show_pending_rate_limit_prompt();
assert!(chat.rate_limit_warnings.primary_index > 0);
assert!(chat.codex_rate_limit_reached_type.is_some());
assert!(matches!(
chat.rate_limit_switch_prompt,
RateLimitSwitchPromptState::Shown
));
assert!(!chat.bottom_pane.no_modal_or_popup_active());
chat.update_account_state(
/*status_account_display*/ None, /*plan_type*/ None,
/*has_chatgpt_account*/ true, /*has_codex_backend_auth*/ true,
);
assert_eq!(chat.rate_limit_warnings.primary_index, 0);
assert_eq!(chat.rate_limit_warnings.secondary_index, 0);
assert_eq!(chat.codex_rate_limit_reached_type, None);
assert!(matches!(
chat.rate_limit_switch_prompt,
RateLimitSwitchPromptState::Idle
));
assert!(chat.rate_limit_snapshots_by_limit_id.is_empty());
assert!(chat.bottom_pane.no_modal_or_popup_active());
}
#[tokio::test]
async fn rate_limit_switch_prompt_respects_hidden_notice() {
let (mut chat, _, _) = make_chatwidget_manual(Some("gpt-5")).await;
@@ -46,8 +46,7 @@ async fn status_command_refresh_updates_cached_limits_for_future_status_outputs(
other => panic!("expected rate-limit refresh request, got {other:?}"),
};
chat.on_rate_limit_snapshot(Some(snapshot(/*percent*/ 92.0)));
chat.finish_status_rate_limit_refresh(first_request_id);
chat.finish_status_rate_limit_refresh(first_request_id, vec![snapshot(/*percent*/ 92.0)]);
drain_insert_history(&mut rx);
chat.dispatch_command(SlashCommand::Status);
@@ -170,10 +169,31 @@ async fn status_command_overlapping_refreshes_update_matching_cells_only() {
"expected /status to avoid transient refresh text in terminal history, got: {second_rendered}"
);
chat.finish_status_rate_limit_refresh(first_request_id);
chat.finish_status_rate_limit_refresh(first_request_id, Vec::new());
pretty_assertions::assert_eq!(chat.refreshing_status_outputs.len(), 1);
chat.on_rate_limit_snapshot(Some(snapshot(/*percent*/ 92.0)));
chat.finish_status_rate_limit_refresh(second_request_id);
chat.finish_status_rate_limit_refresh(second_request_id, vec![snapshot(/*percent*/ 92.0)]);
assert!(chat.refreshing_status_outputs.is_empty());
}
#[tokio::test]
async fn account_update_rejects_stale_status_rate_limit_snapshots() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
chat.dispatch_command(SlashCommand::Status);
assert_matches!(rx.try_recv(), Ok(AppEvent::InsertHistoryCell(_)));
let request_id = match rx.try_recv() {
Ok(AppEvent::RefreshRateLimits {
origin: RateLimitRefreshOrigin::StatusCommand { request_id },
}) => request_id,
other => panic!("expected status refresh request, got {other:?}"),
};
chat.update_account_state(
/*status_account_display*/ None, /*plan_type*/ None,
/*has_chatgpt_account*/ true, /*has_codex_backend_auth*/ true,
);
chat.finish_status_rate_limit_refresh(request_id, vec![snapshot(/*percent*/ 92.0)]);
assert!(chat.rate_limit_snapshots_by_limit_id.is_empty());
}
+193 -12
View File
@@ -13,6 +13,7 @@ async fn usage_command_opens_menu_when_reset_is_available_snapshot() {
let request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
@@ -27,12 +28,13 @@ async fn usage_command_opens_menu_when_reset_is_available_snapshot() {
}
#[tokio::test]
async fn usage_command_can_recheck_reset_availability_after_cached_zero_snapshot() {
async fn usage_command_disables_reset_after_cached_zero_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
let request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 0 }),
));
@@ -42,11 +44,109 @@ async fn usage_command_can_recheck_reset_availability_after_cached_zero_snapshot
"usage_command_menu_without_resets",
render_bottom_popup(&chat, /*width*/ 80)
);
assert_matches!(
rx.try_recv(),
Ok(AppEvent::RefreshRateLimits {
origin: RateLimitRefreshOrigin::UsageMenu { request_id: 1 }
})
);
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity));
}
#[tokio::test]
async fn usage_menu_refresh_enables_newly_available_reset() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
let request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 0 }),
));
chat.dispatch_command(SlashCommand::Usage);
assert_matches!(
rx.try_recv(),
Ok(AppEvent::RefreshRateLimits {
origin: RateLimitRefreshOrigin::UsageMenu { request_id: 1 }
})
);
chat.finish_usage_menu_rate_limit_refresh(
/*request_id*/ 1,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
);
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));
}
#[tokio::test]
async fn usage_menu_refresh_failure_preserves_disabled_known_zero() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
let request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 0 }),
));
chat.dispatch_command(SlashCommand::Usage);
assert_matches!(
rx.try_recv(),
Ok(AppEvent::RefreshRateLimits {
origin: RateLimitRefreshOrigin::UsageMenu { request_id: 1 }
})
);
chat.finish_usage_menu_rate_limit_refresh(
/*request_id*/ 1,
Vec::new(),
Err("backend unavailable".to_string()),
);
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("No usage limit resets available."));
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity));
}
#[tokio::test]
async fn account_update_invalidates_usage_menu_refresh_when_visible_state_is_unchanged() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
let startup_request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
startup_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 0 }),
));
chat.dispatch_command(SlashCommand::Usage);
assert_matches!(
rx.try_recv(),
Ok(AppEvent::RefreshRateLimits {
origin: RateLimitRefreshOrigin::UsageMenu { request_id: 1 }
})
);
chat.update_account_state(
/*status_account_display*/ None, /*plan_type*/ None,
/*has_chatgpt_account*/ true, /*has_codex_backend_auth*/ true,
);
chat.finish_usage_menu_rate_limit_refresh(
/*request_id*/ 1,
vec![snapshot(/*percent*/ 92.0)],
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
);
assert_eq!(chat.available_rate_limit_reset_credits, None);
assert!(chat.rate_limit_snapshots_by_limit_id.is_empty());
assert!(chat.bottom_pane.no_modal_or_popup_active());
}
#[tokio::test]
async fn usage_command_can_check_reset_availability_before_startup_refresh_finishes_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -65,7 +165,7 @@ async fn usage_command_can_check_reset_availability_before_startup_refresh_finis
}
#[tokio::test]
async fn usage_command_disables_reset_for_workspace_accounts() {
async fn usage_command_can_check_reset_availability_for_workspace_accounts() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
chat.plan_type = Some(PlanType::Business);
@@ -74,7 +174,7 @@ async fn usage_command_disables_reset_for_workspace_accounts() {
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));
}
#[tokio::test]
@@ -84,6 +184,7 @@ async fn usage_menu_rate_limit_reset_entry_opens_reset_flow() {
let request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
chat.dispatch_command(SlashCommand::Usage);
@@ -104,6 +205,7 @@ async fn rate_limit_reset_popup_states_snapshot() {
record_popup(&chat, &mut states);
assert!(chat.finish_rate_limit_reset_credits_refresh(
loading_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
record_popup(&chat, &mut states);
@@ -112,6 +214,7 @@ async fn rate_limit_reset_popup_states_snapshot() {
let empty_request_id = chat.show_rate_limit_reset_loading_popup();
assert!(chat.finish_rate_limit_reset_credits_refresh(
empty_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 0 }),
));
record_popup(&chat, &mut states);
@@ -120,6 +223,7 @@ async fn rate_limit_reset_popup_states_snapshot() {
let load_error_request_id = chat.show_rate_limit_reset_loading_popup();
assert!(chat.finish_rate_limit_reset_credits_refresh(
load_error_request_id,
Vec::new(),
Err("backend unavailable".to_string()),
));
record_popup(&chat, &mut states);
@@ -165,6 +269,7 @@ async fn rate_limit_reset_popup_states_snapshot() {
record_popup(&chat, &mut states);
assert!(chat.finish_post_consume_reset_credits_refresh(
success_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
));
record_popup(&chat, &mut states);
@@ -172,12 +277,66 @@ async fn rate_limit_reset_popup_states_snapshot() {
assert_chatwidget_snapshot!("rate_limit_reset_popup_states", states.join("\n---\n"));
}
#[tokio::test]
async fn usage_limit_reset_confirmation_uses_monthly_copy_for_monthly_limits_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
let mut states = Vec::new();
chat.plan_type = Some(PlanType::Free);
let free_request_id = chat.show_rate_limit_reset_loading_popup();
assert!(chat.finish_rate_limit_reset_credits_refresh(
free_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
));
states.push(format!(
"Free:\n{}",
render_bottom_popup(&chat, /*width*/ 80)
));
dismiss_popup(&mut chat);
chat.plan_type = Some(PlanType::Go);
let go_request_id = chat.show_rate_limit_reset_loading_popup();
assert!(chat.finish_rate_limit_reset_credits_refresh(
go_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
));
states.push(format!("Go:\n{}", render_bottom_popup(&chat, /*width*/ 80)));
dismiss_popup(&mut chat);
let mut monthly_business_snapshot = snapshot(/*percent*/ 50.0);
monthly_business_snapshot.plan_type = Some(PlanType::Business);
monthly_business_snapshot
.primary
.as_mut()
.expect("monthly business snapshot primary window")
.window_duration_mins = Some(30 * 24 * 60);
let business_request_id = chat.show_rate_limit_reset_loading_popup();
assert!(chat.finish_rate_limit_reset_credits_refresh(
business_request_id,
vec![monthly_business_snapshot],
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
));
states.push(format!(
"Business with monthly window:\n{}",
render_bottom_popup(&chat, /*width*/ 80)
));
assert_chatwidget_snapshot!(
"usage_limit_reset_confirmation_monthly",
states.join("\n---\n")
);
}
#[tokio::test]
async fn rate_limit_reset_confirmation_selects_cancel_by_default() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let request_id = chat.show_rate_limit_reset_loading_popup();
assert!(chat.finish_rate_limit_reset_credits_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
));
@@ -193,6 +352,7 @@ async fn rate_limit_reset_confirmation_can_use_reset() {
let request_id = chat.show_rate_limit_reset_loading_popup();
assert!(chat.finish_rate_limit_reset_credits_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
));
@@ -226,12 +386,13 @@ async fn rate_limit_reset_retry_reuses_idempotency_key() {
}
#[tokio::test]
async fn no_credit_outcome_allows_reset_availability_recheck() {
async fn no_credit_outcome_disables_reset_entry_in_usage_menu() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
let startup_request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
startup_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
));
let consume_request_id = chat.show_rate_limit_reset_consuming_popup();
@@ -244,10 +405,16 @@ async fn no_credit_outcome_allows_reset_availability_recheck() {
dismiss_popup(&mut chat);
chat.dispatch_command(SlashCommand::Usage);
assert_matches!(
rx.try_recv(),
Ok(AppEvent::RefreshRateLimits {
origin: RateLimitRefreshOrigin::UsageMenu { request_id: 2 }
})
);
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenTokenActivity));
}
#[tokio::test]
@@ -270,6 +437,7 @@ async fn rate_limit_reset_redemption_cannot_be_dismissed_while_in_flight() {
assert!(chat.finish_post_consume_reset_credits_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
));
dismiss_popup(&mut chat);
@@ -300,11 +468,12 @@ async fn already_redeemed_is_an_idempotent_success() {
));
assert!(chat.finish_post_consume_reset_credits_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 0 }),
));
assert!(
render_bottom_popup(&chat, /*width*/ 80)
.contains("Usage reset. You have 0 rate-limit resets left.")
.contains("Usage reset. You have 0 usage limit resets left.")
);
}
@@ -315,6 +484,7 @@ async fn failed_post_consume_refresh_does_not_keep_stale_reset_count() {
let startup_request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
startup_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
let consume_request_id = chat.show_rate_limit_reset_consuming_popup();
@@ -327,6 +497,7 @@ async fn failed_post_consume_refresh_does_not_keep_stale_reset_count() {
assert!(chat.finish_post_consume_reset_credits_refresh(
consume_request_id,
Vec::new(),
Err("backend unavailable".to_string()),
));
dismiss_popup(&mut chat);
@@ -334,7 +505,7 @@ async fn failed_post_consume_refresh_does_not_keep_stale_reset_count() {
let rendered = render_bottom_popup(&chat, /*width*/ 80);
assert!(rendered.contains("Check reset availability."));
assert!(!rendered.contains("You have 2 rate-limit resets available."));
assert!(!rendered.contains("You have 2 usage limit resets available."));
}
#[tokio::test]
@@ -350,6 +521,7 @@ async fn account_change_invalidates_pending_reset_requests() {
assert!(!chat.finish_rate_limit_reset_credits_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
assert!(chat.bottom_pane.no_modal_or_popup_active());
@@ -363,6 +535,7 @@ async fn clearing_pending_reset_hint_preserves_in_flight_redemption() {
let hint_request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
hint_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
@@ -385,6 +558,7 @@ async fn rate_limit_reset_load_result_updates_popup_beneath_overlay() {
assert!(chat.finish_rate_limit_reset_credits_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
assert_eq!(
@@ -395,7 +569,7 @@ async fn rate_limit_reset_load_result_updates_popup_beneath_overlay() {
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(
render_bottom_popup(&chat, /*width*/ 80)
.contains("You have 2 rate-limit resets available.")
.contains("You have 2 usage limit resets available.")
);
}
@@ -413,6 +587,7 @@ async fn rate_limit_reset_success_updates_popup_beneath_overlay() {
));
assert!(chat.finish_post_consume_reset_credits_refresh(
request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 1 }),
));
assert_eq!(
@@ -423,7 +598,7 @@ async fn rate_limit_reset_success_updates_popup_beneath_overlay() {
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
assert!(
render_bottom_popup(&chat, /*width*/ 80)
.contains("Usage reset. You have 1 rate-limit reset left.")
.contains("Usage reset. You have 1 usage limit reset left.")
);
}
@@ -455,6 +630,7 @@ async fn startup_check_shows_available_reset_hint_snapshot() {
assert!(chat.finish_rate_limit_reset_hint_refresh(
hint_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
let rendered = lines_to_single_string(
@@ -477,6 +653,7 @@ async fn startup_reset_hint_waits_for_active_output_snapshot() {
assert!(chat.finish_rate_limit_reset_hint_refresh(
hint_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
@@ -507,6 +684,7 @@ async fn opening_rate_limit_reset_flow_invalidates_in_flight_startup_hint() {
assert!(!chat.finish_rate_limit_reset_hint_refresh(
hint_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
assert!(chat.pending_rate_limit_reset_hint().is_none());
@@ -519,6 +697,7 @@ async fn starting_rate_limit_reset_redemption_clears_deferred_startup_hint() {
let hint_request_id = chat.start_rate_limit_reset_startup_check();
assert!(chat.finish_rate_limit_reset_hint_refresh(
hint_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
assert!(chat.pending_rate_limit_reset_hint().is_some());
@@ -536,13 +715,14 @@ async fn startup_check_omits_reset_hint_when_none_are_available() {
assert!(chat.finish_rate_limit_reset_hint_refresh(
hint_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 0 }),
));
assert!(chat.pending_rate_limit_reset_hint().is_none());
}
#[tokio::test]
async fn startup_check_omits_reset_hint_for_workspace_accounts() {
async fn startup_check_shows_reset_hint_for_workspace_account_with_credit() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
set_chatgpt_auth(&mut chat);
chat.plan_type = Some(PlanType::Business);
@@ -550,10 +730,11 @@ async fn startup_check_omits_reset_hint_for_workspace_accounts() {
assert!(chat.finish_rate_limit_reset_hint_refresh(
hint_request_id,
Vec::new(),
Ok(RateLimitResetCreditsSummary { available_count: 2 }),
));
assert!(chat.pending_rate_limit_reset_hint().is_none());
assert_eq!(chat.available_rate_limit_reset_credits, None);
assert!(chat.pending_rate_limit_reset_hint().is_some());
assert_eq!(chat.available_rate_limit_reset_credits, Some(2));
}
fn consume_response(
+98 -25
View File
@@ -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"
}
}
+1 -1
View File
@@ -103,7 +103,7 @@ impl SlashCommand {
SlashCommand::Import => "import setup, this project, and recent chats from Claude Code",
SlashCommand::Hooks => "view and manage lifecycle hooks",
SlashCommand::Status => "show current session configuration and token usage",
SlashCommand::Usage => "view account usage or use a rate-limit reset",
SlashCommand::Usage => "view account usage or use a usage limit reset",
SlashCommand::DebugConfig => "show config layers and requirement sources for debugging",
SlashCommand::Title => "configure which items appear in the terminal title",
SlashCommand::Statusline => "configure which items appear in the status line",