From d898cc8f3f41217a1020fe5e3255047465076e25 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 30 Apr 2026 22:42:07 -0700 Subject: [PATCH] Format multi-day goal durations in the TUI (#20558) ## Why Goal mode shows elapsed time in compact hour/minute form. That is easy to scan for shorter runs, but once a goal runs past 24 hours, large hour counts become harder to read at a glance. ## What changed Updated `codex-rs/tui/src/goal_display.rs` so unbudgeted goal elapsed time keeps the existing compact format below one day, then switches to a day-aware format once the elapsed time reaches 24 hours: - `23h 59m` - `1d 0h 0m` - `2d 23h 42m` The formatter now covers the 24-hour boundary in unit tests, and the TUI status-line snapshot for a completed elapsed goal now exercises the multi-day display. ## Verification - `cargo test -p codex-tui` Here's my longest-running test task: image --- ...__status_line_goal_complete_elapsed_footer.snap | 2 +- .../tui/src/chatwidget/tests/status_and_layout.rs | 12 +++++++----- codex-rs/tui/src/goal_display.rs | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_goal_complete_elapsed_footer.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_goal_complete_elapsed_footer.snap index 9b9ba2c999..97253d2ec9 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_goal_complete_elapsed_footer.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_line_goal_complete_elapsed_footer.snap @@ -6,4 +6,4 @@ expression: normalized_backend_snapshot(terminal.backend()) " " "› Ask Codex to do anything " " " -" gpt-5.4 Goal achieved (30m) " +" gpt-5.4 Goal achieved (2d 23h 42m) " 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 bce32210d9..36031e9832 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1626,16 +1626,18 @@ async fn status_line_goal_complete_elapsed_footer_snapshot() { chat.show_welcome_banner = false; chat.config.tui_status_line = Some(vec!["model-name".to_string()]); chat.refresh_status_line(); + let mut goal = test_thread_goal( + codex_app_server_protocol::ThreadGoalStatus::Complete, + /*token_budget*/ None, + /*tokens_used*/ 40_000, + ); + goal.time_used_seconds = 2 * 24 * 60 * 60 + 23 * 60 * 60 + 42 * 60; chat.handle_server_notification( ServerNotification::ThreadGoalUpdated( codex_app_server_protocol::ThreadGoalUpdatedNotification { thread_id: "thread-1".to_string(), turn_id: None, - goal: test_thread_goal( - codex_app_server_protocol::ThreadGoalStatus::Complete, - /*token_budget*/ None, - /*tokens_used*/ 40_000, - ), + goal, }, ), /*replay_kind*/ None, diff --git a/codex-rs/tui/src/goal_display.rs b/codex-rs/tui/src/goal_display.rs index 1fdcadd902..d0455054b7 100644 --- a/codex-rs/tui/src/goal_display.rs +++ b/codex-rs/tui/src/goal_display.rs @@ -15,6 +15,12 @@ pub(crate) fn format_goal_elapsed_seconds(seconds: i64) -> String { let hours = minutes / 60; let remaining_minutes = minutes % 60; + if hours >= 24 { + let days = hours / 24; + let remaining_hours = hours % 24; + return format!("{days}d {remaining_hours}h {remaining_minutes}m"); + } + if remaining_minutes == 0 { format!("{hours}h") } else { @@ -64,6 +70,14 @@ mod tests { assert_eq!(format_goal_elapsed_seconds(30 * 60), "30m"); assert_eq!(format_goal_elapsed_seconds(90 * 60), "1h 30m"); assert_eq!(format_goal_elapsed_seconds(2 * 60 * 60), "2h"); + let just_before_one_day = 24 * 60 * 60 - 1; + assert_eq!(format_goal_elapsed_seconds(just_before_one_day), "23h 59m"); + + let one_day = 24 * 60 * 60; + assert_eq!(format_goal_elapsed_seconds(one_day), "1d 0h 0m"); + + let almost_three_days = 2 * 24 * 60 * 60 + 23 * 60 * 60 + 42 * 60; + assert_eq!(format_goal_elapsed_seconds(almost_three_days), "2d 23h 42m"); } fn test_thread_goal(token_budget: Option, tokens_used: i64) -> ThreadGoal {