mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Confirm usage-limit resets before redemption (#33845)
## Why Selecting a usage-limit reset from the picker immediately started redemption, leaving no opportunity to verify the choice. ## What changed - Open a separate confirmation popup before redeeming a selected reset, with `No, go back` selected by default. - Show backend-provided reset titles and descriptions in the picker and confirmation, with generic fallback copy when those details are absent. - Ignore duplicate or stale confirmation and redemption events, while retaining the idempotency key needed to retry a failed redemption. ## Testing - Added coverage for confirmation, cancellation, backend copy, duplicate and stale events, successful redemption, and retry behavior. GitOrigin-RevId: 828945f99f06e8c73941fd864e4aa9ac52729f98
This commit is contained in:
Generated
+1
@@ -633,6 +633,7 @@
|
||||
"anstyle_1.0.13": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.5\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"anstyle_1.0.14": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.23\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"anyhow_1.0.102": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.6\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"backtrace\":[],\"default\":[\"std\"],\"std\":[]}}",
|
||||
"anyhow_1.0.103": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.6\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.108\"}],\"features\":{\"backtrace\":[],\"default\":[\"std\"],\"std\":[]}}",
|
||||
"arbitrary_1.4.2": "{\"dependencies\":[{\"name\":\"derive_arbitrary\",\"optional\":true,\"req\":\"~1.4.0\"},{\"kind\":\"dev\",\"name\":\"exhaustigen\",\"req\":\"^0.1.0\"}],\"features\":{\"derive\":[\"derive_arbitrary\"]}}",
|
||||
"arboard_3.6.1": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"clipboard-win\",\"req\":\"^5.3.1\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"features\":[\"png\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"default_features\":false,\"features\":[\"tiff\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"png\",\"bmp\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(windows)\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(windows)\"},{\"name\":\"objc2\",\"req\":\"^0.6.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"objc2-core-graphics\",\"NSPasteboard\",\"NSPasteboardItem\",\"NSImage\"],\"name\":\"objc2-app-kit\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CFCGTypes\"],\"name\":\"objc2-core-foundation\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CGImage\",\"CGColorSpace\",\"CGDataProvider\"],\"name\":\"objc2-core-graphics\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"NSArray\",\"NSString\",\"NSEnumerator\",\"NSGeometry\",\"NSValue\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_DataExchange\",\"Win32_System_Memory\",\"Win32_System_Ole\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.61.0\",\"target\":\"cfg(windows)\"},{\"name\":\"wl-clipboard-rs\",\"optional\":true,\"req\":\"^0.9.0\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"x11rb\",\"req\":\"^0.13\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"}],\"features\":{\"core-graphics\":[\"dep:objc2-core-graphics\"],\"default\":[\"image-data\"],\"image\":[\"dep:image\"],\"image-data\":[\"dep:objc2-core-graphics\",\"dep:objc2-core-foundation\",\"image\",\"windows-sys\",\"core-graphics\"],\"wayland-data-control\":[\"wl-clipboard-rs\"],\"windows-sys\":[\"windows-sys/Win32_Graphics_Gdi\"],\"wl-clipboard-rs\":[\"dep:wl-clipboard-rs\"]}}",
|
||||
"arc-swap_1.9.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adaptive-barrier\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"~0.7\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"~0.8\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"~0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.177\"}],\"features\":{\"experimental-strategies\":[],\"experimental-thread-local\":[],\"internal-test-strategies\":[],\"weak\":[]}}",
|
||||
|
||||
Generated
+2
-2
@@ -388,9 +388,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "app_test_support"
|
||||
|
||||
@@ -975,17 +975,38 @@ impl App {
|
||||
RateLimitRefreshOrigin::ResetPicker { request_id },
|
||||
);
|
||||
}
|
||||
AppEvent::OpenRateLimitResetConfirmation {
|
||||
picker_request_id,
|
||||
confirmation_gate,
|
||||
credit_id,
|
||||
reset_title,
|
||||
reset_detail,
|
||||
reset_description,
|
||||
} => {
|
||||
self.chat_widget.show_rate_limit_reset_confirmation(
|
||||
picker_request_id,
|
||||
confirmation_gate,
|
||||
credit_id,
|
||||
reset_title,
|
||||
reset_detail,
|
||||
reset_description,
|
||||
);
|
||||
}
|
||||
AppEvent::ConsumeRateLimitResetCredit {
|
||||
idempotency_key,
|
||||
credit_id,
|
||||
} => {
|
||||
let request_id = self.chat_widget.show_rate_limit_reset_consuming_popup();
|
||||
self.consume_rate_limit_reset_credit(
|
||||
app_server,
|
||||
request_id,
|
||||
idempotency_key,
|
||||
credit_id,
|
||||
);
|
||||
if let Some(request_id) = self
|
||||
.chat_widget
|
||||
.start_rate_limit_reset_consumption(&idempotency_key)
|
||||
{
|
||||
self.consume_rate_limit_reset_credit(
|
||||
app_server,
|
||||
request_id,
|
||||
idempotency_key,
|
||||
credit_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
AppEvent::RateLimitResetCreditConsumed {
|
||||
request_id,
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
//! quits without reaching into the app loop or coupling to shutdown/exit sequencing.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||
@@ -332,6 +334,16 @@ pub(crate) enum AppEvent {
|
||||
/// Open the reset-credit flow selected from the `/usage` menu.
|
||||
OpenRateLimitResetCredits,
|
||||
|
||||
/// Confirm the reset credit selected from the reset-credit picker.
|
||||
OpenRateLimitResetConfirmation {
|
||||
picker_request_id: u64,
|
||||
confirmation_gate: Arc<AtomicBool>,
|
||||
credit_id: Option<String>,
|
||||
reset_title: String,
|
||||
reset_detail: Option<String>,
|
||||
reset_description: String,
|
||||
},
|
||||
|
||||
/// Consume one reset credit using a stable idempotency key.
|
||||
ConsumeRateLimitResetCredit {
|
||||
idempotency_key: String,
|
||||
|
||||
@@ -563,6 +563,8 @@ pub(crate) struct ChatWidget {
|
||||
completed_token_activity_output: Option<history_cell::CompositeHistoryCell>,
|
||||
next_token_activity_request_id: u64,
|
||||
pending_rate_limit_reset_request_id: Option<u64>,
|
||||
pending_rate_limit_reset_idempotency_key: Option<String>,
|
||||
rate_limit_reset_picker_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>,
|
||||
|
||||
@@ -130,6 +130,8 @@ impl ChatWidget {
|
||||
completed_token_activity_output: None,
|
||||
next_token_activity_request_id: 0,
|
||||
pending_rate_limit_reset_request_id: None,
|
||||
pending_rate_limit_reset_idempotency_key: None,
|
||||
rate_limit_reset_picker_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,
|
||||
|
||||
@@ -1,76 +1,19 @@
|
||||
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) detail: Option<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);
|
||||
@@ -100,19 +43,23 @@ pub(super) fn reset_credit_options(
|
||||
.unwrap_or_else(|| "Expiration unavailable".to_string()),
|
||||
None => "Does not expire".to_string(),
|
||||
};
|
||||
let reset_label = credit
|
||||
let reset_title = credit
|
||||
.title
|
||||
.as_deref()
|
||||
.filter(|title| !title.trim().is_empty())
|
||||
.unwrap_or_else(|| match credit.reset_type {
|
||||
RateLimitResetType::CodexRateLimits | RateLimitResetType::Unknown => {
|
||||
scope.picker_label()
|
||||
}
|
||||
});
|
||||
.map(str::trim)
|
||||
.filter(|title| !title.is_empty())
|
||||
.unwrap_or("Full reset");
|
||||
let reset_description = credit
|
||||
.description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|description| !description.is_empty())
|
||||
.unwrap_or("Reset your current usage limits.");
|
||||
ResetCreditOption {
|
||||
credit_id: Some(credit.id.clone()),
|
||||
name: reset_label.to_string(),
|
||||
description: format!("{expiration}."),
|
||||
name: reset_title.to_string(),
|
||||
detail: Some(format!("{expiration}.")),
|
||||
description: reset_description.to_string(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -120,8 +67,9 @@ pub(super) fn reset_credit_options(
|
||||
if options.is_empty() {
|
||||
options.push(ResetCreditOption {
|
||||
credit_id: None,
|
||||
name: "Use a reset".to_string(),
|
||||
description: scope.usage_description().to_string(),
|
||||
name: "Full reset".to_string(),
|
||||
detail: None,
|
||||
description: "Reset your current usage limits.".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/usage.rs
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
Use this reset?
|
||||
Full reset (Weekly + 5 hr) · Expires 09:39 on 18 Jun 2026.
|
||||
|
||||
1. Yes, use reset Reset your weekly and 5-hour usage limits.
|
||||
› 2. No, go back Choose a different reset.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
+3
-3
@@ -5,9 +5,9 @@ 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
|
||||
› 1. Full reset Expires 09:39 on 18 Jun
|
||||
2026.
|
||||
3. Full reset Does not expire.
|
||||
2. Full reset Does not expire.
|
||||
3. Cancel
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
+3
-3
@@ -12,9 +12,9 @@ expression: "states.join(\"\\n---\\n\")"
|
||||
Usage limit resets
|
||||
2 usage limit resets available.
|
||||
|
||||
› 1. 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.
|
||||
› 1. Full reset (Weekly + 5 hr) Expires 09:39 on 18 Jun 2026.
|
||||
2. Full reset (Weekly + 5 hr) Expires 08:59 on 27 Jun 2026.
|
||||
3. Cancel
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
---
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/usage.rs
|
||||
expression: "states.join(\"\\n---\\n\")"
|
||||
---
|
||||
Free:
|
||||
Usage limit resets
|
||||
1 usage limit reset available.
|
||||
|
||||
› 1. Cancel
|
||||
2. Full reset (Monthly) Does not expire.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
---
|
||||
Go:
|
||||
Usage limit resets
|
||||
1 usage limit reset available.
|
||||
|
||||
› 1. Cancel
|
||||
2. Use a reset Reset your current monthly usage limit.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
---
|
||||
Business with monthly window:
|
||||
Usage limit resets
|
||||
1 usage limit reset available.
|
||||
|
||||
› 1. Cancel
|
||||
2. Full reset (Monthly) Does not expire.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::super::reset_credits::RateLimitResetScope;
|
||||
use super::super::reset_credits::ResetCreditOption;
|
||||
use super::super::reset_credits::reset_credit_options;
|
||||
use super::*;
|
||||
@@ -58,20 +57,47 @@ fn expiry_timestamp(day: u32, hour: u32, minute: u32) -> i64 {
|
||||
.timestamp()
|
||||
}
|
||||
|
||||
fn show_rate_limit_reset_confirmation_from_event(
|
||||
chat: &mut ChatWidget,
|
||||
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||
) -> u64 {
|
||||
let Ok(AppEvent::OpenRateLimitResetConfirmation {
|
||||
picker_request_id,
|
||||
confirmation_gate,
|
||||
credit_id,
|
||||
reset_title,
|
||||
reset_detail,
|
||||
reset_description,
|
||||
}) = rx.try_recv()
|
||||
else {
|
||||
panic!("expected reset confirmation event");
|
||||
};
|
||||
assert!(chat.show_rate_limit_reset_confirmation(
|
||||
picker_request_id,
|
||||
confirmation_gate,
|
||||
credit_id,
|
||||
reset_title,
|
||||
reset_detail,
|
||||
reset_description,
|
||||
));
|
||||
picker_request_id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_credit_options_use_scope_label_for_unknown_reset_type() {
|
||||
fn reset_credit_options_use_generic_copy_when_backend_copy_is_missing() {
|
||||
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,
|
||||
),
|
||||
reset_credit_options(&detailed_reset_credits(
|
||||
/*available_count*/ 1,
|
||||
vec![credit],
|
||||
)),
|
||||
vec![ResetCreditOption {
|
||||
credit_id: Some("future-credit".to_string()),
|
||||
name: "Full reset (Weekly + 5h)".to_string(),
|
||||
description: "Does not expire.".to_string(),
|
||||
name: "Full reset".to_string(),
|
||||
detail: Some("Does not expire.".to_string()),
|
||||
description: "Reset your current usage limits.".to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
@@ -400,66 +426,67 @@ async fn rate_limit_reset_picker_wraps_expiry_details_snapshot() {
|
||||
}
|
||||
|
||||
#[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();
|
||||
async fn rate_limit_reset_confirmation_uses_backend_copy_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let expiry = expiry_timestamp(/*day*/ 18, /*hour*/ 9, /*minute*/ 39);
|
||||
let mut credit =
|
||||
reset_credit_with_title("weekly-credit", Some(expiry), "Full reset (Weekly + 5 hr)");
|
||||
credit.description = Some("Reset your weekly and 5-hour usage limits.".to_string());
|
||||
let request_id = chat.show_rate_limit_reset_loading_popup();
|
||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||
free_request_id,
|
||||
request_id,
|
||||
Vec::new(),
|
||||
Ok(detailed_reset_credits(
|
||||
/*available_count*/ 1,
|
||||
vec![reset_credit("free-credit", /*expires_at*/ None)],
|
||||
vec![credit],
|
||||
)),
|
||||
));
|
||||
states.push(format!(
|
||||
"Free:\n{}",
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
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(reset_credits(/*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(detailed_reset_credits(
|
||||
/*available_count*/ 1,
|
||||
vec![reset_credit("business-credit", /*expires_at*/ None)],
|
||||
)),
|
||||
));
|
||||
states.push(format!(
|
||||
"Business with monthly window:\n{}",
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
let Ok(AppEvent::OpenRateLimitResetConfirmation {
|
||||
picker_request_id,
|
||||
confirmation_gate,
|
||||
credit_id,
|
||||
reset_title,
|
||||
reset_detail,
|
||||
reset_description,
|
||||
}) = rx.try_recv()
|
||||
else {
|
||||
panic!("expected reset confirmation event");
|
||||
};
|
||||
assert_eq!(
|
||||
(
|
||||
picker_request_id,
|
||||
credit_id.as_deref(),
|
||||
reset_title.as_str(),
|
||||
reset_detail.as_deref(),
|
||||
reset_description.as_str(),
|
||||
),
|
||||
(
|
||||
request_id,
|
||||
Some("weekly-credit"),
|
||||
"Full reset (Weekly + 5 hr)",
|
||||
Some("Expires 09:39 on 18 Jun 2026."),
|
||||
"Reset your weekly and 5-hour usage limits.",
|
||||
)
|
||||
);
|
||||
assert!(chat.show_rate_limit_reset_confirmation(
|
||||
picker_request_id,
|
||||
confirmation_gate,
|
||||
credit_id,
|
||||
reset_title,
|
||||
reset_detail,
|
||||
reset_description,
|
||||
));
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"usage_limit_reset_confirmation_monthly",
|
||||
states.join("\n---\n")
|
||||
"rate_limit_reset_confirmation",
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rate_limit_reset_confirmation_selects_cancel_by_default() {
|
||||
async fn rate_limit_reset_confirmation_no_and_escape_return_to_picker() {
|
||||
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(
|
||||
@@ -467,17 +494,60 @@ async fn rate_limit_reset_confirmation_selects_cancel_by_default() {
|
||||
Vec::new(),
|
||||
Ok(reset_credits(/*available_count*/ 1)),
|
||||
));
|
||||
let picker = render_bottom_popup(&chat, /*width*/ 80);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
show_rate_limit_reset_confirmation_from_event(&mut chat, &mut rx);
|
||||
assert!(rx.try_recv().is_err());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(render_bottom_popup(&chat, /*width*/ 80), picker);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
show_rate_limit_reset_confirmation_from_event(&mut chat, &mut rx);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert_eq!(render_bottom_popup(&chat, /*width*/ 80), picker);
|
||||
|
||||
assert!(chat.bottom_pane.no_modal_or_popup_active());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
show_rate_limit_reset_confirmation_from_event(&mut chat, &mut rx);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(render_bottom_popup(&chat, /*width*/ 80), picker);
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reset_picker_allows_only_one_pending_confirmation() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let request_id = chat.show_rate_limit_reset_loading_popup();
|
||||
let first_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_with_title("credit-1", Some(first_expiry), "First reset"),
|
||||
reset_credit_with_title("credit-2", /*expires_at*/ None, "Second reset"),
|
||||
],
|
||||
)),
|
||||
));
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
show_rate_limit_reset_confirmation_from_event(&mut chat, &mut rx);
|
||||
assert!(rx.try_recv().is_err());
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
show_rate_limit_reset_confirmation_from_event(&mut chat, &mut rx);
|
||||
|
||||
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Second reset · Does not expire."));
|
||||
}
|
||||
|
||||
#[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)
|
||||
@@ -502,18 +572,25 @@ async fn rate_limit_reset_picker_starts_with_soonest_expiries_and_keeps_all_rows
|
||||
);
|
||||
assert!(!rendered.contains("Full reset ("), "{rendered}");
|
||||
|
||||
for _ in 0..9 {
|
||||
for _ in 0..8 {
|
||||
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,
|
||||
Ok(AppEvent::OpenRateLimitResetConfirmation {
|
||||
picker_request_id,
|
||||
confirmation_gate: _,
|
||||
credit_id,
|
||||
}) if Uuid::parse_str(&idempotency_key).is_ok()
|
||||
reset_title,
|
||||
reset_detail,
|
||||
reset_description,
|
||||
}) if picker_request_id == request_id
|
||||
&& credit_id.as_deref() == Some("credit-8")
|
||||
&& reset_title == "Full reset"
|
||||
&& reset_detail.as_deref() == Some("Expires 09:39 on 26 Jun 2026.")
|
||||
&& reset_description == "Reset your current usage limits."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -524,19 +601,46 @@ async fn rate_limit_reset_confirmation_can_use_reset() {
|
||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||
request_id,
|
||||
Vec::new(),
|
||||
Ok(reset_credits(/*available_count*/ 1)),
|
||||
Ok(detailed_reset_credits(
|
||||
/*available_count*/ 1,
|
||||
vec![reset_credit("credit-1", /*expires_at*/ None)],
|
||||
)),
|
||||
));
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
show_rate_limit_reset_confirmation_from_event(&mut chat, &mut rx);
|
||||
assert!(rx.try_recv().is_err());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Up, 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.is_none()
|
||||
let Ok(AppEvent::ConsumeRateLimitResetCredit {
|
||||
idempotency_key,
|
||||
credit_id,
|
||||
}) = rx.try_recv()
|
||||
else {
|
||||
panic!("expected reset consume event");
|
||||
};
|
||||
assert!(Uuid::parse_str(&idempotency_key).is_ok());
|
||||
assert_eq!(credit_id.as_deref(), Some("credit-1"));
|
||||
|
||||
assert_eq!(chat.start_rate_limit_reset_consumption("wrong-key"), None);
|
||||
let consume_request_id = chat
|
||||
.start_rate_limit_reset_consumption(&idempotency_key)
|
||||
.expect("confirmed reset should start consumption");
|
||||
assert_eq!(
|
||||
chat.start_rate_limit_reset_consumption(&idempotency_key),
|
||||
None
|
||||
);
|
||||
assert!(!chat.finish_rate_limit_reset_consume(
|
||||
consume_request_id,
|
||||
idempotency_key,
|
||||
credit_id,
|
||||
Ok(consume_response(
|
||||
ConsumeAccountRateLimitResetCreditOutcome::NothingToReset,
|
||||
)),
|
||||
));
|
||||
dismiss_popup(&mut chat);
|
||||
assert!(chat.bottom_pane.no_modal_or_popup_active());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -552,13 +656,21 @@ async fn rate_limit_reset_retry_reuses_idempotency_key() {
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::ConsumeRateLimitResetCredit {
|
||||
idempotency_key,
|
||||
credit_id,
|
||||
}) if idempotency_key == "stable-redeem-id"
|
||||
&& credit_id.as_deref() == Some("credit-1")
|
||||
let Ok(AppEvent::ConsumeRateLimitResetCredit {
|
||||
idempotency_key,
|
||||
credit_id,
|
||||
}) = rx.try_recv()
|
||||
else {
|
||||
panic!("expected reset retry event");
|
||||
};
|
||||
assert_eq!(
|
||||
(idempotency_key.as_str(), credit_id.as_deref()),
|
||||
("stable-redeem-id", Some("credit-1"))
|
||||
);
|
||||
assert_eq!(chat.start_rate_limit_reset_consumption("wrong-key"), None);
|
||||
assert!(
|
||||
chat.start_rate_limit_reset_consumption(&idempotency_key)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -795,15 +907,49 @@ async fn rate_limit_reset_success_updates_popup_beneath_overlay() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn account_change_dismisses_reset_popup_beneath_overlay() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.show_rate_limit_reset_loading_popup();
|
||||
let request_id = chat.show_rate_limit_reset_loading_popup();
|
||||
assert!(chat.finish_rate_limit_reset_credits_refresh(
|
||||
request_id,
|
||||
Vec::new(),
|
||||
Ok(reset_credits(/*available_count*/ 1)),
|
||||
));
|
||||
assert!(chat.show_rate_limit_reset_confirmation(
|
||||
request_id,
|
||||
Arc::new(AtomicBool::new(true)),
|
||||
/*credit_id*/ None,
|
||||
"Full reset".to_string(),
|
||||
/*reset_detail*/ None,
|
||||
"Reset your current usage limits.".to_string(),
|
||||
));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
let Ok(AppEvent::ConsumeRateLimitResetCredit {
|
||||
idempotency_key,
|
||||
credit_id: None,
|
||||
}) = rx.try_recv()
|
||||
else {
|
||||
panic!("expected reset consume event");
|
||||
};
|
||||
show_usage_test_overlay(&mut chat);
|
||||
|
||||
chat.update_account_state(
|
||||
/*status_account_display*/ None, /*plan_type*/ None,
|
||||
/*has_chatgpt_account*/ false, /*has_codex_backend_auth*/ false,
|
||||
);
|
||||
assert!(!chat.show_rate_limit_reset_confirmation(
|
||||
request_id,
|
||||
Arc::new(AtomicBool::new(true)),
|
||||
/*credit_id*/ None,
|
||||
"Stale reset".to_string(),
|
||||
/*reset_detail*/ None,
|
||||
"This stale confirmation should be ignored.".to_string(),
|
||||
));
|
||||
assert_eq!(
|
||||
chat.start_rate_limit_reset_consumption(&idempotency_key),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
chat.bottom_pane.active_view_id(),
|
||||
Some(TEST_OVERLAY_VIEW_ID)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditOutcome;
|
||||
use codex_app_server_protocol::ConsumeAccountRateLimitResetCreditResponse;
|
||||
use codex_app_server_protocol::RateLimitResetCreditsSummary;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::reset_credits::rate_limit_reset_scope;
|
||||
use super::reset_credits::reset_credit_options;
|
||||
use super::*;
|
||||
|
||||
const USAGE_MENU_VIEW_ID: &str = "usage-menu";
|
||||
const RATE_LIMIT_RESET_VIEW_ID: &str = "rate-limit-reset";
|
||||
const RATE_LIMIT_RESET_CONFIRMATION_VIEW_ID: &str = "rate-limit-reset-confirmation";
|
||||
|
||||
impl ChatWidget {
|
||||
pub(super) fn open_usage_menu(&mut self) {
|
||||
@@ -99,6 +102,8 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn show_rate_limit_reset_loading_popup(&mut self) -> u64 {
|
||||
self.clear_pending_rate_limit_reset_hint();
|
||||
self.pending_rate_limit_reset_idempotency_key = None;
|
||||
self.rate_limit_reset_picker_request_id = None;
|
||||
let request_id = self.take_next_rate_limit_reset_request_id();
|
||||
self.pending_rate_limit_reset_request_id = Some(request_id);
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
@@ -130,11 +135,13 @@ impl ChatWidget {
|
||||
self.on_rate_limit_snapshot(Some(snapshot));
|
||||
}
|
||||
|
||||
let mut shows_picker = false;
|
||||
let params = match result {
|
||||
Ok(response) => {
|
||||
let available_count = response.available_count;
|
||||
let params = if available_count > 0 {
|
||||
self.rate_limit_reset_confirmation_params(&response)
|
||||
shows_picker = true;
|
||||
self.rate_limit_reset_picker_params(request_id, &response)
|
||||
} else {
|
||||
Self::rate_limit_reset_message_params(
|
||||
"You don't have any usage limit resets available.",
|
||||
@@ -150,42 +157,54 @@ impl ChatWidget {
|
||||
let replaced = self
|
||||
.bottom_pane
|
||||
.replace_selection_view_if_present(RATE_LIMIT_RESET_VIEW_ID, params);
|
||||
self.rate_limit_reset_picker_request_id = (replaced && shows_picker).then_some(request_id);
|
||||
if replaced {
|
||||
self.request_redraw();
|
||||
}
|
||||
replaced
|
||||
}
|
||||
|
||||
fn rate_limit_reset_confirmation_params(
|
||||
fn rate_limit_reset_picker_params(
|
||||
&self,
|
||||
picker_request_id: u64,
|
||||
reset_credits: &RateLimitResetCreditsSummary,
|
||||
) -> SelectionViewParams {
|
||||
let scope = rate_limit_reset_scope(&self.rate_limit_snapshots_by_limit_id, self.plan_type);
|
||||
let mut items = vec![SelectionItem {
|
||||
let confirmation_gate = Arc::new(AtomicBool::new(true));
|
||||
let mut items = reset_credit_options(reset_credits)
|
||||
.into_iter()
|
||||
.map(|option| {
|
||||
let confirmation_gate = confirmation_gate.clone();
|
||||
let credit_id = option.credit_id;
|
||||
let reset_title = option.name.clone();
|
||||
let reset_detail = option.detail;
|
||||
let reset_description = option.description;
|
||||
let picker_description = reset_detail
|
||||
.clone()
|
||||
.unwrap_or_else(|| reset_description.clone());
|
||||
SelectionItem {
|
||||
name: option.name,
|
||||
description: Some(picker_description),
|
||||
actions: vec![Box::new(move |tx| {
|
||||
if confirmation_gate.swap(false, Ordering::AcqRel) {
|
||||
tx.send(AppEvent::OpenRateLimitResetConfirmation {
|
||||
picker_request_id,
|
||||
confirmation_gate: confirmation_gate.clone(),
|
||||
credit_id: credit_id.clone(),
|
||||
reset_title: reset_title.clone(),
|
||||
reset_detail: reset_detail.clone(),
|
||||
reset_description: reset_description.clone(),
|
||||
});
|
||||
}
|
||||
})],
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
items.push(SelectionItem {
|
||||
name: "Cancel".to_string(),
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
}];
|
||||
items.extend(
|
||||
reset_credit_options(reset_credits, scope)
|
||||
.into_iter()
|
||||
.map(|option| {
|
||||
let idempotency_key = Uuid::new_v4().to_string();
|
||||
let credit_id = option.credit_id;
|
||||
SelectionItem {
|
||||
name: option.name,
|
||||
description: Some(option.description),
|
||||
actions: vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::ConsumeRateLimitResetCredit {
|
||||
idempotency_key: idempotency_key.clone(),
|
||||
credit_id: credit_id.clone(),
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
title: Some("Usage limit resets".to_string()),
|
||||
@@ -201,6 +220,78 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn show_rate_limit_reset_confirmation(
|
||||
&mut self,
|
||||
picker_request_id: u64,
|
||||
confirmation_gate: Arc<AtomicBool>,
|
||||
credit_id: Option<String>,
|
||||
reset_title: String,
|
||||
reset_detail: Option<String>,
|
||||
reset_description: String,
|
||||
) -> bool {
|
||||
if self.rate_limit_reset_picker_request_id != Some(picker_request_id)
|
||||
|| self
|
||||
.bottom_pane
|
||||
.selected_index_for_active_view(RATE_LIMIT_RESET_VIEW_ID)
|
||||
.is_none()
|
||||
{
|
||||
confirmation_gate.store(true, Ordering::Release);
|
||||
return false;
|
||||
}
|
||||
let idempotency_key = Uuid::new_v4().to_string();
|
||||
self.pending_rate_limit_reset_idempotency_key = Some(idempotency_key.clone());
|
||||
let no_confirmation_gate = confirmation_gate.clone();
|
||||
let subtitle = reset_detail.map_or_else(
|
||||
|| reset_title.clone(),
|
||||
|reset_detail| format!("{reset_title} · {reset_detail}"),
|
||||
);
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_CONFIRMATION_VIEW_ID),
|
||||
title: Some("Use this reset?".to_string()),
|
||||
subtitle: Some(subtitle),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items: vec![
|
||||
SelectionItem {
|
||||
name: "Yes, use reset".to_string(),
|
||||
description: Some(reset_description),
|
||||
actions: vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::ConsumeRateLimitResetCredit {
|
||||
idempotency_key: idempotency_key.clone(),
|
||||
credit_id: credit_id.clone(),
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "No, go back".to_string(),
|
||||
description: Some("Choose a different reset.".to_string()),
|
||||
actions: vec![Box::new(move |_| {
|
||||
no_confirmation_gate.store(true, Ordering::Release);
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
initial_selected_idx: Some(1),
|
||||
on_cancel: Some(Box::new(move |_| {
|
||||
confirmation_gate.store(true, Ordering::Release);
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn start_rate_limit_reset_consumption(
|
||||
&mut self,
|
||||
idempotency_key: &str,
|
||||
) -> Option<u64> {
|
||||
if self.pending_rate_limit_reset_idempotency_key.as_deref() != Some(idempotency_key) {
|
||||
return None;
|
||||
}
|
||||
Some(self.show_rate_limit_reset_consuming_popup())
|
||||
}
|
||||
|
||||
fn rate_limit_reset_message_params(message: &str) -> SelectionViewParams {
|
||||
SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
@@ -241,8 +332,14 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn show_rate_limit_reset_consuming_popup(&mut self) -> u64 {
|
||||
self.clear_pending_rate_limit_reset_hint();
|
||||
self.pending_rate_limit_reset_idempotency_key = None;
|
||||
self.rate_limit_reset_picker_request_id = None;
|
||||
let request_id = self.take_next_rate_limit_reset_request_id();
|
||||
self.pending_rate_limit_reset_request_id = Some(request_id);
|
||||
self.bottom_pane
|
||||
.dismiss_view_by_id(RATE_LIMIT_RESET_CONFIRMATION_VIEW_ID);
|
||||
self.bottom_pane
|
||||
.dismiss_view_by_id(RATE_LIMIT_RESET_VIEW_ID);
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
title: Some("Usage limit resets".to_string()),
|
||||
@@ -307,6 +404,7 @@ impl ChatWidget {
|
||||
}
|
||||
Err(_) => {
|
||||
self.pending_rate_limit_reset_request_id = None;
|
||||
self.pending_rate_limit_reset_idempotency_key = Some(idempotency_key.clone());
|
||||
self.replace_rate_limit_reset_popup(SelectionViewParams {
|
||||
view_id: Some(RATE_LIMIT_RESET_VIEW_ID),
|
||||
title: Some("Usage limit resets".to_string()),
|
||||
@@ -422,6 +520,8 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn clear_pending_rate_limit_reset_requests(&mut self) {
|
||||
self.pending_rate_limit_reset_request_id = None;
|
||||
self.pending_rate_limit_reset_idempotency_key = None;
|
||||
self.rate_limit_reset_picker_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();
|
||||
@@ -429,6 +529,8 @@ impl ChatWidget {
|
||||
self.bottom_pane.dismiss_view_by_id(USAGE_MENU_VIEW_ID);
|
||||
self.bottom_pane
|
||||
.dismiss_view_by_id(RATE_LIMIT_RESET_VIEW_ID);
|
||||
self.bottom_pane
|
||||
.dismiss_view_by_id(RATE_LIMIT_RESET_CONFIRMATION_VIEW_ID);
|
||||
}
|
||||
|
||||
pub(crate) fn clear_pending_rate_limit_reset_hint(&mut self) {
|
||||
|
||||
Reference in New Issue
Block a user