mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
## Why
[#25345](https://github.com/openai/codex/pull/25345) was approved,
green, and squash-merged into its stacked base branch,
`fcoury/tokenmaxxing-api`. Four minutes later, that base branch was
force-pushed back to an API-only rebased head while preparing
[#25344](https://github.com/openai/codex/pull/25344) for `main`. As a
result, the squash commit from #25345 was orphaned and the TUI command
never reached `main` or a release.
This PR relands the orphaned TUI change from
[`411410b8`](411410b85c)
on current `main`.
## What changed
- Add `/usage`, `/usage daily`, `/usage weekly`, and `/usage cumulative`
for account token activity.
- Fetch account usage asynchronously through the existing
`account/usage/read` app-server RPC.
- Render daily, weekly, and cumulative activity with theme-aware
terminal palettes and bounded transient cards.
- Preserve transcript ordering while assistant streams, history
consolidations, active cells, and hooks complete.
- Hide `/usage` from completion when backend auth is unavailable while
keeping typed-command guidance.
- Carry current-main behavior forward for cwd-aware Markdown parsing,
Windows Terminal color detection, and personal access token auth.
- Clear pending usage cards on thread rollback and delay completed cards
until live hook output is committed.
- Add focused regression and snapshot coverage for loading, auth errors,
invalid views, rollback, hook ordering, layout, and charts.
## Prior review
The original implementation was approved by Eric Traut in #25345 after
testing multiple themes and light/dark terminals. This PR preserves that
reviewed implementation while adapting it to current `main` and adding
regression coverage for newer rollback and hook lifecycle behavior.
## Validation
- `just test -p codex-tui token_activity palette renderable
usage_command` — 37 passed.
- Focused rollback, hook-ordering, and error snapshot tests — 4 passed.
- `just fix -p codex-tui` — passed.
- `UV_CACHE_DIR=/private/tmp/codex-uv-cache just fmt` — passed.
- `cargo insta pending-snapshots` — no pending snapshots.
- `just test -p codex-tui` — 2,870 passed; two unrelated guardian
feature-flag tests failed because their expected `OverrideTurnContext`
event was absent:
-
`update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`
-
`update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default`
- `just argument-comment-lint` could not complete because the local
Bazel LLVM `compiler-rt` repository is missing `include/sanitizer/*.h`.
The touched Rust diff was manually inspected and no missing
opaque-literal argument comments were found.
504 lines
15 KiB
Rust
504 lines
15 KiB
Rust
use std::sync::Arc;
|
|
|
|
use crossterm::cursor::SetCursorStyle;
|
|
use ratatui::buffer::Buffer;
|
|
use ratatui::layout::Rect;
|
|
use ratatui::text::Line;
|
|
use ratatui::text::Span;
|
|
use ratatui::widgets::Paragraph;
|
|
use ratatui::widgets::WidgetRef;
|
|
|
|
use crate::render::Insets;
|
|
use crate::render::RectExt as _;
|
|
|
|
pub trait Renderable {
|
|
fn render(&self, area: Rect, buf: &mut Buffer);
|
|
fn desired_height(&self, width: u16) -> u16;
|
|
fn cursor_pos(&self, _area: Rect) -> Option<(u16, u16)> {
|
|
None
|
|
}
|
|
fn cursor_style(&self, _area: Rect) -> SetCursorStyle {
|
|
SetCursorStyle::DefaultUserShape
|
|
}
|
|
}
|
|
|
|
pub enum RenderableItem<'a> {
|
|
Owned(Box<dyn Renderable + 'a>),
|
|
Borrowed(&'a dyn Renderable),
|
|
}
|
|
|
|
impl<'a> Renderable for RenderableItem<'a> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
match self {
|
|
RenderableItem::Owned(child) => child.render(area, buf),
|
|
RenderableItem::Borrowed(child) => child.render(area, buf),
|
|
}
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
match self {
|
|
RenderableItem::Owned(child) => child.desired_height(width),
|
|
RenderableItem::Borrowed(child) => child.desired_height(width),
|
|
}
|
|
}
|
|
|
|
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
|
match self {
|
|
RenderableItem::Owned(child) => child.cursor_pos(area),
|
|
RenderableItem::Borrowed(child) => child.cursor_pos(area),
|
|
}
|
|
}
|
|
|
|
fn cursor_style(&self, area: Rect) -> SetCursorStyle {
|
|
match self {
|
|
RenderableItem::Owned(child) => child.cursor_style(area),
|
|
RenderableItem::Borrowed(child) => child.cursor_style(area),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<Box<dyn Renderable + 'a>> for RenderableItem<'a> {
|
|
fn from(value: Box<dyn Renderable + 'a>) -> Self {
|
|
RenderableItem::Owned(value)
|
|
}
|
|
}
|
|
|
|
impl<'a, R> From<R> for Box<dyn Renderable + 'a>
|
|
where
|
|
R: Renderable + 'a,
|
|
{
|
|
fn from(value: R) -> Self {
|
|
Box::new(value)
|
|
}
|
|
}
|
|
|
|
impl Renderable for () {
|
|
fn render(&self, _area: Rect, _buf: &mut Buffer) {}
|
|
fn desired_height(&self, _width: u16) -> u16 {
|
|
0
|
|
}
|
|
}
|
|
|
|
impl Renderable for &str {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.render_ref(area, buf);
|
|
}
|
|
fn desired_height(&self, _width: u16) -> u16 {
|
|
1
|
|
}
|
|
}
|
|
|
|
impl Renderable for String {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.render_ref(area, buf);
|
|
}
|
|
fn desired_height(&self, _width: u16) -> u16 {
|
|
1
|
|
}
|
|
}
|
|
|
|
impl<'a> Renderable for Span<'a> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.render_ref(area, buf);
|
|
}
|
|
fn desired_height(&self, _width: u16) -> u16 {
|
|
1
|
|
}
|
|
}
|
|
|
|
impl<'a> Renderable for Line<'a> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
WidgetRef::render_ref(self, area, buf);
|
|
}
|
|
fn desired_height(&self, _width: u16) -> u16 {
|
|
1
|
|
}
|
|
}
|
|
|
|
impl<'a> Renderable for Paragraph<'a> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.render_ref(area, buf);
|
|
}
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.line_count(width) as u16
|
|
}
|
|
}
|
|
|
|
impl<R: Renderable> Renderable for Option<R> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
if let Some(renderable) = self {
|
|
renderable.render(area, buf);
|
|
}
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
if let Some(renderable) = self {
|
|
renderable.desired_height(width)
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
|
self.as_ref()
|
|
.and_then(|renderable| renderable.cursor_pos(area))
|
|
}
|
|
|
|
fn cursor_style(&self, area: Rect) -> SetCursorStyle {
|
|
self.as_ref()
|
|
.map_or(SetCursorStyle::DefaultUserShape, |renderable| {
|
|
renderable.cursor_style(area)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl<R: Renderable> Renderable for Arc<R> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.as_ref().render(area, buf);
|
|
}
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.as_ref().desired_height(width)
|
|
}
|
|
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
|
self.as_ref().cursor_pos(area)
|
|
}
|
|
fn cursor_style(&self, area: Rect) -> SetCursorStyle {
|
|
self.as_ref().cursor_style(area)
|
|
}
|
|
}
|
|
|
|
pub struct ColumnRenderable<'a> {
|
|
children: Vec<RenderableItem<'a>>,
|
|
}
|
|
|
|
impl Renderable for ColumnRenderable<'_> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
let mut y = area.y;
|
|
for child in &self.children {
|
|
let child_area = Rect::new(area.x, y, area.width, child.desired_height(area.width))
|
|
.intersection(area);
|
|
if !child_area.is_empty() {
|
|
child.render(child_area, buf);
|
|
}
|
|
y += child_area.height;
|
|
}
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.children
|
|
.iter()
|
|
.map(|child| child.desired_height(width))
|
|
.sum()
|
|
}
|
|
|
|
/// Returns the cursor position of the first child that has a cursor position, offset by the
|
|
/// child's position in the column.
|
|
///
|
|
/// It is generally assumed that either zero or one child will have a cursor position.
|
|
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
|
let mut y = area.y;
|
|
for child in &self.children {
|
|
let child_area = Rect::new(area.x, y, area.width, child.desired_height(area.width))
|
|
.intersection(area);
|
|
if !child_area.is_empty()
|
|
&& let Some((px, py)) = child.cursor_pos(child_area)
|
|
{
|
|
return Some((px, py));
|
|
}
|
|
y += child_area.height;
|
|
}
|
|
None
|
|
}
|
|
|
|
fn cursor_style(&self, area: Rect) -> SetCursorStyle {
|
|
let mut y = area.y;
|
|
for child in &self.children {
|
|
let child_area = Rect::new(area.x, y, area.width, child.desired_height(area.width))
|
|
.intersection(area);
|
|
if !child_area.is_empty() && child.cursor_pos(child_area).is_some() {
|
|
return child.cursor_style(child_area);
|
|
}
|
|
y += child_area.height;
|
|
}
|
|
SetCursorStyle::DefaultUserShape
|
|
}
|
|
}
|
|
|
|
impl<'a> ColumnRenderable<'a> {
|
|
pub fn new() -> Self {
|
|
Self { children: vec![] }
|
|
}
|
|
|
|
pub fn with<I, T>(children: I) -> Self
|
|
where
|
|
I: IntoIterator<Item = T>,
|
|
T: Into<RenderableItem<'a>>,
|
|
{
|
|
Self {
|
|
children: children.into_iter().map(Into::into).collect(),
|
|
}
|
|
}
|
|
|
|
pub fn push(&mut self, child: impl Into<Box<dyn Renderable + 'a>>) {
|
|
self.children.push(RenderableItem::Owned(child.into()));
|
|
}
|
|
}
|
|
|
|
pub struct FlexChild<'a> {
|
|
flex: i32,
|
|
child: RenderableItem<'a>,
|
|
}
|
|
|
|
pub struct FlexRenderable<'a> {
|
|
children: Vec<FlexChild<'a>>,
|
|
}
|
|
|
|
/// Lays out children in a column, with the ability to specify a flex factor for each child.
|
|
///
|
|
/// Children with flex factor > 0 will be allocated the remaining space after the non-flex children,
|
|
/// proportional to the flex factor.
|
|
impl<'a> FlexRenderable<'a> {
|
|
pub fn new() -> Self {
|
|
Self { children: vec![] }
|
|
}
|
|
|
|
pub fn push(&mut self, flex: i32, child: impl Into<RenderableItem<'a>>) {
|
|
self.children.push(FlexChild {
|
|
flex,
|
|
child: child.into(),
|
|
});
|
|
}
|
|
|
|
/// Loosely inspired by Flutter's Flex widget.
|
|
///
|
|
/// Ref https://github.com/flutter/flutter/blob/3fd81edbf1e015221e143c92b2664f4371bdc04a/packages/flutter/lib/src/rendering/flex.dart#L1205-L1209
|
|
fn allocate(&self, area: Rect) -> Vec<Rect> {
|
|
let mut allocated_rects = Vec::with_capacity(self.children.len());
|
|
let mut child_sizes = vec![0; self.children.len()];
|
|
let mut allocated_size = 0;
|
|
let mut flex_children = Vec::new();
|
|
|
|
// 1. Allocate space to non-flex children.
|
|
let max_size = area.height;
|
|
for (i, FlexChild { flex, child }) in self.children.iter().enumerate() {
|
|
if *flex > 0 {
|
|
flex_children.push((i, *flex as u16, child.desired_height(area.width)));
|
|
} else {
|
|
child_sizes[i] = child
|
|
.desired_height(area.width)
|
|
.min(max_size.saturating_sub(allocated_size));
|
|
allocated_size += child_sizes[i];
|
|
}
|
|
}
|
|
let free_space = max_size.saturating_sub(allocated_size);
|
|
// 2. Satisfy flex children that need less than their proportional share so their unused
|
|
// space can be redistributed instead of leaving blank rows.
|
|
let mut remaining_space = free_space;
|
|
while !flex_children.is_empty() {
|
|
let total_flex = flex_children.iter().map(|(_, flex, _)| *flex).sum::<u16>();
|
|
let mut satisfied_any = false;
|
|
flex_children.retain(|(i, flex, desired_height)| {
|
|
let proportional_share =
|
|
(u32::from(remaining_space) * u32::from(*flex) / u32::from(total_flex)) as u16;
|
|
if *desired_height <= proportional_share {
|
|
child_sizes[*i] = *desired_height;
|
|
remaining_space = remaining_space.saturating_sub(*desired_height);
|
|
satisfied_any = true;
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
if !satisfied_any {
|
|
break;
|
|
}
|
|
}
|
|
// 3. Divide the remaining space proportionally. The final child absorbs rounding slack.
|
|
let total_flex = flex_children.iter().map(|(_, flex, _)| *flex).sum::<u16>();
|
|
let mut allocated_flex_space = 0;
|
|
let last_flex_child_idx = flex_children.last().map(|(i, _, _)| *i);
|
|
for (i, flex, desired_height) in flex_children {
|
|
let max_child_extent = if Some(i) == last_flex_child_idx {
|
|
remaining_space.saturating_sub(allocated_flex_space)
|
|
} else {
|
|
(u32::from(remaining_space) * u32::from(flex) / u32::from(total_flex)) as u16
|
|
};
|
|
let child_size = desired_height.min(max_child_extent);
|
|
child_sizes[i] = child_size;
|
|
allocated_flex_space += child_size;
|
|
}
|
|
|
|
let mut y = area.y;
|
|
for size in child_sizes {
|
|
let child_area = Rect::new(area.x, y, area.width, size);
|
|
allocated_rects.push(child_area);
|
|
y += child_area.height;
|
|
}
|
|
allocated_rects
|
|
}
|
|
}
|
|
|
|
impl<'a> Renderable for FlexRenderable<'a> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.allocate(area)
|
|
.into_iter()
|
|
.zip(self.children.iter())
|
|
.for_each(|(rect, child)| {
|
|
child.child.render(rect, buf);
|
|
});
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.allocate(Rect::new(0, 0, width, u16::MAX))
|
|
.last()
|
|
.map(|rect| rect.bottom())
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
|
self.allocate(area)
|
|
.into_iter()
|
|
.zip(self.children.iter())
|
|
.find_map(|(rect, child)| child.child.cursor_pos(rect))
|
|
}
|
|
|
|
fn cursor_style(&self, area: Rect) -> SetCursorStyle {
|
|
self.allocate(area)
|
|
.into_iter()
|
|
.zip(self.children.iter())
|
|
.find_map(|(rect, child)| {
|
|
child
|
|
.child
|
|
.cursor_pos(rect)
|
|
.map(|_| child.child.cursor_style(rect))
|
|
})
|
|
.unwrap_or(SetCursorStyle::DefaultUserShape)
|
|
}
|
|
}
|
|
|
|
pub struct RowRenderable<'a> {
|
|
children: Vec<(u16, RenderableItem<'a>)>,
|
|
}
|
|
|
|
impl Renderable for RowRenderable<'_> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
let mut x = area.x;
|
|
for (width, child) in &self.children {
|
|
let available_width = area.width.saturating_sub(x - area.x);
|
|
let child_area = Rect::new(x, area.y, (*width).min(available_width), area.height);
|
|
if child_area.is_empty() {
|
|
break;
|
|
}
|
|
child.render(child_area, buf);
|
|
x = x.saturating_add(*width);
|
|
}
|
|
}
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
let mut max_height = 0;
|
|
let mut width_remaining = width;
|
|
for (child_width, child) in &self.children {
|
|
let w = (*child_width).min(width_remaining);
|
|
if w == 0 {
|
|
break;
|
|
}
|
|
let height = child.desired_height(w);
|
|
if height > max_height {
|
|
max_height = height;
|
|
}
|
|
width_remaining = width_remaining.saturating_sub(w);
|
|
}
|
|
max_height
|
|
}
|
|
|
|
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
|
let mut x = area.x;
|
|
for (width, child) in &self.children {
|
|
let available_width = area.width.saturating_sub(x - area.x);
|
|
let child_area = Rect::new(x, area.y, (*width).min(available_width), area.height);
|
|
if !child_area.is_empty()
|
|
&& let Some(pos) = child.cursor_pos(child_area)
|
|
{
|
|
return Some(pos);
|
|
}
|
|
x = x.saturating_add(*width);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn cursor_style(&self, area: Rect) -> SetCursorStyle {
|
|
let mut x = area.x;
|
|
for (width, child) in &self.children {
|
|
let available_width = area.width.saturating_sub(x - area.x);
|
|
let child_area = Rect::new(x, area.y, (*width).min(available_width), area.height);
|
|
if !child_area.is_empty() && child.cursor_pos(child_area).is_some() {
|
|
return child.cursor_style(child_area);
|
|
}
|
|
x = x.saturating_add(*width);
|
|
}
|
|
SetCursorStyle::DefaultUserShape
|
|
}
|
|
}
|
|
|
|
impl<'a> RowRenderable<'a> {
|
|
pub fn new() -> Self {
|
|
Self { children: vec![] }
|
|
}
|
|
|
|
pub fn push(&mut self, width: u16, child: impl Into<Box<dyn Renderable>>) {
|
|
self.children
|
|
.push((width, RenderableItem::Owned(child.into())));
|
|
}
|
|
}
|
|
|
|
pub struct InsetRenderable<'a> {
|
|
child: RenderableItem<'a>,
|
|
insets: Insets,
|
|
}
|
|
|
|
impl<'a> Renderable for InsetRenderable<'a> {
|
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
|
self.child.render(area.inset(self.insets), buf);
|
|
}
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.child
|
|
.desired_height(width - self.insets.left - self.insets.right)
|
|
+ self.insets.top
|
|
+ self.insets.bottom
|
|
}
|
|
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
|
self.child.cursor_pos(area.inset(self.insets))
|
|
}
|
|
|
|
fn cursor_style(&self, area: Rect) -> SetCursorStyle {
|
|
self.child.cursor_style(area.inset(self.insets))
|
|
}
|
|
}
|
|
|
|
impl<'a> InsetRenderable<'a> {
|
|
pub fn new(child: impl Into<RenderableItem<'a>>, insets: Insets) -> Self {
|
|
Self {
|
|
child: child.into(),
|
|
insets,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait RenderableExt<'a> {
|
|
fn inset(self, insets: Insets) -> RenderableItem<'a>;
|
|
}
|
|
|
|
impl<'a, R> RenderableExt<'a> for R
|
|
where
|
|
R: Renderable + 'a,
|
|
{
|
|
fn inset(self, insets: Insets) -> RenderableItem<'a> {
|
|
let child: RenderableItem<'a> =
|
|
RenderableItem::Owned(Box::new(self) as Box<dyn Renderable + 'a>);
|
|
RenderableItem::Owned(Box::new(InsetRenderable { child, insets }))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "renderable_tests.rs"]
|
|
mod tests;
|