From c3953649156e15b67e572cb9e38bc825a811c24e Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Tue, 25 Aug 2026 14:35:12 +0000 Subject: [PATCH] Keep the active goal clock current in the TUI (#40622) ## Why The active goal's elapsed-time label could become stale while the TUI was idle because it was refreshed only as part of a redraw. ## What changed - Refresh the goal status indicator when the terminal-title timer fires. - Request a redraw only when the displayed elapsed-time label changes. ## Testing - Add a regression test covering the minute transition and confirming that an unchanged label does not trigger another redraw. GitOrigin-RevId: f10a1c2334cf7a5f1d2ee90d7138114479998c8d --- codex-rs/tui/src/app/startup.rs | 1 + codex-rs/tui/src/chatwidget/settings.rs | 2 +- .../src/chatwidget/tests/terminal_title.rs | 59 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/app/startup.rs b/codex-rs/tui/src/app/startup.rs index d17f58a30d..ece191b3c6 100644 --- a/codex-rs/tui/src/app/startup.rs +++ b/codex-rs/tui/src/app/startup.rs @@ -808,6 +808,7 @@ See the Codex keymap documentation for supported actions and examples." None => std::future::pending().await, } } => { + app.chat_widget.refresh_goal_status_indicator_for_time_tick(); app.chat_widget.refresh_terminal_title(); AppRunControl::Continue } diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index 9ab73e1145..d11445895b 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -583,7 +583,7 @@ impl ChatWidget { self.bottom_pane.set_goal_status_indicator(goal_indicator); } - pub(super) fn refresh_goal_status_indicator_for_time_tick(&mut self) { + pub(crate) fn refresh_goal_status_indicator_for_time_tick(&mut self) { if self.collaboration_mode_indicator().is_some() { return; } diff --git a/codex-rs/tui/src/chatwidget/tests/terminal_title.rs b/codex-rs/tui/src/chatwidget/tests/terminal_title.rs index 731fe0e593..8a7a438a6c 100644 --- a/codex-rs/tui/src/chatwidget/tests/terminal_title.rs +++ b/codex-rs/tui/src/chatwidget/tests/terminal_title.rs @@ -1,8 +1,67 @@ //! Terminal-title focused tests for live chatwidget status-surface behavior. use super::*; +use crate::bottom_pane::goal_status_indicator_line; use pretty_assertions::assert_eq; +#[tokio::test] +async fn goal_clock_refresh_redraws_only_when_elapsed_label_changes() { + let (frame_requester, mut draw_rx) = FrameRequester::test_channel(); + let (mut chat, _rx, _op_rx) = make_chatwidget_manual_with_auth( + /*model_override*/ None, + /*has_chatgpt_account*/ false, + /*has_codex_backend_auth*/ false, + frame_requester, + ) + .await; + chat.set_feature_enabled(Feature::Goals, /*enabled*/ true); + chat.bottom_pane.set_task_running(/*running*/ true); + chat.bottom_pane.hide_status_indicator(); + let observed_at = Instant::now() - Duration::from_secs(/*secs*/ 90); + chat.turn_lifecycle.goal_status_active_turn_started_at = Some(observed_at); + let goal = AppThreadGoal { + thread_id: "thread-1".to_string(), + objective: "Keep improving the benchmark".to_string(), + status: AppThreadGoalStatus::Active, + token_budget: None, + tokens_used: 0, + time_used_seconds: 60, + created_at: 0, + updated_at: 0, + }; + chat.on_thread_goal_updated(goal.clone(), /*turn_id*/ None); + let initial_indicator = chat.current_goal_status_indicator.clone(); + while draw_rx.try_recv().is_ok() {} + + chat.current_goal_status = Some(GoalStatusState::new(goal, observed_at)); + chat.refresh_goal_status_indicator_for_time_tick(); + chat.refresh_terminal_title(); + assert!(draw_rx.try_recv().is_ok()); + assert!(chat.terminal_title_next_refresh.is_some()); + + chat.refresh_goal_status_indicator_for_time_tick(); + chat.refresh_terminal_title(); + assert_eq!( + draw_rx.try_recv(), + Err(tokio::sync::mpsc::error::TryRecvError::Empty) + ); + let labels = [ + initial_indicator.as_ref(), + chat.current_goal_status_indicator.as_ref(), + ] + .map(|indicator| { + let line = goal_status_indicator_line(indicator).expect("active goal indicator"); + line.spans + .iter() + .map(|span| span.content.as_ref()) + .collect::() + }); + insta::assert_snapshot!(labels.join("\n"), @r" + Pursuing goal (1m) + Pursuing goal (2m) + "); +} + #[tokio::test] async fn terminal_title_shows_action_required_while_exec_approval_is_pending() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;