fix(tui) turn timing incremental (#9599)

## Summary
When we send multiple assistant messages, reset the timer so "Worked for
2m 36s" is the time since the last time we showed the message, rather
than an ever-increasing number.

We could instead change the copy so it's more clearly a running counter.

## Testing
- [x] ran locally

<img width="903" height="732" alt="Screenshot 2026-01-21 at 1 42 51 AM"
src="https://github.com/user-attachments/assets/bb4d827b-3a0e-48ba-bd6a-d8cd65d8e892"
/>
This commit is contained in:
Dylan Hurd 2026-01-21 15:59:56 -08:00 committed by GitHub
parent 5dad1b956e
commit f1240ff4fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 62 additions and 2 deletions

View file

@ -488,6 +488,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<u64>,
last_rendered_width: std::cell::Cell<Option<usize>>,
// Feedback sink for /feedback
@ -1542,7 +1547,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;
@ -1562,6 +1568,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) {
@ -1906,6 +1923,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,
@ -2022,6 +2040,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,

View file

@ -838,6 +838,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,
@ -868,6 +869,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,

View file

@ -433,6 +433,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<u64>,
last_rendered_width: std::cell::Cell<Option<usize>>,
// Feedback sink for /feedback
@ -1333,7 +1338,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;
@ -1356,6 +1362,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) {
@ -1691,6 +1708,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,
@ -1805,6 +1823,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,

View file

@ -826,6 +826,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,
@ -855,6 +856,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,