From c0960c0f4955cf529e34d037f6e5794da09795e0 Mon Sep 17 00:00:00 2001
From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com>
Date: Fri, 26 Sep 2025 22:49:59 -0700
Subject: [PATCH] tui: separator above final agent message (#4324)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds a separator line before the final agent message
---
codex-rs/tui/src/bottom_pane/mod.rs | 4 +++
codex-rs/tui/src/chatwidget.rs | 15 ++++++++
...et__tests__binary_size_ideal_response.snap | 5 +++
codex-rs/tui/src/chatwidget/tests.rs | 1 +
codex-rs/tui/src/history_cell.rs | 34 +++++++++++++++++++
codex-rs/tui/src/status_indicator_widget.rs | 4 +--
6 files changed, 61 insertions(+), 2 deletions(-)
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())
}
}