From 99f7758a577740f32df3aad53502e948142758a4 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sun, 27 Sep 2026 22:46:53 +0000 Subject: [PATCH] Show short turn durations in TUI completion footers (#48807) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Completion footers hid known durations unless they exceeded 60 seconds, leaving short turns without elapsed-time information. ## What changed Show all known durations for live and restored turns, rendering sub-second durations as `Worked for <1s`. Separate completion metadata with `•` instead of `·`. ## Testing Update live completion, history replay, and separator tests to cover short durations, missing timestamps, and the revised footer format. Refresh affected TUI snapshots. GitOrigin-RevId: 666e765db5426c5f9cc00d73f71cab649ad34947 --- ...turn_tips__tests__turn_tip_placements.snap | 15 +++---- ...ow_answers_without_overlap_duplicates.snap | 8 ++-- ..._safety_retry_committed_steer_history.snap | 3 +- codex-rs/tui/src/app/turn_tips_tests.rs | 2 +- codex-rs/tui/src/chatwidget/completion.rs | 3 +- ...t_markdown_code_blocks_vt100_snapshot.snap | 5 ++- ..._tests__completion_after_plain_answer.snap | 4 +- ...completion_after_streamed_tool_answer.snap | 4 +- ...ked_for_uses_cumulative_turn_duration.snap | 4 +- ...ests__slash_export_completion_message.snap | 5 ++- ...ified_exec_non_empty_then_empty_after.snap | 5 ++- ...d_exec_wait_after_final_agent_message.snap | 5 ++- ...ec_wait_before_streamed_agent_message.snap | 5 ++- ...ied_exec_waiting_multiple_empty_after.snap | 5 ++- .../tui/src/chatwidget/tests/app_server.rs | 5 ++- .../tests/completion_styling_tests.rs | 44 ++++++++++++------- codex-rs/tui/src/chatwidget/tests/helpers.rs | 4 +- codex-rs/tui/src/history_cell/separators.rs | 8 ++-- .../tui/src/history_cell/separators_tests.rs | 24 +++++----- ...our_hour_time_with_dates_and_wrapping.snap | 25 +++++------ codex-rs/tui/src/history_cell/tests.rs | 2 +- 21 files changed, 103 insertions(+), 82 deletions(-) diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__turn_tips__tests__turn_tip_placements.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__turn_tips__tests__turn_tip_placements.snap index e1ce4af0ff..71b29376ac 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__turn_tips__tests__turn_tip_placements.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__turn_tips__tests__turn_tip_placements.snap @@ -1,6 +1,5 @@ --- source: tui/src/app/turn_tips_tests.rs -assertion_line: 337 expression: "crate::chatwidget::tests::helpers::normalize_snapshot_paths(screens.join(\"\\n\\n\"))" --- working=true, response rows=1, 80x12 @@ -31,7 +30,7 @@ working=false, response rows=1, 80x12 • Done. └ Tip: Try /help. - [completion time] + Worked for [duration] • [completion time] › Ask Codex to do anything @@ -45,7 +44,7 @@ working=false, response rows=30, 80x12 • Done. └ Tip: Try /help. - [completion time] + Worked for [duration] • [completion time] › Ask Codex to do anything @@ -56,7 +55,7 @@ working=false, response rows=30, 80x12 working=false, response rows=1, 80x9 • Done. - [completion time] + Worked for [duration] • [completion time] › Ask Codex to do anything @@ -68,7 +67,7 @@ working=false, response rows=1, 80x10 › Show a response. • Done. - [completion time] + Worked for [duration] • [completion time] › Ask Codex to do anything @@ -81,7 +80,7 @@ working=false, response rows=1, 80x11 • Done. └ Tip: Try /help. - [completion time] + Worked for [duration] • [completion time] › Ask Codex to do anything @@ -90,11 +89,11 @@ working=false, response rows=1, 80x11 ? for shortcuts working=false, response rows=1, 12x12 -Response row -0 • Done. + Worked + for <1s • [completion time] diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__pagination_completion_tests__older_pagination_completion_footers_follow_answers_without_overlap_duplicates.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__pagination_completion_tests__older_pagination_completion_footers_follow_answers_without_overlap_duplicates.snap index eedea6813b..2328726bb2 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__pagination_completion_tests__older_pagination_completion_footers_follow_answers_without_overlap_duplicates.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__pagination_completion_tests__older_pagination_completion_footers_follow_answers_without_overlap_duplicates.snap @@ -1,11 +1,11 @@ --- source: tui/src/app/tests/pagination_completion_tests.rs -expression: "snapshots.join(\"\\n\\n--- next overlapping page ---\\n\\n\")" +expression: "afternoon_clock.replace_all(&snapshots.join(\"\\n\\n--- next overlapping page ---\\n\\n\"),\n\"at 2:$1 PM\",)" --- Earlier messages are available — press ctrl+t to view the full transcript • Middle answer - Worked for 2m 5s · Sep 6, 2000 at 2:33 PM + Worked for 2m 5s • Sep 6, 2000 at 2:33 PM --- next overlapping page --- @@ -15,7 +15,7 @@ Earlier messages are available — press ctrl+t to view the full transcript • Oldest answer - Worked for 2m 5s · Sep 6, 2000 at 2:32 PM + Worked for 2m 5s • Sep 6, 2000 at 2:32 PM › Middle prompt @@ -23,4 +23,4 @@ Earlier messages are available — press ctrl+t to view the full transcript • Middle answer - Worked for 2m 5s · Sep 6, 2000 at 2:33 PM + Worked for 2m 5s • Sep 6, 2000 at 2:33 PM diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__safety_buffering__safety_retry_committed_steer_history.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__safety_buffering__safety_retry_committed_steer_history.snap index d6b4a05039..94774a3f19 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__safety_buffering__safety_retry_committed_steer_history.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__safety_buffering__safety_retry_committed_steer_history.snap @@ -1,7 +1,8 @@ --- source: tui/src/app/tests/safety_buffering.rs +assertion_line: 960 expression: rendered_retry --- › Handle the safety-buffered request Keep the accepted steer -⚠ Full-history hydration is deprecated for paginated threads; omit `includeTurns` or set it to `false`, then page with `thread/turns/list` and `thread/items/list`.• done [completion time] +⚠ Full-history hydration is deprecated for paginated threads; omit `includeTurns` or set it to `false`, then page with `thread/turns/list` and `thread/items/list`.• done Worked for [duration] • [completion time] diff --git a/codex-rs/tui/src/app/turn_tips_tests.rs b/codex-rs/tui/src/app/turn_tips_tests.rs index 919887145d..070cfe534a 100644 --- a/codex-rs/tui/src/app/turn_tips_tests.rs +++ b/codex-rs/tui/src/app/turn_tips_tests.rs @@ -17,7 +17,7 @@ fn notification(method: &str, thread: ThreadId, turn: usize, status: &str) -> Se "method": method, "params": { "threadId": thread.to_string(), "turn": { "id": turn.to_string(), "items": [], "itemsView": "full", "status": status, - "error": null, "startedAt": null, "completedAt": null, "durationMs": null, + "error": null, "startedAt": null, "completedAt": null, "durationMs": 500, }}, })) .unwrap() diff --git a/codex-rs/tui/src/chatwidget/completion.rs b/codex-rs/tui/src/chatwidget/completion.rs index 1778b49135..3b59e3066e 100644 --- a/codex-rs/tui/src/chatwidget/completion.rs +++ b/codex-rs/tui/src/chatwidget/completion.rs @@ -28,8 +28,7 @@ impl ChatWidget { } else { None } - }) - .filter(|seconds| *seconds > 60); + }); let completed_at = turn .completed_at .and_then(|timestamp| chrono::DateTime::from_timestamp(timestamp, /*nsecs*/ 0)) diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_markdown_code_blocks_vt100_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_markdown_code_blocks_vt100_snapshot.snap index 1cf72689d0..5ddc6d4830 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_markdown_code_blocks_vt100_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_markdown_code_blocks_vt100_snapshot.snap @@ -1,6 +1,7 @@ --- source: tui/src/chatwidget/tests/status_and_layout.rs -expression: "normalize_completion_timestamps(&\n(normalize_snapshot_paths(term.backend().vt100().screen().contents())))" +assertion_line: 5824 +expression: normalize_snapshot_paths(term.backend().vt100().screen().contents()) --- @@ -50,4 +51,4 @@ expression: "normalize_completion_timestamps(&\n(normalize_snapshot_paths(term.b "path": "C:\\Program Files\\App", "regex": "^foo.*(bar)?$" } - [completion time] + Worked for [duration] • [completion time] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completion_after_plain_answer.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completion_after_plain_answer.snap index 6cb6d51d2c..7ba0f20925 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completion_after_plain_answer.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completion_after_plain_answer.snap @@ -1,7 +1,7 @@ --- source: tui/src/chatwidget/tests/completion_styling_tests.rs -expression: normalize_completion_timestamps(& (text)) +expression: text --- • The change is ready. - Worked for [duration] · [completion time] + Worked for [duration] • [completion time] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completion_after_streamed_tool_answer.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completion_after_streamed_tool_answer.snap index a546d2a849..01ca8ec133 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completion_after_streamed_tool_answer.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__completion_after_streamed_tool_answer.snap @@ -1,10 +1,10 @@ --- source: tui/src/chatwidget/tests/completion_styling_tests.rs -expression: normalize_completion_timestamps(& (text)) +expression: text --- • Ran echo ready └ ready • The change is ready. - Worked for [duration] · [completion time] + Worked for [duration] • [completion time] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__final_worked_for_uses_cumulative_turn_duration.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__final_worked_for_uses_cumulative_turn_duration.snap index a420630cb3..16eb4559d8 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__final_worked_for_uses_cumulative_turn_duration.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__final_worked_for_uses_cumulative_turn_duration.snap @@ -1,10 +1,10 @@ --- source: tui/src/chatwidget/tests/exec_flow.rs -expression: normalize_completion_timestamps(& (combined)) +expression: combined --- • Ran echo preparing └ preparing • Final response. - Worked for [duration] · [completion time] + Worked for [duration] • [completion time] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_export_completion_message.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_export_completion_message.snap index 09c60318a4..0d4cc390d5 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_export_completion_message.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_export_completion_message.snap @@ -1,8 +1,9 @@ --- source: tui/src/chatwidget/tests/slash_commands.rs -expression: "normalize_completion_timestamps(&\n(cells.iter().map(|cell|\nlines_to_single_string(cell).trim().to_string()).collect::>().join(\"\\n\")))" +assertion_line: 2193 +expression: "cells.iter().map(|cell|\nlines_to_single_string(cell).trim().to_string()).collect::>().join(\"\\n\")" --- -[completion time] +Worked for [duration] • [completion time] • Saved conversation to conversation.md • Copied conversation to clipboard ■ Copy failed: clipboard unavailable diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_non_empty_then_empty_after.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_non_empty_then_empty_after.snap index 47da08676a..56d0cf87ec 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_non_empty_then_empty_after.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_non_empty_then_empty_after.snap @@ -1,10 +1,11 @@ --- source: tui/src/chatwidget/tests/exec_flow.rs -expression: normalize_completion_timestamps(& (combined)) +assertion_line: 1309 +expression: combined --- ↳ Interacted with background terminal · just fix └ pwd • Waited for background terminal · just fix - [completion time] + Worked for [duration] • [completion time] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_after_final_agent_message.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_after_final_agent_message.snap index 3c7394baec..12b80d4b1c 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_after_final_agent_message.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_after_final_agent_message.snap @@ -1,9 +1,10 @@ --- source: tui/src/chatwidget/tests/exec_flow.rs -expression: normalize_completion_timestamps(& (combined)) +assertion_line: 1068 +expression: combined --- • Waited for background terminal · cargo test -p codex-core • Final response. - [completion time] + Worked for [duration] • [completion time] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_before_streamed_agent_message.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_before_streamed_agent_message.snap index 6787e58365..7fddebca2e 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_before_streamed_agent_message.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_wait_before_streamed_agent_message.snap @@ -1,9 +1,10 @@ --- source: tui/src/chatwidget/tests/exec_flow.rs -expression: normalize_completion_timestamps(& (combined)) +assertion_line: 1092 +expression: combined --- • Waited for background terminal · cargo test -p codex-core • Streaming response. - [completion time] + Worked for [duration] • [completion time] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_waiting_multiple_empty_after.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_waiting_multiple_empty_after.snap index cbe7b03f99..82b401feb2 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_waiting_multiple_empty_after.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__unified_exec_waiting_multiple_empty_after.snap @@ -1,10 +1,11 @@ --- source: tui/src/chatwidget/tests/exec_flow.rs +assertion_line: 1229 expression: combined --- Chat: - [completion time] + Worked for [duration] • [completion time] Transcript: • Waited for background terminal · just fix - [completion time] + Worked for [duration] • [completion time] diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 5853c4138f..e03e997d85 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -936,7 +936,10 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item( .iter() .map(|lines| lines_to_single_string(lines).trim().to_string()) .collect::>(); - assert_eq!(completion_cells, vec!["[completion time]"]); + assert_eq!( + completion_cells, + vec!["Worked for [duration] • [completion time]"] + ); assert!(!chat.bottom_pane.is_task_running()); assert!(chat.bottom_pane.status_widget().is_none()); assert_eq!( diff --git a/codex-rs/tui/src/chatwidget/tests/completion_styling_tests.rs b/codex-rs/tui/src/chatwidget/tests/completion_styling_tests.rs index acc4d28d40..6838e3a6a4 100644 --- a/codex-rs/tui/src/chatwidget/tests/completion_styling_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/completion_styling_tests.rs @@ -93,15 +93,15 @@ async fn completion_follows_plain_and_streamed_tool_answers() { } #[tokio::test] -async fn completion_live_applies_duration_threshold_and_preserves_timestamp_fallback() { +async fn completion_live_shows_known_durations_and_preserves_timestamp_fallback() { for (duration_ms, completed_at, prefix) in [ - (598, Some(COMPLETED_AT), ""), - (1_000, Some(COMPLETED_AT), ""), - (60_000, Some(COMPLETED_AT), ""), - (60_999, Some(COMPLETED_AT), ""), - (61_000, Some(COMPLETED_AT), "Worked for 1m 1s · "), - (125_000, Some(COMPLETED_AT), "Worked for 2m 5s · "), - (1_000, None, ""), + (598, Some(COMPLETED_AT), "Worked for <1s • "), + (1_000, Some(COMPLETED_AT), "Worked for 1s • "), + (60_000, Some(COMPLETED_AT), "Worked for 1m 0s • "), + (60_999, Some(COMPLETED_AT), "Worked for 1m 0s • "), + (61_000, Some(COMPLETED_AT), "Worked for 1m 1s • "), + (125_000, Some(COMPLETED_AT), "Worked for 2m 5s • "), + (1_000, None, "Worked for 1s • "), ] { let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await; handle_turn_started(&mut chat, "turn-1"); @@ -131,17 +131,26 @@ async fn completion_replay_preserves_metadata_and_input_without_live_side_effect ( Some(125_000), Some(COMPLETED_AT), - format!("Worked for 2m 5s · {done}"), + format!("Worked for 2m 5s • {done}"), ), (None, None, String::new()), - (Some(598), Some(COMPLETED_AT), done.clone()), - (Some(60_000), Some(COMPLETED_AT), done.clone()), + (None, Some(COMPLETED_AT), done.clone()), + ( + Some(598), + Some(COMPLETED_AT), + format!("Worked for <1s • {done}"), + ), + ( + Some(60_000), + Some(COMPLETED_AT), + format!("Worked for 1m 0s • {done}"), + ), ( Some(61_000), Some(COMPLETED_AT), - format!("Worked for 1m 1s · {done}"), + format!("Worked for 1m 1s • {done}"), ), - (Some(60_000), None, String::new()), + (Some(60_000), None, "Worked for 1m 0s".to_string()), (Some(125_000), None, "Worked for 2m 5s".to_string()), ] { for replay_kind in [ @@ -190,7 +199,10 @@ async fn completion_replay_waits_for_older_turn_items_to_load() { let newer = completed_turn(Some(1_000), Some(COMPLETED_AT)); chat.replay_thread_turns(vec![unloaded, newer], ReplayKind::ResumeInitialMessages); - assert_eq!(completion_labels(&mut rx), saved_completion_label()); + assert_eq!( + completion_labels(&mut rx), + format!("Worked for 1s • {}", saved_completion_label()) + ); chat.replay_thread_turns(vec![older], ReplayKind::ThreadSnapshot); let older_time = Local @@ -203,7 +215,7 @@ async fn completion_replay_waits_for_older_turn_items_to_load() { .to_string(); assert_eq!( completion_labels(&mut rx), - format!("Worked for 2m 5s · {older_time}"), + format!("Worked for 2m 5s • {older_time}"), ); } @@ -265,7 +277,7 @@ fn completion_snapshot_normalization_preserves_clock_only_message_lines() { &footer, lines_to_single_string(&footer.display_lines(/*width*/ 80)) ), - " Worked for [duration] · [completion time]\n" + " Worked for [duration] • [completion time]\n" ); } } diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 7272f282a1..6565e1e4d1 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -1829,7 +1829,7 @@ pub(crate) fn normalize_completion_timestamps( } static COMPLETION_FOOTER: std::sync::LazyLock = std::sync::LazyLock::new( || { - regex_lite::Regex::new(r"(?m)^(?P[ \t]*)(?PWorked for (?:[0-9]+h )?(?:[0-9]+m )?[0-9]+s · )?(?:[A-Z][a-z]{2} [0-9]{1,2}(?:, [0-9]{4})? at )?[0-9]{1,2}:[0-9]{2}(?: (?:AM|PM))?(?P[ \t]*)$") + regex_lite::Regex::new(r"(?m)^(?P[ \t]*)(?PWorked for (?:<1s|(?:[0-9]+h )?(?:[0-9]+m )?[0-9]+s) • )?(?:[A-Z][a-z]{2} [0-9]{1,2}(?:, [0-9]{4})? at )?[0-9]{1,2}:[0-9]{2}(?: (?:AM|PM))?(?P[ \t]*)$") .expect("valid completion footer pattern") }, ); @@ -1838,7 +1838,7 @@ pub(crate) fn normalize_completion_timestamps( let indent = &captures["indent"]; let padding = &captures["padding"]; let duration = if captures.name("duration").is_some() { - "Worked for [duration] · " + "Worked for [duration] • " } else { "" }; diff --git a/codex-rs/tui/src/history_cell/separators.rs b/codex-rs/tui/src/history_cell/separators.rs index c1df99746f..7c398870a9 100644 --- a/codex-rs/tui/src/history_cell/separators.rs +++ b/codex-rs/tui/src/history_cell/separators.rs @@ -11,7 +11,7 @@ use chrono::NaiveDate; /// /// The timestamp records when the turn actually finished, including when restored from history. /// Times use the host's clock preference; other local days include the date and other years the year. -/// Durations are shown only above sixty seconds; shorter turns still show their timestamp. +/// Known durations are always shown; sub-second durations display as less than one second. /// Absent metadata occupies no transcript rows. /// The display date is fixed at construction so crossing midnight cannot invalidate cached heights; /// restoring the conversation constructs new cells and refreshes whether the timestamp needs a date. @@ -58,7 +58,7 @@ impl FinalMessageSeparator { fn label(&self, today: NaiveDate) -> Option { let mut label_parts = Vec::new(); - if let Some(elapsed_seconds) = self.elapsed_seconds.filter(|seconds| *seconds > 60) { + if let Some(elapsed_seconds) = self.elapsed_seconds { let hours = elapsed_seconds / 3_600; let minutes = (elapsed_seconds % 3_600) / 60; let seconds = elapsed_seconds % 60; @@ -66,6 +66,8 @@ impl FinalMessageSeparator { format!("{hours}h {minutes}m {seconds}s") } else if minutes > 0 { format!("{minutes}m {seconds}s") + } else if seconds == 0 { + "<1s".to_string() } else { format!("{seconds}s") }; @@ -88,7 +90,7 @@ impl FinalMessageSeparator { if let Some(metrics_label) = self.runtime_metrics.and_then(runtime_metrics_label) { label_parts.push(metrics_label); } - (!label_parts.is_empty()).then(|| label_parts.join(" · ")) + (!label_parts.is_empty()).then(|| label_parts.join(" • ")) } } impl HistoryCell for FinalMessageSeparator { diff --git a/codex-rs/tui/src/history_cell/separators_tests.rs b/codex-rs/tui/src/history_cell/separators_tests.rs index 4770cb324c..06b90c13df 100644 --- a/codex-rs/tui/src/history_cell/separators_tests.rs +++ b/codex-rs/tui/src/history_cell/separators_tests.rs @@ -19,7 +19,7 @@ fn completed_at() -> DateTime { } #[test] -fn completion_label_shows_duration_only_above_sixty_seconds() { +fn completion_label_shows_known_durations_including_short_turns() { let completed_at = completed_at(); let labels = [ None, @@ -41,12 +41,12 @@ fn completion_label_shows_duration_only_above_sixty_seconds() { insta::assert_snapshot!(labels.join("\n"), @" 2:32 PM - 2:32 PM - 2:32 PM - 2:32 PM - Worked for 1m 1s · 2:32 PM - Worked for 2m 5s · 2:32 PM - Worked for 1h 0m 5s · 2:32 PM + Worked for <1s • 2:32 PM + Worked for 12s • 2:32 PM + Worked for 1m 0s • 2:32 PM + Worked for 1m 1s • 2:32 PM + Worked for 2m 5s • 2:32 PM + Worked for 1h 0m 5s • 2:32 PM "); } @@ -60,9 +60,9 @@ fn completion_label_includes_date_when_viewed_on_another_day() { .with_completed_at(completed_at, ClockFormat::TwelveHour); let tomorrow = completed_at.date_naive().succ_opt().expect("next day"); - insta::assert_snapshot!(cell.label(tomorrow).expect("completion label"), @"Worked for 2m 5s · Sep 6 at 2:32 PM"); + insta::assert_snapshot!(cell.label(tomorrow).expect("completion label"), @"Worked for 2m 5s • Sep 6 at 2:32 PM"); let next_year = tomorrow.with_year(/*year*/ 2001).expect("valid next year"); - insta::assert_snapshot!(cell.label(next_year).expect("completion label"), @"Worked for 2m 5s · Sep 6, 2000 at 2:32 PM"); + insta::assert_snapshot!(cell.label(next_year).expect("completion label"), @"Worked for 2m 5s • Sep 6, 2000 at 2:32 PM"); } #[test] @@ -149,12 +149,12 @@ fn completion_wraps_metadata_and_preserves_unwrapped_raw_text() { let rendered = lines.iter().map(ToString::to_string).collect::>(); insta::assert_snapshot!(rendered.join("\n"), @r" - Worked for 2m 5s · Sep - 6, 2000 at 2:32 PM · + Worked for 2m 5s • Sep + 6, 2000 at 2:32 PM • Local tools: 3 calls (2.5s) "); - insta::assert_snapshot!(cell.raw_lines()[0].to_string(), @"Worked for 2m 5s · Sep 6, 2000 at 2:32 PM · Local tools: 3 calls (2.5s)"); + insta::assert_snapshot!(cell.raw_lines()[0].to_string(), @"Worked for 2m 5s • Sep 6, 2000 at 2:32 PM • Local tools: 3 calls (2.5s)"); for width in [0, 1, 5, 24] { assert!( cell.display_lines(width) diff --git a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__separators__tests__completion_uses_twenty_four_hour_time_with_dates_and_wrapping.snap b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__separators__tests__completion_uses_twenty_four_hour_time_with_dates_and_wrapping.snap index 0897371be0..f865c4ecc4 100644 --- a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__separators__tests__completion_uses_twenty_four_hour_time_with_dates_and_wrapping.snap +++ b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__separators__tests__completion_uses_twenty_four_hour_time_with_dates_and_wrapping.snap @@ -1,20 +1,19 @@ --- source: tui/src/history_cell/separators_tests.rs -assertion_line: 101 expression: "labels.join(\"\\n\")" --- -Worked for 2m 5s · 00:25 -Worked for 2m 5s · Sep 6 at 00:25 -Worked for 2m 5s · Sep 6, 2000 at 00:25 - Worked for 2m 5s · Sep +Worked for 2m 5s • 00:25 +Worked for 2m 5s • Sep 6 at 00:25 +Worked for 2m 5s • Sep 6, 2000 at 00:25 + Worked for 2m 5s • Sep 6, 2000 at 00:25 -Worked for 2m 5s · 12:25 -Worked for 2m 5s · Sep 6 at 12:25 -Worked for 2m 5s · Sep 6, 2000 at 12:25 - Worked for 2m 5s · Sep +Worked for 2m 5s • 12:25 +Worked for 2m 5s • Sep 6 at 12:25 +Worked for 2m 5s • Sep 6, 2000 at 12:25 + Worked for 2m 5s • Sep 6, 2000 at 12:25 -Worked for 2m 5s · 23:25 -Worked for 2m 5s · Sep 6 at 23:25 -Worked for 2m 5s · Sep 6, 2000 at 23:25 - Worked for 2m 5s · Sep +Worked for 2m 5s • 23:25 +Worked for 2m 5s • Sep 6 at 23:25 +Worked for 2m 5s • Sep 6, 2000 at 23:25 + Worked for 2m 5s • Sep 6, 2000 at 23:25 diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 3d841f5eb4..03c953fad7 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -696,7 +696,7 @@ fn final_message_separator_preserves_runtime_metrics_for_short_turns() { let rendered = render_lines(&cell.display_lines(/*width*/ 600)); assert_eq!(rendered.len(), 1); - assert!(rendered[0].starts_with(" Local tools:")); + assert!(rendered[0].starts_with(" Worked for 12s • Local tools:")); assert!(rendered[0].contains("Local tools: 3 calls (2.5s)")); assert!(rendered[0].contains("Inference: 2 calls (1.2s)")); assert!(rendered[0].contains("WebSocket: 1 events send (700ms)"));