mirror of
https://github.com/openai/codex.git
synced 2026-11-04 00:24:43 +00:00
[codex-cli] Show reset details in redemption picker (#30488)
## Why Users can see that usage-limit reset credits are available, but not which credits they have, when each one expires, or which credit will be consumed. The TUI should use the supported rate-limit RPC for that information without maintaining a second reset-credit request path. ## What changed - load reset-credit details through the existing `account/rateLimits/read` refresh when the user opens **Redeem usage limit reset** - show available credits sorted by expiry, using the backend title when present and a scope-based fallback otherwise - consume the exact selected credit and preserve its idempotency key across retries - fall back to the existing generic reset action when the RPC returns a positive count without detail rows - remove the dedicated list-RPC event, request, response-combining, and TUI state plumbing - handle a selected credit becoming unavailable without incorrectly caching the user's total count as zero - handle forward-compatible unknown reset types with the existing scope-based fallback label ## TUI preview Rendered from the final standard-width and narrow `insta` snapshots in this PR. <img width="1280" height="570" alt="pr30488-reset-picker-preview" src="https://github.com/user-attachments/assets/8b84cd33-cb35-4a37-aaed-dbd6b62bff51" /> ## Validation - `just test -p codex-tui chatwidget::tests::usage` (32 passed) - `just fix -p codex-tui` - no pending `insta` snapshots Uses the reset-credit details added by #30395. Fixes #29618.
This commit is contained in:
10 files changed
+526
-256
No files matched your search
@@ -83,7 +83,8 @@ impl App {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let request = fetch_account_rate_limits(request_handle);
|
let request = fetch_account_rate_limits(request_handle);
|
||||||
let result = match origin {
|
let result = match origin {
|
||||||
RateLimitRefreshOrigin::ResetConsume { .. } => {
|
RateLimitRefreshOrigin::ResetConsume { .. }
|
||||||
|
| RateLimitRefreshOrigin::ResetPicker { .. } => {
|
||||||
tokio::time::timeout(RATE_LIMIT_RESET_REQUEST_TIMEOUT, request)
|
tokio::time::timeout(RATE_LIMIT_RESET_REQUEST_TIMEOUT, request)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| "account/rateLimits/read timed out in TUI".to_string())
|
.map_err(|_| "account/rateLimits/read timed out in TUI".to_string())
|
||||||
@@ -118,37 +119,23 @@ impl App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn refresh_rate_limit_reset_credits(
|
|
||||||
&mut self,
|
|
||||||
app_server: &AppServerSession,
|
|
||||||
request_id: u64,
|
|
||||||
) {
|
|
||||||
let request_handle = app_server.request_handle();
|
|
||||||
let app_event_tx = self.app_event_tx.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let result = tokio::time::timeout(
|
|
||||||
RATE_LIMIT_RESET_REQUEST_TIMEOUT,
|
|
||||||
fetch_account_rate_limits(request_handle),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| "account/rateLimits/read timed out in TUI".to_string())
|
|
||||||
.and_then(|result| result.map_err(|err| err.to_string()));
|
|
||||||
app_event_tx.send(AppEvent::RateLimitResetCreditsLoaded { request_id, result });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn consume_rate_limit_reset_credit(
|
pub(super) fn consume_rate_limit_reset_credit(
|
||||||
&mut self,
|
&mut self,
|
||||||
app_server: &AppServerSession,
|
app_server: &AppServerSession,
|
||||||
request_id: u64,
|
request_id: u64,
|
||||||
idempotency_key: String,
|
idempotency_key: String,
|
||||||
|
credit_id: Option<String>,
|
||||||
) {
|
) {
|
||||||
let request_handle = app_server.request_handle();
|
let request_handle = app_server.request_handle();
|
||||||
let app_event_tx = self.app_event_tx.clone();
|
let app_event_tx = self.app_event_tx.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let result = tokio::time::timeout(
|
let result = tokio::time::timeout(
|
||||||
RATE_LIMIT_RESET_REQUEST_TIMEOUT,
|
RATE_LIMIT_RESET_REQUEST_TIMEOUT,
|
||||||
consume_rate_limit_reset_credit_request(request_handle, idempotency_key.clone()),
|
consume_rate_limit_reset_credit_request(
|
||||||
|
request_handle,
|
||||||
|
idempotency_key.clone(),
|
||||||
|
credit_id.clone(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| "account/rateLimitResetCredit/consume timed out in TUI".to_string())
|
.map_err(|_| "account/rateLimitResetCredit/consume timed out in TUI".to_string())
|
||||||
@@ -156,6 +143,7 @@ impl App {
|
|||||||
app_event_tx.send(AppEvent::RateLimitResetCreditConsumed {
|
app_event_tx.send(AppEvent::RateLimitResetCreditConsumed {
|
||||||
request_id,
|
request_id,
|
||||||
idempotency_key,
|
idempotency_key,
|
||||||
|
credit_id,
|
||||||
result,
|
result,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -793,6 +781,7 @@ pub(super) async fn fetch_account_token_activity(
|
|||||||
pub(super) async fn consume_rate_limit_reset_credit_request(
|
pub(super) async fn consume_rate_limit_reset_credit_request(
|
||||||
request_handle: AppServerRequestHandle,
|
request_handle: AppServerRequestHandle,
|
||||||
idempotency_key: String,
|
idempotency_key: String,
|
||||||
|
credit_id: Option<String>,
|
||||||
) -> Result<ConsumeAccountRateLimitResetCreditResponse> {
|
) -> Result<ConsumeAccountRateLimitResetCreditResponse> {
|
||||||
let request_id = RequestId::String(format!("consume-rate-limit-reset-{}", Uuid::new_v4()));
|
let request_id = RequestId::String(format!("consume-rate-limit-reset-{}", Uuid::new_v4()));
|
||||||
request_handle
|
request_handle
|
||||||
@@ -800,7 +789,7 @@ pub(super) async fn consume_rate_limit_reset_credit_request(
|
|||||||
request_id,
|
request_id,
|
||||||
params: ConsumeAccountRateLimitResetCreditParams {
|
params: ConsumeAccountRateLimitResetCreditParams {
|
||||||
idempotency_key,
|
idempotency_key,
|
||||||
credit_id: None,
|
credit_id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -865,6 +865,16 @@ impl App {
|
|||||||
}
|
}
|
||||||
RateLimitRefreshOrigin::UsageMenu { request_id } => {
|
RateLimitRefreshOrigin::UsageMenu { request_id } => {
|
||||||
self.chat_widget.finish_usage_menu_rate_limit_refresh(
|
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()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
RateLimitRefreshOrigin::ResetPicker { request_id } => {
|
||||||
|
self.chat_widget.finish_rate_limit_reset_credits_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
snapshots,
|
snapshots,
|
||||||
rate_limit_reset_credits.ok_or_else(|| {
|
rate_limit_reset_credits.ok_or_else(|| {
|
||||||
@@ -905,6 +915,13 @@ impl App {
|
|||||||
Err(err),
|
Err(err),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
RateLimitRefreshOrigin::ResetPicker { request_id } => {
|
||||||
|
self.chat_widget.finish_rate_limit_reset_credits_refresh(
|
||||||
|
request_id,
|
||||||
|
Vec::new(),
|
||||||
|
Err(err),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -914,38 +931,27 @@ impl App {
|
|||||||
}
|
}
|
||||||
AppEvent::OpenRateLimitResetCredits => {
|
AppEvent::OpenRateLimitResetCredits => {
|
||||||
let request_id = self.chat_widget.show_rate_limit_reset_loading_popup();
|
let request_id = self.chat_widget.show_rate_limit_reset_loading_popup();
|
||||||
self.refresh_rate_limit_reset_credits(app_server, request_id);
|
self.refresh_rate_limits(
|
||||||
|
app_server,
|
||||||
|
RateLimitRefreshOrigin::ResetPicker { request_id },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
AppEvent::RateLimitResetCreditsLoaded { request_id, result } => match result {
|
AppEvent::ConsumeRateLimitResetCredit {
|
||||||
Ok(response) => {
|
idempotency_key,
|
||||||
let rate_limit_reset_credits = response.rate_limit_reset_credits.clone();
|
credit_id,
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
AppEvent::ConsumeRateLimitResetCredit { idempotency_key } => {
|
|
||||||
let request_id = self.chat_widget.show_rate_limit_reset_consuming_popup();
|
let request_id = self.chat_widget.show_rate_limit_reset_consuming_popup();
|
||||||
self.consume_rate_limit_reset_credit(app_server, request_id, idempotency_key);
|
self.consume_rate_limit_reset_credit(
|
||||||
|
app_server,
|
||||||
|
request_id,
|
||||||
|
idempotency_key,
|
||||||
|
credit_id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
AppEvent::RateLimitResetCreditConsumed {
|
AppEvent::RateLimitResetCreditConsumed {
|
||||||
request_id,
|
request_id,
|
||||||
idempotency_key,
|
idempotency_key,
|
||||||
|
credit_id,
|
||||||
result,
|
result,
|
||||||
} => {
|
} => {
|
||||||
if let Err(err) = &result {
|
if let Err(err) = &result {
|
||||||
@@ -956,6 +962,7 @@ impl App {
|
|||||||
if self.chat_widget.finish_rate_limit_reset_consume(
|
if self.chat_widget.finish_rate_limit_reset_consume(
|
||||||
request_id,
|
request_id,
|
||||||
idempotency_key,
|
idempotency_key,
|
||||||
|
credit_id,
|
||||||
result,
|
result,
|
||||||
) {
|
) {
|
||||||
self.refresh_rate_limits(
|
self.refresh_rate_limits(
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ pub(crate) struct PluginRemoteSectionError {
|
|||||||
/// invocation and must call `finish_status_rate_limit_refresh` when done so the
|
/// invocation and must call `finish_status_rate_limit_refresh` when done so the
|
||||||
/// card stops showing a "refreshing" state. A `UsageMenu` refreshes a cached
|
/// card stops showing a "refreshing" state. A `UsageMenu` refreshes a cached
|
||||||
/// zero reset count so the disabled menu entry can become available without a
|
/// zero reset count so the disabled menu entry can become available without a
|
||||||
/// restart.
|
/// restart. A `ResetPicker` refreshes the rate limits and detailed reset-credit
|
||||||
|
/// rows before showing redemption choices.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum RateLimitRefreshOrigin {
|
pub(crate) enum RateLimitRefreshOrigin {
|
||||||
/// Eagerly fetched after bootstrap for `/status` data and reset availability.
|
/// Eagerly fetched after bootstrap for `/status` data and reset availability.
|
||||||
@@ -130,6 +131,8 @@ pub(crate) enum RateLimitRefreshOrigin {
|
|||||||
StatusCommand { request_id: u64 },
|
StatusCommand { request_id: u64 },
|
||||||
/// User reopened `/usage` while the cached reset-credit count was zero.
|
/// User reopened `/usage` while the cached reset-credit count was zero.
|
||||||
UsageMenu { request_id: u64 },
|
UsageMenu { request_id: u64 },
|
||||||
|
/// User opened the reset-credit picker.
|
||||||
|
ResetPicker { request_id: u64 },
|
||||||
/// Refresh requested after a reset credit was successfully consumed.
|
/// Refresh requested after a reset credit was successfully consumed.
|
||||||
ResetConsume { request_id: u64 },
|
ResetConsume { request_id: u64 },
|
||||||
}
|
}
|
||||||
@@ -323,21 +326,17 @@ pub(crate) enum AppEvent {
|
|||||||
/// Open the reset-credit flow selected from the `/usage` menu.
|
/// Open the reset-credit flow selected from the `/usage` menu.
|
||||||
OpenRateLimitResetCredits,
|
OpenRateLimitResetCredits,
|
||||||
|
|
||||||
/// Result of reading the current reset-credit balance.
|
|
||||||
RateLimitResetCreditsLoaded {
|
|
||||||
request_id: u64,
|
|
||||||
result: Result<GetAccountRateLimitsResponse, String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Consume one reset credit using a stable idempotency key.
|
/// Consume one reset credit using a stable idempotency key.
|
||||||
ConsumeRateLimitResetCredit {
|
ConsumeRateLimitResetCredit {
|
||||||
idempotency_key: String,
|
idempotency_key: String,
|
||||||
|
credit_id: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Result of consuming one reset credit.
|
/// Result of consuming one reset credit.
|
||||||
RateLimitResetCreditConsumed {
|
RateLimitResetCreditConsumed {
|
||||||
request_id: u64,
|
request_id: u64,
|
||||||
idempotency_key: String,
|
idempotency_key: String,
|
||||||
|
credit_id: Option<String>,
|
||||||
result: Result<ConsumeAccountRateLimitResetCreditResponse, String>,
|
result: Result<ConsumeAccountRateLimitResetCreditResponse, String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -386,6 +386,7 @@ use self::rate_limits::RateLimitWarningState;
|
|||||||
use self::rate_limits::app_server_rate_limit_error_kind;
|
use self::rate_limits::app_server_rate_limit_error_kind;
|
||||||
pub(crate) use self::rate_limits::fallback_limit_label;
|
pub(crate) use self::rate_limits::fallback_limit_label;
|
||||||
use self::rate_limits::is_app_server_cyber_policy_error;
|
use self::rate_limits::is_app_server_cyber_policy_error;
|
||||||
|
mod reset_credits;
|
||||||
pub(crate) use self::rate_limits::limit_label_for_window;
|
pub(crate) use self::rate_limits::limit_label_for_window;
|
||||||
mod reasoning_shortcuts;
|
mod reasoning_shortcuts;
|
||||||
mod rendering;
|
mod rendering;
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
use crate::status::RateLimitSnapshotDisplay;
|
||||||
|
use chrono::DateTime;
|
||||||
|
use chrono::Local;
|
||||||
|
use chrono::Utc;
|
||||||
|
use codex_app_server_protocol::RateLimitResetCreditStatus;
|
||||||
|
use codex_app_server_protocol::RateLimitResetCreditsSummary;
|
||||||
|
use codex_app_server_protocol::RateLimitResetType;
|
||||||
|
use codex_protocol::account::PlanType;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use super::rate_limits::get_limits_duration;
|
||||||
|
|
||||||
|
pub(super) enum RateLimitResetScope {
|
||||||
|
Monthly,
|
||||||
|
WeeklyAndFiveHour,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RateLimitResetScope {
|
||||||
|
pub(super) fn picker_label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Monthly => "Full reset (Monthly)",
|
||||||
|
Self::WeeklyAndFiveHour => "Full reset (Weekly + 5h)",
|
||||||
|
Self::Unknown => "Full reset",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn usage_description(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Monthly => "Reset your current monthly usage limit.",
|
||||||
|
Self::WeeklyAndFiveHour => "Reset your current 5-hour and weekly usage limits.",
|
||||||
|
Self::Unknown => "Reset your current usage limits.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub(super) struct ResetCreditOption {
|
||||||
|
pub(super) credit_id: Option<String>,
|
||||||
|
pub(super) name: String,
|
||||||
|
pub(super) description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn rate_limit_reset_scope(
|
||||||
|
rate_limits: &BTreeMap<String, RateLimitSnapshotDisplay>,
|
||||||
|
plan_type: Option<PlanType>,
|
||||||
|
) -> RateLimitResetScope {
|
||||||
|
let window_labels = rate_limits
|
||||||
|
.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()
|
||||||
|
.filter_map(|window| window.window_minutes.and_then(get_limits_duration))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if window_labels.iter().any(|label| label == "monthly")
|
||||||
|
|| matches!(plan_type, Some(PlanType::Free | PlanType::Go))
|
||||||
|
{
|
||||||
|
RateLimitResetScope::Monthly
|
||||||
|
} else if window_labels
|
||||||
|
.iter()
|
||||||
|
.any(|label| label == "5h" || label == "weekly")
|
||||||
|
{
|
||||||
|
RateLimitResetScope::WeeklyAndFiveHour
|
||||||
|
} else {
|
||||||
|
RateLimitResetScope::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reset_credit_options(
|
||||||
|
summary: &RateLimitResetCreditsSummary,
|
||||||
|
scope: RateLimitResetScope,
|
||||||
|
) -> Vec<ResetCreditOption> {
|
||||||
|
let available_count = summary.available_count.max(0);
|
||||||
|
let detail_limit = usize::try_from(available_count).unwrap_or(usize::MAX);
|
||||||
|
let mut available_credits = summary
|
||||||
|
.credits
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter(|credit| credit.status == RateLimitResetCreditStatus::Available)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
available_credits.sort_by_key(|credit| credit.expires_at.unwrap_or(i64::MAX));
|
||||||
|
|
||||||
|
let mut options = available_credits
|
||||||
|
.into_iter()
|
||||||
|
.take(detail_limit)
|
||||||
|
.map(|credit| {
|
||||||
|
let expiration = match credit.expires_at {
|
||||||
|
Some(expires_at) => DateTime::<Utc>::from_timestamp(expires_at, 0)
|
||||||
|
.map(|expires_at| {
|
||||||
|
format!(
|
||||||
|
"Expires {}",
|
||||||
|
expires_at
|
||||||
|
.with_timezone(&Local)
|
||||||
|
.format("%H:%M on %-d %b %Y")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "Expiration unavailable".to_string()),
|
||||||
|
None => "Does not expire".to_string(),
|
||||||
|
};
|
||||||
|
let reset_label = credit
|
||||||
|
.title
|
||||||
|
.as_deref()
|
||||||
|
.filter(|title| !title.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| match credit.reset_type {
|
||||||
|
RateLimitResetType::CodexRateLimits | RateLimitResetType::Unknown => {
|
||||||
|
scope.picker_label()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ResetCreditOption {
|
||||||
|
credit_id: Some(credit.id.clone()),
|
||||||
|
name: reset_label.to_string(),
|
||||||
|
description: format!("{expiration}."),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if options.is_empty() {
|
||||||
|
options.push(ResetCreditOption {
|
||||||
|
credit_id: None,
|
||||||
|
name: "Use a reset".to_string(),
|
||||||
|
description: scope.usage_description().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
options
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests/usage.rs
|
||||||
|
expression: "render_bottom_popup(&chat, 44)"
|
||||||
|
---
|
||||||
|
Usage limit resets
|
||||||
|
2 usage limit resets available.
|
||||||
|
|
||||||
|
› 1. Cancel
|
||||||
|
2. Full reset Expires 09:39 on 18 Jun
|
||||||
|
2026.
|
||||||
|
3. Full reset Does not expire.
|
||||||
|
|
||||||
|
Press enter to confirm or esc to go back
|
||||||
+6
-4
@@ -10,10 +10,11 @@ expression: "states.join(\"\\n---\\n\")"
|
|||||||
Press enter to confirm or esc to go back
|
Press enter to confirm or esc to go back
|
||||||
---
|
---
|
||||||
Usage limit resets
|
Usage limit resets
|
||||||
You have 2 usage limit resets available.
|
2 usage limit resets available.
|
||||||
|
|
||||||
1. Use a reset Reset your current 5-hour and weekly usage limits.
|
› 1. Cancel
|
||||||
› 2. Cancel
|
2. Full reset (Weekly + 5 hr) Expires 09:39 on 18 Jun 2026.
|
||||||
|
3. Full reset (Weekly + 5 hr) Expires 08:59 on 27 Jun 2026.
|
||||||
|
|
||||||
Press enter to confirm or esc to go back
|
Press enter to confirm or esc to go back
|
||||||
---
|
---
|
||||||
@@ -27,7 +28,8 @@ expression: "states.join(\"\\n---\\n\")"
|
|||||||
Usage limit resets
|
Usage limit resets
|
||||||
Couldn't load usage limit resets. Please try again.
|
Couldn't load usage limit resets. Please try again.
|
||||||
|
|
||||||
› 1. Close
|
› 1. Try again
|
||||||
|
2. Close
|
||||||
|
|
||||||
Press enter to confirm or esc to go back
|
Press enter to confirm or esc to go back
|
||||||
---
|
---
|
||||||
|
|||||||
+9
-9
@@ -4,27 +4,27 @@ expression: "states.join(\"\\n---\\n\")"
|
|||||||
---
|
---
|
||||||
Free:
|
Free:
|
||||||
Usage limit resets
|
Usage limit resets
|
||||||
You have 1 usage limit reset available.
|
1 usage limit reset available.
|
||||||
|
|
||||||
1. Use a reset Reset your current monthly usage limit.
|
› 1. Cancel
|
||||||
› 2. Cancel
|
2. Full reset (Monthly) Does not expire.
|
||||||
|
|
||||||
Press enter to confirm or esc to go back
|
Press enter to confirm or esc to go back
|
||||||
---
|
---
|
||||||
Go:
|
Go:
|
||||||
Usage limit resets
|
Usage limit resets
|
||||||
You have 1 usage limit reset available.
|
1 usage limit reset available.
|
||||||
|
|
||||||
1. Use a reset Reset your current monthly usage limit.
|
› 1. Cancel
|
||||||
› 2. Cancel
|
2. Use a reset Reset your current monthly usage limit.
|
||||||
|
|
||||||
Press enter to confirm or esc to go back
|
Press enter to confirm or esc to go back
|
||||||
---
|
---
|
||||||
Business with monthly window:
|
Business with monthly window:
|
||||||
Usage limit resets
|
Usage limit resets
|
||||||
You have 1 usage limit reset available.
|
1 usage limit reset available.
|
||||||
|
|
||||||
1. Use a reset Reset your current monthly usage limit.
|
› 1. Cancel
|
||||||
› 2. Cancel
|
2. Full reset (Monthly) Does not expire.
|
||||||
|
|
||||||
Press enter to confirm or esc to go back
|
Press enter to confirm or esc to go back
|
||||||
@@ -1,11 +1,81 @@
|
|||||||
|
use super::super::reset_credits::RateLimitResetScope;
|
||||||
|
use super::super::reset_credits::ResetCreditOption;
|
||||||
|
use super::super::reset_credits::reset_credit_options;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use chrono::TimeZone;
|
||||||
use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditOutcome;
|
use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditOutcome;
|
||||||
use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse;
|
use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse;
|
||||||
|
use codex_app_server_protocol::RateLimitResetCredit;
|
||||||
|
use codex_app_server_protocol::RateLimitResetCreditStatus;
|
||||||
use codex_app_server_protocol::RateLimitResetCreditsSummary;
|
use codex_app_server_protocol::RateLimitResetCreditsSummary;
|
||||||
|
use codex_app_server_protocol::RateLimitResetType;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const TEST_OVERLAY_VIEW_ID: &str = "usage-test-overlay";
|
const TEST_OVERLAY_VIEW_ID: &str = "usage-test-overlay";
|
||||||
|
|
||||||
|
fn reset_credits(available_count: i64) -> RateLimitResetCreditsSummary {
|
||||||
|
RateLimitResetCreditsSummary {
|
||||||
|
available_count,
|
||||||
|
credits: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detailed_reset_credits(
|
||||||
|
available_count: i64,
|
||||||
|
credits: Vec<RateLimitResetCredit>,
|
||||||
|
) -> RateLimitResetCreditsSummary {
|
||||||
|
RateLimitResetCreditsSummary {
|
||||||
|
available_count,
|
||||||
|
credits: Some(credits),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_credit(id: &str, expires_at: Option<i64>) -> RateLimitResetCredit {
|
||||||
|
RateLimitResetCredit {
|
||||||
|
id: id.to_string(),
|
||||||
|
reset_type: RateLimitResetType::CodexRateLimits,
|
||||||
|
status: RateLimitResetCreditStatus::Available,
|
||||||
|
granted_at: 0,
|
||||||
|
expires_at,
|
||||||
|
title: None,
|
||||||
|
description: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_credit_with_title(id: &str, expires_at: Option<i64>, title: &str) -> RateLimitResetCredit {
|
||||||
|
RateLimitResetCredit {
|
||||||
|
title: Some(title.to_string()),
|
||||||
|
..reset_credit(id, expires_at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expiry_timestamp(day: u32, hour: u32, minute: u32) -> i64 {
|
||||||
|
chrono::Local
|
||||||
|
.with_ymd_and_hms(2026, 6, day, hour, minute, 0)
|
||||||
|
.single()
|
||||||
|
.expect("valid test timestamp")
|
||||||
|
.timestamp()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_credit_options_use_scope_label_for_unknown_reset_type() {
|
||||||
|
let mut credit = reset_credit("future-credit", /*expires_at*/ None);
|
||||||
|
credit.reset_type = RateLimitResetType::Unknown;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reset_credit_options(
|
||||||
|
&detailed_reset_credits(/*available_count*/ 1, vec![credit]),
|
||||||
|
RateLimitResetScope::WeeklyAndFiveHour,
|
||||||
|
),
|
||||||
|
vec![ResetCreditOption {
|
||||||
|
credit_id: Some("future-credit".to_string()),
|
||||||
|
name: "Full reset (Weekly + 5h)".to_string(),
|
||||||
|
description: "Does not expire.".to_string(),
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn usage_command_opens_menu_when_reset_is_available_snapshot() {
|
async fn usage_command_opens_menu_when_reset_is_available_snapshot() {
|
||||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
@@ -14,10 +84,7 @@ async fn usage_command_opens_menu_when_reset_is_available_snapshot() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
chat.dispatch_command(SlashCommand::Usage);
|
chat.dispatch_command(SlashCommand::Usage);
|
||||||
@@ -38,10 +105,7 @@ async fn usage_command_disables_reset_after_cached_zero_snapshot() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 0)),
|
||||||
available_count: 0,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
chat.dispatch_command(SlashCommand::Usage);
|
chat.dispatch_command(SlashCommand::Usage);
|
||||||
@@ -69,10 +133,7 @@ async fn usage_menu_refresh_enables_newly_available_reset() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 0)),
|
||||||
available_count: 0,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
chat.dispatch_command(SlashCommand::Usage);
|
chat.dispatch_command(SlashCommand::Usage);
|
||||||
@@ -85,10 +146,7 @@ async fn usage_menu_refresh_enables_newly_available_reset() {
|
|||||||
chat.finish_usage_menu_rate_limit_refresh(
|
chat.finish_usage_menu_rate_limit_refresh(
|
||||||
/*request_id*/ 1,
|
/*request_id*/ 1,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 1)),
|
||||||
available_count: 1,
|
|
||||||
credits: None,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
|
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
|
||||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||||
@@ -104,10 +162,7 @@ async fn usage_menu_refresh_failure_preserves_disabled_known_zero() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 0)),
|
||||||
available_count: 0,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
chat.dispatch_command(SlashCommand::Usage);
|
chat.dispatch_command(SlashCommand::Usage);
|
||||||
@@ -137,10 +192,7 @@ async fn account_update_invalidates_usage_menu_refresh_when_visible_state_is_unc
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
startup_request_id,
|
startup_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 0)),
|
||||||
available_count: 0,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
chat.dispatch_command(SlashCommand::Usage);
|
chat.dispatch_command(SlashCommand::Usage);
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
@@ -157,10 +209,7 @@ async fn account_update_invalidates_usage_menu_refresh_when_visible_state_is_unc
|
|||||||
chat.finish_usage_menu_rate_limit_refresh(
|
chat.finish_usage_menu_rate_limit_refresh(
|
||||||
/*request_id*/ 1,
|
/*request_id*/ 1,
|
||||||
vec![snapshot(/*percent*/ 92.0)],
|
vec![snapshot(/*percent*/ 92.0)],
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(chat.available_rate_limit_reset_credits, None);
|
assert_eq!(chat.available_rate_limit_reset_credits, None);
|
||||||
@@ -206,10 +255,7 @@ async fn usage_menu_rate_limit_reset_entry_opens_reset_flow() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
chat.dispatch_command(SlashCommand::Usage);
|
chat.dispatch_command(SlashCommand::Usage);
|
||||||
|
|
||||||
@@ -227,13 +273,38 @@ async fn rate_limit_reset_popup_states_snapshot() {
|
|||||||
|
|
||||||
let loading_request_id = chat.show_rate_limit_reset_loading_popup();
|
let loading_request_id = chat.show_rate_limit_reset_loading_popup();
|
||||||
record_popup(&chat, &mut states);
|
record_popup(&chat, &mut states);
|
||||||
|
let first_expiry = expiry_timestamp(/*day*/ 18, /*hour*/ 9, /*minute*/ 39);
|
||||||
|
let second_expiry = expiry_timestamp(/*day*/ 27, /*hour*/ 8, /*minute*/ 59);
|
||||||
|
let mut rate_limit_snapshot = snapshot(/*percent*/ 50.0);
|
||||||
|
rate_limit_snapshot.limit_id = Some("codex".to_string());
|
||||||
|
rate_limit_snapshot
|
||||||
|
.primary
|
||||||
|
.as_mut()
|
||||||
|
.expect("primary window")
|
||||||
|
.window_duration_mins = Some(5 * 60);
|
||||||
|
rate_limit_snapshot.secondary = Some(RateLimitWindow {
|
||||||
|
used_percent: 50,
|
||||||
|
window_duration_mins: Some(7 * 24 * 60),
|
||||||
|
resets_at: None,
|
||||||
|
});
|
||||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
loading_request_id,
|
loading_request_id,
|
||||||
Vec::new(),
|
vec![rate_limit_snapshot],
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(detailed_reset_credits(
|
||||||
available_count: 2,
|
/*available_count*/ 2,
|
||||||
credits: None
|
vec![
|
||||||
}),
|
reset_credit_with_title(
|
||||||
|
"credit-2",
|
||||||
|
Some(second_expiry),
|
||||||
|
"Full reset (Weekly + 5 hr)",
|
||||||
|
),
|
||||||
|
reset_credit_with_title(
|
||||||
|
"credit-1",
|
||||||
|
Some(first_expiry),
|
||||||
|
"Full reset (Weekly + 5 hr)",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
));
|
));
|
||||||
record_popup(&chat, &mut states);
|
record_popup(&chat, &mut states);
|
||||||
|
|
||||||
@@ -242,10 +313,7 @@ async fn rate_limit_reset_popup_states_snapshot() {
|
|||||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
empty_request_id,
|
empty_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 0)),
|
||||||
available_count: 0,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
record_popup(&chat, &mut states);
|
record_popup(&chat, &mut states);
|
||||||
|
|
||||||
@@ -264,6 +332,7 @@ async fn rate_limit_reset_popup_states_snapshot() {
|
|||||||
assert!(!chat.finish_rate_limit_reset_consume(
|
assert!(!chat.finish_rate_limit_reset_consume(
|
||||||
consuming_request_id,
|
consuming_request_id,
|
||||||
"redeem-1".to_string(),
|
"redeem-1".to_string(),
|
||||||
|
/*credit_id*/ None,
|
||||||
Err("request timed out".to_string()),
|
Err("request timed out".to_string()),
|
||||||
));
|
));
|
||||||
record_popup(&chat, &mut states);
|
record_popup(&chat, &mut states);
|
||||||
@@ -300,16 +369,36 @@ async fn rate_limit_reset_popup_states_snapshot() {
|
|||||||
assert!(chat.finish_post_consume_reset_credits_refresh(
|
assert!(chat.finish_post_consume_reset_credits_refresh(
|
||||||
success_request_id,
|
success_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 1)),
|
||||||
available_count: 1,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
record_popup(&chat, &mut states);
|
record_popup(&chat, &mut states);
|
||||||
|
|
||||||
assert_chatwidget_snapshot!("rate_limit_reset_popup_states", states.join("\n---\n"));
|
assert_chatwidget_snapshot!("rate_limit_reset_popup_states", states.join("\n---\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rate_limit_reset_picker_wraps_expiry_details_snapshot() {
|
||||||
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
let request_id = chat.show_rate_limit_reset_loading_popup();
|
||||||
|
let expiry = expiry_timestamp(/*day*/ 18, /*hour*/ 9, /*minute*/ 39);
|
||||||
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
|
request_id,
|
||||||
|
Vec::new(),
|
||||||
|
Ok(detailed_reset_credits(
|
||||||
|
/*available_count*/ 2,
|
||||||
|
vec![
|
||||||
|
reset_credit("credit-2", /*expires_at*/ None),
|
||||||
|
reset_credit("credit-1", Some(expiry)),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_chatwidget_snapshot!(
|
||||||
|
"rate_limit_reset_picker_narrow",
|
||||||
|
render_bottom_popup(&chat, /*width*/ 44)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn usage_limit_reset_confirmation_uses_monthly_copy_for_monthly_limits_snapshot() {
|
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;
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
@@ -321,10 +410,10 @@ async fn usage_limit_reset_confirmation_uses_monthly_copy_for_monthly_limits_sna
|
|||||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
free_request_id,
|
free_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(detailed_reset_credits(
|
||||||
available_count: 1,
|
/*available_count*/ 1,
|
||||||
credits: None
|
vec![reset_credit("free-credit", /*expires_at*/ None)],
|
||||||
}),
|
)),
|
||||||
));
|
));
|
||||||
states.push(format!(
|
states.push(format!(
|
||||||
"Free:\n{}",
|
"Free:\n{}",
|
||||||
@@ -337,10 +426,7 @@ async fn usage_limit_reset_confirmation_uses_monthly_copy_for_monthly_limits_sna
|
|||||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
go_request_id,
|
go_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 1)),
|
||||||
available_count: 1,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
states.push(format!("Go:\n{}", render_bottom_popup(&chat, /*width*/ 80)));
|
states.push(format!("Go:\n{}", render_bottom_popup(&chat, /*width*/ 80)));
|
||||||
|
|
||||||
@@ -356,10 +442,10 @@ async fn usage_limit_reset_confirmation_uses_monthly_copy_for_monthly_limits_sna
|
|||||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
business_request_id,
|
business_request_id,
|
||||||
vec![monthly_business_snapshot],
|
vec![monthly_business_snapshot],
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(detailed_reset_credits(
|
||||||
available_count: 1,
|
/*available_count*/ 1,
|
||||||
credits: None
|
vec![reset_credit("business-credit", /*expires_at*/ None)],
|
||||||
}),
|
)),
|
||||||
));
|
));
|
||||||
states.push(format!(
|
states.push(format!(
|
||||||
"Business with monthly window:\n{}",
|
"Business with monthly window:\n{}",
|
||||||
@@ -379,10 +465,7 @@ async fn rate_limit_reset_confirmation_selects_cancel_by_default() {
|
|||||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 1)),
|
||||||
available_count: 1,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||||
@@ -391,6 +474,49 @@ async fn rate_limit_reset_confirmation_selects_cancel_by_default() {
|
|||||||
assert!(rx.try_recv().is_err());
|
assert!(rx.try_recv().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rate_limit_reset_picker_starts_with_soonest_expiries_and_keeps_all_rows_reachable() {
|
||||||
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
|
chat.plan_type = Some(PlanType::Business);
|
||||||
|
let request_id = chat.show_rate_limit_reset_loading_popup();
|
||||||
|
let first_expiry = expiry_timestamp(/*day*/ 18, /*hour*/ 9, /*minute*/ 39);
|
||||||
|
let credits = (0..9)
|
||||||
|
.rev()
|
||||||
|
.map(|index| {
|
||||||
|
reset_credit(
|
||||||
|
&format!("credit-{index}"),
|
||||||
|
Some(first_expiry + i64::from(index) * 86_400),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
|
request_id,
|
||||||
|
Vec::new(),
|
||||||
|
Ok(detailed_reset_credits(/*available_count*/ 9, credits)),
|
||||||
|
));
|
||||||
|
|
||||||
|
let rendered = render_bottom_popup(&chat, /*width*/ 80);
|
||||||
|
assert!(
|
||||||
|
rendered.contains("Expires 09:39 on 18 Jun 2026."),
|
||||||
|
"{rendered}"
|
||||||
|
);
|
||||||
|
assert!(!rendered.contains("Full reset ("), "{rendered}");
|
||||||
|
|
||||||
|
for _ in 0..9 {
|
||||||
|
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::ConsumeRateLimitResetCredit {
|
||||||
|
idempotency_key,
|
||||||
|
credit_id,
|
||||||
|
}) if Uuid::parse_str(&idempotency_key).is_ok()
|
||||||
|
&& credit_id.as_deref() == Some("credit-8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn rate_limit_reset_confirmation_can_use_reset() {
|
async fn rate_limit_reset_confirmation_can_use_reset() {
|
||||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
@@ -398,19 +524,18 @@ async fn rate_limit_reset_confirmation_can_use_reset() {
|
|||||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 1)),
|
||||||
available_count: 1,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
|
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
|
||||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||||
|
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
rx.try_recv(),
|
rx.try_recv(),
|
||||||
Ok(AppEvent::ConsumeRateLimitResetCredit { idempotency_key })
|
Ok(AppEvent::ConsumeRateLimitResetCredit {
|
||||||
if Uuid::parse_str(&idempotency_key).is_ok()
|
idempotency_key,
|
||||||
|
credit_id,
|
||||||
|
}) if Uuid::parse_str(&idempotency_key).is_ok() && credit_id.is_none()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,6 +546,7 @@ async fn rate_limit_reset_retry_reuses_idempotency_key() {
|
|||||||
assert!(!chat.finish_rate_limit_reset_consume(
|
assert!(!chat.finish_rate_limit_reset_consume(
|
||||||
request_id,
|
request_id,
|
||||||
"stable-redeem-id".to_string(),
|
"stable-redeem-id".to_string(),
|
||||||
|
Some("credit-1".to_string()),
|
||||||
Err("response lost".to_string()),
|
Err("response lost".to_string()),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -428,8 +554,11 @@ async fn rate_limit_reset_retry_reuses_idempotency_key() {
|
|||||||
|
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
rx.try_recv(),
|
rx.try_recv(),
|
||||||
Ok(AppEvent::ConsumeRateLimitResetCredit { idempotency_key })
|
Ok(AppEvent::ConsumeRateLimitResetCredit {
|
||||||
if idempotency_key == "stable-redeem-id"
|
idempotency_key,
|
||||||
|
credit_id,
|
||||||
|
}) if idempotency_key == "stable-redeem-id"
|
||||||
|
&& credit_id.as_deref() == Some("credit-1")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,10 +570,7 @@ async fn no_credit_outcome_disables_reset_entry_in_usage_menu() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
startup_request_id,
|
startup_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 1)),
|
||||||
available_count: 1,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
let consume_request_id = chat.show_rate_limit_reset_consuming_popup();
|
let consume_request_id = chat.show_rate_limit_reset_consuming_popup();
|
||||||
assert!(!finish_reset_consume_outcome(
|
assert!(!finish_reset_consume_outcome(
|
||||||
@@ -466,6 +592,23 @@ async fn no_credit_outcome_disables_reset_entry_in_usage_menu() {
|
|||||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, 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::OpenTokenActivity));
|
||||||
|
|
||||||
|
chat.available_rate_limit_reset_credits = Some(2);
|
||||||
|
let consume_request_id = chat.show_rate_limit_reset_consuming_popup();
|
||||||
|
assert!(!chat.finish_rate_limit_reset_consume(
|
||||||
|
consume_request_id,
|
||||||
|
"redeem-selected".to_string(),
|
||||||
|
Some("stale-credit".to_string()),
|
||||||
|
Ok(consume_response(
|
||||||
|
ConsumeAccountRateLimitResetCreditOutcome::NoCredit
|
||||||
|
)),
|
||||||
|
));
|
||||||
|
assert_eq!(chat.available_rate_limit_reset_credits, None);
|
||||||
|
assert!(
|
||||||
|
render_bottom_popup(&chat, /*width*/ 80).contains("That reset is no longer available.")
|
||||||
|
);
|
||||||
|
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||||
|
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenRateLimitResetCredits));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -489,10 +632,7 @@ async fn rate_limit_reset_redemption_cannot_be_dismissed_while_in_flight() {
|
|||||||
assert!(chat.finish_post_consume_reset_credits_refresh(
|
assert!(chat.finish_post_consume_reset_credits_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 1)),
|
||||||
available_count: 1,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
dismiss_popup(&mut chat);
|
dismiss_popup(&mut chat);
|
||||||
assert!(chat.bottom_pane.no_modal_or_popup_active());
|
assert!(chat.bottom_pane.no_modal_or_popup_active());
|
||||||
@@ -523,10 +663,7 @@ async fn already_redeemed_is_an_idempotent_success() {
|
|||||||
assert!(chat.finish_post_consume_reset_credits_refresh(
|
assert!(chat.finish_post_consume_reset_credits_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 0)),
|
||||||
available_count: 0,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
assert!(
|
assert!(
|
||||||
render_bottom_popup(&chat, /*width*/ 80)
|
render_bottom_popup(&chat, /*width*/ 80)
|
||||||
@@ -542,10 +679,7 @@ async fn failed_post_consume_refresh_does_not_keep_stale_reset_count() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
startup_request_id,
|
startup_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
let consume_request_id = chat.show_rate_limit_reset_consuming_popup();
|
let consume_request_id = chat.show_rate_limit_reset_consuming_popup();
|
||||||
assert!(finish_reset_consume_outcome(
|
assert!(finish_reset_consume_outcome(
|
||||||
@@ -582,10 +716,7 @@ async fn account_change_invalidates_pending_reset_requests() {
|
|||||||
assert!(!chat.finish_rate_limit_reset_credits_refresh(
|
assert!(!chat.finish_rate_limit_reset_credits_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
assert!(chat.bottom_pane.no_modal_or_popup_active());
|
assert!(chat.bottom_pane.no_modal_or_popup_active());
|
||||||
}
|
}
|
||||||
@@ -599,10 +730,7 @@ async fn clearing_pending_reset_hint_preserves_in_flight_redemption() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
hint_request_id,
|
hint_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
chat.clear_pending_rate_limit_reset_hint();
|
chat.clear_pending_rate_limit_reset_hint();
|
||||||
@@ -625,10 +753,7 @@ async fn rate_limit_reset_load_result_updates_popup_beneath_overlay() {
|
|||||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
chat.bottom_pane.active_view_id(),
|
chat.bottom_pane.active_view_id(),
|
||||||
@@ -636,10 +761,7 @@ async fn rate_limit_reset_load_result_updates_popup_beneath_overlay() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||||
assert!(
|
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("2 usage limit resets available."));
|
||||||
render_bottom_popup(&chat, /*width*/ 80)
|
|
||||||
.contains("You have 2 usage limit resets available.")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -657,10 +779,7 @@ async fn rate_limit_reset_success_updates_popup_beneath_overlay() {
|
|||||||
assert!(chat.finish_post_consume_reset_credits_refresh(
|
assert!(chat.finish_post_consume_reset_credits_refresh(
|
||||||
request_id,
|
request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 1)),
|
||||||
available_count: 1,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
chat.bottom_pane.active_view_id(),
|
chat.bottom_pane.active_view_id(),
|
||||||
@@ -703,10 +822,7 @@ async fn startup_check_shows_available_reset_hint_snapshot() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
hint_request_id,
|
hint_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
let rendered = lines_to_single_string(
|
let rendered = lines_to_single_string(
|
||||||
&chat
|
&chat
|
||||||
@@ -729,10 +845,7 @@ async fn startup_reset_hint_waits_for_active_output_snapshot() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
hint_request_id,
|
hint_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(chat.usage_history_insertion_blocked());
|
assert!(chat.usage_history_insertion_blocked());
|
||||||
@@ -763,10 +876,7 @@ async fn opening_rate_limit_reset_flow_invalidates_in_flight_startup_hint() {
|
|||||||
assert!(!chat.finish_rate_limit_reset_hint_refresh(
|
assert!(!chat.finish_rate_limit_reset_hint_refresh(
|
||||||
hint_request_id,
|
hint_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
assert!(chat.pending_rate_limit_reset_hint().is_none());
|
assert!(chat.pending_rate_limit_reset_hint().is_none());
|
||||||
}
|
}
|
||||||
@@ -779,10 +889,7 @@ async fn starting_rate_limit_reset_redemption_clears_deferred_startup_hint() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
hint_request_id,
|
hint_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
assert!(chat.pending_rate_limit_reset_hint().is_some());
|
assert!(chat.pending_rate_limit_reset_hint().is_some());
|
||||||
|
|
||||||
@@ -800,10 +907,7 @@ async fn startup_check_omits_reset_hint_when_none_are_available() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
hint_request_id,
|
hint_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 0)),
|
||||||
available_count: 0,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
assert!(chat.pending_rate_limit_reset_hint().is_none());
|
assert!(chat.pending_rate_limit_reset_hint().is_none());
|
||||||
}
|
}
|
||||||
@@ -818,10 +922,7 @@ async fn startup_check_shows_reset_hint_for_workspace_account_with_credit() {
|
|||||||
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
assert!(chat.finish_rate_limit_reset_hint_refresh(
|
||||||
hint_request_id,
|
hint_request_id,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Ok(RateLimitResetCreditsSummary {
|
Ok(reset_credits(/*available_count*/ 2)),
|
||||||
available_count: 2,
|
|
||||||
credits: None
|
|
||||||
}),
|
|
||||||
));
|
));
|
||||||
assert!(chat.pending_rate_limit_reset_hint().is_some());
|
assert!(chat.pending_rate_limit_reset_hint().is_some());
|
||||||
assert_eq!(chat.available_rate_limit_reset_credits, Some(2));
|
assert_eq!(chat.available_rate_limit_reset_credits, Some(2));
|
||||||
@@ -842,6 +943,7 @@ fn finish_reset_consume_outcome(
|
|||||||
chat.finish_rate_limit_reset_consume(
|
chat.finish_rate_limit_reset_consume(
|
||||||
request_id,
|
request_id,
|
||||||
idempotency_key.to_string(),
|
idempotency_key.to_string(),
|
||||||
|
/*credit_id*/ None,
|
||||||
Ok(consume_response(outcome)),
|
Ok(consume_response(outcome)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse;
|
|||||||
use codex_app_server_protocol::RateLimitResetCreditsSummary;
|
use codex_app_server_protocol::RateLimitResetCreditsSummary;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::rate_limits::get_limits_duration;
|
use super::reset_credits::rate_limit_reset_scope;
|
||||||
|
use super::reset_credits::reset_credit_options;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const USAGE_MENU_VIEW_ID: &str = "usage-menu";
|
const USAGE_MENU_VIEW_ID: &str = "usage-menu";
|
||||||
@@ -131,18 +132,20 @@ impl ChatWidget {
|
|||||||
|
|
||||||
let params = match result {
|
let params = match result {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
self.available_rate_limit_reset_credits = Some(response.available_count);
|
let available_count = response.available_count;
|
||||||
if response.available_count > 0 {
|
let params = if available_count > 0 {
|
||||||
self.rate_limit_reset_confirmation_params(response.available_count)
|
self.rate_limit_reset_confirmation_params(&response)
|
||||||
} else {
|
} else {
|
||||||
Self::rate_limit_reset_message_params(
|
Self::rate_limit_reset_message_params(
|
||||||
"You don't have any usage limit resets available.",
|
"You don't have any usage limit resets available.",
|
||||||
)
|
)
|
||||||
}
|
};
|
||||||
|
self.available_rate_limit_reset_credits = Some(available_count);
|
||||||
|
params
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
Self::reset_refresh_params("Couldn't load usage limit resets. Please try again.")
|
||||||
}
|
}
|
||||||
Err(_) => Self::rate_limit_reset_message_params(
|
|
||||||
"Couldn't load usage limit resets. Please try again.",
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
let replaced = self
|
let replaced = self
|
||||||
.bottom_pane
|
.bottom_pane
|
||||||
@@ -153,56 +156,47 @@ impl ChatWidget {
|
|||||||
replaced
|
replaced
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rate_limit_reset_confirmation_params(&self, available_count: i64) -> SelectionViewParams {
|
fn rate_limit_reset_confirmation_params(
|
||||||
let idempotency_key = Uuid::new_v4().to_string();
|
&self,
|
||||||
let has_monthly_window = self
|
reset_credits: &RateLimitResetCreditsSummary,
|
||||||
.rate_limit_snapshots_by_limit_id
|
) -> SelectionViewParams {
|
||||||
.iter()
|
let scope = rate_limit_reset_scope(&self.rate_limit_snapshots_by_limit_id, self.plan_type);
|
||||||
.find(|(limit_id, _)| limit_id.eq_ignore_ascii_case("codex"))
|
let mut items = vec![SelectionItem {
|
||||||
.into_iter()
|
name: "Cancel".to_string(),
|
||||||
.flat_map(|(_, snapshot)| [snapshot.primary.as_ref(), snapshot.secondary.as_ref()])
|
dismiss_on_select: true,
|
||||||
.flatten()
|
..Default::default()
|
||||||
.any(|window| {
|
}];
|
||||||
window
|
items.extend(
|
||||||
.window_minutes
|
reset_credit_options(reset_credits, scope)
|
||||||
.and_then(get_limits_duration)
|
.into_iter()
|
||||||
.as_deref()
|
.map(|option| {
|
||||||
== Some("monthly")
|
let idempotency_key = Uuid::new_v4().to_string();
|
||||||
});
|
let credit_id = option.credit_id;
|
||||||
let reset_description = if has_monthly_window
|
SelectionItem {
|
||||||
|| matches!(self.plan_type, Some(PlanType::Free | PlanType::Go))
|
name: option.name,
|
||||||
{
|
description: Some(option.description),
|
||||||
"Reset your current monthly usage limit."
|
actions: vec![Box::new(move |tx| {
|
||||||
} else {
|
tx.send(AppEvent::ConsumeRateLimitResetCredit {
|
||||||
"Reset your current 5-hour and weekly usage limits."
|
idempotency_key: idempotency_key.clone(),
|
||||||
};
|
credit_id: credit_id.clone(),
|
||||||
|
});
|
||||||
|
})],
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
SelectionViewParams {
|
SelectionViewParams {
|
||||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||||
title: Some("Usage limit resets".to_string()),
|
title: Some("Usage limit resets".to_string()),
|
||||||
subtitle: Some(format!(
|
subtitle: Some(format!(
|
||||||
"You have {available_count} {} available.",
|
"{} {} available.",
|
||||||
reset_label(available_count)
|
reset_credits.available_count,
|
||||||
|
reset_label(reset_credits.available_count)
|
||||||
)),
|
)),
|
||||||
footer_hint: Some(standard_popup_hint_line()),
|
footer_hint: Some(standard_popup_hint_line()),
|
||||||
items: vec![
|
items,
|
||||||
SelectionItem {
|
initial_selected_idx: Some(0),
|
||||||
name: "Use a reset".to_string(),
|
|
||||||
description: Some(reset_description.to_string()),
|
|
||||||
actions: vec![Box::new(move |tx| {
|
|
||||||
tx.send(AppEvent::ConsumeRateLimitResetCredit {
|
|
||||||
idempotency_key: idempotency_key.clone(),
|
|
||||||
});
|
|
||||||
})],
|
|
||||||
dismiss_on_select: true,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
SelectionItem {
|
|
||||||
name: "Cancel".to_string(),
|
|
||||||
dismiss_on_select: true,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
],
|
|
||||||
initial_selected_idx: Some(1),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,6 +215,30 @@ impl ChatWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reset_refresh_params(message: &str) -> SelectionViewParams {
|
||||||
|
SelectionViewParams {
|
||||||
|
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||||
|
title: Some("Usage limit resets".to_string()),
|
||||||
|
subtitle: Some(message.to_string()),
|
||||||
|
items: vec![
|
||||||
|
SelectionItem {
|
||||||
|
name: "Try again".to_string(),
|
||||||
|
actions: vec![Box::new(|tx| {
|
||||||
|
tx.send(AppEvent::OpenRateLimitResetCredits);
|
||||||
|
})],
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
SelectionItem {
|
||||||
|
name: "Close".to_string(),
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn show_rate_limit_reset_consuming_popup(&mut self) -> u64 {
|
pub(crate) fn show_rate_limit_reset_consuming_popup(&mut self) -> u64 {
|
||||||
self.clear_pending_rate_limit_reset_hint();
|
self.clear_pending_rate_limit_reset_hint();
|
||||||
let request_id = self.take_next_rate_limit_reset_request_id();
|
let request_id = self.take_next_rate_limit_reset_request_id();
|
||||||
@@ -245,6 +263,7 @@ impl ChatWidget {
|
|||||||
&mut self,
|
&mut self,
|
||||||
request_id: u64,
|
request_id: u64,
|
||||||
idempotency_key: String,
|
idempotency_key: String,
|
||||||
|
credit_id: Option<String>,
|
||||||
result: Result<ConsumeAccountRateLimitResetCreditResponse, String>,
|
result: Result<ConsumeAccountRateLimitResetCreditResponse, String>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if self.pending_rate_limit_reset_request_id != Some(request_id) {
|
if self.pending_rate_limit_reset_request_id != Some(request_id) {
|
||||||
@@ -269,6 +288,13 @@ impl ChatWidget {
|
|||||||
ConsumeAccountRateLimitResetCreditOutcome::NothingToReset => {
|
ConsumeAccountRateLimitResetCreditOutcome::NothingToReset => {
|
||||||
"Your usage does not need a reset right now."
|
"Your usage does not need a reset right now."
|
||||||
}
|
}
|
||||||
|
ConsumeAccountRateLimitResetCreditOutcome::NoCredit if credit_id.is_some() => {
|
||||||
|
self.available_rate_limit_reset_credits = None;
|
||||||
|
self.replace_rate_limit_reset_popup(Self::reset_refresh_params(
|
||||||
|
"That reset is no longer available. Refresh to see your current resets.",
|
||||||
|
));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
ConsumeAccountRateLimitResetCreditOutcome::NoCredit => {
|
ConsumeAccountRateLimitResetCreditOutcome::NoCredit => {
|
||||||
self.available_rate_limit_reset_credits = Some(0);
|
self.available_rate_limit_reset_credits = Some(0);
|
||||||
"No usage limit resets are available."
|
"No usage limit resets are available."
|
||||||
@@ -291,6 +317,7 @@ impl ChatWidget {
|
|||||||
actions: vec![Box::new(move |tx| {
|
actions: vec![Box::new(move |tx| {
|
||||||
tx.send(AppEvent::ConsumeRateLimitResetCredit {
|
tx.send(AppEvent::ConsumeRateLimitResetCredit {
|
||||||
idempotency_key: idempotency_key.clone(),
|
idempotency_key: idempotency_key.clone(),
|
||||||
|
credit_id: credit_id.clone(),
|
||||||
});
|
});
|
||||||
})],
|
})],
|
||||||
dismiss_on_select: true,
|
dismiss_on_select: true,
|
||||||
@@ -325,11 +352,11 @@ impl ChatWidget {
|
|||||||
|
|
||||||
let message = match result {
|
let message = match result {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
self.available_rate_limit_reset_credits = Some(response.available_count);
|
let available_count = response.available_count;
|
||||||
|
self.available_rate_limit_reset_credits = Some(available_count);
|
||||||
format!(
|
format!(
|
||||||
"Usage reset. You have {} {} left.",
|
"Usage reset. You have {available_count} {} left.",
|
||||||
response.available_count,
|
reset_label(available_count)
|
||||||
reset_label(response.available_count)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Err(_) => "Usage reset.".to_string(),
|
Err(_) => "Usage reset.".to_string(),
|
||||||
@@ -386,8 +413,9 @@ impl ChatWidget {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Ok(response) = result {
|
if let Ok(response) = result {
|
||||||
self.available_rate_limit_reset_credits = Some(response.available_count);
|
let available_count = response.available_count;
|
||||||
self.set_rate_limit_reset_available_hint(response.available_count);
|
self.available_rate_limit_reset_credits = Some(available_count);
|
||||||
|
self.set_rate_limit_reset_available_hint(available_count);
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user