diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index b6c93d28..c65067b5 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -102,6 +102,10 @@ impl BottomPane { } } + pub fn status_widget(&self) -> Option<&StatusIndicatorWidget> { + self.status.as_ref() + } + fn active_view(&self) -> Option<&dyn BottomPaneView> { self.view_stack.last().map(std::convert::AsRef::as_ref) } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 85ecad28..9a36addb 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -254,6 +254,8 @@ pub(crate) struct ChatWidget { // List of ghost commits corresponding to each turn. ghost_snapshots: Vec, ghost_snapshots_disabled: bool, + // Whether to add a final message separator after the last message + needs_final_message_separator: bool, } struct UserMessage { @@ -648,6 +650,14 @@ 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.config.clone())); } if let Some(controller) = self.stream_controller.as_mut() @@ -901,6 +911,7 @@ impl ChatWidget { is_review_mode: false, ghost_snapshots: Vec::new(), ghost_snapshots_disabled: true, + needs_final_message_separator: false, } } @@ -962,6 +973,7 @@ impl ChatWidget { is_review_mode: false, ghost_snapshots: Vec::new(), ghost_snapshots_disabled: true, + needs_final_message_separator: false, } } @@ -1188,6 +1200,7 @@ impl ChatWidget { fn flush_active_cell(&mut self) { if let Some(active) = self.active_cell.take() { + self.needs_final_message_separator = true; self.app_event_tx.send(AppEvent::InsertHistoryCell(active)); } } @@ -1200,6 +1213,7 @@ impl ChatWidget { if !cell.display_lines(u16::MAX).is_empty() { // Only break exec grouping if the cell renders visible lines. self.flush_active_cell(); + self.needs_final_message_separator = true; } self.app_event_tx.send(AppEvent::InsertHistoryCell(cell)); } @@ -1241,6 +1255,7 @@ impl ChatWidget { if !text.is_empty() { self.add_to_history(history_cell::new_user_prompt(text)); } + self.needs_final_message_separator = false; } fn capture_ghost_snapshot(&mut self) { diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__binary_size_ideal_response.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__binary_size_ideal_response.snap index 97709b61..e3121774 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__binary_size_ideal_response.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__binary_size_ideal_response.snap @@ -1,5 +1,6 @@ --- source: tui/src/chatwidget/tests.rs +assertion_line: 1152 expression: "lines[start_idx..].join(\"\\n\")" --- • I need to check the codex-rs repository to explain why the project's binaries @@ -9,6 +10,8 @@ expression: "lines[start_idx..].join(\"\\n\")" is set up. I should look into the Cargo.toml file to confirm features and profiles without needing to edit any code. Let's get started on this! +─ Worked for 0s ──────────────────────────────────────────────────────────────── + • I’m going to scan the workspace and Cargo manifests to see build profiles and dependencies that impact binary size. Then I’ll summarize the main causes. @@ -110,6 +113,8 @@ expression: "lines[start_idx..].join(\"\\n\")" "Main Causes" and "Build-Mode Notes." I can also include brief suggestions for reducing size, but I want to stay focused on answering the user's question. +─ Worked for 0s ──────────────────────────────────────────────────────────────── + • Here’s what’s driving size in this workspace’s binaries. Main Causes diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 0c9ebc73..f3799ad3 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -340,6 +340,7 @@ fn make_chatwidget_manual() -> ( is_review_mode: false, ghost_snapshots: Vec::new(), ghost_snapshots_disabled: false, + needs_final_message_separator: false, }; (widget, rx, op_rx) } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 2951ca5b..f93042f9 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1090,6 +1090,40 @@ pub(crate) fn new_reasoning_summary_block( Box::new(new_reasoning_block(full_reasoning_buffer, config)) } +#[derive(Debug)] +pub struct FinalMessageSeparator { + elapsed_seconds: Option, +} +impl FinalMessageSeparator { + pub(crate) fn new(elapsed_seconds: Option) -> Self { + Self { elapsed_seconds } + } +} +impl HistoryCell for FinalMessageSeparator { + fn display_lines(&self, width: u16) -> Vec> { + let elapsed_seconds = self + .elapsed_seconds + .map(super::status_indicator_widget::fmt_elapsed_compact); + if let Some(elapsed_seconds) = elapsed_seconds { + let worked_for = format!("─ Worked for {elapsed_seconds} ─"); + let worked_for_width = worked_for.width(); + vec![ + Line::from_iter([ + worked_for, + "─".repeat((width as usize).saturating_sub(worked_for_width)), + ]) + .dim(), + ] + } else { + vec![Line::from_iter(["─".repeat(width as usize).dim()])] + } + } + + fn transcript_lines(&self) -> Vec> { + vec![] + } +} + fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> { let args_str = invocation .arguments diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index 96f2c49f..b6fa2fd5 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -34,7 +34,7 @@ pub(crate) struct StatusIndicatorWidget { // Format elapsed seconds into a compact human-friendly form used by the status line. // Examples: 0s, 59s, 1m 00s, 59m 59s, 1h 00m 00s, 2h 03m 09s -fn fmt_elapsed_compact(elapsed_secs: u64) -> String { +pub fn fmt_elapsed_compact(elapsed_secs: u64) -> String { if elapsed_secs < 60 { return format!("{elapsed_secs}s"); } @@ -142,7 +142,7 @@ impl StatusIndicatorWidget { elapsed.as_secs() } - fn elapsed_seconds(&self) -> u64 { + pub fn elapsed_seconds(&self) -> u64 { self.elapsed_seconds_at(Instant::now()) } }