diff --git a/codex-rs/tui/src/chatwidget/statusline_state/usage.rs b/codex-rs/tui/src/chatwidget/statusline_state/usage.rs index b6e6e6bed4..92d13d7fb3 100644 --- a/codex-rs/tui/src/chatwidget/statusline_state/usage.rs +++ b/codex-rs/tui/src/chatwidget/statusline_state/usage.rs @@ -6,10 +6,21 @@ use serde::Serialize; #[derive(Serialize)] pub(super) struct Usage { + context: Context, #[serde(skip_serializing_if = "Option::is_none")] total: Option, } +#[derive(Serialize)] +struct Context { + #[serde(skip_serializing_if = "Option::is_none")] + context_window_size: Option, + #[serde(skip_serializing_if = "Option::is_none")] + used_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + used_percentage: Option, +} + #[derive(Serialize)] struct Counts { total_input_tokens: i64, @@ -52,7 +63,17 @@ impl Counts { impl From<&ChatWidget> for Usage { fn from(chat: &ChatWidget) -> Self { + let context_window_size = chat.status_line_context_window_size(); Self { + context: Context { + context_window_size, + used_tokens: chat + .token_info + .as_ref() + .map(|info| info.last_token_usage.tokens_in_context_window()), + used_percentage: context_window_size + .and_then(|_| chat.status_line_context_used_percent()), + }, total: chat .token_info .as_ref() diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index ad137a4933..e91093a55f 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -388,6 +388,7 @@ async fn app_server_model_verification_renders_warning() { async fn context_indicator_shows_used_tokens_when_window_unknown() { let (mut chat, _rx, _ops) = make_chatwidget_manual(Some("unknown-model")).await; + chat.thread_id = Some(ThreadId::new()); chat.config.model_context_window = None; let auto_compact_limit = 200_000; chat.config.model_auto_compact_token_limit = Some(auto_compact_limit); @@ -399,18 +400,39 @@ async fn context_indicator_shows_used_tokens_when_window_unknown() { ..TokenUsage::default() }; let token_info = TokenUsageInfo { - total_token_usage: token_usage.clone(), - last_token_usage: token_usage, + total_token_usage: token_usage, + last_token_usage: TokenUsage { + total_tokens: 40_000, + ..TokenUsage::default() + }, model_context_window: None, }; - handle_token_count(&mut chat, Some(token_info)); + handle_token_count(&mut chat, Some(token_info.clone())); assert_eq!(chat.bottom_pane.context_window_percent(), None); assert_eq!( chat.bottom_pane.context_window_used_tokens(), Some(total_tokens) ); + let state = serde_json::to_value(chat.statusline_state()).unwrap(); + assert_eq!( + state["thread"]["usage"]["context"], + json!({"used_tokens": 40_000}) + ); + + handle_token_count( + &mut chat, + Some(TokenUsageInfo { + model_context_window: Some(68_000), + ..token_info + }), + ); + let state = serde_json::to_value(chat.statusline_state()).unwrap(); + assert_eq!( + state["thread"]["usage"]["context"], + json!({"context_window_size": 68_000, "used_tokens": 40_000, "used_percentage": 50}) + ); } #[tokio::test] diff --git a/codex-rs/tui/src/chatwidget/tests/statusline_state_tests.rs b/codex-rs/tui/src/chatwidget/tests/statusline_state_tests.rs index 4c6657cfbe..bee75b3750 100644 --- a/codex-rs/tui/src/chatwidget/tests/statusline_state_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/statusline_state_tests.rs @@ -34,7 +34,7 @@ async fn statusline_state_projects_the_displayed_settings() { "config": {"statusline_items": ["run-state"]}, "thread": { "workspace": {"cwd": expected_cwd}, - "usage": {}, + "usage": {"context": {}}, "id": id, "name": "Saved carts", "agent": {"state": "ready"}, "model": {"id": "gpt-5.5", "display_name": "Test model", "provider": "openai", "reasoning_level": "none", "service_tier": "acme-priority", "service_tier_display_name": "Turbo"}, @@ -92,3 +92,30 @@ async fn statusline_totals_use_the_reported_token_breakdown() { assert_eq!(state["thread"]["usage"].get("total").is_some(), has_total); } } + +#[tokio::test] +async fn statusline_context_uses_the_footer_window_and_percentage() { + let (mut chat, _events, _ops) = make_chatwidget_manual(Some("gpt-5.2")).await; + chat.handle_thread_session(configured_thread_session(ThreadId::new())); + chat.config.model_context_window = Some(80_000); + helpers::handle_token_count(&mut chat, /*info*/ None); + let state = serde_json::to_value(chat.statusline_state()).unwrap(); + assert_eq!( + state["thread"]["usage"]["context"], + json!({"context_window_size": 80_000}) + ); + + // The last response occupies half the usable window after the footer's fixed overhead. + let mut info = helpers::make_token_info( + /*total_tokens*/ 56_000, /*context_window*/ 100_000, + ); + info.total_token_usage.total_tokens = 220_000; + helpers::handle_token_count(&mut chat, Some(info)); + let state = serde_json::to_value(chat.statusline_state()).unwrap(); + assert_eq!( + state["thread"]["usage"]["context"], + json!({ + "context_window_size": 100_000, "used_tokens": 56_000, "used_percentage": 50 + }) + ); +}