From 6cfc012e9dfe4fbe4a413420bde6e96895d31770 Mon Sep 17 00:00:00 2001 From: Enrique Moreno Tent Date: Sat, 6 Sep 2025 00:06:36 +0200 Subject: [PATCH] feat(tui): show minutes/hours in thinking timer (#3220) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What - Show compact elapsed time in the TUI status indicator: Xs, MmSSs, HhMMmSSs. - Add private helper fmt_elapsed_compact with a unit test. Why - Seconds‑only becomes hard to read during longer runs; minutes/hours improve clarity without extra noise. How - Implemented in codex-rs/tui/src/status_indicator_widget.rs only. - The helper is used when rendering the existing “Working/Thinking” timer. - No changes to codex-common::elapsed::format_duration or other crates. Scope/Impact - TUI‑only; no public API changes; minimal risk. - Snapshot tests should remain unchanged (most show “0s”). Before/After - Working (65s • Esc to interrupt) → Working (1m05s • Esc to interrupt) - Working (3723s • …) → Working (1h02m03s • …) Tests - Unit: fmt_elapsed_compact_formats_seconds_minutes_hours. - Local checks: cargo fmt --all, cargo clippy -p codex-tui -- -D warnings, cargo test -p codex-tui. Notes - Open to adjusting the exact format or moving the helper if maintainers prefer a shared location. Signed-off-by: Enrique Moreno Tent --- codex-rs/tui/src/status_indicator_widget.rs | 36 ++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index 8f492c062..5b2fe720c 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -31,6 +31,23 @@ pub(crate) struct StatusIndicatorWidget { frame_requester: FrameRequester, } +// Format elapsed seconds into a compact human-friendly form used by the status line. +// Examples: 0s, 59s, 1m00s, 59m59s, 1h00m00s, 2h03m09s +fn fmt_elapsed_compact(elapsed_secs: u64) -> String { + if elapsed_secs < 60 { + return format!("{elapsed_secs}s"); + } + if elapsed_secs < 3600 { + let minutes = elapsed_secs / 60; + let seconds = elapsed_secs % 60; + return format!("{minutes}m{seconds:02}s"); + } + let hours = elapsed_secs / 3600; + let minutes = (elapsed_secs % 3600) / 60; + let seconds = elapsed_secs % 60; + format!("{hours}h{minutes:02}m{seconds:02}s") +} + impl StatusIndicatorWidget { pub(crate) fn new(app_event_tx: AppEventSender, frame_requester: FrameRequester) -> Self { Self { @@ -136,13 +153,14 @@ impl WidgetRef for StatusIndicatorWidget { self.frame_requester .schedule_frame_in(Duration::from_millis(32)); let elapsed = self.elapsed_seconds(); + let pretty_elapsed = fmt_elapsed_compact(elapsed); // Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback. let mut spans = vec![" ".into()]; spans.extend(shimmer_spans(&self.header)); spans.extend(vec![ " ".into(), - format!("({elapsed}s • ").dim(), + format!("({pretty_elapsed} • ").dim(), "Esc".dim().bold(), " to interrupt)".dim(), ]); @@ -184,6 +202,22 @@ mod tests { use std::time::Instant; use tokio::sync::mpsc::unbounded_channel; + use pretty_assertions::assert_eq; + + #[test] + fn fmt_elapsed_compact_formats_seconds_minutes_hours() { + assert_eq!(fmt_elapsed_compact(0), "0s"); + assert_eq!(fmt_elapsed_compact(1), "1s"); + assert_eq!(fmt_elapsed_compact(59), "59s"); + assert_eq!(fmt_elapsed_compact(60), "1m00s"); + assert_eq!(fmt_elapsed_compact(61), "1m01s"); + assert_eq!(fmt_elapsed_compact(3 * 60 + 5), "3m05s"); + assert_eq!(fmt_elapsed_compact(59 * 60 + 59), "59m59s"); + assert_eq!(fmt_elapsed_compact(3600), "1h00m00s"); + assert_eq!(fmt_elapsed_compact(3600 + 60 + 1), "1h01m01s"); + assert_eq!(fmt_elapsed_compact(25 * 3600 + 2 * 60 + 3), "25h02m03s"); + } + #[test] fn renders_with_working_header() { let (tx_raw, _rx) = unbounded_channel::();