diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 851668728..c0d600732 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -136,6 +136,7 @@ impl BottomPane { self.request_redraw(); } + #[cfg(test)] pub fn status_widget(&self) -> Option<&StatusIndicatorWidget> { self.status.as_ref() } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 82f00f9ca..d7a451e82 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4,6 +4,7 @@ use std::collections::VecDeque; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use std::time::Instant; use codex_app_server_protocol::AuthMode; use codex_backend_client::Client as BackendClient; @@ -333,6 +334,8 @@ pub(crate) struct ChatWidget { feedback: codex_feedback::CodexFeedback, // Current session rollout path (if known) current_rollout_path: Option, + // Current task start time + task_started_at: Option, } struct UserMessage { @@ -518,17 +521,20 @@ impl ChatWidget { self.set_status_header(String::from("Working")); self.full_reasoning_buffer.clear(); self.reasoning_buffer.clear(); + self.task_started_at = Some(Instant::now()); self.request_redraw(); } fn on_task_complete(&mut self, last_agent_message: Option) { // If a stream is currently active, finalize it. self.flush_answer_stream_with_separator(); + self.add_final_message_separator(); // Mark task stopped and request redraw now that all content is in history. self.bottom_pane.set_task_running(false); self.running_commands.clear(); self.suppressed_exec_calls.clear(); self.last_unified_wait = None; + self.task_started_at = None; self.request_redraw(); // If there is a queued user message, send exactly one now to begin the next turn. @@ -664,6 +670,7 @@ impl ChatWidget { self.suppressed_exec_calls.clear(); self.last_unified_wait = None; self.stream_controller = None; + self.task_started_at = None; self.maybe_show_pending_rate_limit_prompt(); } pub(crate) fn get_model_family(&self) -> ModelFamily { @@ -1007,14 +1014,6 @@ impl ChatWidget { self.flush_active_cell(); if self.stream_controller.is_none() { - if self.needs_final_message_separator { - let elapsed_seconds = self - .bottom_pane - .status_widget() - .map(super::status_indicator_widget::StatusIndicatorWidget::elapsed_seconds); - self.add_to_history(history_cell::FinalMessageSeparator::new(elapsed_seconds)); - self.needs_final_message_separator = false; - } self.stream_controller = Some(StreamController::new( self.last_rendered_width.get().map(|w| w.saturating_sub(2)), )); @@ -1027,6 +1026,16 @@ impl ChatWidget { self.request_redraw(); } + fn add_final_message_separator(&mut self) { + if self.needs_final_message_separator { + let elapsed_seconds = self + .task_started_at + .map(|start_time| start_time.elapsed().as_secs()); + self.add_to_history(history_cell::FinalMessageSeparator::new(elapsed_seconds)); + self.needs_final_message_separator = false; + } + } + 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) { @@ -1330,6 +1339,7 @@ impl ChatWidget { last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, + task_started_at: None, }; widget.prefetch_rate_limits(); @@ -1415,6 +1425,7 @@ impl ChatWidget { last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, + task_started_at: None, }; widget.prefetch_rate_limits(); 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 1ed73b5fa..8f9b55927 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,6 @@ --- source: tui/src/chatwidget/tests.rs -expression: visual +expression: term.backend().vt100().screen().contents() --- • -- Indented code block (4 spaces) SELECT * @@ -16,3 +16,4 @@ expression: visual "path": "C:\\Program Files\\App", "regex": "^foo.*(bar)?$" } +─ Worked for 0s ──────────────────────────────────────────────────────────────── diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index bd85a9edc..f03346ee5 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -442,6 +442,7 @@ fn make_chatwidget_manual( last_rendered_width: std::cell::Cell::new(None), feedback: codex_feedback::CodexFeedback::new(), current_rollout_path: None, + task_started_at: None, }; (widget, rx, op_rx) } diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index 642b9ca2b..e850c32fe 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -125,13 +125,10 @@ impl StatusIndicatorWidget { elapsed } + #[cfg(test)] fn elapsed_seconds_at(&self, now: Instant) -> u64 { self.elapsed_duration_at(now).as_secs() } - - pub fn elapsed_seconds(&self) -> u64 { - self.elapsed_seconds_at(Instant::now()) - } } impl Renderable for StatusIndicatorWidget {