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" />
150 lines
4.9 KiB
Rust
150 lines
4.9 KiB
Rust
//! Render composition for the main chat widget surface.
|
|
|
|
use super::*;
|
|
|
|
impl ChatWidget {
|
|
pub(super) fn as_renderable(&self) -> RenderableItem<'_> {
|
|
let active_cell_right_reserve = self.ambient_pet_wrap_reserved_cols();
|
|
let active_cell_renderable = match &self.transcript.active_cell {
|
|
Some(cell) => RenderableItem::Owned(Box::new(TranscriptAreaRenderable {
|
|
child: cell.as_ref(),
|
|
top: 1,
|
|
right: active_cell_right_reserve,
|
|
})),
|
|
None => RenderableItem::Owned(Box::new(())),
|
|
};
|
|
let active_hook_cell_renderable = match &self.active_hook_cell {
|
|
Some(cell) if cell.should_render() => {
|
|
RenderableItem::Owned(Box::new(TranscriptAreaRenderable {
|
|
child: cell,
|
|
top: 1,
|
|
right: active_cell_right_reserve,
|
|
}))
|
|
}
|
|
_ => RenderableItem::Owned(Box::new(())),
|
|
};
|
|
let mut flex = FlexRenderable::new();
|
|
flex.push(/*flex*/ 1, active_cell_renderable);
|
|
flex.push(/*flex*/ 0, active_hook_cell_renderable);
|
|
if let Some(cell) = self.pending_token_activity_output() {
|
|
flex.push(
|
|
/*flex*/ 1,
|
|
RenderableItem::Owned(Box::new(TranscriptAreaRenderable {
|
|
child: cell,
|
|
top: 1,
|
|
right: active_cell_right_reserve,
|
|
})),
|
|
);
|
|
}
|
|
if let Some(cell) = self.pending_rate_limit_reset_hint() {
|
|
flex.push(
|
|
/*flex*/ 1,
|
|
RenderableItem::Owned(Box::new(TranscriptAreaRenderable {
|
|
child: cell,
|
|
top: 1,
|
|
right: active_cell_right_reserve,
|
|
})),
|
|
);
|
|
}
|
|
flex.push(
|
|
/*flex*/ 0,
|
|
RenderableItem::Owned(Box::new(BottomPaneComposerReserveRenderable {
|
|
bottom_pane: &self.bottom_pane,
|
|
right_reserve: active_cell_right_reserve,
|
|
}))
|
|
.inset(Insets::tlbr(
|
|
/*top*/ 1, /*left*/ 0, /*bottom*/ 0, /*right*/ 0,
|
|
)),
|
|
);
|
|
RenderableItem::Owned(Box::new(flex))
|
|
}
|
|
}
|
|
|
|
struct BottomPaneComposerReserveRenderable<'a> {
|
|
bottom_pane: &'a BottomPane,
|
|
right_reserve: u16,
|
|
}
|
|
|
|
impl Renderable for BottomPaneComposerReserveRenderable<'_> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.bottom_pane
|
|
.render_with_composer_right_reserve(area, buf, self.right_reserve);
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.bottom_pane
|
|
.desired_height_with_composer_right_reserve(width, self.right_reserve)
|
|
}
|
|
|
|
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
|
self.bottom_pane
|
|
.cursor_pos_with_composer_right_reserve(area, self.right_reserve)
|
|
}
|
|
|
|
fn cursor_style(&self, area: Rect) -> crossterm::cursor::SetCursorStyle {
|
|
self.bottom_pane
|
|
.cursor_style_with_composer_right_reserve(area, self.right_reserve)
|
|
}
|
|
}
|
|
|
|
struct TranscriptAreaRenderable<'a> {
|
|
child: &'a dyn HistoryCell,
|
|
top: u16,
|
|
right: u16,
|
|
}
|
|
|
|
impl Renderable for TranscriptAreaRenderable<'_> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
let area = self.child_area(area);
|
|
let lines = self.child.display_lines(area.width);
|
|
let paragraph = Paragraph::new(Text::from(lines)).wrap(Wrap { trim: false });
|
|
let y = if area.height == 0 {
|
|
0
|
|
} else {
|
|
let overflow = paragraph
|
|
.line_count(area.width)
|
|
.saturating_sub(usize::from(area.height));
|
|
u16::try_from(overflow).unwrap_or(u16::MAX)
|
|
};
|
|
Clear.render(area, buf);
|
|
paragraph.scroll((y, 0)).render(area, buf);
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
let child_width = width.saturating_sub(self.right).max(1);
|
|
HistoryCell::desired_height(self.child, child_width) + self.top
|
|
}
|
|
}
|
|
|
|
impl TranscriptAreaRenderable<'_> {
|
|
fn child_area(&self, area: Rect) -> Rect {
|
|
let y = area.y.saturating_add(self.top);
|
|
let height = area.height.saturating_sub(self.top);
|
|
Rect::new(
|
|
area.x,
|
|
y,
|
|
area.width.saturating_sub(self.right).max(1),
|
|
height,
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Renderable for ChatWidget {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.as_renderable().render(area, buf);
|
|
self.last_rendered_width.set(Some(area.width as usize));
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.as_renderable().desired_height(width)
|
|
}
|
|
|
|
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
|
self.as_renderable().cursor_pos(area)
|
|
}
|
|
|
|
fn cursor_style(&self, area: Rect) -> crossterm::cursor::SetCursorStyle {
|
|
self.as_renderable().cursor_style(area)
|
|
}
|
|
}
|