feat(tui/limits): show compact limit usage

This commit is contained in:
James Peter
2025-09-22 14:51:09 +10:00
parent 1a82571395
commit 06bcddfda5
3 changed files with 236 additions and 59 deletions
+12
View File
@@ -5206,9 +5206,21 @@ impl ChatWidget<'_> {
}
fn rate_limit_reset_info(&self) -> RateLimitResetInfo {
let auto_compact_limit = self
.config
.model_auto_compact_token_limit
.and_then(|limit| (limit > 0).then_some(limit as u64));
let session_tokens_used = if auto_compact_limit.is_some() {
Some(self.total_token_usage.total_tokens)
} else {
None
};
RateLimitResetInfo {
primary_last_reset: self.rate_limit_last_primary_reset_at,
weekly_last_reset: self.rate_limit_last_weekly_reset_at,
session_tokens_used,
auto_compact_limit,
}
}
+1 -6
View File
@@ -155,14 +155,9 @@ pub async fn run_main(
disable_response_storage: cli.oss.then_some(true),
show_raw_agent_reasoning: cli.oss.then_some(true),
debug: Some(cli.debug),
<<<<<<< HEAD
// Enable web search by default (no CLI flag).
tools_web_search_request: Some(true),
tools_web_search_request: Some(cli.web_search),
mcp_servers: None,
experimental_client_tools: None,
=======
tools_web_search_request: Some(cli.web_search),
>>>>>>> origin/main
};
// Parse `-c` overrides from the CLI.
+223 -53
View File
@@ -2,6 +2,7 @@ use crate::colors;
use chrono::{DateTime, Local, Utc};
use codex_common::elapsed::format_duration;
use codex_core::protocol::RateLimitSnapshotEvent;
use codex_protocol::num_format::format_with_separators;
use ratatui::prelude::*;
use ratatui::style::Stylize;
@@ -11,7 +12,8 @@ const UNUSED_CELL: &str = "░░";
const BAR_SLOTS: usize = 20;
const BAR_FILLED: &str = "▰";
const BAR_EMPTY: &str = "▱";
const SECTION_INDENT: &str = " ";
const SECTION_INDENT: &str = " ";
const FIELD_INDENT: &str = " ";
const CHART_INDENT: &str = " ";
/// Aggregated output used by the `/limits` command.
@@ -46,6 +48,8 @@ pub(crate) struct GridConfig {
pub(crate) struct RateLimitResetInfo {
pub(crate) primary_last_reset: Option<DateTime<Utc>>,
pub(crate) weekly_last_reset: Option<DateTime<Utc>>,
pub(crate) session_tokens_used: Option<u64>,
pub(crate) auto_compact_limit: Option<u64>,
}
/// Default gauge configuration used by the TUI.
@@ -163,6 +167,9 @@ fn build_summary_lines(
reset_info.weekly_last_reset,
));
lines.push("".into());
lines.extend(build_compact_lines(&reset_info));
lines.push("".into());
lines.push(section_header("Chart"));
lines.push("".into());
@@ -183,12 +190,12 @@ fn section_header(title: &str) -> Line<'static> {
fn build_bar_line(label: &str, percent: f64, suffix: &str, style: Style) -> Line<'static> {
let mut spans: Vec<Span<'static>> = Vec::new();
spans.push(Span::raw(field_prefix(label)));
spans.push(Span::styled(render_percent_bar(percent), style));
spans.extend(render_percent_bar(percent));
let mut text = format_percent(percent);
if !suffix.is_empty() {
text.push_str(suffix);
}
spans.push(Span::raw(format!(" {text}")));
spans.push(Span::styled(format!(" {text}"), style));
Line::from(spans)
}
@@ -199,20 +206,20 @@ fn build_hourly_window_line(metrics: &RateLimitMetrics) -> Line<'static> {
.primary_to_weekly_ratio_percent
.unwrap_or(0.0)
.clamp(0.0, 100.0);
spans.push(Span::styled(
render_percent_bar(ratio),
Style::default()
.fg(colors::info())
.add_modifier(Modifier::BOLD),
));
spans.extend(render_percent_bar(ratio));
let primary = format_minutes_short(metrics.primary_window_minutes);
let weekly = format_minutes_short(metrics.weekly_window_minutes);
let detail = metrics
.primary_to_weekly_ratio_percent
.map(|value| format!("{} ({primary} / {weekly})", format_percent(value)))
.unwrap_or_else(|| format!("({primary} / {weekly})"));
spans.push(Span::raw(format!(" {detail}")));
if let Some(value) = metrics.primary_to_weekly_ratio_percent {
spans.push(Span::styled(
format!(" {}", format_percent(value)),
Style::default().fg(colors::text()),
));
}
spans.push(Span::styled(
format!(" ({primary} / {weekly})"),
Style::default().fg(colors::dim()),
));
Line::from(spans)
}
@@ -220,16 +227,23 @@ fn build_hourly_reset_line(
window_minutes: u64,
last_reset: Option<DateTime<Utc>>,
) -> Line<'static> {
let text = if let (Some(last), true) = (last_reset, window_minutes > 0) {
if let (Some(last), true) = (last_reset, window_minutes > 0) {
if let Some((remaining, timestamp)) = compute_reset_eta(window_minutes, last) {
format!("{SECTION_INDENT}Resets in ≈{remaining} @ {timestamp}")
} else {
format!("{SECTION_INDENT}Reset timing updating…")
return Line::from(vec![
Span::raw(format!("{FIELD_INDENT}Resets at ≈{timestamp} ")),
Span::styled(
format!("({remaining})"),
Style::default().fg(colors::dim()),
),
]);
}
} else {
format!("{SECTION_INDENT}Reset shown once next window detected")
};
Line::from(vec![Span::raw(text)])
return Line::from(vec![Span::raw(format!(
"{FIELD_INDENT}Reset timing updating…"
))]);
}
Line::from(vec![Span::raw(format!(
"{FIELD_INDENT}Reset shown once next window detected"
))])
}
fn build_weekly_window_line(
@@ -242,23 +256,128 @@ fn build_weekly_window_line(
.map(format_duration)
.unwrap_or_else(|| "unknown".to_string());
let window = format_minutes_short(weekly_minutes);
Line::from(vec![Span::raw(prefix), Span::raw(format!("({since} / {window})"))])
Line::from(vec![
Span::raw(prefix),
Span::styled(
format!("({since} / {window})"),
Style::default().fg(colors::dim()),
),
])
}
fn build_weekly_reset_line(
window_minutes: u64,
last_reset: Option<DateTime<Utc>>,
) -> Line<'static> {
let text = if let (Some(last), true) = (last_reset, window_minutes > 0) {
if let (Some(last), true) = (last_reset, window_minutes > 0) {
if let Some((remaining, timestamp)) = compute_reset_eta(window_minutes, last) {
format!("{SECTION_INDENT}Resets in ≈{remaining} @ {timestamp}")
} else {
format!("{SECTION_INDENT}Reset timing updating…")
return Line::from(vec![
Span::raw(format!("{FIELD_INDENT}Resets at ≈{timestamp} ")),
Span::styled(
format!("({remaining})"),
Style::default().fg(colors::dim()),
),
]);
}
return Line::from(vec![Span::raw(format!(
"{FIELD_INDENT}Reset timing updating…"
))]);
}
Line::from(vec![Span::raw(format!(
"{FIELD_INDENT}Reset shown once next window detected"
))])
}
fn build_compact_lines(reset_info: &RateLimitResetInfo) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(section_header("Compact Limit"));
match reset_info.auto_compact_limit {
Some(limit) => {
if let Some(used) = reset_info.session_tokens_used {
lines.push(build_compact_tokens_line(used, limit));
lines.push(build_compact_status_line(used, limit));
} else {
lines.push(Line::from(vec![Span::styled(
format!("{FIELD_INDENT}Session usage updating…"),
Style::default().fg(colors::dim()),
)]));
}
}
None => {
lines.push(Line::from(vec![Span::styled(
format!("{FIELD_INDENT}Auto-compact threshold disabled"),
Style::default().fg(colors::dim()),
)]));
}
}
lines
}
fn build_compact_tokens_line(used: u64, limit: u64) -> Line<'static> {
let percent = if limit == 0 {
0.0
} else {
format!("{SECTION_INDENT}Reset shown once next window detected")
(used as f64 / limit as f64) * 100.0
};
Line::from(vec![Span::raw(text)])
let percent_display = if percent > 100.0 {
format!("{percent:.0}%")
} else {
format_percent(percent)
};
let mut spans: Vec<Span<'static>> = Vec::new();
spans.push(Span::raw(field_prefix("Tokens")));
spans.extend(render_percent_bar(percent));
spans.push(Span::styled(
format!(" {percent_display}"),
Style::default().fg(colors::text()),
));
let used_fmt = format_with_separators(used);
let limit_fmt = format_with_separators(limit);
spans.push(Span::styled(
format!(" ({used_fmt} / {limit_fmt})"),
Style::default().fg(colors::dim()),
));
Line::from(spans)
}
fn build_compact_status_line(used: u64, limit: u64) -> Line<'static> {
if used < limit {
let remaining = limit - used;
return Line::from(vec![
Span::raw(field_prefix("Remaining")),
Span::styled(
format!(
"{} tokens until compact",
format_with_separators(remaining)
),
Style::default().fg(colors::dim()),
),
]);
}
if used == limit {
return Line::from(vec![
Span::raw(field_prefix("Status")),
Span::styled(
"Auto-compact will trigger on the next turn".to_string(),
Style::default().fg(colors::warning()),
),
]);
}
let overage = used.saturating_sub(limit);
Line::from(vec![
Span::raw(field_prefix("Status")),
Span::styled(
format!("Exceeded by {} tokens", format_with_separators(overage)),
Style::default().fg(colors::error()),
),
])
}
fn compute_reset_eta(
@@ -320,34 +439,52 @@ fn build_legend_lines(show_gauge: bool) -> Vec<Line<'static>> {
let mut lines = Vec::new();
lines.push(Line::from(vec![
Span::raw(CHART_INDENT),
Span::styled(
CHART_INDENT.to_string(),
Style::default().fg(colors::dim()),
),
Span::styled(
WEEKLY_CELL.to_string(),
Style::default()
.fg(colors::text())
.add_modifier(Modifier::BOLD),
),
Span::raw(" weekly usage"),
Span::styled(
" weekly usage".to_string(),
Style::default().fg(colors::dim()),
),
]));
lines.push(Line::from(vec![
Span::raw(CHART_INDENT),
Span::styled(
CHART_INDENT.to_string(),
Style::default().fg(colors::dim()),
),
Span::styled(
HOURLY_CELL.to_string(),
Style::default()
.fg(colors::primary())
.add_modifier(Modifier::BOLD),
),
Span::styled(
" hourly headroom".to_string(),
Style::default().fg(colors::dim()),
),
]));
lines.push(Line::from(vec![
Span::styled(
CHART_INDENT.to_string(),
Style::default().fg(colors::dim()),
),
Span::styled(
UNUSED_CELL.to_string(),
Style::default()
.fg(colors::info())
.add_modifier(Modifier::BOLD),
),
Span::raw(" hourly headroom"),
]));
lines.push(Line::from(vec![
Span::raw(CHART_INDENT),
Span::styled(
UNUSED_CELL.to_string(),
Style::default()
.fg(colors::success())
.add_modifier(Modifier::BOLD),
" unused weekly".to_string(),
Style::default().fg(colors::dim()),
),
Span::raw(" unused weekly"),
]));
lines
}
@@ -356,10 +493,10 @@ fn field_prefix(label: &str) -> String {
let target_width = 7usize;
let padding = 2 + target_width.saturating_sub(label.len());
let spaces = " ".repeat(padding);
format!("{SECTION_INDENT}{label}:{spaces}")
format!("{FIELD_INDENT}{label}:{spaces}")
}
fn render_percent_bar(percent: f64) -> String {
fn render_percent_bar(percent: f64) -> Vec<Span<'static>> {
let clamped = percent.clamp(0.0, 100.0);
let mut filled = ((clamped / 100.0) * BAR_SLOTS as f64).round() as usize;
if clamped > 0.0 && filled == 0 {
@@ -367,14 +504,32 @@ fn render_percent_bar(percent: f64) -> String {
}
let filled = filled.min(BAR_SLOTS);
let empty = BAR_SLOTS.saturating_sub(filled);
let mut bar = String::with_capacity(BAR_SLOTS * BAR_FILLED.len());
for _ in 0..filled {
bar.push_str(BAR_FILLED);
let mut spans: Vec<Span<'static>> = Vec::new();
if filled > 0 {
spans.push(Span::styled(
BAR_FILLED.repeat(filled),
Style::default()
.fg(colors::primary())
.add_modifier(Modifier::BOLD),
));
}
for _ in 0..empty {
bar.push_str(BAR_EMPTY);
if empty > 0 {
spans.push(Span::styled(
BAR_EMPTY.repeat(empty),
Style::default()
.fg(colors::info())
.add_modifier(Modifier::BOLD),
));
}
bar
if spans.is_empty() {
spans.push(Span::styled(
BAR_EMPTY.repeat(BAR_SLOTS),
Style::default()
.fg(colors::info())
.add_modifier(Modifier::BOLD),
));
}
spans
}
fn format_percent(percent: f64) -> String {
@@ -394,9 +549,24 @@ fn format_minutes_short(minutes: u64) -> String {
let weeks = minutes / 10_080;
return format!("{weeks} {}", if weeks == 1 { "week" } else { "weeks" });
}
if minutes % 1_440 == 0 {
if minutes >= 1_440 {
let days = minutes / 1_440;
return format!("{days} {}", if days == 1 { "day" } else { "days" });
let remainder = minutes % 1_440;
if remainder == 0 {
return format!("{days} {}", if days == 1 { "day" } else { "days" });
}
let hours = remainder / 60;
if hours > 0 {
return format!("{days}d {hours}h");
}
let leftover_minutes = remainder % 60;
if leftover_minutes > 0 {
return format!("{days}d {leftover_minutes}m");
}
return format!("{days}d");
}
if minutes % 60 == 0 {
let hours = minutes / 60;
@@ -525,12 +695,12 @@ impl GridLayout {
} else if cell_index < counts.dark_cells + counts.green_cells {
Span::styled(
HOURLY_CELL.to_string(),
Style::default().fg(colors::info()),
Style::default().fg(colors::primary()),
)
} else {
Span::styled(
UNUSED_CELL.to_string(),
Style::default().fg(colors::success()),
Style::default().fg(colors::info()),
)
};
spans.push(span);