tui: separator above final agent message (#4324)
Adds a separator line before the final agent message <img width="1011" height="884" alt="Screenshot 2025-09-26 at 4 55 01 PM" src="https://github.com/user-attachments/assets/7c91adbf-6035-4578-8b88-a6921f11bcbc" />
This commit is contained in:
@@ -102,6 +102,10 @@ impl BottomPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn status_widget(&self) -> Option<&StatusIndicatorWidget> {
|
||||||
|
self.status.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
fn active_view(&self) -> Option<&dyn BottomPaneView> {
|
fn active_view(&self) -> Option<&dyn BottomPaneView> {
|
||||||
self.view_stack.last().map(std::convert::AsRef::as_ref)
|
self.view_stack.last().map(std::convert::AsRef::as_ref)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ pub(crate) struct ChatWidget {
|
|||||||
// List of ghost commits corresponding to each turn.
|
// List of ghost commits corresponding to each turn.
|
||||||
ghost_snapshots: Vec<GhostCommit>,
|
ghost_snapshots: Vec<GhostCommit>,
|
||||||
ghost_snapshots_disabled: bool,
|
ghost_snapshots_disabled: bool,
|
||||||
|
// Whether to add a final message separator after the last message
|
||||||
|
needs_final_message_separator: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UserMessage {
|
struct UserMessage {
|
||||||
@@ -648,6 +650,14 @@ impl ChatWidget {
|
|||||||
self.flush_active_cell();
|
self.flush_active_cell();
|
||||||
|
|
||||||
if self.stream_controller.is_none() {
|
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()));
|
self.stream_controller = Some(StreamController::new(self.config.clone()));
|
||||||
}
|
}
|
||||||
if let Some(controller) = self.stream_controller.as_mut()
|
if let Some(controller) = self.stream_controller.as_mut()
|
||||||
@@ -901,6 +911,7 @@ impl ChatWidget {
|
|||||||
is_review_mode: false,
|
is_review_mode: false,
|
||||||
ghost_snapshots: Vec::new(),
|
ghost_snapshots: Vec::new(),
|
||||||
ghost_snapshots_disabled: true,
|
ghost_snapshots_disabled: true,
|
||||||
|
needs_final_message_separator: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,6 +973,7 @@ impl ChatWidget {
|
|||||||
is_review_mode: false,
|
is_review_mode: false,
|
||||||
ghost_snapshots: Vec::new(),
|
ghost_snapshots: Vec::new(),
|
||||||
ghost_snapshots_disabled: true,
|
ghost_snapshots_disabled: true,
|
||||||
|
needs_final_message_separator: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1188,6 +1200,7 @@ impl ChatWidget {
|
|||||||
|
|
||||||
fn flush_active_cell(&mut self) {
|
fn flush_active_cell(&mut self) {
|
||||||
if let Some(active) = self.active_cell.take() {
|
if let Some(active) = self.active_cell.take() {
|
||||||
|
self.needs_final_message_separator = true;
|
||||||
self.app_event_tx.send(AppEvent::InsertHistoryCell(active));
|
self.app_event_tx.send(AppEvent::InsertHistoryCell(active));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1200,6 +1213,7 @@ impl ChatWidget {
|
|||||||
if !cell.display_lines(u16::MAX).is_empty() {
|
if !cell.display_lines(u16::MAX).is_empty() {
|
||||||
// Only break exec grouping if the cell renders visible lines.
|
// Only break exec grouping if the cell renders visible lines.
|
||||||
self.flush_active_cell();
|
self.flush_active_cell();
|
||||||
|
self.needs_final_message_separator = true;
|
||||||
}
|
}
|
||||||
self.app_event_tx.send(AppEvent::InsertHistoryCell(cell));
|
self.app_event_tx.send(AppEvent::InsertHistoryCell(cell));
|
||||||
}
|
}
|
||||||
@@ -1241,6 +1255,7 @@ impl ChatWidget {
|
|||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
self.add_to_history(history_cell::new_user_prompt(text));
|
self.add_to_history(history_cell::new_user_prompt(text));
|
||||||
}
|
}
|
||||||
|
self.needs_final_message_separator = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn capture_ghost_snapshot(&mut self) {
|
fn capture_ghost_snapshot(&mut self) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: tui/src/chatwidget/tests.rs
|
source: tui/src/chatwidget/tests.rs
|
||||||
|
assertion_line: 1152
|
||||||
expression: "lines[start_idx..].join(\"\\n\")"
|
expression: "lines[start_idx..].join(\"\\n\")"
|
||||||
---
|
---
|
||||||
• I need to check the codex-rs repository to explain why the project's binaries
|
• 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
|
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!
|
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
|
• 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.
|
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
|
"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.
|
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.
|
• Here’s what’s driving size in this workspace’s binaries.
|
||||||
|
|
||||||
Main Causes
|
Main Causes
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ fn make_chatwidget_manual() -> (
|
|||||||
is_review_mode: false,
|
is_review_mode: false,
|
||||||
ghost_snapshots: Vec::new(),
|
ghost_snapshots: Vec::new(),
|
||||||
ghost_snapshots_disabled: false,
|
ghost_snapshots_disabled: false,
|
||||||
|
needs_final_message_separator: false,
|
||||||
};
|
};
|
||||||
(widget, rx, op_rx)
|
(widget, rx, op_rx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1090,6 +1090,40 @@ pub(crate) fn new_reasoning_summary_block(
|
|||||||
Box::new(new_reasoning_block(full_reasoning_buffer, config))
|
Box::new(new_reasoning_block(full_reasoning_buffer, config))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FinalMessageSeparator {
|
||||||
|
elapsed_seconds: Option<u64>,
|
||||||
|
}
|
||||||
|
impl FinalMessageSeparator {
|
||||||
|
pub(crate) fn new(elapsed_seconds: Option<u64>) -> Self {
|
||||||
|
Self { elapsed_seconds }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl HistoryCell for FinalMessageSeparator {
|
||||||
|
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||||
|
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<Line<'static>> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
|
fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
|
||||||
let args_str = invocation
|
let args_str = invocation
|
||||||
.arguments
|
.arguments
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub(crate) struct StatusIndicatorWidget {
|
|||||||
|
|
||||||
// Format elapsed seconds into a compact human-friendly form used by the status line.
|
// 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
|
// 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 {
|
if elapsed_secs < 60 {
|
||||||
return format!("{elapsed_secs}s");
|
return format!("{elapsed_secs}s");
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ impl StatusIndicatorWidget {
|
|||||||
elapsed.as_secs()
|
elapsed.as_secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn elapsed_seconds(&self) -> u64 {
|
pub fn elapsed_seconds(&self) -> u64 {
|
||||||
self.elapsed_seconds_at(Instant::now())
|
self.elapsed_seconds_at(Instant::now())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user