diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a004c472f89..7f0a4d410c8 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -484,6 +484,11 @@ pub(crate) struct ChatWidget { // This gates rendering of the "Worked for …" separator so purely conversational turns don't // show an empty divider. It is reset when the separator is emitted. had_work_activity: bool, + // Status-indicator elapsed seconds captured at the last emitted final-message separator. + // + // This lets the separator show per-chunk work time (since the previous separator) rather than + // the total task-running time reported by the status indicator. + last_separator_elapsed_secs: Option, last_rendered_width: std::cell::Cell>, // Feedback sink for /feedback @@ -1528,7 +1533,8 @@ impl ChatWidget { let elapsed_seconds = self .bottom_pane .status_widget() - .map(super::status_indicator_widget::StatusIndicatorWidget::elapsed_seconds); + .map(super::status_indicator_widget::StatusIndicatorWidget::elapsed_seconds) + .map(|current| self.worked_elapsed_from(current)); self.add_to_history(history_cell::FinalMessageSeparator::new(elapsed_seconds)); self.needs_final_message_separator = false; self.had_work_activity = false; @@ -1548,6 +1554,17 @@ impl ChatWidget { self.request_redraw(); } + fn worked_elapsed_from(&mut self, current_elapsed: u64) -> u64 { + let baseline = match self.last_separator_elapsed_secs { + Some(last) if current_elapsed < last => 0, + Some(last) => last, + None => 0, + }; + let elapsed = current_elapsed.saturating_sub(baseline); + self.last_separator_elapsed_secs = Some(current_elapsed); + elapsed + } + pub(crate) fn handle_exec_end_now(&mut self, ev: ExecCommandEndEvent) { let running = self.running_commands.remove(&ev.call_id); if self.suppressed_exec_calls.remove(&ev.call_id) { @@ -1884,6 +1901,7 @@ impl ChatWidget { pre_review_token_info: None, needs_final_message_separator: false, had_work_activity: false, + last_separator_elapsed_secs: None, last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, @@ -1998,6 +2016,7 @@ impl ChatWidget { pre_review_token_info: None, needs_final_message_separator: false, had_work_activity: false, + last_separator_elapsed_secs: None, last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 0610a640c76..8ec734ee43a 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -817,6 +817,7 @@ async fn make_chatwidget_manual( pre_review_token_info: None, needs_final_message_separator: false, had_work_activity: false, + last_separator_elapsed_secs: None, last_rendered_width: std::cell::Cell::new(None), feedback: codex_feedback::CodexFeedback::new(), current_rollout_path: None, @@ -847,6 +848,16 @@ fn set_chatgpt_auth(chat: &mut ChatWidget) { )); } +#[tokio::test] +async fn worked_elapsed_from_resets_when_timer_restarts() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; + assert_eq!(chat.worked_elapsed_from(5), 5); + assert_eq!(chat.worked_elapsed_from(9), 4); + // Simulate status timer resetting (e.g., status indicator recreated for a new task). + assert_eq!(chat.worked_elapsed_from(3), 3); + assert_eq!(chat.worked_elapsed_from(7), 4); +} + pub(crate) async fn make_chatwidget_manual_with_sender() -> ( ChatWidget, AppEventSender, diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index 9888128baa1..7921ccf4455 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -430,6 +430,11 @@ pub(crate) struct ChatWidget { // This gates rendering of the "Worked for …" separator so purely conversational turns don't // show an empty divider. It is reset when the separator is emitted. had_work_activity: bool, + // Status-indicator elapsed seconds captured at the last emitted final-message separator. + // + // This lets the separator show per-chunk work time (since the previous separator) rather than + // the total task-running time reported by the status indicator. + last_separator_elapsed_secs: Option, last_rendered_width: std::cell::Cell>, // Feedback sink for /feedback @@ -1330,7 +1335,8 @@ impl ChatWidget { let elapsed_seconds = self .bottom_pane .status_widget() - .map(super::status_indicator_widget::StatusIndicatorWidget::elapsed_seconds); + .map(super::status_indicator_widget::StatusIndicatorWidget::elapsed_seconds) + .map(|current| self.worked_elapsed_from(current)); self.add_to_history(history_cell::FinalMessageSeparator::new(elapsed_seconds)); self.needs_final_message_separator = false; self.had_work_activity = false; @@ -1353,6 +1359,17 @@ impl ChatWidget { } } + fn worked_elapsed_from(&mut self, current_elapsed: u64) -> u64 { + let baseline = match self.last_separator_elapsed_secs { + Some(last) if current_elapsed < last => 0, + Some(last) => last, + None => 0, + }; + let elapsed = current_elapsed.saturating_sub(baseline); + self.last_separator_elapsed_secs = Some(current_elapsed); + elapsed + } + pub(crate) fn handle_exec_end_now(&mut self, ev: ExecCommandEndEvent) { let running = self.running_commands.remove(&ev.call_id); if self.suppressed_exec_calls.remove(&ev.call_id) { @@ -1686,6 +1703,7 @@ impl ChatWidget { pre_review_token_info: None, needs_final_message_separator: false, had_work_activity: false, + last_separator_elapsed_secs: None, last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, @@ -1798,6 +1816,7 @@ impl ChatWidget { pre_review_token_info: None, needs_final_message_separator: false, had_work_activity: false, + last_separator_elapsed_secs: None, last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index 005b015a677..01f7a239f3d 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -804,6 +804,7 @@ async fn make_chatwidget_manual( pre_review_token_info: None, needs_final_message_separator: false, had_work_activity: false, + last_separator_elapsed_secs: None, last_rendered_width: std::cell::Cell::new(None), feedback: codex_feedback::CodexFeedback::new(), current_rollout_path: None, @@ -833,6 +834,16 @@ fn set_chatgpt_auth(chat: &mut ChatWidget) { )); } +#[tokio::test] +async fn worked_elapsed_from_resets_when_timer_restarts() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; + assert_eq!(chat.worked_elapsed_from(5), 5); + assert_eq!(chat.worked_elapsed_from(9), 4); + // Simulate status timer resetting (e.g., status indicator recreated for a new task). + assert_eq!(chat.worked_elapsed_from(3), 3); + assert_eq!(chat.worked_elapsed_from(7), 4); +} + pub(crate) async fn make_chatwidget_manual_with_sender() -> ( ChatWidget, AppEventSender,