Compare commits

...

1 Commits

Author SHA1 Message Date
Charles Cunningham
7b4bf556bc tui: remove "Worked for" separators during assistant turns 2026-01-26 13:34:18 -08:00
6 changed files with 6 additions and 148 deletions

View File

@@ -215,10 +215,6 @@ impl BottomPane {
self.request_redraw();
}
pub fn status_widget(&self) -> Option<&StatusIndicatorWidget> {
self.status.as_ref()
}
pub fn skills(&self) -> Option<&Vec<SkillMetadata>> {
self.composer.skills()
}

View File

@@ -487,25 +487,8 @@ pub(crate) struct ChatWidget {
is_review_mode: bool,
// Snapshot of token usage to restore after review mode exits.
pre_review_token_info: Option<Option<TokenUsageInfo>>,
// Whether the next streamed assistant content should be preceded by a final message separator.
//
// This is set whenever we insert a visible history cell that conceptually belongs to a turn.
// The separator itself is only rendered if the turn recorded "work" activity (see
// `had_work_activity`).
needs_final_message_separator: bool,
// Whether the current turn performed "work" (exec commands, MCP tool calls, patch applications).
//
// 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,
// Whether the current turn emitted a plan update.
saw_plan_update_this_turn: 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
feedback: codex_feedback::CodexFeedback,
@@ -685,7 +668,6 @@ impl ChatWidget {
let Some(wait) = self.unified_exec_wait_streak.take() else {
return;
};
self.needs_final_message_separator = true;
let cell = history_cell::new_unified_exec_interaction(wait.command_display, String::new());
self.app_event_tx
.send(AppEvent::InsertHistoryCell(Box::new(cell)));
@@ -1626,21 +1608,6 @@ impl ChatWidget {
self.flush_active_cell();
if self.stream_controller.is_none() {
// If the previous turn inserted non-stream history (exec output, patch status, MCP
// calls), render a separator before starting the next streamed assistant message.
if self.needs_final_message_separator && self.had_work_activity {
let elapsed_seconds = self
.bottom_pane
.status_widget()
.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;
} else if self.needs_final_message_separator {
// Reset the flag even if we don't show separator (no work was done)
self.needs_final_message_separator = false;
}
self.stream_controller = Some(StreamController::new(
self.last_rendered_width.get().map(|w| w.saturating_sub(2)),
));
@@ -1653,17 +1620,6 @@ 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) {
@@ -1719,8 +1675,6 @@ impl ChatWidget {
self.request_redraw();
}
}
// Mark that actual work was done (command executed)
self.had_work_activity = true;
}
pub(crate) fn handle_patch_apply_end_now(
@@ -1732,8 +1686,6 @@ impl ChatWidget {
if !event.success {
self.add_to_history(history_cell::new_patch_apply_failure(event.stderr));
}
// Mark that actual work was done (patch applied)
self.had_work_activity = true;
}
pub(crate) fn handle_exec_approval_now(&mut self, id: String, ev: ExecApprovalRequestEvent) {
@@ -1905,8 +1857,6 @@ impl ChatWidget {
if let Some(extra) = extra_cell {
self.add_boxed_history(extra);
}
// Mark that actual work was done (MCP tool call)
self.had_work_activity = true;
}
pub(crate) fn new(common: ChatWidgetInit, thread_manager: Arc<ThreadManager>) -> Self {
@@ -2006,10 +1956,7 @@ impl ChatWidget {
quit_shortcut_key: None,
is_review_mode: false,
pre_review_token_info: None,
needs_final_message_separator: false,
had_work_activity: false,
saw_plan_update_this_turn: false,
last_separator_elapsed_secs: None,
last_rendered_width: std::cell::Cell::new(None),
feedback,
current_rollout_path: None,
@@ -2128,9 +2075,6 @@ impl ChatWidget {
quit_shortcut_key: None,
is_review_mode: false,
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,
@@ -2249,10 +2193,7 @@ impl ChatWidget {
quit_shortcut_key: None,
is_review_mode: false,
pre_review_token_info: None,
needs_final_message_separator: false,
had_work_activity: false,
saw_plan_update_this_turn: false,
last_separator_elapsed_secs: None,
last_rendered_width: std::cell::Cell::new(None),
feedback,
current_rollout_path: None,
@@ -2718,7 +2659,6 @@ 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));
}
}
@@ -2739,7 +2679,6 @@ impl ChatWidget {
if !keep_placeholder_header_active && !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));
}
@@ -2853,8 +2792,6 @@ impl ChatWidget {
local_image_paths,
));
}
self.needs_final_message_separator = false;
}
/// Replay a subset of initial events into the UI to seed the transcript when
@@ -3084,9 +3021,6 @@ impl ChatWidget {
event.local_images,
));
}
// User messages reset separator state so the next agent response doesn't add a stray break.
self.needs_final_message_separator = false;
}
/// Exit the UI immediately without waiting for shutdown.

View File

@@ -9,8 +9,6 @@ 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 ────────────────────────────────────────────────────────────────
• Im going to scan the workspace and Cargo manifests to see build profiles and
dependencies that impact binary size. Then Ill summarize the main causes.
@@ -112,8 +110,6 @@ 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 ────────────────────────────────────────────────────────────────
• Heres whats driving size in this workspaces binaries.
Main Causes

View File

@@ -846,10 +846,7 @@ async fn make_chatwidget_manual(
quit_shortcut_key: None,
is_review_mode: false,
pre_review_token_info: None,
needs_final_message_separator: false,
had_work_activity: false,
saw_plan_update_this_turn: false,
last_separator_elapsed_secs: None,
last_rendered_width: std::cell::Cell::new(None),
feedback: codex_feedback::CodexFeedback::new(),
current_rollout_path: None,
@@ -880,16 +877,6 @@ 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,
@@ -1708,7 +1695,6 @@ async fn streaming_final_answer_keeps_task_running_state() {
chat.on_commit_tick();
assert!(chat.bottom_pane.is_task_running());
assert!(chat.bottom_pane.status_widget().is_none());
chat.bottom_pane
.set_composer_text("queued submission".to_string(), Vec::new(), Vec::new());
@@ -2482,14 +2468,7 @@ async fn undo_started_hides_interrupt_hint() {
msg: EventMsg::UndoStarted(UndoStartedEvent { message: None }),
});
let status = chat
.bottom_pane
.status_widget()
.expect("status indicator should be active");
assert!(
!status.interrupt_hint_visible(),
"undo should hide the interrupt hint because the operation cannot be cancelled"
);
assert_eq!(chat.current_status_header, "Undo in progress...");
}
/// The commit picker shows only commit subjects (no timestamps).
@@ -4358,12 +4337,9 @@ async fn stream_error_updates_status_indicator() {
cells.is_empty(),
"expected no history cell for StreamError event"
);
let status = chat
.bottom_pane
.status_widget()
.expect("status indicator should be visible");
assert_eq!(status.header(), msg);
assert_eq!(status.details(), Some(details));
assert!(chat.bottom_pane.is_task_running());
assert_eq!(chat.current_status_header, msg);
assert_eq!(chat.retry_status_header.as_deref(), Some("Working"));
}
#[tokio::test]
@@ -4411,12 +4387,7 @@ async fn stream_recovery_restores_previous_status_header() {
}),
});
let status = chat
.bottom_pane
.status_widget()
.expect("status indicator should be visible");
assert_eq!(status.header(), "Working");
assert_eq!(status.details(), None);
assert_eq!(chat.current_status_header, "Working");
assert!(chat.retry_status_header.is_none());
}

View File

@@ -1769,42 +1769,6 @@ pub(crate) fn new_reasoning_summary_block(full_reasoning_buffer: String) -> Box<
))
}
#[derive(Debug)]
/// A visual divider between turns, optionally showing how long the assistant "worked for".
///
/// This separator is only emitted for turns that performed concrete work (e.g., running commands,
/// applying patches, making MCP tool calls), so purely conversational turns do not show an empty
/// divider.
pub struct FinalMessageSeparator {
elapsed_seconds: Option<u64>,
}
impl FinalMessageSeparator {
/// Creates a separator; `elapsed_seconds` typically comes from the status indicator timer.
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 format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
let args_str = invocation
.arguments

View File

@@ -149,14 +149,11 @@ 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())
}
/// Wrap the details text into a fixed width and return the lines, truncating if necessary.
fn wrapped_details_lines(&self, width: u16) -> Vec<Line<'static>> {
let Some(details) = self.details.as_deref() else {