From 434eb4fd494a92174b2f916295f3a79ff0ad5fd9 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 22 Sep 2025 11:13:34 -0700 Subject: [PATCH] Add limits to /status (#4053) Add limits to status image --- codex-rs/tui/src/chatwidget.rs | 1 + codex-rs/tui/src/history_cell.rs | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b4204f3a..be27fbec 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1368,6 +1368,7 @@ impl ChatWidget { &self.config, usage_ref, &self.conversation_id, + self.rate_limit_snapshot.as_ref(), )); } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index c060715e..89d3fd69 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -58,6 +58,10 @@ use std::time::Instant; use tracing::error; use unicode_width::UnicodeWidthStr; +const STATUS_LIMIT_BAR_SEGMENTS: usize = 20; +const STATUS_LIMIT_BAR_FILLED: &str = "█"; +const STATUS_LIMIT_BAR_EMPTY: &str = " "; + #[derive(Clone, Debug)] pub(crate) struct CommandOutput { pub(crate) exit_code: i32, @@ -1123,6 +1127,7 @@ pub(crate) fn new_status_output( config: &Config, usage: &TokenUsage, session_id: &Option, + rate_limits: Option<&RateLimitSnapshotEvent>, ) -> PlainHistoryCell { let mut lines: Vec> = Vec::new(); lines.push("/status".magenta().into()); @@ -1283,6 +1288,9 @@ pub(crate) fn new_status_output( format_with_separators(usage.blended_total()).into(), ])); + lines.push("".into()); + lines.extend(build_status_limit_lines(rate_limits)); + PlainHistoryCell { lines } } @@ -1640,6 +1648,62 @@ fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> { invocation_spans.into() } +fn build_status_limit_lines(snapshot: Option<&RateLimitSnapshotEvent>) -> Vec> { + let mut lines: Vec> = + vec![vec![padded_emoji("⏱️").into(), "Usage Limits".bold()].into()]; + + match snapshot { + Some(snapshot) => { + let rows = [ + ("5h limit".to_string(), snapshot.primary_used_percent), + ("Weekly limit".to_string(), snapshot.weekly_used_percent), + ]; + let label_width = rows + .iter() + .map(|(label, _)| UnicodeWidthStr::width(label.as_str())) + .max() + .unwrap_or(0); + for (label, percent) in rows { + lines.push(build_status_limit_line(&label, percent, label_width)); + } + } + None => lines.push(" • Rate limit data not available yet.".dim().into()), + } + + lines +} + +fn build_status_limit_line(label: &str, percent_used: f64, label_width: usize) -> Line<'static> { + let clamped_percent = percent_used.clamp(0.0, 100.0); + let progress = render_status_limit_progress_bar(clamped_percent); + let summary = format_status_limit_summary(clamped_percent); + + let mut spans: Vec> = Vec::with_capacity(5); + let padded_label = format!("{label: String { + let ratio = (percent_used / 100.0).clamp(0.0, 1.0); + let filled = (ratio * STATUS_LIMIT_BAR_SEGMENTS as f64).round() as usize; + let filled = filled.min(STATUS_LIMIT_BAR_SEGMENTS); + let empty = STATUS_LIMIT_BAR_SEGMENTS.saturating_sub(filled); + format!( + "[{}{}]", + STATUS_LIMIT_BAR_FILLED.repeat(filled), + STATUS_LIMIT_BAR_EMPTY.repeat(empty) + ) +} + +fn format_status_limit_summary(percent_used: f64) -> String { + format!("{percent_used:.0}% used") +} + #[cfg(test)] mod tests { use super::*;