Show context window usage while tasks run (#4536)
## Summary - show the remaining context window percentage in `/status` alongside existing token usage details - replace the composer shortcut prompt with the context window percentage (or an unavailable message) while a task is running - update TUI snapshots to reflect the new context window line ## Testing - cargo test -p codex-tui ------ https://chatgpt.com/codex/tasks/task_i_68dc6e7397ac8321909d7daff25a396c
This commit is contained in:
@@ -108,6 +108,7 @@ pub(crate) struct ChatComposer {
|
|||||||
custom_prompts: Vec<CustomPrompt>,
|
custom_prompts: Vec<CustomPrompt>,
|
||||||
footer_mode: FooterMode,
|
footer_mode: FooterMode,
|
||||||
footer_hint_override: Option<Vec<(String, String)>>,
|
footer_hint_override: Option<Vec<(String, String)>>,
|
||||||
|
context_window_percent: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Popup state – at most one can be visible at any time.
|
/// Popup state – at most one can be visible at any time.
|
||||||
@@ -150,6 +151,7 @@ impl ChatComposer {
|
|||||||
custom_prompts: Vec::new(),
|
custom_prompts: Vec::new(),
|
||||||
footer_mode: FooterMode::ShortcutPrompt,
|
footer_mode: FooterMode::ShortcutPrompt,
|
||||||
footer_hint_override: None,
|
footer_hint_override: None,
|
||||||
|
context_window_percent: None,
|
||||||
};
|
};
|
||||||
// Apply configuration via the setter to keep side-effects centralized.
|
// Apply configuration via the setter to keep side-effects centralized.
|
||||||
this.set_disable_paste_burst(disable_paste_burst);
|
this.set_disable_paste_burst(disable_paste_burst);
|
||||||
@@ -1317,6 +1319,7 @@ impl ChatComposer {
|
|||||||
esc_backtrack_hint: self.esc_backtrack_hint,
|
esc_backtrack_hint: self.esc_backtrack_hint,
|
||||||
use_shift_enter_hint: self.use_shift_enter_hint,
|
use_shift_enter_hint: self.use_shift_enter_hint,
|
||||||
is_task_running: self.is_task_running,
|
is_task_running: self.is_task_running,
|
||||||
|
context_window_percent: self.context_window_percent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1447,6 +1450,12 @@ impl ChatComposer {
|
|||||||
self.is_task_running = running;
|
self.is_task_running = running;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_context_window_percent(&mut self, percent: Option<u8>) {
|
||||||
|
if self.context_window_percent != percent {
|
||||||
|
self.context_window_percent = percent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) {
|
pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) {
|
||||||
self.esc_backtrack_hint = show;
|
self.esc_backtrack_hint = show;
|
||||||
if show {
|
if show {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use ratatui::buffer::Buffer;
|
|||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui::style::Stylize;
|
use ratatui::style::Stylize;
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
|
use ratatui::text::Span;
|
||||||
use ratatui::widgets::WidgetRef;
|
use ratatui::widgets::WidgetRef;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ pub(crate) struct FooterProps {
|
|||||||
pub(crate) esc_backtrack_hint: bool,
|
pub(crate) esc_backtrack_hint: bool,
|
||||||
pub(crate) use_shift_enter_hint: bool,
|
pub(crate) use_shift_enter_hint: bool,
|
||||||
pub(crate) is_task_running: bool,
|
pub(crate) is_task_running: bool,
|
||||||
|
pub(crate) context_window_percent: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -75,7 +77,13 @@ fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
|
|||||||
FooterMode::CtrlCReminder => vec![ctrl_c_reminder_line(CtrlCReminderState {
|
FooterMode::CtrlCReminder => vec![ctrl_c_reminder_line(CtrlCReminderState {
|
||||||
is_task_running: props.is_task_running,
|
is_task_running: props.is_task_running,
|
||||||
})],
|
})],
|
||||||
FooterMode::ShortcutPrompt => vec![dim_line(indent_text("? for shortcuts"))],
|
FooterMode::ShortcutPrompt => {
|
||||||
|
if props.is_task_running {
|
||||||
|
vec![context_window_line(props.context_window_percent)]
|
||||||
|
} else {
|
||||||
|
vec![dim_line(indent_text("? for shortcuts"))]
|
||||||
|
}
|
||||||
|
}
|
||||||
FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState {
|
FooterMode::ShortcutOverlay => shortcut_overlay_lines(ShortcutsState {
|
||||||
use_shift_enter_hint: props.use_shift_enter_hint,
|
use_shift_enter_hint: props.use_shift_enter_hint,
|
||||||
esc_backtrack_hint: props.esc_backtrack_hint,
|
esc_backtrack_hint: props.esc_backtrack_hint,
|
||||||
@@ -211,6 +219,21 @@ fn dim_line(text: String) -> Line<'static> {
|
|||||||
Line::from(text).dim()
|
Line::from(text).dim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn context_window_line(percent: Option<u8>) -> Line<'static> {
|
||||||
|
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||||
|
spans.push(indent_text("").into());
|
||||||
|
match percent {
|
||||||
|
Some(percent) => {
|
||||||
|
spans.push(format!("{percent}%").bold());
|
||||||
|
spans.push(" context left".dim());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
spans.push("? for shortcuts".dim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Line::from(spans)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
enum ShortcutId {
|
enum ShortcutId {
|
||||||
Commands,
|
Commands,
|
||||||
@@ -398,6 +421,7 @@ mod tests {
|
|||||||
esc_backtrack_hint: false,
|
esc_backtrack_hint: false,
|
||||||
use_shift_enter_hint: false,
|
use_shift_enter_hint: false,
|
||||||
is_task_running: false,
|
is_task_running: false,
|
||||||
|
context_window_percent: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -408,6 +432,7 @@ mod tests {
|
|||||||
esc_backtrack_hint: true,
|
esc_backtrack_hint: true,
|
||||||
use_shift_enter_hint: true,
|
use_shift_enter_hint: true,
|
||||||
is_task_running: false,
|
is_task_running: false,
|
||||||
|
context_window_percent: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -418,6 +443,7 @@ mod tests {
|
|||||||
esc_backtrack_hint: false,
|
esc_backtrack_hint: false,
|
||||||
use_shift_enter_hint: false,
|
use_shift_enter_hint: false,
|
||||||
is_task_running: false,
|
is_task_running: false,
|
||||||
|
context_window_percent: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -428,6 +454,7 @@ mod tests {
|
|||||||
esc_backtrack_hint: false,
|
esc_backtrack_hint: false,
|
||||||
use_shift_enter_hint: false,
|
use_shift_enter_hint: false,
|
||||||
is_task_running: true,
|
is_task_running: true,
|
||||||
|
context_window_percent: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -438,6 +465,7 @@ mod tests {
|
|||||||
esc_backtrack_hint: false,
|
esc_backtrack_hint: false,
|
||||||
use_shift_enter_hint: false,
|
use_shift_enter_hint: false,
|
||||||
is_task_running: false,
|
is_task_running: false,
|
||||||
|
context_window_percent: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -448,6 +476,18 @@ mod tests {
|
|||||||
esc_backtrack_hint: true,
|
esc_backtrack_hint: true,
|
||||||
use_shift_enter_hint: false,
|
use_shift_enter_hint: false,
|
||||||
is_task_running: false,
|
is_task_running: false,
|
||||||
|
context_window_percent: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
snapshot_footer(
|
||||||
|
"footer_shortcuts_context_running",
|
||||||
|
FooterProps {
|
||||||
|
mode: FooterMode::ShortcutPrompt,
|
||||||
|
esc_backtrack_hint: false,
|
||||||
|
use_shift_enter_hint: false,
|
||||||
|
is_task_running: true,
|
||||||
|
context_window_percent: Some(72),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ pub(crate) struct BottomPane {
|
|||||||
status: Option<StatusIndicatorWidget>,
|
status: Option<StatusIndicatorWidget>,
|
||||||
/// Queued user messages to show under the status indicator.
|
/// Queued user messages to show under the status indicator.
|
||||||
queued_user_messages: Vec<String>,
|
queued_user_messages: Vec<String>,
|
||||||
|
context_window_percent: Option<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct BottomPaneParams {
|
pub(crate) struct BottomPaneParams {
|
||||||
@@ -100,6 +101,7 @@ impl BottomPane {
|
|||||||
status: None,
|
status: None,
|
||||||
queued_user_messages: Vec::new(),
|
queued_user_messages: Vec::new(),
|
||||||
esc_backtrack_hint: false,
|
esc_backtrack_hint: false,
|
||||||
|
context_window_percent: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +343,16 @@ impl BottomPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_context_window_percent(&mut self, percent: Option<u8>) {
|
||||||
|
if self.context_window_percent == percent {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context_window_percent = percent;
|
||||||
|
self.composer.set_context_window_percent(percent);
|
||||||
|
self.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
/// Show a generic list selection view with the provided items.
|
/// Show a generic list selection view with the provided items.
|
||||||
pub(crate) fn show_selection_view(&mut self, params: list_selection_view::SelectionViewParams) {
|
pub(crate) fn show_selection_view(&mut self, params: list_selection_view::SelectionViewParams) {
|
||||||
let view = list_selection_view::ListSelectionView::new(params, self.app_event_tx.clone());
|
let view = list_selection_view::ListSelectionView::new(params, self.app_event_tx.clone());
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/bottom_pane/footer.rs
|
||||||
|
expression: terminal.backend()
|
||||||
|
---
|
||||||
|
" 72% context left "
|
||||||
@@ -396,8 +396,16 @@ impl ChatWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_token_info(&mut self, info: Option<TokenUsageInfo>) {
|
pub(crate) fn set_token_info(&mut self, info: Option<TokenUsageInfo>) {
|
||||||
if info.is_some() {
|
if let Some(info) = info {
|
||||||
self.token_info = info;
|
let context_window = info
|
||||||
|
.model_context_window
|
||||||
|
.or(self.config.model_context_window);
|
||||||
|
let percent = context_window.map(|window| {
|
||||||
|
info.last_token_usage
|
||||||
|
.percent_of_context_window_remaining(window)
|
||||||
|
});
|
||||||
|
self.bottom_pane.set_context_window_percent(percent);
|
||||||
|
self.token_info = Some(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1555,16 +1563,16 @@ impl ChatWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_status_output(&mut self) {
|
pub(crate) fn add_status_output(&mut self) {
|
||||||
let default_usage;
|
let default_usage = TokenUsage::default();
|
||||||
let usage_ref = if let Some(ti) = &self.token_info {
|
let (total_usage, context_usage) = if let Some(ti) = &self.token_info {
|
||||||
&ti.total_token_usage
|
(&ti.total_token_usage, Some(&ti.last_token_usage))
|
||||||
} else {
|
} else {
|
||||||
default_usage = TokenUsage::default();
|
(&default_usage, Some(&default_usage))
|
||||||
&default_usage
|
|
||||||
};
|
};
|
||||||
self.add_to_history(crate::status::new_status_output(
|
self.add_to_history(crate::status::new_status_output(
|
||||||
&self.config,
|
&self.config,
|
||||||
usage_ref,
|
total_usage,
|
||||||
|
context_usage,
|
||||||
&self.conversation_id,
|
&self.conversation_id,
|
||||||
self.rate_limit_snapshot.as_ref(),
|
self.rate_limit_snapshot.as_ref(),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: tui/src/chatwidget/tests.rs
|
source: tui/src/chatwidget/tests.rs
|
||||||
assertion_line: 1445
|
|
||||||
expression: terminal.backend()
|
expression: terminal.backend()
|
||||||
---
|
---
|
||||||
" "
|
" "
|
||||||
|
|||||||
@@ -29,11 +29,19 @@ use super::rate_limits::compose_rate_limit_data;
|
|||||||
use super::rate_limits::format_status_limit_summary;
|
use super::rate_limits::format_status_limit_summary;
|
||||||
use super::rate_limits::render_status_limit_progress_bar;
|
use super::rate_limits::render_status_limit_progress_bar;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct StatusContextWindowData {
|
||||||
|
percent_remaining: u8,
|
||||||
|
tokens_in_context: u64,
|
||||||
|
window: u64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct StatusTokenUsageData {
|
pub(crate) struct StatusTokenUsageData {
|
||||||
total: u64,
|
total: u64,
|
||||||
input: u64,
|
input: u64,
|
||||||
output: u64,
|
output: u64,
|
||||||
|
context_window: Option<StatusContextWindowData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -52,12 +60,13 @@ struct StatusHistoryCell {
|
|||||||
|
|
||||||
pub(crate) fn new_status_output(
|
pub(crate) fn new_status_output(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
usage: &TokenUsage,
|
total_usage: &TokenUsage,
|
||||||
|
context_usage: Option<&TokenUsage>,
|
||||||
session_id: &Option<ConversationId>,
|
session_id: &Option<ConversationId>,
|
||||||
rate_limits: Option<&RateLimitSnapshotDisplay>,
|
rate_limits: Option<&RateLimitSnapshotDisplay>,
|
||||||
) -> CompositeHistoryCell {
|
) -> CompositeHistoryCell {
|
||||||
let command = PlainHistoryCell::new(vec!["/status".magenta().into()]);
|
let command = PlainHistoryCell::new(vec!["/status".magenta().into()]);
|
||||||
let card = StatusHistoryCell::new(config, usage, session_id, rate_limits);
|
let card = StatusHistoryCell::new(config, total_usage, context_usage, session_id, rate_limits);
|
||||||
|
|
||||||
CompositeHistoryCell::new(vec![Box::new(command), Box::new(card)])
|
CompositeHistoryCell::new(vec![Box::new(command), Box::new(card)])
|
||||||
}
|
}
|
||||||
@@ -65,7 +74,8 @@ pub(crate) fn new_status_output(
|
|||||||
impl StatusHistoryCell {
|
impl StatusHistoryCell {
|
||||||
fn new(
|
fn new(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
usage: &TokenUsage,
|
total_usage: &TokenUsage,
|
||||||
|
context_usage: Option<&TokenUsage>,
|
||||||
session_id: &Option<ConversationId>,
|
session_id: &Option<ConversationId>,
|
||||||
rate_limits: Option<&RateLimitSnapshotDisplay>,
|
rate_limits: Option<&RateLimitSnapshotDisplay>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -84,10 +94,19 @@ impl StatusHistoryCell {
|
|||||||
let agents_summary = compose_agents_summary(config);
|
let agents_summary = compose_agents_summary(config);
|
||||||
let account = compose_account_display(config);
|
let account = compose_account_display(config);
|
||||||
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
|
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
|
||||||
|
let context_window = config.model_context_window.and_then(|window| {
|
||||||
|
context_usage.map(|usage| StatusContextWindowData {
|
||||||
|
percent_remaining: usage.percent_of_context_window_remaining(window),
|
||||||
|
tokens_in_context: usage.tokens_in_context_window(),
|
||||||
|
window,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
let token_usage = StatusTokenUsageData {
|
let token_usage = StatusTokenUsageData {
|
||||||
total: usage.blended_total(),
|
total: total_usage.blended_total(),
|
||||||
input: usage.non_cached_input(),
|
input: total_usage.non_cached_input(),
|
||||||
output: usage.output_tokens,
|
output: total_usage.output_tokens,
|
||||||
|
context_window,
|
||||||
};
|
};
|
||||||
let rate_limits = compose_rate_limit_data(rate_limits);
|
let rate_limits = compose_rate_limit_data(rate_limits);
|
||||||
|
|
||||||
@@ -123,6 +142,22 @@ impl StatusHistoryCell {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn context_window_spans(&self) -> Option<Vec<Span<'static>>> {
|
||||||
|
let context = self.token_usage.context_window.as_ref()?;
|
||||||
|
let percent = context.percent_remaining;
|
||||||
|
let used_fmt = format_tokens_compact(context.tokens_in_context);
|
||||||
|
let window_fmt = format_tokens_compact(context.window);
|
||||||
|
|
||||||
|
Some(vec![
|
||||||
|
Span::from(format!("{percent}% left")),
|
||||||
|
Span::from(" (").dim(),
|
||||||
|
Span::from(used_fmt).dim(),
|
||||||
|
Span::from(" / ").dim(),
|
||||||
|
Span::from(window_fmt).dim(),
|
||||||
|
Span::from(")").dim(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
fn rate_limit_lines(
|
fn rate_limit_lines(
|
||||||
&self,
|
&self,
|
||||||
available_inner_width: usize,
|
available_inner_width: usize,
|
||||||
@@ -235,6 +270,9 @@ impl HistoryCell for StatusHistoryCell {
|
|||||||
push_label(&mut labels, &mut seen, "Session");
|
push_label(&mut labels, &mut seen, "Session");
|
||||||
}
|
}
|
||||||
push_label(&mut labels, &mut seen, "Token usage");
|
push_label(&mut labels, &mut seen, "Token usage");
|
||||||
|
if self.token_usage.context_window.is_some() {
|
||||||
|
push_label(&mut labels, &mut seen, "Context window");
|
||||||
|
}
|
||||||
self.collect_rate_limit_labels(&mut seen, &mut labels);
|
self.collect_rate_limit_labels(&mut seen, &mut labels);
|
||||||
|
|
||||||
let formatter = FieldFormatter::from_labels(labels.iter().map(String::as_str));
|
let formatter = FieldFormatter::from_labels(labels.iter().map(String::as_str));
|
||||||
@@ -266,6 +304,10 @@ impl HistoryCell for StatusHistoryCell {
|
|||||||
lines.push(Line::from(Vec::<Span<'static>>::new()));
|
lines.push(Line::from(Vec::<Span<'static>>::new()));
|
||||||
lines.push(formatter.line("Token usage", self.token_usage_spans()));
|
lines.push(formatter.line("Token usage", self.token_usage_spans()));
|
||||||
|
|
||||||
|
if let Some(spans) = self.context_window_spans() {
|
||||||
|
lines.push(formatter.line("Context window", spans));
|
||||||
|
}
|
||||||
|
|
||||||
lines.extend(self.rate_limit_lines(available_inner_width, &formatter));
|
lines.extend(self.rate_limit_lines(available_inner_width, &formatter));
|
||||||
|
|
||||||
let content_width = lines.iter().map(line_display_width).max().unwrap_or(0);
|
let content_width = lines.iter().map(line_display_width).max().unwrap_or(0);
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ expression: sanitized
|
|||||||
---
|
---
|
||||||
/status
|
/status
|
||||||
|
|
||||||
╭───────────────────────────────────────────────────────────────────────────╮
|
╭────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ >_ OpenAI Codex (v0.0.0) │
|
│ >_ OpenAI Codex (v0.0.0) │
|
||||||
│ │
|
│ │
|
||||||
│ Model: gpt-5-codex (reasoning none, summaries auto) │
|
│ Model: gpt-5-codex (reasoning none, summaries auto) │
|
||||||
│ Directory: [[workspace]] │
|
│ Directory: [[workspace]] │
|
||||||
│ Approval: on-request │
|
│ Approval: on-request │
|
||||||
│ Sandbox: read-only │
|
│ Sandbox: read-only │
|
||||||
│ Agents.md: <none> │
|
│ Agents.md: <none> │
|
||||||
│ │
|
│ │
|
||||||
│ Token usage: 1.2K total (800 input + 400 output) │
|
│ Token usage: 1.2K total (800 input + 400 output) │
|
||||||
│ Monthly limit: [██░░░░░░░░░░░░░░░░░░] 12% used (resets 07:08 on 7 May) │
|
│ Context window: 100% left (1.2K / 272K) │
|
||||||
╰───────────────────────────────────────────────────────────────────────────╯
|
│ Monthly limit: [██░░░░░░░░░░░░░░░░░░] 12% used (resets 07:08 on 7 May) │
|
||||||
|
╰────────────────────────────────────────────────────────────────────────────╯
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ expression: sanitized
|
|||||||
---
|
---
|
||||||
/status
|
/status
|
||||||
|
|
||||||
╭───────────────────────────────────────────────────────────────────╮
|
╭─────────────────────────────────────────────────────────────────────╮
|
||||||
│ >_ OpenAI Codex (v0.0.0) │
|
│ >_ OpenAI Codex (v0.0.0) │
|
||||||
│ │
|
│ │
|
||||||
│ Model: gpt-5-codex (reasoning high, summaries detailed) │
|
│ Model: gpt-5-codex (reasoning high, summaries detailed) │
|
||||||
│ Directory: [[workspace]] │
|
│ Directory: [[workspace]] │
|
||||||
│ Approval: on-request │
|
│ Approval: on-request │
|
||||||
│ Sandbox: workspace-write │
|
│ Sandbox: workspace-write │
|
||||||
│ Agents.md: <none> │
|
│ Agents.md: <none> │
|
||||||
│ │
|
│ │
|
||||||
│ Token usage: 1.9K total (1K input + 900 output) │
|
│ Token usage: 1.9K total (1K input + 900 output) │
|
||||||
│ 5h limit: [███████████████░░░░░] 72% used (resets 03:14) │
|
│ Context window: 100% left (2.1K / 272K) │
|
||||||
│ Weekly limit: [█████████░░░░░░░░░░░] 45% used (resets 03:24) │
|
│ 5h limit: [███████████████░░░░░] 72% used (resets 03:14) │
|
||||||
╰───────────────────────────────────────────────────────────────────╯
|
│ Weekly limit: [█████████░░░░░░░░░░░] 45% used (resets 03:24) │
|
||||||
|
╰─────────────────────────────────────────────────────────────────────╯
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ expression: sanitized
|
|||||||
---
|
---
|
||||||
/status
|
/status
|
||||||
|
|
||||||
╭──────────────────────────────────────────────────────────────╮
|
╭─────────────────────────────────────────────────────────────────╮
|
||||||
│ >_ OpenAI Codex (v0.0.0) │
|
│ >_ OpenAI Codex (v0.0.0) │
|
||||||
│ │
|
│ │
|
||||||
│ Model: gpt-5-codex (reasoning none, summaries auto) │
|
│ Model: gpt-5-codex (reasoning none, summaries auto) │
|
||||||
│ Directory: [[workspace]] │
|
│ Directory: [[workspace]] │
|
||||||
│ Approval: on-request │
|
│ Approval: on-request │
|
||||||
│ Sandbox: read-only │
|
│ Sandbox: read-only │
|
||||||
│ Agents.md: <none> │
|
│ Agents.md: <none> │
|
||||||
│ │
|
│ │
|
||||||
│ Token usage: 750 total (500 input + 250 output) │
|
│ Token usage: 750 total (500 input + 250 output) │
|
||||||
│ Limits: data not available yet │
|
│ Context window: 100% left (750 / 272K) │
|
||||||
╰──────────────────────────────────────────────────────────────╯
|
│ Limits: data not available yet │
|
||||||
|
╰─────────────────────────────────────────────────────────────────╯
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ expression: sanitized
|
|||||||
---
|
---
|
||||||
/status
|
/status
|
||||||
|
|
||||||
╭──────────────────────────────────────────────────────────────╮
|
╭─────────────────────────────────────────────────────────────────╮
|
||||||
│ >_ OpenAI Codex (v0.0.0) │
|
│ >_ OpenAI Codex (v0.0.0) │
|
||||||
│ │
|
│ │
|
||||||
│ Model: gpt-5-codex (reasoning none, summaries auto) │
|
│ Model: gpt-5-codex (reasoning none, summaries auto) │
|
||||||
│ Directory: [[workspace]] │
|
│ Directory: [[workspace]] │
|
||||||
│ Approval: on-request │
|
│ Approval: on-request │
|
||||||
│ Sandbox: read-only │
|
│ Sandbox: read-only │
|
||||||
│ Agents.md: <none> │
|
│ Agents.md: <none> │
|
||||||
│ │
|
│ │
|
||||||
│ Token usage: 750 total (500 input + 250 output) │
|
│ Token usage: 750 total (500 input + 250 output) │
|
||||||
│ Limits: send a message to load usage data │
|
│ Context window: 100% left (750 / 272K) │
|
||||||
╰──────────────────────────────────────────────────────────────╯
|
│ Limits: send a message to load usage data │
|
||||||
|
╰─────────────────────────────────────────────────────────────────╯
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ expression: sanitized
|
|||||||
╭────────────────────────────────────────────╮
|
╭────────────────────────────────────────────╮
|
||||||
│ >_ OpenAI Codex (v0.0.0) │
|
│ >_ OpenAI Codex (v0.0.0) │
|
||||||
│ │
|
│ │
|
||||||
│ Model: gpt-5-codex (reasoning hig │
|
│ Model: gpt-5-codex (reasoning │
|
||||||
│ Directory: [[workspace]] │
|
│ Directory: [[workspace]] │
|
||||||
│ Approval: on-request │
|
│ Approval: on-request │
|
||||||
│ Sandbox: read-only │
|
│ Sandbox: read-only │
|
||||||
│ Agents.md: <none> │
|
│ Agents.md: <none> │
|
||||||
│ │
|
│ │
|
||||||
│ Token usage: 1.9K total (1K input + 90 │
|
│ Token usage: 1.9K total (1K input + │
|
||||||
│ 5h limit: [███████████████░░░░░] 72% │
|
│ Context window: 100% left (2.1K / 272K) │
|
||||||
│ (resets 03:14) │
|
│ 5h limit: [███████████████░░░░░] │
|
||||||
|
│ (resets 03:14) │
|
||||||
╰────────────────────────────────────────────╯
|
╰────────────────────────────────────────────╯
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ fn status_snapshot_includes_reasoning_details() {
|
|||||||
.expect("timestamp");
|
.expect("timestamp");
|
||||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||||
|
|
||||||
let composite = new_status_output(&config, &usage, &None, Some(&rate_display));
|
let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display));
|
||||||
let mut rendered_lines = render_lines(&composite.display_lines(80));
|
let mut rendered_lines = render_lines(&composite.display_lines(80));
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
for line in &mut rendered_lines {
|
for line in &mut rendered_lines {
|
||||||
@@ -144,7 +144,7 @@ fn status_snapshot_includes_monthly_limit() {
|
|||||||
.expect("timestamp");
|
.expect("timestamp");
|
||||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||||
|
|
||||||
let composite = new_status_output(&config, &usage, &None, Some(&rate_display));
|
let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display));
|
||||||
let mut rendered_lines = render_lines(&composite.display_lines(80));
|
let mut rendered_lines = render_lines(&composite.display_lines(80));
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
for line in &mut rendered_lines {
|
for line in &mut rendered_lines {
|
||||||
@@ -170,7 +170,7 @@ fn status_card_token_usage_excludes_cached_tokens() {
|
|||||||
total_tokens: 2_100,
|
total_tokens: 2_100,
|
||||||
};
|
};
|
||||||
|
|
||||||
let composite = new_status_output(&config, &usage, &None, None);
|
let composite = new_status_output(&config, &usage, Some(&usage), &None, None);
|
||||||
let rendered = render_lines(&composite.display_lines(120));
|
let rendered = render_lines(&composite.display_lines(120));
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -211,7 +211,7 @@ fn status_snapshot_truncates_in_narrow_terminal() {
|
|||||||
.expect("timestamp");
|
.expect("timestamp");
|
||||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||||
|
|
||||||
let composite = new_status_output(&config, &usage, &None, Some(&rate_display));
|
let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display));
|
||||||
let mut rendered_lines = render_lines(&composite.display_lines(46));
|
let mut rendered_lines = render_lines(&composite.display_lines(46));
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
for line in &mut rendered_lines {
|
for line in &mut rendered_lines {
|
||||||
@@ -238,7 +238,7 @@ fn status_snapshot_shows_missing_limits_message() {
|
|||||||
total_tokens: 750,
|
total_tokens: 750,
|
||||||
};
|
};
|
||||||
|
|
||||||
let composite = new_status_output(&config, &usage, &None, None);
|
let composite = new_status_output(&config, &usage, Some(&usage), &None, None);
|
||||||
let mut rendered_lines = render_lines(&composite.display_lines(80));
|
let mut rendered_lines = render_lines(&composite.display_lines(80));
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
for line in &mut rendered_lines {
|
for line in &mut rendered_lines {
|
||||||
@@ -274,7 +274,7 @@ fn status_snapshot_shows_empty_limits_message() {
|
|||||||
.expect("timestamp");
|
.expect("timestamp");
|
||||||
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
|
||||||
|
|
||||||
let composite = new_status_output(&config, &usage, &None, Some(&rate_display));
|
let composite = new_status_output(&config, &usage, Some(&usage), &None, Some(&rate_display));
|
||||||
let mut rendered_lines = render_lines(&composite.display_lines(80));
|
let mut rendered_lines = render_lines(&composite.display_lines(80));
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
for line in &mut rendered_lines {
|
for line in &mut rendered_lines {
|
||||||
@@ -284,3 +284,41 @@ fn status_snapshot_shows_empty_limits_message() {
|
|||||||
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
||||||
assert_snapshot!(sanitized);
|
assert_snapshot!(sanitized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_context_window_uses_last_usage() {
|
||||||
|
let temp_home = TempDir::new().expect("temp home");
|
||||||
|
let mut config = test_config(&temp_home);
|
||||||
|
config.model_context_window = Some(272_000);
|
||||||
|
|
||||||
|
let total_usage = TokenUsage {
|
||||||
|
input_tokens: 12_800,
|
||||||
|
cached_input_tokens: 0,
|
||||||
|
output_tokens: 879,
|
||||||
|
reasoning_output_tokens: 0,
|
||||||
|
total_tokens: 102_000,
|
||||||
|
};
|
||||||
|
let last_usage = TokenUsage {
|
||||||
|
input_tokens: 12_800,
|
||||||
|
cached_input_tokens: 0,
|
||||||
|
output_tokens: 879,
|
||||||
|
reasoning_output_tokens: 0,
|
||||||
|
total_tokens: 13_679,
|
||||||
|
};
|
||||||
|
|
||||||
|
let composite = new_status_output(&config, &total_usage, Some(&last_usage), &None, None);
|
||||||
|
let rendered_lines = render_lines(&composite.display_lines(80));
|
||||||
|
let context_line = rendered_lines
|
||||||
|
.into_iter()
|
||||||
|
.find(|line| line.contains("Context window"))
|
||||||
|
.expect("context line");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
context_line.contains("13.7K / 272K"),
|
||||||
|
"expected context line to reflect last usage tokens, got: {context_line}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!context_line.contains("102K"),
|
||||||
|
"context line should not use total aggregated tokens, got: {context_line}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user