From 598d6ff0561f6372c69227bac719e76f39928e87 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 9 Apr 2026 07:52:07 -0700 Subject: [PATCH] Render statusline context as a meter (#17170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: The statusline reported context as an “X% left” value, which could be mistaken for quota, and context usage was included in the default footer. Solution: Render configured context status items as a filling context meter, preserve `context-used` as a legacy alias while hiding it from the setup menu, and remove context from the default statusline. It will still be available as an opt-in option for users who want to see it. image --- codex-rs/config/src/types.rs | 3 +- codex-rs/core/config.schema.json | 2 +- ..._snapshot_uses_runtime_preview_values.snap | 4 +- .../tui/src/bottom_pane/status_line_setup.rs | 61 +++++++++++++---- codex-rs/tui/src/chatwidget.rs | 3 +- ..._review_denied_renders_denied_request.snap | 6 +- ...artup_failure_renders_warning_history.snap | 10 +-- ...i__chatwidget__tests__chatwidget_tall.snap | 6 +- ...compact_queues_user_messages_snapshot.snap | 6 +- ...pproved_exec_renders_approved_request.snap | 6 +- ...ec_renders_warning_and_denied_request.snap | 6 +- ...allel_reviews_render_aggregate_status.snap | 6 +- ...et__tests__mcp_startup_header_booting.snap | 6 +- ..._tests__preamble_keeps_working_status.snap | 6 +- ..._review_queues_user_messages_snapshot.snap | 6 +- ...line_model_with_reasoning_fast_footer.snap | 6 +- ...atwidget__tests__status_widget_active.snap | 6 +- ...ed_exec_begin_restores_working_status.snap | 6 +- ...renders_command_in_single_details_row.snap | 6 +- .../tui/src/chatwidget/status_surfaces.rs | 65 +++++++++++++++++-- .../src/chatwidget/tests/status_and_layout.rs | 40 ++++++++++-- 21 files changed, 191 insertions(+), 75 deletions(-) diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index cee727169..9d1626dbd 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -549,8 +549,7 @@ pub struct Tui { /// Ordered list of status line item identifiers. /// /// When set, the TUI renders the selected items as the status line. - /// When unset, the TUI defaults to: `model-with-reasoning`, `context-remaining`, and - /// `current-dir`. + /// When unset, the TUI defaults to: `model-with-reasoning` and `current-dir`. #[serde(default)] pub status_line: Option>, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index bcc5193b2..c3c29aa55 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1864,7 +1864,7 @@ }, "status_line": { "default": null, - "description": "Ordered list of status line item identifiers.\n\nWhen set, the TUI renders the selected items as the status line. When unset, the TUI defaults to: `model-with-reasoning`, `context-remaining`, and `current-dir`.", + "description": "Ordered list of status line item identifiers.\n\nWhen set, the TUI renders the selected items as the status line. When unset, the TUI defaults to: `model-with-reasoning` and `current-dir`.", "items": { "type": "string" }, diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap index 20aca1e33..94d9cd450 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_uses_runtime_preview_values.snap @@ -13,9 +13,9 @@ expression: "render_lines(&view, 72)" [x] git-branch Current Git branch (omitted when unavaila… [ ] model-with-reasoning Current model name with reasoning level [ ] project-root Project root directory (omitted when unav… - [ ] context-remaining Percentage of context window remaining (o… - [ ] context-used Percentage of context window used (omitte… + [ ] context-usage Visual meter of context window usage (omi… [ ] five-hour-limit Remaining usage on 5-hour usage limit (om… + [ ] weekly-limit Remaining usage on weekly usage limit (om… gpt-5-codex · ~/codex-rs · jif/statusline-preview Use ↑↓ to navigate, ←→ to move, space to select, enter to confirm, esc diff --git a/codex-rs/tui/src/bottom_pane/status_line_setup.rs b/codex-rs/tui/src/bottom_pane/status_line_setup.rs index cb0229e77..cdda2d7ca 100644 --- a/codex-rs/tui/src/bottom_pane/status_line_setup.rs +++ b/codex-rs/tui/src/bottom_pane/status_line_setup.rs @@ -12,7 +12,7 @@ //! - Model information (name, reasoning level) //! - Directory paths (current dir, project root) //! - Git information (branch name) -//! - Context usage (remaining %, used %, window size) +//! - Context usage (meter, window size) //! - Usage limits (5-hour, weekly) //! - Session info (ID, tokens used) //! - Application version @@ -22,7 +22,6 @@ use ratatui::layout::Rect; use ratatui::text::Line; use std::collections::BTreeMap; use std::collections::HashSet; -use strum::IntoEnumIterator; use strum_macros::Display; use strum_macros::EnumIter; use strum_macros::EnumString; @@ -63,11 +62,15 @@ pub(crate) enum StatusLineItem { /// Current git branch name (if in a repository). GitBranch, - /// Percentage of context window remaining. - ContextRemaining, - - /// Percentage of context window used. - ContextUsed, + /// Visual meter of context window usage. + /// + /// Also accepts legacy `context-remaining` and `context-used` config values. + #[strum( + to_string = "context-usage", + serialize = "context-remaining", + serialize = "context-used" + )] + ContextUsage, /// Remaining usage on the 5-hour rate limit. FiveHourLimit, @@ -106,11 +109,8 @@ impl StatusLineItem { StatusLineItem::CurrentDir => "Current working directory", StatusLineItem::ProjectRoot => "Project root directory (omitted when unavailable)", StatusLineItem::GitBranch => "Current Git branch (omitted when unavailable)", - StatusLineItem::ContextRemaining => { - "Percentage of context window remaining (omitted when unknown)" - } - StatusLineItem::ContextUsed => { - "Percentage of context window used (omitted when unknown)" + StatusLineItem::ContextUsage => { + "Visual meter of context window usage (omitted when unknown)" } StatusLineItem::FiveHourLimit => { "Remaining usage on 5-hour usage limit (omitted when unavailable)" @@ -133,6 +133,24 @@ impl StatusLineItem { } } +const SELECTABLE_STATUS_LINE_ITEMS: &[StatusLineItem] = &[ + StatusLineItem::ModelName, + StatusLineItem::ModelWithReasoning, + StatusLineItem::CurrentDir, + StatusLineItem::ProjectRoot, + StatusLineItem::GitBranch, + StatusLineItem::ContextUsage, + StatusLineItem::FiveHourLimit, + StatusLineItem::WeeklyLimit, + StatusLineItem::CodexVersion, + StatusLineItem::ContextWindowSize, + StatusLineItem::UsedTokens, + StatusLineItem::TotalInputTokens, + StatusLineItem::TotalOutputTokens, + StatusLineItem::SessionId, + StatusLineItem::FastMode, +]; + /// Runtime values used to preview the current status-line selection. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub(crate) struct StatusLinePreviewData { @@ -209,7 +227,7 @@ impl StatusLineSetupView { } } - for item in StatusLineItem::iter() { + for item in SELECTABLE_STATUS_LINE_ITEMS.iter().cloned() { let item_id = item.to_string(); if used_ids.contains(&item_id) { continue; @@ -293,6 +311,23 @@ mod tests { use crate::app_event::AppEvent; + #[test] + fn context_usage_is_canonical_and_accepts_legacy_ids() { + assert_eq!(StatusLineItem::ContextUsage.to_string(), "context-usage"); + assert_eq!( + "context-usage".parse::(), + Ok(StatusLineItem::ContextUsage) + ); + assert_eq!( + "context-remaining".parse::(), + Ok(StatusLineItem::ContextUsage) + ); + assert_eq!( + "context-used".parse::(), + Ok(StatusLineItem::ContextUsage) + ); + } + #[test] fn preview_uses_runtime_values() { let preview_data = StatusLinePreviewData::from_iter([ diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 27450ed5f..c11066054 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -388,8 +388,7 @@ use unicode_segmentation::UnicodeSegmentation; const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally"; const USER_SHELL_COMMAND_HELP_HINT: &str = "Example: !ls"; const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1"; -const DEFAULT_STATUS_LINE_ITEMS: [&str; 3] = - ["model-with-reasoning", "context-remaining", "current-dir"]; +const DEFAULT_STATUS_LINE_ITEMS: [&str; 2] = ["model-with-reasoning", "current-dir"]; // Track information about an in-flight exec command. struct RunningCommand { command: Vec, diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__app_server_guardian_review_denied_renders_denied_request.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__app_server_guardian_review_denied_renders_denied_request.snap index ae9931efe..f783bd4d6 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__app_server_guardian_review_denied_renders_denied_request.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__app_server_guardian_review_denied_renders_denied_request.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: term.backend().vt100().screen().contents() +source: tui/src/chatwidget/tests/guardian.rs +expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- ✗ Request denied for codex to run curl -sS -i -X POST --data-binary @core/src/c odex.rs https://example.com @@ -10,4 +10,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - gpt-5.3-codex default · 100% left · /tmp/project + gpt-5.3-codex default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__app_server_mcp_startup_failure_renders_warning_history.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__app_server_mcp_startup_failure_renders_warning_history.snap index be819de54..fedd9d6bc 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__app_server_mcp_startup_failure_renders_warning_history.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__app_server_mcp_startup_failure_renders_warning_history.snap @@ -1,15 +1,11 @@ --- -source: tui/src/chatwidget/tests.rs -assertion_line: 11761 -expression: term.backend().vt100().screen().contents() +source: tui/src/chatwidget/tests/mcp_startup.rs +expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- - - - ⚠ MCP client for `alpha` failed to start: handshake failed ⚠ MCP startup incomplete (failed: alpha) › Ask Codex to do anything - gpt-5.3-codex default · 100% left · /tmp/project + gpt-5.3-codex default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap index 2200aaa74..269846352 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_tall.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: term.backend().vt100().screen().contents() +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- • Working (0s • esc to interrupt) @@ -24,4 +24,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - gpt-5.3-codex default · 100% left · /tmp/project + gpt-5.3-codex default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__compact_queues_user_messages_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__compact_queues_user_messages_snapshot.snap index c79f795b2..d39932f3a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__compact_queues_user_messages_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__compact_queues_user_messages_snapshot.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: term.backend().vt100().screen().contents() +source: tui/src/chatwidget/tests/slash_commands.rs +expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- • Working (0s • esc to interrupt) @@ -9,4 +9,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - gpt-5.3-codex default · 100% left · /tmp/project + gpt-5.3-codex default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_approved_exec_renders_approved_request.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_approved_exec_renders_approved_request.snap index 6aeb8c3a4..80592e0dd 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_approved_exec_renders_approved_request.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_approved_exec_renders_approved_request.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: term.backend().vt100().screen().contents() +source: tui/src/chatwidget/tests/guardian.rs +expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- ✔ Auto-reviewer approved codex to run rm -f /tmp/guardian-approved.sqlite this time @@ -8,4 +8,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - gpt-5.3-codex default · 100% left · /tmp/project + gpt-5.3-codex default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_denied_exec_renders_warning_and_denied_request.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_denied_exec_renders_warning_and_denied_request.snap index 5cc6b31da..d910d6d6f 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_denied_exec_renders_warning_and_denied_request.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_denied_exec_renders_warning_and_denied_request.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: term.backend().vt100().screen().contents() +source: tui/src/chatwidget/tests/guardian.rs +expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- ⚠ Automatic approval review denied (risk: high): The planned action would transmit the full contents of a workspace source file (`core/src/codex.rs`) to @@ -14,4 +14,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - gpt-5.3-codex default · 100% left · /tmp/project + gpt-5.3-codex default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_parallel_reviews_render_aggregate_status.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_parallel_reviews_render_aggregate_status.snap index b5fba2904..821c8d1a6 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_parallel_reviews_render_aggregate_status.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__guardian_parallel_reviews_render_aggregate_status.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: rendered +source: tui/src/chatwidget/tests/guardian.rs +expression: normalize_snapshot_paths(rendered) --- • Reviewing 2 approval requests (0s • esc to interrupt) └ • rm -rf '/tmp/guardian target 1' @@ -9,4 +9,4 @@ expression: rendered › Ask Codex to do anything - gpt-5.3-codex default · 100% left · /tmp/project + gpt-5.3-codex default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap index 06e47fc82..f3936540a 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__mcp_startup_header_booting.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/mcp_startup.rs +expression: normalized_backend_snapshot(terminal.backend()) --- " " "• Booting MCP server: alpha (0s • esc to interrupt) " @@ -8,4 +8,4 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" gpt-5.3-codex default · 100% left · /tmp/project " +" gpt-5.3-codex default · /tmp/project " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__preamble_keeps_working_status.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__preamble_keeps_working_status.snap index 7b1381c96..ad7fa5253 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__preamble_keeps_working_status.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__preamble_keeps_working_status.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/exec_flow.rs +expression: normalized_backend_snapshot(terminal.backend()) --- " " "• Working (0s • esc to interrupt) " @@ -8,4 +8,4 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" gpt-5.3-codex default · 100% left · /tmp/project " +" gpt-5.3-codex default · /tmp/project " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap index c60607c07..bb4a8cefe 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__review_queues_user_messages_snapshot.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: term.backend().vt100().screen().contents() +source: tui/src/chatwidget/tests/review_mode.rs +expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- • Working (0s • esc to interrupt) @@ -9,4 +9,4 @@ expression: term.backend().vt100().screen().contents() › Ask Codex to do anything - gpt-5.3-codex default · 100% left · /tmp/project + gpt-5.3-codex default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_fast_footer.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_fast_footer.snap index 6355decd6..c71a57060 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_fast_footer.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_model_with_reasoning_fast_footer.snap @@ -1,9 +1,9 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalized_backend_snapshot(terminal.backend()) --- " " " " "› Ask Codex to do anything " " " -" gpt-5.4 xhigh fast · 100% left · /tmp/project " +" gpt-5.4 xhigh fast · Context [ ] · /tmp/project " 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 e4fea3544..fea8a4db0 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,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/status_and_layout.rs +expression: normalized_backend_snapshot(terminal.backend()) --- " " "• Analyzing (0s • esc to interrupt) " @@ -8,4 +8,4 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" gpt-5.3-codex default · 100% left · /tmp/project " +" gpt-5.3-codex default · /tmp/project " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_begin_restores_working_status.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_begin_restores_working_status.snap index 9146406a6..7a2d0b4b9 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_begin_restores_working_status.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_begin_restores_working_status.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: terminal.backend() +source: tui/src/chatwidget/tests/exec_flow.rs +expression: normalized_backend_snapshot(terminal.backend()) --- " " "• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view…" @@ -8,4 +8,4 @@ expression: terminal.backend() " " "› Ask Codex to do anything " " " -" gpt-5.3-codex default · 100% left · /tmp/project " +" gpt-5.3-codex default · /tmp/project " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_status_renders_command_in_single_details_row.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_status_renders_command_in_single_details_row.snap index 3b7c1ecc4..a5b839e00 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_status_renders_command_in_single_details_row.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_status_renders_command_in_single_details_row.snap @@ -1,6 +1,6 @@ --- -source: tui/src/chatwidget/tests.rs -expression: rendered +source: tui/src/chatwidget/tests/exec_flow.rs +expression: normalize_snapshot_paths(rendered) --- • Waiting for background terminal (0s • esc to … └ cargo test -p codex-core -- --exact… @@ -8,4 +8,4 @@ expression: rendered › Ask Codex to do anything - gpt-5.3-codex default · 100% left · /tmp/proj… + gpt-5.3-codex default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index d80003cc5..bd58fd4e8 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -450,12 +450,9 @@ impl ChatWidget { Some(format!("{} used", format_tokens_compact(total))) } } - StatusLineItem::ContextRemaining => self - .status_line_context_remaining_percent() - .map(|remaining| format!("{remaining}% left")), - StatusLineItem::ContextUsed => self + StatusLineItem::ContextUsage => self .status_line_context_used_percent() - .map(|used| format!("{used}% used")), + .map(format_context_used_meter), StatusLineItem::FiveHourLimit => { let window = self .rate_limit_snapshots_by_limit_id @@ -660,3 +657,61 @@ where } (items, invalid) } + +fn format_context_used_meter(used_percent: i64) -> String { + const METER_WIDTH: usize = 5; + const EIGHTHS_PER_CELL: i64 = 8; + const PARTIAL_BLOCKS: [&str; 8] = ["", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]; + + let used_percent = used_percent.clamp(0, 100); + let total_eighths = (used_percent * METER_WIDTH as i64 * EIGHTHS_PER_CELL + 50) / 100; + let filled_cells = (total_eighths / EIGHTHS_PER_CELL) as usize; + let partial_eighths = (total_eighths % EIGHTHS_PER_CELL) as usize; + + let mut meter = String::with_capacity(METER_WIDTH); + meter.push_str(&"█".repeat(filled_cells)); + meter.push_str(PARTIAL_BLOCKS[partial_eighths]); + + let occupied_cells = filled_cells + usize::from(partial_eighths > 0); + meter.push_str(&" ".repeat(METER_WIDTH.saturating_sub(occupied_cells))); + + format!("Context [{meter}]") +} + +#[cfg(test)] +mod tests { + use super::format_context_used_meter; + use pretty_assertions::assert_eq; + + #[test] + fn context_meter_uses_five_cells_with_partial_blocks() { + assert_eq!( + format_context_used_meter(/*used_percent*/ 100), + "Context [█████]" + ); + assert_eq!( + format_context_used_meter(/*used_percent*/ 50), + "Context [██▌ ]" + ); + assert_eq!( + format_context_used_meter(/*used_percent*/ 10), + "Context [▌ ]" + ); + assert_eq!( + format_context_used_meter(/*used_percent*/ 0), + "Context [ ]" + ); + } + + #[test] + fn context_meter_clamps_out_of_range_values() { + assert_eq!( + format_context_used_meter(/*used_percent*/ 125), + "Context [█████]" + ); + assert_eq!( + format_context_used_meter(/*used_percent*/ -1), + "Context [ ]" + ); + } +} 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 c5545d59e..b04ce74a1 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -866,6 +866,36 @@ async fn status_line_invalid_items_warn_once() { ); } +#[tokio::test] +async fn status_line_legacy_context_used_renders_context_meter() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.thread_id = Some(ThreadId::new()); + chat.config.tui_status_line = Some(vec!["context-used".to_string()]); + + chat.refresh_status_line(); + + assert_eq!(status_line_text(&chat), Some("Context [ ]".to_string())); + assert!( + drain_insert_history(&mut rx).is_empty(), + "legacy context-used should remain a valid status line item" + ); +} + +#[tokio::test] +async fn status_line_legacy_context_remaining_renders_context_meter() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.thread_id = Some(ThreadId::new()); + chat.config.tui_status_line = Some(vec!["context-remaining".to_string()]); + + chat.refresh_status_line(); + + assert_eq!(status_line_text(&chat), Some("Context [ ]".to_string())); + assert!( + drain_insert_history(&mut rx).is_empty(), + "legacy context-remaining should remain a valid status line item" + ); +} + #[tokio::test] async fn status_line_branch_state_resets_when_git_branch_disabled() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -965,7 +995,7 @@ async fn status_line_model_with_reasoning_includes_fast_for_fast_capable_models( chat.config.cwd = test_project_path().abs(); chat.config.tui_status_line = Some(vec![ "model-with-reasoning".to_string(), - "context-remaining".to_string(), + "context-usage".to_string(), "current-dir".to_string(), ]); chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh)); @@ -978,7 +1008,7 @@ async fn status_line_model_with_reasoning_includes_fast_for_fast_capable_models( assert_eq!( status_line_text(&chat), - Some(format!("gpt-5.4 xhigh fast · 100% left · {test_cwd}")) + Some(format!("gpt-5.4 xhigh fast · Context [ ] · {test_cwd}")) ); chat.set_model("gpt-5.3-codex"); @@ -986,7 +1016,9 @@ async fn status_line_model_with_reasoning_includes_fast_for_fast_capable_models( assert_eq!( status_line_text(&chat), - Some(format!("gpt-5.3-codex xhigh · 100% left · {test_cwd}")) + Some(format!( + "gpt-5.3-codex xhigh · Context [ ] · {test_cwd}" + )) ); } @@ -1073,7 +1105,7 @@ async fn status_line_model_with_reasoning_fast_footer_snapshot() { chat.config.cwd = test_project_path().abs(); chat.config.tui_status_line = Some(vec![ "model-with-reasoning".to_string(), - "context-remaining".to_string(), + "context-usage".to_string(), "current-dir".to_string(), ]); chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));