diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 34b73d67..f387a3d5 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -108,6 +108,7 @@ pub(crate) struct ChatComposer { custom_prompts: Vec, footer_mode: FooterMode, footer_hint_override: Option>, + context_window_percent: Option, } /// Popup state – at most one can be visible at any time. @@ -150,6 +151,7 @@ impl ChatComposer { custom_prompts: Vec::new(), footer_mode: FooterMode::ShortcutPrompt, footer_hint_override: None, + context_window_percent: None, }; // Apply configuration via the setter to keep side-effects centralized. this.set_disable_paste_burst(disable_paste_burst); @@ -1317,6 +1319,7 @@ impl ChatComposer { esc_backtrack_hint: self.esc_backtrack_hint, use_shift_enter_hint: self.use_shift_enter_hint, 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; } + pub(crate) fn set_context_window_percent(&mut self, percent: Option) { + if self.context_window_percent != percent { + self.context_window_percent = percent; + } + } + pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) { self.esc_backtrack_hint = show; if show { diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 06a5b5c7..133c0b34 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -5,6 +5,7 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; +use ratatui::text::Span; use ratatui::widgets::WidgetRef; use std::iter; @@ -14,6 +15,7 @@ pub(crate) struct FooterProps { pub(crate) esc_backtrack_hint: bool, pub(crate) use_shift_enter_hint: bool, pub(crate) is_task_running: bool, + pub(crate) context_window_percent: Option, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -75,7 +77,13 @@ fn footer_lines(props: FooterProps) -> Vec> { FooterMode::CtrlCReminder => vec![ctrl_c_reminder_line(CtrlCReminderState { 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 { use_shift_enter_hint: props.use_shift_enter_hint, esc_backtrack_hint: props.esc_backtrack_hint, @@ -211,6 +219,21 @@ fn dim_line(text: String) -> Line<'static> { Line::from(text).dim() } +fn context_window_line(percent: Option) -> Line<'static> { + let mut spans: Vec> = 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)] enum ShortcutId { Commands, @@ -398,6 +421,7 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, + context_window_percent: None, }, ); @@ -408,6 +432,7 @@ mod tests { esc_backtrack_hint: true, use_shift_enter_hint: true, is_task_running: false, + context_window_percent: None, }, ); @@ -418,6 +443,7 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, + context_window_percent: None, }, ); @@ -428,6 +454,7 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: true, + context_window_percent: None, }, ); @@ -438,6 +465,7 @@ mod tests { esc_backtrack_hint: false, use_shift_enter_hint: false, is_task_running: false, + context_window_percent: None, }, ); @@ -448,6 +476,18 @@ mod tests { esc_backtrack_hint: true, use_shift_enter_hint: 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), }, ); } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index fccf5651..32ce90d0 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -68,6 +68,7 @@ pub(crate) struct BottomPane { status: Option, /// Queued user messages to show under the status indicator. queued_user_messages: Vec, + context_window_percent: Option, } pub(crate) struct BottomPaneParams { @@ -100,6 +101,7 @@ impl BottomPane { status: None, queued_user_messages: Vec::new(), 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) { + 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. 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()); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap new file mode 100644 index 00000000..77b3796c --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__footer__tests__footer_shortcuts_context_running.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/bottom_pane/footer.rs +expression: terminal.backend() +--- +" 72% context left " diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index fecfd8e0..5973f619 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -396,8 +396,16 @@ impl ChatWidget { } pub(crate) fn set_token_info(&mut self, info: Option) { - if info.is_some() { - self.token_info = info; + if let Some(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) { - let default_usage; - let usage_ref = if let Some(ti) = &self.token_info { - &ti.total_token_usage + let default_usage = TokenUsage::default(); + let (total_usage, context_usage) = if let Some(ti) = &self.token_info { + (&ti.total_token_usage, Some(&ti.last_token_usage)) } else { - default_usage = TokenUsage::default(); - &default_usage + (&default_usage, Some(&default_usage)) }; self.add_to_history(crate::status::new_status_output( &self.config, - usage_ref, + total_usage, + context_usage, &self.conversation_id, self.rate_limit_snapshot.as_ref(), )); diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap index dab45b3b..e4ae90ba 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap @@ -1,6 +1,5 @@ --- source: tui/src/chatwidget/tests.rs -assertion_line: 1445 expression: terminal.backend() --- " " diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 35f096e7..2674fc3e 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -29,11 +29,19 @@ use super::rate_limits::compose_rate_limit_data; use super::rate_limits::format_status_limit_summary; 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)] pub(crate) struct StatusTokenUsageData { total: u64, input: u64, output: u64, + context_window: Option, } #[derive(Debug)] @@ -52,12 +60,13 @@ struct StatusHistoryCell { pub(crate) fn new_status_output( config: &Config, - usage: &TokenUsage, + total_usage: &TokenUsage, + context_usage: Option<&TokenUsage>, session_id: &Option, rate_limits: Option<&RateLimitSnapshotDisplay>, ) -> CompositeHistoryCell { 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)]) } @@ -65,7 +74,8 @@ pub(crate) fn new_status_output( impl StatusHistoryCell { fn new( config: &Config, - usage: &TokenUsage, + total_usage: &TokenUsage, + context_usage: Option<&TokenUsage>, session_id: &Option, rate_limits: Option<&RateLimitSnapshotDisplay>, ) -> Self { @@ -84,10 +94,19 @@ impl StatusHistoryCell { let agents_summary = compose_agents_summary(config); let account = compose_account_display(config); 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 { - total: usage.blended_total(), - input: usage.non_cached_input(), - output: usage.output_tokens, + total: total_usage.blended_total(), + input: total_usage.non_cached_input(), + output: total_usage.output_tokens, + context_window, }; let rate_limits = compose_rate_limit_data(rate_limits); @@ -123,6 +142,22 @@ impl StatusHistoryCell { ] } + fn context_window_spans(&self) -> Option>> { + 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( &self, 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, "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); let formatter = FieldFormatter::from_labels(labels.iter().map(String::as_str)); @@ -266,6 +304,10 @@ impl HistoryCell for StatusHistoryCell { lines.push(Line::from(Vec::>::new())); 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)); let content_width = lines.iter().map(line_display_width).max().unwrap_or(0); diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap index 67ed88cc..73767d98 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_monthly_limit.snap @@ -4,15 +4,16 @@ expression: sanitized --- /status -╭───────────────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Model: gpt-5-codex (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ -│ Agents.md: │ -│ │ -│ Token usage: 1.2K total (800 input + 400 output) │ -│ Monthly limit: [██░░░░░░░░░░░░░░░░░░] 12% used (resets 07:08 on 7 May) │ -╰───────────────────────────────────────────────────────────────────────────╯ +╭────────────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Model: gpt-5-codex (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Approval: on-request │ +│ Sandbox: read-only │ +│ Agents.md: │ +│ │ +│ Token usage: 1.2K total (800 input + 400 output) │ +│ Context window: 100% left (1.2K / 272K) │ +│ Monthly limit: [██░░░░░░░░░░░░░░░░░░] 12% used (resets 07:08 on 7 May) │ +╰────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap index c445d116..20904760 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_reasoning_details.snap @@ -4,16 +4,17 @@ expression: sanitized --- /status -╭───────────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Model: gpt-5-codex (reasoning high, summaries detailed) │ -│ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: workspace-write │ -│ Agents.md: │ -│ │ -│ Token usage: 1.9K total (1K input + 900 output) │ -│ 5h limit: [███████████████░░░░░] 72% used (resets 03:14) │ -│ Weekly limit: [█████████░░░░░░░░░░░] 45% used (resets 03:24) │ -╰───────────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Model: gpt-5-codex (reasoning high, summaries detailed) │ +│ Directory: [[workspace]] │ +│ Approval: on-request │ +│ Sandbox: workspace-write │ +│ Agents.md: │ +│ │ +│ Token usage: 1.9K total (1K input + 900 output) │ +│ Context window: 100% left (2.1K / 272K) │ +│ 5h limit: [███████████████░░░░░] 72% used (resets 03:14) │ +│ Weekly limit: [█████████░░░░░░░░░░░] 45% used (resets 03:24) │ +╰─────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap index 2766f070..0ac26c5d 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_empty_limits_message.snap @@ -4,15 +4,16 @@ expression: sanitized --- /status -╭──────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Model: gpt-5-codex (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ -│ Agents.md: │ -│ │ -│ Token usage: 750 total (500 input + 250 output) │ -│ Limits: data not available yet │ -╰──────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Model: gpt-5-codex (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Approval: on-request │ +│ Sandbox: read-only │ +│ Agents.md: │ +│ │ +│ Token usage: 750 total (500 input + 250 output) │ +│ Context window: 100% left (750 / 272K) │ +│ Limits: data not available yet │ +╰─────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap index 21627522..d59140a3 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_shows_missing_limits_message.snap @@ -4,15 +4,16 @@ expression: sanitized --- /status -╭──────────────────────────────────────────────────────────────╮ -│ >_ OpenAI Codex (v0.0.0) │ -│ │ -│ Model: gpt-5-codex (reasoning none, summaries auto) │ -│ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ -│ Agents.md: │ -│ │ -│ Token usage: 750 total (500 input + 250 output) │ -│ Limits: send a message to load usage data │ -╰──────────────────────────────────────────────────────────────╯ +╭─────────────────────────────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ Model: gpt-5-codex (reasoning none, summaries auto) │ +│ Directory: [[workspace]] │ +│ Approval: on-request │ +│ Sandbox: read-only │ +│ Agents.md: │ +│ │ +│ Token usage: 750 total (500 input + 250 output) │ +│ Context window: 100% left (750 / 272K) │ +│ Limits: send a message to load usage data │ +╰─────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap index a943c852..fd429dd9 100644 --- a/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap +++ b/codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_truncates_in_narrow_terminal.snap @@ -7,13 +7,14 @@ expression: sanitized ╭────────────────────────────────────────────╮ │ >_ OpenAI Codex (v0.0.0) │ │ │ -│ Model: gpt-5-codex (reasoning hig │ +│ Model: gpt-5-codex (reasoning │ │ Directory: [[workspace]] │ -│ Approval: on-request │ -│ Sandbox: read-only │ -│ Agents.md: │ +│ Approval: on-request │ +│ Sandbox: read-only │ +│ Agents.md: │ │ │ -│ Token usage: 1.9K total (1K input + 90 │ -│ 5h limit: [███████████████░░░░░] 72% │ -│ (resets 03:14) │ +│ Token usage: 1.9K total (1K input + │ +│ Context window: 100% left (2.1K / 272K) │ +│ 5h limit: [███████████████░░░░░] │ +│ (resets 03:14) │ ╰────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index b095819d..eff8d8f4 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -103,7 +103,7 @@ fn status_snapshot_includes_reasoning_details() { .expect("timestamp"); 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)); if cfg!(windows) { for line in &mut rendered_lines { @@ -144,7 +144,7 @@ fn status_snapshot_includes_monthly_limit() { .expect("timestamp"); 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)); if cfg!(windows) { for line in &mut rendered_lines { @@ -170,7 +170,7 @@ fn status_card_token_usage_excludes_cached_tokens() { 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)); assert!( @@ -211,7 +211,7 @@ fn status_snapshot_truncates_in_narrow_terminal() { .expect("timestamp"); 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)); if cfg!(windows) { for line in &mut rendered_lines { @@ -238,7 +238,7 @@ fn status_snapshot_shows_missing_limits_message() { 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)); if cfg!(windows) { for line in &mut rendered_lines { @@ -274,7 +274,7 @@ fn status_snapshot_shows_empty_limits_message() { .expect("timestamp"); 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)); if cfg!(windows) { 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"); 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}" + ); +}