diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 0702eff5..bbcc3df2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -128,7 +128,7 @@ impl RateLimitWarningState { { let threshold = RATE_LIMIT_WARNING_THRESHOLDS[self.weekly_index]; warnings.push(format!( - "Weekly usage exceeded {threshold:.0}% of the limit. Run /limits for detailed usage." + "Weekly usage exceeded {threshold:.0}% of the limit. Check /status to review usage." )); self.weekly_index += 1; } @@ -138,7 +138,7 @@ impl RateLimitWarningState { { let threshold = RATE_LIMIT_WARNING_THRESHOLDS[self.hourly_index]; warnings.push(format!( - "Hourly usage exceeded {threshold:.0}% of the limit. Run /limits for detailed usage." + "Hourly usage exceeded {threshold:.0}% of the limit. Check /status to review usage." )); self.hourly_index += 1; } @@ -996,9 +996,6 @@ impl ChatWidget { SlashCommand::Status => { self.add_status_output(); } - SlashCommand::Limits => { - self.add_limits_output(); - } SlashCommand::Mcp => { self.add_mcp_output(); } @@ -1355,15 +1352,6 @@ impl ChatWidget { self.request_redraw(); } - pub(crate) fn add_limits_output(&mut self) { - if let Some(snapshot) = &self.rate_limit_snapshot { - self.add_to_history(history_cell::new_limits_output(snapshot)); - } else { - self.add_to_history(history_cell::new_limits_unavailable()); - } - self.request_redraw(); - } - pub(crate) fn add_status_output(&mut self) { let default_usage; let usage_ref = if let Some(ti) = &self.token_info { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_placeholder.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_placeholder.snap deleted file mode 100644 index fa37b220..00000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_placeholder.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: tui/src/chatwidget/tests.rs -expression: visual ---- -[magenta]/limits[/] - -[bold]Rate limit usage snapshot[/] -[dim] Tip: run `/limits` right after Codex replies for freshest numbers.[/] - Real usage data is not available yet. -[dim] Send a message to Codex, then run /limits again.[/] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_basic.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_basic.snap deleted file mode 100644 index ced44668..00000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_basic.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: tui/src/chatwidget/tests.rs -expression: visual ---- -[magenta]/limits[/] - -[bold]Rate limit usage snapshot[/] -[dim] Tip: run `/limits` right after Codex replies for freshest numbers.[/] - • Hourly limit[dim] (≈5 hours window)[/]: [dark-gray+bold]30.0% used[/] - • Weekly limit[dim] (≈1 week window)[/]: [dark-gray+bold]60.0% used[/] -[green] Within current limits[/] - -[dim] ╭─────────────────────────────────────────────────╮[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/][dim]│[/] - [dim]│[/][green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/][dim]│[/] - [dim]│[/][green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] -[dim] ╰─────────────────────────────────────────────────╯[/] - -[bold]Legend[/] - • [dark-gray+bold]Dark gray[/] = weekly usage so far - • [green+bold]Green[/] = hourly capacity still available - • [bold]Default[/] = weekly capacity beyond the hourly window diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_hourly_remaining.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_hourly_remaining.snap deleted file mode 100644 index defc5f21..00000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_hourly_remaining.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: tui/src/chatwidget/tests.rs -expression: visual ---- -[magenta]/limits[/] - -[bold]Rate limit usage snapshot[/] -[dim] Tip: run `/limits` right after Codex replies for freshest numbers.[/] - • Hourly limit[dim] (≈5 hours window)[/]: [dark-gray+bold]0.0% used[/] - • Weekly limit[dim] (≈1 week window)[/]: [dark-gray+bold]20.0% used[/] -[green] Within current limits[/] - -[dim] ╭─────────────────────────────────────────────────╮[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/][dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] -[dim] ╰─────────────────────────────────────────────────╯[/] - -[bold]Legend[/] - • [dark-gray+bold]Dark gray[/] = weekly usage so far - • [green+bold]Green[/] = hourly capacity still available - • [bold]Default[/] = weekly capacity beyond the hourly window diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_mixed_usage.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_mixed_usage.snap deleted file mode 100644 index 86c82d92..00000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_mixed_usage.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: tui/src/chatwidget/tests.rs -expression: visual ---- -[magenta]/limits[/] - -[bold]Rate limit usage snapshot[/] -[dim] Tip: run `/limits` right after Codex replies for freshest numbers.[/] - • Hourly limit[dim] (≈5 hours window)[/]: [dark-gray+bold]20.0% used[/] - • Weekly limit[dim] (≈1 week window)[/]: [dark-gray+bold]20.0% used[/] -[green] Within current limits[/] - -[dim] ╭─────────────────────────────────────────────────╮[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/] [dark-gray](>_)[/][dim]│[/] - [dim]│[/][green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] [green](>_)[/] (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] -[dim] ╰─────────────────────────────────────────────────╯[/] - -[bold]Legend[/] - • [dark-gray+bold]Dark gray[/] = weekly usage so far - • [green+bold]Green[/] = hourly capacity still available - • [bold]Default[/] = weekly capacity beyond the hourly window diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_weekly_heavy.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_weekly_heavy.snap deleted file mode 100644 index a1650545..00000000 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__limits_snapshot_weekly_heavy.snap +++ /dev/null @@ -1,29 +0,0 @@ ---- -source: tui/src/chatwidget/tests.rs -expression: visual ---- -[magenta]/limits[/] - -[bold]Rate limit usage snapshot[/] -[dim] Tip: run `/limits` right after Codex replies for freshest numbers.[/] - • Hourly limit[dim] (≈5 hours window)[/]: [dark-gray+bold]98.0% used[/] - • Weekly limit[dim] (≈1 week window)[/]: [dark-gray+bold]0.0% used[/] -[green] Within current limits[/] - -[dim] ╭─────────────────────────────────────────────────╮[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] - [dim]│[/](>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_) (>_)[dim]│[/] -[dim] ╰─────────────────────────────────────────────────╯[/] - -[bold]Legend[/] - • [dark-gray+bold]Dark gray[/] = weekly usage so far - • [green+bold]Green[/] = hourly capacity still available - • [bold]Default[/] = weekly capacity beyond the hourly window diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index c867155b..028d909b 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -25,7 +25,6 @@ use codex_core::protocol::InputMessageKind; use codex_core::protocol::Op; use codex_core::protocol::PatchApplyBeginEvent; use codex_core::protocol::PatchApplyEndEvent; -use codex_core::protocol::RateLimitSnapshotEvent; use codex_core::protocol::ReviewCodeLocation; use codex_core::protocol::ReviewFinding; use codex_core::protocol::ReviewLineRange; @@ -40,8 +39,6 @@ use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; use insta::assert_snapshot; use pretty_assertions::assert_eq; -use ratatui::style::Color; -use ratatui::style::Modifier; use std::fs::File; use std::io::BufRead; use std::io::BufReader; @@ -380,115 +377,6 @@ fn lines_to_single_string(lines: &[ratatui::text::Line<'static>]) -> String { s } -fn styled_lines_to_string(lines: &[ratatui::text::Line<'static>]) -> String { - let mut out = String::new(); - for line in lines { - for span in &line.spans { - let mut tags: Vec<&str> = Vec::new(); - if let Some(color) = span.style.fg { - let name = match color { - Color::Black => "black", - Color::Blue => "blue", - Color::Cyan => "cyan", - Color::DarkGray => "dark-gray", - Color::Gray => "gray", - Color::Green => "green", - Color::LightBlue => "light-blue", - Color::LightCyan => "light-cyan", - Color::LightGreen => "light-green", - Color::LightMagenta => "light-magenta", - Color::LightRed => "light-red", - Color::LightYellow => "light-yellow", - Color::Magenta => "magenta", - Color::Red => "red", - Color::Rgb(_, _, _) => "rgb", - Color::Indexed(_) => "indexed", - Color::Reset => "reset", - Color::Yellow => "yellow", - Color::White => "white", - }; - tags.push(name); - } - let modifiers = span.style.add_modifier; - if modifiers.contains(Modifier::BOLD) { - tags.push("bold"); - } - if modifiers.contains(Modifier::DIM) { - tags.push("dim"); - } - if modifiers.contains(Modifier::ITALIC) { - tags.push("italic"); - } - if modifiers.contains(Modifier::UNDERLINED) { - tags.push("underlined"); - } - if !tags.is_empty() { - out.push('['); - out.push_str(&tags.join("+")); - out.push(']'); - } - out.push_str(&span.content); - if !tags.is_empty() { - out.push_str("[/]"); - } - } - out.push('\n'); - } - out -} - -fn sample_rate_limit_snapshot( - primary_used_percent: f64, - weekly_used_percent: f64, - ratio_percent: f64, -) -> RateLimitSnapshotEvent { - RateLimitSnapshotEvent { - primary_used_percent, - weekly_used_percent, - primary_to_weekly_ratio_percent: ratio_percent, - primary_window_minutes: 300, - weekly_window_minutes: 10_080, - } -} - -fn capture_limits_snapshot(snapshot: Option) -> String { - let lines = match snapshot { - Some(ref snapshot) => history_cell::new_limits_output(snapshot).display_lines(80), - None => history_cell::new_limits_unavailable().display_lines(80), - }; - styled_lines_to_string(&lines) -} - -#[test] -fn limits_placeholder() { - let visual = capture_limits_snapshot(None); - assert_snapshot!(visual); -} - -#[test] -fn limits_snapshot_basic() { - let visual = capture_limits_snapshot(Some(sample_rate_limit_snapshot(30.0, 60.0, 40.0))); - assert_snapshot!(visual); -} - -#[test] -fn limits_snapshot_hourly_remaining() { - let visual = capture_limits_snapshot(Some(sample_rate_limit_snapshot(0.0, 20.0, 10.0))); - assert_snapshot!(visual); -} - -#[test] -fn limits_snapshot_mixed_usage() { - let visual = capture_limits_snapshot(Some(sample_rate_limit_snapshot(20.0, 20.0, 10.0))); - assert_snapshot!(visual); -} - -#[test] -fn limits_snapshot_weekly_heavy() { - let visual = capture_limits_snapshot(Some(sample_rate_limit_snapshot(98.0, 0.0, 10.0))); - assert_snapshot!(visual); -} - #[test] fn rate_limit_warnings_emit_thresholds() { let mut state = RateLimitWarningState::default(); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 89d3fd69..d4697218 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -2,9 +2,6 @@ use crate::diff_render::create_diff_summary; use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; use crate::markdown::append_markdown; -use crate::rate_limits_view::DEFAULT_GRID_CONFIG; -use crate::rate_limits_view::LimitsView; -use crate::rate_limits_view::build_limits_view; use crate::render::line_utils::line_to_static; use crate::render::line_utils::prefix_lines; use crate::render::line_utils::push_owned_lines; @@ -229,20 +226,6 @@ impl HistoryCell for PlainHistoryCell { } } -#[derive(Debug)] -pub(crate) struct LimitsHistoryCell { - display: LimitsView, -} - -impl HistoryCell for LimitsHistoryCell { - fn display_lines(&self, width: u16) -> Vec> { - let mut lines = self.display.summary_lines.clone(); - lines.extend(self.display.gauge_lines(width)); - lines.extend(self.display.legend_lines.clone()); - lines - } -} - #[derive(Debug)] pub(crate) struct TranscriptOnlyHistoryCell { lines: Vec>, @@ -1096,26 +1079,6 @@ pub(crate) fn new_completed_mcp_tool_call( Box::new(PlainHistoryCell { lines }) } -pub(crate) fn new_limits_output(snapshot: &RateLimitSnapshotEvent) -> LimitsHistoryCell { - LimitsHistoryCell { - display: build_limits_view(snapshot, DEFAULT_GRID_CONFIG), - } -} - -pub(crate) fn new_limits_unavailable() -> PlainHistoryCell { - PlainHistoryCell { - lines: vec![ - "/limits".magenta().into(), - "".into(), - vec!["Rate limit usage snapshot".bold()].into(), - vec![" Tip: run `/limits` right after Codex replies for freshest numbers.".dim()] - .into(), - vec![" Real usage data is not available yet.".into()].into(), - vec![" Send a message to Codex, then run /limits again.".dim()].into(), - ], - } -} - #[allow(clippy::disallowed_methods)] pub(crate) fn new_warning_event(message: String) -> PlainHistoryCell { PlainHistoryCell { diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 42295782..83aa2e23 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -55,7 +55,6 @@ mod markdown_stream; mod new_model_popup; pub mod onboarding; mod pager_overlay; -mod rate_limits_view; mod render; mod resume_picker; mod session_log; diff --git a/codex-rs/tui/src/rate_limits_view.rs b/codex-rs/tui/src/rate_limits_view.rs deleted file mode 100644 index 72fc6e97..00000000 --- a/codex-rs/tui/src/rate_limits_view.rs +++ /dev/null @@ -1,504 +0,0 @@ -use codex_core::protocol::RateLimitSnapshotEvent; -use ratatui::prelude::*; -use ratatui::style::Stylize; - -/// Aggregated output used by the `/limits` command. -/// It contains the rendered summary lines, optional legend, -/// and the precomputed gauge state when one can be shown. -#[derive(Debug)] -pub(crate) struct LimitsView { - pub(crate) summary_lines: Vec>, - pub(crate) legend_lines: Vec>, - grid_state: Option, - grid: GridConfig, -} - -impl LimitsView { - /// Render the gauge for the provided width if the data supports it. - pub(crate) fn gauge_lines(&self, width: u16) -> Vec> { - match self.grid_state { - Some(state) => render_limit_grid(state, self.grid, width), - None => Vec::new(), - } - } -} - -/// Configuration for the simple grid gauge rendered by `/limits`. -#[derive(Clone, Copy, Debug)] -pub(crate) struct GridConfig { - pub(crate) weekly_slots: usize, - pub(crate) logo: &'static str, -} - -/// Default gauge configuration used by the TUI. -pub(crate) const DEFAULT_GRID_CONFIG: GridConfig = GridConfig { - weekly_slots: 100, - logo: "(>_)", -}; - -/// Build the lines and optional gauge used by the `/limits` view. -pub(crate) fn build_limits_view( - snapshot: &RateLimitSnapshotEvent, - grid_config: GridConfig, -) -> LimitsView { - let metrics = RateLimitMetrics::from_snapshot(snapshot); - let grid_state = extract_capacity_fraction(snapshot) - .and_then(|fraction| compute_grid_state(&metrics, fraction)) - .map(|state| scale_grid_state(state, grid_config)); - - LimitsView { - summary_lines: build_summary_lines(&metrics), - legend_lines: build_legend_lines(grid_state.is_some()), - grid_state, - grid: grid_config, - } -} - -#[derive(Debug)] -struct RateLimitMetrics { - hourly_used: f64, - weekly_used: f64, - hourly_remaining: f64, - weekly_remaining: f64, - hourly_window_label: String, - weekly_window_label: String, - hourly_reset_hint: String, - weekly_reset_hint: String, -} - -impl RateLimitMetrics { - fn from_snapshot(snapshot: &RateLimitSnapshotEvent) -> Self { - let hourly_used = snapshot.primary_used_percent.clamp(0.0, 100.0); - let weekly_used = snapshot.weekly_used_percent.clamp(0.0, 100.0); - Self { - hourly_used, - weekly_used, - hourly_remaining: (100.0 - hourly_used).max(0.0), - weekly_remaining: (100.0 - weekly_used).max(0.0), - hourly_window_label: format_window_label(Some(snapshot.primary_window_minutes)), - weekly_window_label: format_window_label(Some(snapshot.weekly_window_minutes)), - hourly_reset_hint: format_reset_hint(Some(snapshot.primary_window_minutes)), - weekly_reset_hint: format_reset_hint(Some(snapshot.weekly_window_minutes)), - } - } - - fn hourly_exhausted(&self) -> bool { - self.hourly_remaining <= 0.0 - } - - fn weekly_exhausted(&self) -> bool { - self.weekly_remaining <= 0.0 - } -} - -fn format_window_label(minutes: Option) -> String { - approximate_duration(minutes) - .map(|(value, unit)| format!("≈{value} {} window", pluralize_unit(unit, value))) - .unwrap_or_else(|| "window unknown".to_string()) -} - -fn format_reset_hint(minutes: Option) -> String { - approximate_duration(minutes) - .map(|(value, unit)| format!("≈{value} {}", pluralize_unit(unit, value))) - .unwrap_or_else(|| "unknown".to_string()) -} - -fn approximate_duration(minutes: Option) -> Option<(u64, DurationUnit)> { - let minutes = minutes?; - if minutes == 0 { - return Some((1, DurationUnit::Minute)); - } - if minutes < 60 { - return Some((minutes, DurationUnit::Minute)); - } - if minutes < 1_440 { - let hours = ((minutes as f64) / 60.0).round().max(1.0) as u64; - return Some((hours, DurationUnit::Hour)); - } - let days = ((minutes as f64) / 1_440.0).round().max(1.0) as u64; - if days >= 7 { - let weeks = ((days as f64) / 7.0).round().max(1.0) as u64; - Some((weeks, DurationUnit::Week)) - } else { - Some((days, DurationUnit::Day)) - } -} - -fn pluralize_unit(unit: DurationUnit, value: u64) -> String { - match unit { - DurationUnit::Minute => { - if value == 1 { - "minute".to_string() - } else { - "minutes".to_string() - } - } - DurationUnit::Hour => { - if value == 1 { - "hour".to_string() - } else { - "hours".to_string() - } - } - DurationUnit::Day => { - if value == 1 { - "day".to_string() - } else { - "days".to_string() - } - } - DurationUnit::Week => { - if value == 1 { - "week".to_string() - } else { - "weeks".to_string() - } - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum DurationUnit { - Minute, - Hour, - Day, - Week, -} - -#[derive(Clone, Copy, Debug)] -struct GridState { - weekly_used_ratio: f64, - hourly_remaining_ratio: f64, -} - -fn build_summary_lines(metrics: &RateLimitMetrics) -> Vec> { - let mut lines: Vec> = vec![ - "/limits".magenta().into(), - "".into(), - vec!["Rate limit usage snapshot".bold()].into(), - vec![" Tip: run `/limits` right after Codex replies for freshest numbers.".dim()].into(), - build_usage_line( - " • Hourly limit", - &metrics.hourly_window_label, - metrics.hourly_used, - ), - build_usage_line( - " • Weekly limit", - &metrics.weekly_window_label, - metrics.weekly_used, - ), - ]; - lines.push(build_status_line(metrics)); - lines -} - -fn build_usage_line(label: &str, window_label: &str, used_percent: f64) -> Line<'static> { - Line::from(vec![ - label.to_string().into(), - format!(" ({window_label})").dim(), - ": ".into(), - format!("{used_percent:.1}% used").dark_gray().bold(), - ]) -} - -fn build_status_line(metrics: &RateLimitMetrics) -> Line<'static> { - let mut spans: Vec> = Vec::new(); - if metrics.weekly_exhausted() || metrics.hourly_exhausted() { - spans.push(" Rate limited: ".into()); - let reason = match (metrics.hourly_exhausted(), metrics.weekly_exhausted()) { - (true, true) => "weekly and hourly windows exhausted", - (true, false) => "hourly window exhausted", - (false, true) => "weekly window exhausted", - (false, false) => unreachable!(), - }; - spans.push(reason.red()); - if metrics.hourly_exhausted() { - spans.push(" — hourly resets in ".into()); - spans.push(metrics.hourly_reset_hint.clone().dim()); - } - if metrics.weekly_exhausted() { - spans.push(" — weekly resets in ".into()); - spans.push(metrics.weekly_reset_hint.clone().dim()); - } - } else { - spans.push(" Within current limits".green()); - } - Line::from(spans) -} - -fn build_legend_lines(show_gauge: bool) -> Vec> { - if !show_gauge { - return Vec::new(); - } - vec![ - vec!["Legend".bold()].into(), - vec![ - " • ".into(), - "Dark gray".dark_gray().bold(), - " = weekly usage so far".into(), - ] - .into(), - vec![ - " • ".into(), - "Green".green().bold(), - " = hourly capacity still available".into(), - ] - .into(), - vec![ - " • ".into(), - "Default".bold(), - " = weekly capacity beyond the hourly window".into(), - ] - .into(), - ] -} - -fn extract_capacity_fraction(snapshot: &RateLimitSnapshotEvent) -> Option { - let ratio = snapshot.primary_to_weekly_ratio_percent; - if ratio.is_finite() { - Some((ratio / 100.0).clamp(0.0, 1.0)) - } else { - None - } -} - -fn compute_grid_state(metrics: &RateLimitMetrics, capacity_fraction: f64) -> Option { - if capacity_fraction <= 0.0 { - return None; - } - - let weekly_used_ratio = (metrics.weekly_used / 100.0).clamp(0.0, 1.0); - let weekly_remaining_ratio = (1.0 - weekly_used_ratio).max(0.0); - - let hourly_used_ratio = (metrics.hourly_used / 100.0).clamp(0.0, 1.0); - let hourly_used_within_capacity = - (hourly_used_ratio * capacity_fraction).min(capacity_fraction); - let hourly_remaining_within_capacity = - (capacity_fraction - hourly_used_within_capacity).max(0.0); - - let hourly_remaining_ratio = hourly_remaining_within_capacity.min(weekly_remaining_ratio); - - Some(GridState { - weekly_used_ratio, - hourly_remaining_ratio, - }) -} - -fn scale_grid_state(state: GridState, grid: GridConfig) -> GridState { - if grid.weekly_slots == 0 { - return GridState { - weekly_used_ratio: 0.0, - hourly_remaining_ratio: 0.0, - }; - } - state -} - -/// Convert the grid state to rendered lines for the TUI. -fn render_limit_grid(state: GridState, grid_config: GridConfig, width: u16) -> Vec> { - GridLayout::new(grid_config, width) - .map(|layout| layout.render(state)) - .unwrap_or_default() -} - -/// Precomputed layout information for the usage grid. -struct GridLayout { - size: usize, - inner_width: usize, - config: GridConfig, -} - -impl GridLayout { - const MIN_SIDE: usize = 4; - const MAX_SIDE: usize = 12; - const PREFIX: &'static str = " "; - - fn new(config: GridConfig, width: u16) -> Option { - if config.weekly_slots == 0 || config.logo.is_empty() { - return None; - } - let cell_width = config.logo.chars().count(); - if cell_width == 0 { - return None; - } - - let available_inner = width.saturating_sub((Self::PREFIX.len() + 2) as u16) as usize; - if available_inner == 0 { - return None; - } - - let base_side = (config.weekly_slots as f64) - .sqrt() - .round() - .clamp(1.0, Self::MAX_SIDE as f64) as usize; - let width_limited_side = - ((available_inner + 1) / (cell_width + 1)).clamp(1, Self::MAX_SIDE); - - let mut side = base_side.min(width_limited_side); - if width_limited_side >= Self::MIN_SIDE { - side = side.max(Self::MIN_SIDE.min(width_limited_side)); - } - let side = side.clamp(1, Self::MAX_SIDE); - if side == 0 { - return None; - } - - let inner_width = side * cell_width + side.saturating_sub(1); - Some(Self { - size: side, - inner_width, - config, - }) - } - - /// Render the grid into styled lines for the history cell. - fn render(&self, state: GridState) -> Vec> { - let counts = self.cell_counts(state); - let mut lines = Vec::new(); - lines.push("".into()); - lines.push(self.render_border('╭', '╮')); - - let mut cell_index = 0isize; - for _ in 0..self.size { - let mut spans: Vec> = Vec::new(); - spans.push(Self::PREFIX.into()); - spans.push("│".dim()); - - for col in 0..self.size { - if col > 0 { - spans.push(" ".into()); - } - let span = if cell_index < counts.dark_cells { - self.config.logo.dark_gray() - } else if cell_index < counts.dark_cells + counts.green_cells { - self.config.logo.green() - } else { - self.config.logo.into() - }; - spans.push(span); - cell_index += 1; - } - - spans.push("│".dim()); - lines.push(Line::from(spans)); - } - - lines.push(self.render_border('╰', '╯')); - lines.push("".into()); - - if counts.white_cells == 0 { - lines.push(vec![" (No unused weekly capacity remaining)".dim()].into()); - lines.push("".into()); - } - - lines - } - - fn render_border(&self, left: char, right: char) -> Line<'static> { - let mut text = String::from(Self::PREFIX); - text.push(left); - text.push_str(&"─".repeat(self.inner_width)); - text.push(right); - vec![Span::from(text).dim()].into() - } - - /// Translate usage ratios into the number of coloured cells. - fn cell_counts(&self, state: GridState) -> GridCellCounts { - let total_cells = self.size * self.size; - let mut dark_cells = (state.weekly_used_ratio * total_cells as f64).round() as isize; - dark_cells = dark_cells.clamp(0, total_cells as isize); - let mut green_cells = (state.hourly_remaining_ratio * total_cells as f64).round() as isize; - if dark_cells + green_cells > total_cells as isize { - green_cells = (total_cells as isize - dark_cells).max(0); - } - let white_cells = (total_cells as isize - dark_cells - green_cells).max(0); - - GridCellCounts { - dark_cells, - green_cells, - white_cells, - } - } -} - -/// Number of weekly (dark), hourly (green) and unused (default) cells. -struct GridCellCounts { - dark_cells: isize, - green_cells: isize, - white_cells: isize, -} - -#[cfg(test)] -mod tests { - use super::*; - - fn snapshot() -> RateLimitSnapshotEvent { - RateLimitSnapshotEvent { - primary_used_percent: 30.0, - weekly_used_percent: 60.0, - primary_to_weekly_ratio_percent: 40.0, - primary_window_minutes: 300, - weekly_window_minutes: 10_080, - } - } - - #[test] - fn approximate_duration_handles_hours_and_weeks() { - assert_eq!( - approximate_duration(Some(299)), - Some((5, DurationUnit::Hour)) - ); - assert_eq!( - approximate_duration(Some(10_080)), - Some((1, DurationUnit::Week)) - ); - assert_eq!( - approximate_duration(Some(90)), - Some((2, DurationUnit::Hour)) - ); - } - - #[test] - fn build_display_constructs_summary_and_gauge() { - let display = build_limits_view(&snapshot(), DEFAULT_GRID_CONFIG); - assert!(display.summary_lines.iter().any(|line| { - line.spans - .iter() - .any(|span| span.content.contains("Weekly limit")) - })); - assert!(display.summary_lines.iter().any(|line| { - line.spans - .iter() - .any(|span| span.content.contains("Hourly limit")) - })); - assert!(!display.gauge_lines(80).is_empty()); - } - - #[test] - fn hourly_and_weekly_percentages_are_not_swapped() { - let display = build_limits_view(&snapshot(), DEFAULT_GRID_CONFIG); - let summary = display - .summary_lines - .iter() - .map(|line| { - line.spans - .iter() - .map(|span| span.content.as_ref()) - .collect::() - }) - .collect::>() - .join("\n"); - - assert!(summary.contains("Hourly limit (≈5 hours window): 30.0% used")); - assert!(summary.contains("Weekly limit (≈1 week window): 60.0% used")); - } - - #[test] - fn build_display_without_ratio_skips_gauge() { - let mut s = snapshot(); - s.primary_to_weekly_ratio_percent = f64::NAN; - let display = build_limits_view(&s, DEFAULT_GRID_CONFIG); - assert!(display.gauge_lines(80).is_empty()); - assert!(display.legend_lines.is_empty()); - } -} diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 6570cf68..f043d62f 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -21,7 +21,6 @@ pub enum SlashCommand { Diff, Mention, Status, - Limits, Mcp, Logout, Quit, @@ -41,7 +40,6 @@ impl SlashCommand { SlashCommand::Diff => "show git diff (including untracked files)", SlashCommand::Mention => "mention a file", SlashCommand::Status => "show current session configuration and token usage", - SlashCommand::Limits => "visualize weekly and hourly rate limits", SlashCommand::Model => "choose what model and reasoning effort to use", SlashCommand::Approvals => "choose what Codex can do without approval", SlashCommand::Mcp => "list configured MCP tools", @@ -70,7 +68,6 @@ impl SlashCommand { SlashCommand::Diff | SlashCommand::Mention | SlashCommand::Status - | SlashCommand::Limits | SlashCommand::Mcp | SlashCommand::Quit => true,