mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why Codex users can earn personal rate-limit reset credits, but the CLI does not currently provide a way to view or redeem them. The `/usage` command restored in #27925 is intended to be the entry point for usage-related actions, so reset redemption belongs there rather than in a separate dashed slash command. Depends on #28143 for the app-server and backend-client reset-credit APIs. ## What changed - Turn bare `/usage` into a menu with entries for token activity and earned rate-limit resets while preserving `/usage daily`, `/usage weekly`, and `/usage cumulative`. - Add loading, empty, confirmation, success, retry, and error states with a caller-generated UUID idempotency key reused across retries of the same logical reset. - Show an availability hint only for backend-classified rate-limit errors with credits available. - Hide the reset entry for workspace accounts. ## Validation - `just test -p codex-tui chatwidget::tests::usage` — 19 passed. - `just fix -p codex-tui` — passed. - `just fmt` — passed. - `cargo insta pending-snapshots` from `codex-rs/tui` — no pending snapshots. ## Examples <img width="1168" height="304" alt="image" src="https://github.com/user-attachments/assets/caa4c1e3-e996-494d-ae17-50b521f5dce8" /> <img width="908" height="260" alt="image" src="https://github.com/user-attachments/assets/e38a726b-77cc-4bd0-9ea8-9f3ad21c5768" /> ### Reset flow <img width="1509" height="312" alt="image" src="https://github.com/user-attachments/assets/d987013c-78a5-48a2-ad8d-c61ad267a327" /> <img width="585" height="190" alt="image" src="https://github.com/user-attachments/assets/de32be19-79b9-4a3e-8574-6f1c208c98ae" /> <img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/88a165cf-796d-4fdc-a7bc-ea89917573da" /> <img width="512" height="193" alt="image" src="https://github.com/user-attachments/assets/d2353998-5aa8-442e-a5f8-3a8a5b832753" />
145 lines
4.7 KiB
Rust
145 lines
4.7 KiB
Rust
//! Hook run lifecycle handling for `ChatWidget`.
|
|
//!
|
|
//! This module keeps active hook cells, hook timers, and hook completion output
|
|
//! together.
|
|
|
|
use super::*;
|
|
|
|
impl ChatWidget {
|
|
/// Drop transient live hook status without flushing it into history.
|
|
pub(super) fn clear_active_hook_cell(&mut self) {
|
|
if self.active_hook_cell.take().is_some() {
|
|
self.bump_active_cell_revision();
|
|
self.request_pending_usage_output_insertion();
|
|
}
|
|
}
|
|
|
|
pub(super) fn on_hook_started(&mut self, run: codex_app_server_protocol::HookRunSummary) {
|
|
self.record_visible_turn_activity();
|
|
self.flush_answer_stream_with_separator();
|
|
self.flush_completed_hook_output();
|
|
match self.active_hook_cell.as_mut() {
|
|
Some(cell) => {
|
|
cell.start_run(run);
|
|
self.bump_active_cell_revision();
|
|
}
|
|
None => {
|
|
self.active_hook_cell = Some(history_cell::new_active_hook_cell(
|
|
run,
|
|
self.config.animations,
|
|
));
|
|
self.bump_active_cell_revision();
|
|
}
|
|
}
|
|
self.request_redraw();
|
|
}
|
|
|
|
pub(super) fn on_hook_completed(
|
|
&mut self,
|
|
completed: codex_app_server_protocol::HookRunSummary,
|
|
) {
|
|
let completed_existing_run = self
|
|
.active_hook_cell
|
|
.as_mut()
|
|
.map(|cell| cell.complete_run(completed.clone()))
|
|
.unwrap_or(false);
|
|
if completed_existing_run {
|
|
self.bump_active_cell_revision();
|
|
} else {
|
|
match self.active_hook_cell.as_mut() {
|
|
Some(cell) => {
|
|
cell.add_completed_run(completed);
|
|
self.bump_active_cell_revision();
|
|
}
|
|
None => {
|
|
let cell =
|
|
history_cell::new_completed_hook_cell(completed, self.config.animations);
|
|
if !cell.is_empty() {
|
|
self.active_hook_cell = Some(cell);
|
|
self.bump_active_cell_revision();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.flush_completed_hook_output();
|
|
self.finish_active_hook_cell_if_idle();
|
|
self.request_redraw();
|
|
}
|
|
|
|
pub(super) fn flush_completed_hook_output(&mut self) {
|
|
let Some(completed_cell) = self
|
|
.active_hook_cell
|
|
.as_mut()
|
|
.and_then(HookCell::take_completed_persistent_runs)
|
|
else {
|
|
return;
|
|
};
|
|
let active_cell_is_empty = self
|
|
.active_hook_cell
|
|
.as_ref()
|
|
.is_some_and(HookCell::is_empty);
|
|
if active_cell_is_empty {
|
|
self.active_hook_cell = None;
|
|
}
|
|
self.bump_active_cell_revision();
|
|
self.transcript.needs_final_message_separator = true;
|
|
self.app_event_tx
|
|
.send(AppEvent::InsertHistoryCell(Box::new(completed_cell)));
|
|
self.request_pending_usage_output_insertion();
|
|
}
|
|
|
|
pub(super) fn finish_active_hook_cell_if_idle(&mut self) {
|
|
let Some(cell) = self.active_hook_cell.as_ref() else {
|
|
return;
|
|
};
|
|
if cell.is_empty() {
|
|
self.active_hook_cell = None;
|
|
self.bump_active_cell_revision();
|
|
self.request_pending_usage_output_insertion();
|
|
return;
|
|
}
|
|
if cell.should_flush()
|
|
&& let Some(cell) = self.active_hook_cell.take()
|
|
{
|
|
self.bump_active_cell_revision();
|
|
self.transcript.needs_final_message_separator = true;
|
|
self.app_event_tx
|
|
.send(AppEvent::InsertHistoryCell(Box::new(cell)));
|
|
self.request_pending_usage_output_insertion();
|
|
}
|
|
}
|
|
|
|
pub(super) fn update_due_hook_visibility(&mut self) {
|
|
let Some(cell) = self.active_hook_cell.as_mut() else {
|
|
return;
|
|
};
|
|
let now = Instant::now();
|
|
if cell.advance_time(now) {
|
|
self.bump_active_cell_revision();
|
|
}
|
|
self.finish_active_hook_cell_if_idle();
|
|
}
|
|
|
|
pub(super) fn schedule_hook_timer_if_needed(&self) {
|
|
if self.config.animations
|
|
&& self
|
|
.active_hook_cell
|
|
.as_ref()
|
|
.is_some_and(HookCell::has_visible_running_run)
|
|
{
|
|
self.frame_requester
|
|
.schedule_frame_in(Duration::from_millis(50));
|
|
}
|
|
|
|
let Some(deadline) = self
|
|
.active_hook_cell
|
|
.as_ref()
|
|
.and_then(HookCell::next_timer_deadline)
|
|
else {
|
|
return;
|
|
};
|
|
let delay = deadline.saturating_duration_since(Instant::now());
|
|
self.frame_requester.schedule_frame_in(delay);
|
|
}
|
|
}
|