Skip to content

Commit c72b2ad

Browse files
authored
adding messaging for stale rate limits + when no rate limits are cached (#5570)
1 parent 80783a7 commit c72b2ad

File tree

6 files changed

+207
-41
lines changed

6 files changed

+207
-41
lines changed

codex-rs/tui/src/chatwidget.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,6 +1631,7 @@ impl ChatWidget {
16311631
context_usage,
16321632
&self.conversation_id,
16331633
self.rate_limit_snapshot.as_ref(),
1634+
Local::now(),
16341635
));
16351636
}
16361637

codex-rs/tui/src/status/card.rs

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use crate::history_cell::HistoryCell;
33
use crate::history_cell::PlainHistoryCell;
44
use crate::history_cell::with_border_with_inner_width;
55
use crate::version::CODEX_CLI_VERSION;
6+
use chrono::DateTime;
7+
use chrono::Local;
68
use codex_common::create_config_summary_entries;
79
use codex_core::config::Config;
810
use codex_core::protocol::SandboxPolicy;
@@ -25,6 +27,7 @@ use super::helpers::format_directory_display;
2527
use super::helpers::format_tokens_compact;
2628
use super::rate_limits::RateLimitSnapshotDisplay;
2729
use super::rate_limits::StatusRateLimitData;
30+
use super::rate_limits::StatusRateLimitRow;
2831
use super::rate_limits::compose_rate_limit_data;
2932
use super::rate_limits::format_status_limit_summary;
3033
use super::rate_limits::render_status_limit_progress_bar;
@@ -64,9 +67,17 @@ pub(crate) fn new_status_output(
6467
context_usage: Option<&TokenUsage>,
6568
session_id: &Option<ConversationId>,
6669
rate_limits: Option<&RateLimitSnapshotDisplay>,
70+
now: DateTime<Local>,
6771
) -> CompositeHistoryCell {
6872
let command = PlainHistoryCell::new(vec!["/status".magenta().into()]);
69-
let card = StatusHistoryCell::new(config, total_usage, context_usage, session_id, rate_limits);
73+
let card = StatusHistoryCell::new(
74+
config,
75+
total_usage,
76+
context_usage,
77+
session_id,
78+
rate_limits,
79+
now,
80+
);
7081

7182
CompositeHistoryCell::new(vec![Box::new(command), Box::new(card)])
7283
}
@@ -78,6 +89,7 @@ impl StatusHistoryCell {
7889
context_usage: Option<&TokenUsage>,
7990
session_id: &Option<ConversationId>,
8091
rate_limits: Option<&RateLimitSnapshotDisplay>,
92+
now: DateTime<Local>,
8193
) -> Self {
8294
let config_entries = create_config_summary_entries(config);
8395
let (model_name, model_details) = compose_model_display(config, &config_entries);
@@ -108,7 +120,7 @@ impl StatusHistoryCell {
108120
output: total_usage.output_tokens,
109121
context_window,
110122
};
111-
let rate_limits = compose_rate_limit_data(rate_limits);
123+
let rate_limits = compose_rate_limit_data(rate_limits, now);
112124

113125
Self {
114126
model_name,
@@ -171,47 +183,66 @@ impl StatusHistoryCell {
171183
];
172184
}
173185

174-
let mut lines = Vec::with_capacity(rows_data.len() * 2);
175-
176-
for row in rows_data {
177-
let value_spans = vec![
178-
Span::from(render_status_limit_progress_bar(row.percent_used)),
179-
Span::from(" "),
180-
Span::from(format_status_limit_summary(row.percent_used)),
181-
];
182-
let base_spans = formatter.full_spans(row.label.as_str(), value_spans);
183-
let base_line = Line::from(base_spans.clone());
184-
185-
if let Some(resets_at) = row.resets_at.as_ref() {
186-
let resets_span = Span::from(format!("(resets {resets_at})")).dim();
187-
let mut inline_spans = base_spans.clone();
188-
inline_spans.push(Span::from(" ").dim());
189-
inline_spans.push(resets_span.clone());
190-
191-
if line_display_width(&Line::from(inline_spans.clone()))
192-
<= available_inner_width
193-
{
194-
lines.push(Line::from(inline_spans));
195-
} else {
196-
lines.push(base_line);
197-
lines.push(formatter.continuation(vec![resets_span]));
198-
}
199-
} else {
200-
lines.push(base_line);
201-
}
202-
}
203-
186+
self.rate_limit_row_lines(rows_data, available_inner_width, formatter)
187+
}
188+
StatusRateLimitData::Stale(rows_data) => {
189+
let mut lines =
190+
self.rate_limit_row_lines(rows_data, available_inner_width, formatter);
191+
lines.push(formatter.line(
192+
"Warning",
193+
vec![Span::from("limits may be stale - start new turn to refresh.").dim()],
194+
));
204195
lines
205196
}
206197
StatusRateLimitData::Missing => {
207198
vec![formatter.line(
208199
"Limits",
209-
vec![Span::from("send a message to load usage data").dim()],
200+
vec![
201+
Span::from("visit ").dim(),
202+
"chatgpt.com/codex/settings/usage".cyan().underlined(),
203+
],
210204
)]
211205
}
212206
}
213207
}
214208

209+
fn rate_limit_row_lines(
210+
&self,
211+
rows: &[StatusRateLimitRow],
212+
available_inner_width: usize,
213+
formatter: &FieldFormatter,
214+
) -> Vec<Line<'static>> {
215+
let mut lines = Vec::with_capacity(rows.len().saturating_mul(2));
216+
217+
for row in rows {
218+
let value_spans = vec![
219+
Span::from(render_status_limit_progress_bar(row.percent_used)),
220+
Span::from(" "),
221+
Span::from(format_status_limit_summary(row.percent_used)),
222+
];
223+
let base_spans = formatter.full_spans(row.label.as_str(), value_spans);
224+
let base_line = Line::from(base_spans.clone());
225+
226+
if let Some(resets_at) = row.resets_at.as_ref() {
227+
let resets_span = Span::from(format!("(resets {resets_at})")).dim();
228+
let mut inline_spans = base_spans.clone();
229+
inline_spans.push(Span::from(" ").dim());
230+
inline_spans.push(resets_span.clone());
231+
232+
if line_display_width(&Line::from(inline_spans.clone())) <= available_inner_width {
233+
lines.push(Line::from(inline_spans));
234+
} else {
235+
lines.push(base_line);
236+
lines.push(formatter.continuation(vec![resets_span]));
237+
}
238+
} else {
239+
lines.push(base_line);
240+
}
241+
}
242+
243+
lines
244+
}
245+
215246
fn collect_rate_limit_labels(&self, seen: &mut BTreeSet<String>, labels: &mut Vec<String>) {
216247
match &self.rate_limits {
217248
StatusRateLimitData::Available(rows) => {
@@ -223,6 +254,12 @@ impl StatusHistoryCell {
223254
}
224255
}
225256
}
257+
StatusRateLimitData::Stale(rows) => {
258+
for row in rows {
259+
push_label(labels, seen, row.label.as_str());
260+
}
261+
push_label(labels, seen, "Warning");
262+
}
226263
StatusRateLimitData::Missing => push_label(labels, seen, "Limits"),
227264
}
228265
}

codex-rs/tui/src/status/rate_limits.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::chatwidget::get_limits_duration;
22

33
use super::helpers::format_reset_timestamp;
44
use chrono::DateTime;
5+
use chrono::Duration as ChronoDuration;
56
use chrono::Local;
67
use chrono::Utc;
78
use codex_core::protocol::RateLimitSnapshot;
@@ -21,9 +22,12 @@ pub(crate) struct StatusRateLimitRow {
2122
#[derive(Debug, Clone)]
2223
pub(crate) enum StatusRateLimitData {
2324
Available(Vec<StatusRateLimitRow>),
25+
Stale(Vec<StatusRateLimitRow>),
2426
Missing,
2527
}
2628

29+
pub(crate) const RATE_LIMIT_STALE_THRESHOLD_MINUTES: i64 = 15;
30+
2731
#[derive(Debug, Clone)]
2832
pub(crate) struct RateLimitWindowDisplay {
2933
pub used_percent: f64,
@@ -49,6 +53,7 @@ impl RateLimitWindowDisplay {
4953

5054
#[derive(Debug, Clone)]
5155
pub(crate) struct RateLimitSnapshotDisplay {
56+
pub captured_at: DateTime<Local>,
5257
pub primary: Option<RateLimitWindowDisplay>,
5358
pub secondary: Option<RateLimitWindowDisplay>,
5459
}
@@ -58,6 +63,7 @@ pub(crate) fn rate_limit_snapshot_display(
5863
captured_at: DateTime<Local>,
5964
) -> RateLimitSnapshotDisplay {
6065
RateLimitSnapshotDisplay {
66+
captured_at,
6167
primary: snapshot
6268
.primary
6369
.as_ref()
@@ -71,6 +77,7 @@ pub(crate) fn rate_limit_snapshot_display(
7177

7278
pub(crate) fn compose_rate_limit_data(
7379
snapshot: Option<&RateLimitSnapshotDisplay>,
80+
now: DateTime<Local>,
7481
) -> StatusRateLimitData {
7582
match snapshot {
7683
Some(snapshot) => {
@@ -102,8 +109,13 @@ pub(crate) fn compose_rate_limit_data(
102109
});
103110
}
104111

112+
let is_stale = now.signed_duration_since(snapshot.captured_at)
113+
> ChronoDuration::minutes(RATE_LIMIT_STALE_THRESHOLD_MINUTES);
114+
105115
if rows.is_empty() {
106116
StatusRateLimitData::Available(vec![])
117+
} else if is_stale {
118+
StatusRateLimitData::Stale(rows)
107119
} else {
108120
StatusRateLimitData::Available(rows)
109121
}

codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ expression: sanitized
1515
│ │
1616
│ Token usage: 750 total (500 input + 250 output) │
1717
│ Context window: 100% left (750 used / 272K) │
18-
│ Limits: send a message to load usage data
18+
│ Limits: visit chatgpt.com/codex/settings/usage │
1919
╰─────────────────────────────────────────────────────────────────╯
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
source: tui/src/status/tests.rs
3+
expression: sanitized
4+
---
5+
/status
6+
7+
╭─────────────────────────────────────────────────────────────────────╮
8+
>_ OpenAI Codex (v0.0.0) │
9+
│ │
10+
Model: gpt-5-codex (reasoning none, summaries auto) │
11+
Directory: [[workspace]] │
12+
Approval: on-request
13+
Sandbox: read-only
14+
Agents.md: <none> │
15+
│ │
16+
│ Token usage: 1.9K total (1K input + 900 output) │
17+
│ Context window: 100% left (2.1K used / 272K) │
18+
│ 5h limit: [███████████████░░░░░] 72% used (resets 03:14) │
19+
│ Weekly limit: [████████░░░░░░░░░░░░] 40% used (resets 03:34) │
20+
│ Warning: limits may be stale - start new turn to refresh. │
21+
╰─────────────────────────────────────────────────────────────────────╯

0 commit comments

Comments
 (0)