Tui: Rate limits (#3977)
### /limits: show rate limits graph <img width="442" height="287" alt="image" src="https://github.com/user-attachments/assets/3e29a241-a4b0-4df8-bf71-43dc4dd805ca" /> ### Warning on close to rate limits: <img width="507" height="96" alt="image" src="https://github.com/user-attachments/assets/732a958b-d240-4a89-8289-caa92de83537" /> Based on #3965
This commit is contained in:
@@ -25,6 +25,7 @@ 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;
|
||||
@@ -39,6 +40,8 @@ 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;
|
||||
@@ -320,6 +323,8 @@ fn make_chatwidget_manual() -> (
|
||||
session_header: SessionHeader::new(cfg.model.clone()),
|
||||
initial_user_message: None,
|
||||
token_info: None,
|
||||
rate_limit_snapshot: None,
|
||||
rate_limit_warnings: RateLimitWarningState::default(),
|
||||
stream: StreamController::new(cfg),
|
||||
running_commands: HashMap::new(),
|
||||
task_complete_pending: false,
|
||||
@@ -375,6 +380,158 @@ 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<RateLimitSnapshotEvent>) -> 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();
|
||||
let mut warnings: Vec<String> = Vec::new();
|
||||
|
||||
warnings.extend(state.take_warnings(10.0, 55.0));
|
||||
warnings.extend(state.take_warnings(55.0, 10.0));
|
||||
warnings.extend(state.take_warnings(10.0, 80.0));
|
||||
warnings.extend(state.take_warnings(80.0, 10.0));
|
||||
warnings.extend(state.take_warnings(10.0, 95.0));
|
||||
warnings.extend(state.take_warnings(95.0, 10.0));
|
||||
|
||||
assert_eq!(
|
||||
warnings.len(),
|
||||
6,
|
||||
"expected one warning per threshold per limit"
|
||||
);
|
||||
assert!(
|
||||
warnings
|
||||
.iter()
|
||||
.any(|w| w.contains("Hourly usage exceeded 50%")),
|
||||
"expected hourly 50% warning"
|
||||
);
|
||||
assert!(
|
||||
warnings
|
||||
.iter()
|
||||
.any(|w| w.contains("Weekly usage exceeded 50%")),
|
||||
"expected weekly 50% warning"
|
||||
);
|
||||
assert!(
|
||||
warnings
|
||||
.iter()
|
||||
.any(|w| w.contains("Hourly usage exceeded 90%")),
|
||||
"expected hourly 90% warning"
|
||||
);
|
||||
assert!(
|
||||
warnings
|
||||
.iter()
|
||||
.any(|w| w.contains("Weekly usage exceeded 90%")),
|
||||
"expected weekly 90% warning"
|
||||
);
|
||||
}
|
||||
|
||||
// (removed experimental resize snapshot test)
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user