Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use codex_core::protocol::McpToolCallBeginEvent;
use codex_core::protocol::McpToolCallEndEvent;
use codex_core::protocol::Op;
use codex_core::protocol::PatchApplyBeginEvent;
use codex_core::protocol::RateLimitSnapshotEvent;
use codex_core::protocol::ReviewRequest;
use codex_core::protocol::StreamErrorEvent;
use codex_core::protocol::TaskCompleteEvent;
Expand Down Expand Up @@ -103,6 +104,42 @@ struct RunningCommand {
parsed_cmd: Vec<ParsedCommand>,
}

const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [50.0, 75.0, 90.0];

#[derive(Default)]
struct RateLimitWarningState {
weekly_index: usize,
hourly_index: usize,
}

impl RateLimitWarningState {
fn take_warnings(&mut self, weekly_used_percent: f64, hourly_used_percent: f64) -> Vec<String> {
let mut warnings = Vec::new();

while self.weekly_index < RATE_LIMIT_WARNING_THRESHOLDS.len()
&& weekly_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.weekly_index]
{
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."
));
self.weekly_index += 1;
}

while self.hourly_index < RATE_LIMIT_WARNING_THRESHOLDS.len()
&& hourly_used_percent >= RATE_LIMIT_WARNING_THRESHOLDS[self.hourly_index]
{
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."
));
self.hourly_index += 1;
}

warnings
}
}

/// Common initialization parameters shared by all `ChatWidget` constructors.
pub(crate) struct ChatWidgetInit {
pub(crate) config: Config,
Expand All @@ -124,6 +161,8 @@ pub(crate) struct ChatWidget {
session_header: SessionHeader,
initial_user_message: Option<UserMessage>,
token_info: Option<TokenUsageInfo>,
rate_limit_snapshot: Option<RateLimitSnapshotEvent>,
rate_limit_warnings: RateLimitWarningState,
// Stream lifecycle controller
stream: StreamController,
running_commands: HashMap<String, RunningCommand>,
Expand Down Expand Up @@ -285,6 +324,21 @@ impl ChatWidget {
self.bottom_pane.set_token_usage(info.clone());
self.token_info = info;
}

fn on_rate_limit_snapshot(&mut self, snapshot: Option<RateLimitSnapshotEvent>) {
if let Some(snapshot) = snapshot {
let warnings = self
.rate_limit_warnings
.take_warnings(snapshot.weekly_used_percent, snapshot.primary_used_percent);
self.rate_limit_snapshot = Some(snapshot);
if !warnings.is_empty() {
for warning in warnings {
self.add_to_history(history_cell::new_warning_event(warning));
}
self.request_redraw();
}
}
}
/// Finalize any active exec as failed and stop/clear running UI state.
fn finalize_turn(&mut self) {
// Ensure any spinner is replaced by a red ✗ and flushed into history.
Expand Down Expand Up @@ -699,6 +753,8 @@ impl ChatWidget {
initial_images,
),
token_info: None,
rate_limit_snapshot: None,
rate_limit_warnings: RateLimitWarningState::default(),
stream: StreamController::new(config),
running_commands: HashMap::new(),
task_complete_pending: false,
Expand Down Expand Up @@ -756,6 +812,8 @@ impl ChatWidget {
initial_images,
),
token_info: None,
rate_limit_snapshot: None,
rate_limit_warnings: RateLimitWarningState::default(),
stream: StreamController::new(config),
running_commands: HashMap::new(),
task_complete_pending: false,
Expand Down Expand Up @@ -929,6 +987,9 @@ impl ChatWidget {
SlashCommand::Status => {
self.add_status_output();
}
SlashCommand::Limits => {
self.add_limits_output();
}
SlashCommand::Mcp => {
self.add_mcp_output();
}
Expand Down Expand Up @@ -1106,7 +1167,10 @@ impl ChatWidget {
EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
self.on_task_complete(last_agent_message)
}
EventMsg::TokenCount(ev) => self.set_token_info(ev.info),
EventMsg::TokenCount(ev) => {
self.set_token_info(ev.info);
self.on_rate_limit_snapshot(ev.rate_limits);
}
EventMsg::Error(ErrorEvent { message }) => self.on_error(message),
EventMsg::TurnAborted(ev) => match ev.reason {
TurnAbortReason::Interrupted => {
Expand Down Expand Up @@ -1282,6 +1346,15 @@ 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
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.[/]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
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
Loading
Loading