diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index e9fc5df59..851668728 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::queued_user_messages::QueuedUserMessages; -use crate::bottom_pane::unified_exec_footer::UnifiedExecFooter; use crate::render::renderable::FlexRenderable; use crate::render::renderable::Renderable; use crate::render::renderable::RenderableItem; @@ -41,7 +40,6 @@ mod queued_user_messages; mod scroll_state; mod selection_popup_common; mod textarea; -mod unified_exec_footer; pub(crate) use feedback_view::FeedbackNoteView; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -78,8 +76,6 @@ pub(crate) struct BottomPane { /// Inline status indicator shown above the composer while a task is running. status: Option, - /// Unified exec session summary shown above the composer. - unified_exec_footer: UnifiedExecFooter, /// Queued user messages to show above the composer while a turn is running. queued_user_messages: QueuedUserMessages, context_window_percent: Option, @@ -127,7 +123,6 @@ impl BottomPane { is_task_running: false, ctrl_c_quit_hint: false, status: None, - unified_exec_footer: UnifiedExecFooter::new(), queued_user_messages: QueuedUserMessages::new(), esc_backtrack_hint: false, animations_enabled, @@ -398,12 +393,6 @@ impl BottomPane { self.request_redraw(); } - pub(crate) fn set_unified_exec_sessions(&mut self, sessions: Vec) { - if self.unified_exec_footer.set_sessions(sessions) { - self.request_redraw(); - } - } - /// Update custom prompts available for the slash popup. pub(crate) fn set_custom_prompts(&mut self, prompts: Vec) { self.composer.set_custom_prompts(prompts); @@ -534,14 +523,8 @@ impl BottomPane { if let Some(status) = &self.status { flex.push(0, RenderableItem::Borrowed(status)); } - if !self.unified_exec_footer.is_empty() { - flex.push(0, RenderableItem::Borrowed(&self.unified_exec_footer)); - } flex.push(1, RenderableItem::Borrowed(&self.queued_user_messages)); - if self.status.is_some() - || !self.unified_exec_footer.is_empty() - || !self.queued_user_messages.messages.is_empty() - { + if self.status.is_some() || !self.queued_user_messages.messages.is_empty() { flex.push(0, RenderableItem::Owned("".into())); } let mut flex2 = FlexRenderable::new(); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__unified_exec_footer__tests__render_more_sessions.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__unified_exec_footer__tests__render_more_sessions.snap deleted file mode 100644 index 90bfa7600..000000000 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__unified_exec_footer__tests__render_more_sessions.snap +++ /dev/null @@ -1,22 +0,0 @@ ---- -source: tui/src/bottom_pane/unified_exec_footer.rs -expression: "format!(\"{buf:?}\")" ---- -Buffer { - area: Rect { x: 0, y: 0, width: 50, height: 2 }, - content: [ - "Background terminal running: echo hello · rg "foo"", - " src · 1 more running ", - ], - styles: [ - x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 28, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 29, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE, - x: 39, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 42, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE, - x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 29, y: 1, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE, - x: 32, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 49, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - ] -} diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__unified_exec_footer__tests__render_two_sessions.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__unified_exec_footer__tests__render_two_sessions.snap deleted file mode 100644 index 0828a62ef..000000000 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__unified_exec_footer__tests__render_two_sessions.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: tui/src/bottom_pane/unified_exec_footer.rs -expression: "format!(\"{buf:?}\")" ---- -Buffer { - area: Rect { x: 0, y: 0, width: 50, height: 2 }, - content: [ - "Background terminal running: echo hello · rg "foo"", - " src ", - ], - styles: [ - x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 28, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 29, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE, - x: 39, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 42, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE, - x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 29, y: 1, fg: Cyan, bg: Reset, underline: Reset, modifier: NONE, - x: 32, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - ] -} diff --git a/codex-rs/tui/src/bottom_pane/unified_exec_footer.rs b/codex-rs/tui/src/bottom_pane/unified_exec_footer.rs deleted file mode 100644 index 80ec1fb62..000000000 --- a/codex-rs/tui/src/bottom_pane/unified_exec_footer.rs +++ /dev/null @@ -1,125 +0,0 @@ -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; - -use crate::render::renderable::Renderable; -use crate::text_formatting::truncate_text; -use crate::wrapping::RtOptions; -use crate::wrapping::word_wrap_lines; - -const MAX_SESSION_LABEL_GRAPHEMES: usize = 48; -const MAX_VISIBLE_SESSIONS: usize = 2; - -pub(crate) struct UnifiedExecFooter { - sessions: Vec, -} - -impl UnifiedExecFooter { - pub(crate) fn new() -> Self { - Self { - sessions: Vec::new(), - } - } - - pub(crate) fn set_sessions(&mut self, sessions: Vec) -> bool { - if self.sessions == sessions { - return false; - } - self.sessions = sessions; - true - } - - pub(crate) fn is_empty(&self) -> bool { - self.sessions.is_empty() - } - - fn render_lines(&self, width: u16) -> Vec> { - if self.sessions.is_empty() || width < 4 { - return Vec::new(); - } - - let label = "Background terminal running:"; - let mut spans = Vec::new(); - spans.push(label.dim()); - spans.push(" ".into()); - - let visible = self.sessions.iter().take(MAX_VISIBLE_SESSIONS); - let mut visible_count = 0usize; - for (idx, command) in visible.enumerate() { - if idx > 0 { - spans.push(" · ".dim()); - } - let truncated = truncate_text(command, MAX_SESSION_LABEL_GRAPHEMES); - spans.push(truncated.cyan()); - visible_count += 1; - } - - let remaining = self.sessions.len().saturating_sub(visible_count); - if remaining > 0 { - spans.push(" · ".dim()); - spans.push(format!("{remaining} more running").dim()); - } - - let indent = " ".repeat(label.len() + 1); - let line = Line::from(spans); - word_wrap_lines( - std::iter::once(line), - RtOptions::new(width as usize).subsequent_indent(Line::from(indent).dim()), - ) - } -} - -impl Renderable for UnifiedExecFooter { - fn render(&self, area: Rect, buf: &mut Buffer) { - if area.is_empty() { - return; - } - - Paragraph::new(self.render_lines(area.width)).render(area, buf); - } - - fn desired_height(&self, width: u16) -> u16 { - self.render_lines(width).len() as u16 - } -} - -#[cfg(test)] -mod tests { - use super::*; - use insta::assert_snapshot; - use pretty_assertions::assert_eq; - - #[test] - fn desired_height_empty() { - let footer = UnifiedExecFooter::new(); - assert_eq!(footer.desired_height(40), 0); - } - - #[test] - fn render_two_sessions() { - let mut footer = UnifiedExecFooter::new(); - footer.set_sessions(vec!["echo hello".to_string(), "rg \"foo\" src".to_string()]); - let width = 50; - let height = footer.desired_height(width); - let mut buf = Buffer::empty(Rect::new(0, 0, width, height)); - footer.render(Rect::new(0, 0, width, height), &mut buf); - assert_snapshot!("render_two_sessions", format!("{buf:?}")); - } - - #[test] - fn render_more_sessions() { - let mut footer = UnifiedExecFooter::new(); - footer.set_sessions(vec![ - "echo hello".to_string(), - "rg \"foo\" src".to_string(), - "cat README.md".to_string(), - ]); - let width = 50; - let height = footer.desired_height(width); - let mut buf = Buffer::empty(Rect::new(0, 0, width, height)); - footer.render(Rect::new(0, 0, width, height), &mut buf); - assert_snapshot!("render_more_sessions", format!("{buf:?}")); - } -} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index d04fc5b7a..37cd004a1 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -99,7 +99,6 @@ use crate::diff_render::display_path_for; use crate::exec_cell::CommandOutput; use crate::exec_cell::ExecCell; use crate::exec_cell::new_active_exec_command; -use crate::exec_command::strip_bash_lc_and_escape; use crate::get_git_diff::get_git_diff; use crate::history_cell; use crate::history_cell::AgentMessageCell; @@ -150,11 +149,6 @@ struct RunningCommand { source: ExecCommandSource, } -struct UnifiedExecSessionSummary { - key: String, - command_display: String, -} - struct UnifiedExecWaitState { command_display: String, } @@ -169,13 +163,6 @@ impl UnifiedExecWaitState { } } -fn is_unified_exec_source(source: ExecCommandSource) -> bool { - matches!( - source, - ExecCommandSource::UnifiedExecStartup | ExecCommandSource::UnifiedExecInteraction - ) -} - const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [75.0, 90.0, 95.0]; const NUDGE_MODEL_SLUG: &str = "gpt-5.1-codex-mini"; const RATE_LIMIT_SWITCH_PROMPT_THRESHOLD: f64 = 90.0; @@ -313,7 +300,6 @@ pub(crate) struct ChatWidget { suppressed_exec_calls: HashSet, last_unified_wait: Option, task_complete_pending: bool, - unified_exec_sessions: Vec, mcp_startup_status: Option>, // Queue of interruptive UI events deferred during an active write cycle interrupts: InterruptManager, @@ -842,10 +828,6 @@ impl ChatWidget { fn on_exec_command_begin(&mut self, ev: ExecCommandBeginEvent) { self.flush_answer_stream_with_separator(); - if is_unified_exec_source(ev.source) { - self.track_unified_exec_session_begin(&ev); - return; - } let ev2 = ev.clone(); self.defer_or_handle(|q| q.push_exec_begin(ev), |s| s.handle_exec_begin_now(ev2)); } @@ -857,18 +839,8 @@ impl ChatWidget { // TODO: Handle streaming exec output if/when implemented } - fn on_terminal_interaction(&mut self, ev: TerminalInteractionEvent) { - self.flush_answer_stream_with_separator(); - let key = Self::unified_exec_session_key(Some(&ev.process_id), &ev.call_id); - let command_display = self - .unified_exec_sessions - .iter() - .find(|session| session.key == key) - .map(|session| session.command_display.clone()); - self.add_to_history(history_cell::new_unified_exec_interaction( - command_display, - ev.stdin, - )); + fn on_terminal_interaction(&mut self, _ev: TerminalInteractionEvent) { + // TODO: Handle once design is ready } fn on_patch_apply_begin(&mut self, event: PatchApplyBeginEvent) { @@ -896,58 +868,10 @@ impl ChatWidget { } fn on_exec_command_end(&mut self, ev: ExecCommandEndEvent) { - if is_unified_exec_source(ev.source) { - self.track_unified_exec_session_end(&ev); - return; - } let ev2 = ev.clone(); self.defer_or_handle(|q| q.push_exec_end(ev), |s| s.handle_exec_end_now(ev2)); } - fn unified_exec_session_key(process_id: Option<&str>, call_id: &str) -> String { - process_id.unwrap_or(call_id).to_string() - } - - fn track_unified_exec_session_begin(&mut self, ev: &ExecCommandBeginEvent) { - if ev.source != ExecCommandSource::UnifiedExecStartup { - return; - } - let key = Self::unified_exec_session_key(ev.process_id.as_deref(), &ev.call_id); - let command_display = strip_bash_lc_and_escape(&ev.command); - if let Some(existing) = self - .unified_exec_sessions - .iter_mut() - .find(|session| session.key == key) - { - existing.command_display = command_display; - } else { - self.unified_exec_sessions.push(UnifiedExecSessionSummary { - key, - command_display, - }); - } - self.sync_unified_exec_footer(); - } - - fn track_unified_exec_session_end(&mut self, ev: &ExecCommandEndEvent) { - let key = Self::unified_exec_session_key(ev.process_id.as_deref(), &ev.call_id); - let before = self.unified_exec_sessions.len(); - self.unified_exec_sessions - .retain(|session| session.key != key); - if self.unified_exec_sessions.len() != before { - self.sync_unified_exec_footer(); - } - } - - fn sync_unified_exec_footer(&mut self) { - let sessions = self - .unified_exec_sessions - .iter() - .map(|session| session.command_display.clone()) - .collect(); - self.bottom_pane.set_unified_exec_sessions(sessions); - } - fn on_mcp_tool_call_begin(&mut self, ev: McpToolCallBeginEvent) { let ev2 = ev.clone(); self.defer_or_handle(|q| q.push_mcp_begin(ev), |s| s.handle_mcp_begin_now(ev2)); @@ -1395,7 +1319,6 @@ impl ChatWidget { suppressed_exec_calls: HashSet::new(), last_unified_wait: None, task_complete_pending: false, - unified_exec_sessions: Vec::new(), mcp_startup_status: None, interrupts: InterruptManager::new(), reasoning_buffer: String::new(), @@ -1481,7 +1404,6 @@ impl ChatWidget { suppressed_exec_calls: HashSet::new(), last_unified_wait: None, task_complete_pending: false, - unified_exec_sessions: Vec::new(), mcp_startup_status: None, interrupts: InterruptManager::new(), reasoning_buffer: String::new(), @@ -1949,20 +1871,12 @@ impl ChatWidget { EventMsg::ElicitationRequest(ev) => { self.on_elicitation_request(ev); } - EventMsg::ExecCommandBegin(ev) => { - if !from_replay || !is_unified_exec_source(ev.source) { - self.on_exec_command_begin(ev); - } - } + EventMsg::ExecCommandBegin(ev) => self.on_exec_command_begin(ev), EventMsg::TerminalInteraction(delta) => self.on_terminal_interaction(delta), EventMsg::ExecCommandOutputDelta(delta) => self.on_exec_command_output_delta(delta), EventMsg::PatchApplyBegin(ev) => self.on_patch_apply_begin(ev), EventMsg::PatchApplyEnd(ev) => self.on_patch_apply_end(ev), - EventMsg::ExecCommandEnd(ev) => { - if !from_replay || !is_unified_exec_source(ev.source) { - self.on_exec_command_end(ev); - } - } + EventMsg::ExecCommandEnd(ev) => self.on_exec_command_end(ev), EventMsg::ViewImageToolCall(ev) => self.on_view_image_tool_call(ev), EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev), EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev), diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 4447bc0b9..362b1678f 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -426,7 +426,6 @@ fn make_chatwidget_manual( suppressed_exec_calls: HashSet::new(), last_unified_wait: None, task_complete_pending: false, - unified_exec_sessions: Vec::new(), mcp_startup_status: None, interrupts: InterruptManager::new(), reasoning_buffer: String::new(), @@ -1220,7 +1219,7 @@ fn exec_end_without_begin_uses_event_command() { } #[test] -fn exec_history_skips_unified_exec_startup_commands() { +fn exec_history_shows_unified_exec_startup_commands() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); let begin = begin_exec_with_source( @@ -1237,31 +1236,11 @@ fn exec_history_skips_unified_exec_startup_commands() { end_exec(&mut chat, begin, "echo unified exec startup\n", "", 0); let cells = drain_insert_history(&mut rx); + assert_eq!(cells.len(), 1, "expected finalized exec cell to flush"); + let blob = lines_to_single_string(&cells[0]); assert!( - cells.is_empty(), - "expected unified exec startup to render in footer only" - ); -} - -#[test] -fn unified_exec_end_after_task_complete_is_suppressed() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); - - let begin = begin_exec_with_source( - &mut chat, - "call-startup", - "echo unified exec startup", - ExecCommandSource::UnifiedExecStartup, - ); - drain_insert_history(&mut rx); - - chat.on_task_complete(None); - end_exec(&mut chat, begin, "", "", 0); - - let cells = drain_insert_history(&mut rx); - assert!( - cells.is_empty(), - "expected unified exec end after task complete to be suppressed" + blob.contains("• Ran echo unified exec startup"), + "expected startup command to render: {blob:?}" ); } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 2c0a37ece..5440040f6 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -375,72 +375,6 @@ impl HistoryCell for PrefixedWrappedHistoryCell { } } -#[derive(Debug)] -pub(crate) struct UnifiedExecInteractionCell { - command_display: Option, - stdin: String, -} - -impl UnifiedExecInteractionCell { - pub(crate) fn new(command_display: Option, stdin: String) -> Self { - Self { - command_display, - stdin, - } - } -} - -impl HistoryCell for UnifiedExecInteractionCell { - fn display_lines(&self, width: u16) -> Vec> { - if width == 0 { - return Vec::new(); - } - let wrap_width = width as usize; - - let mut header_spans = vec!["↳ ".dim(), "Interacted with background terminal".bold()]; - if let Some(command) = &self.command_display - && !command.is_empty() - { - header_spans.push(" · ".dim()); - header_spans.push(command.clone().dim()); - } - let header = Line::from(header_spans); - - let mut out: Vec> = Vec::new(); - let header_wrapped = word_wrap_line(&header, RtOptions::new(wrap_width)); - push_owned_lines(&header_wrapped, &mut out); - - let input_lines: Vec> = if self.stdin.is_empty() { - vec![vec!["(waited)".dim()].into()] - } else { - self.stdin - .lines() - .map(|line| Line::from(line.to_string())) - .collect() - }; - - let input_wrapped = word_wrap_lines( - input_lines, - RtOptions::new(wrap_width) - .initial_indent(Line::from(" └ ".dim())) - .subsequent_indent(Line::from(" ".dim())), - ); - out.extend(input_wrapped); - out - } - - fn desired_height(&self, width: u16) -> u16 { - self.display_lines(width).len() as u16 - } -} - -pub(crate) fn new_unified_exec_interaction( - command_display: Option, - stdin: String, -) -> UnifiedExecInteractionCell { - UnifiedExecInteractionCell::new(command_display, stdin) -} - fn truncate_exec_snippet(full_cmd: &str) -> String { let mut snippet = match full_cmd.split_once('\n') { Some((first, _)) => format!("{first} ..."), @@ -1624,31 +1558,6 @@ mod tests { render_lines(&cell.transcript_lines(u16::MAX)) } - #[test] - fn unified_exec_interaction_cell_renders_input() { - let cell = - new_unified_exec_interaction(Some("echo hello".to_string()), "ls\npwd".to_string()); - let lines = render_transcript(&cell); - assert_eq!( - lines, - vec![ - "↳ Interacted with background terminal · echo hello", - " └ ls", - " pwd", - ], - ); - } - - #[test] - fn unified_exec_interaction_cell_renders_wait() { - let cell = new_unified_exec_interaction(None, String::new()); - let lines = render_transcript(&cell); - assert_eq!( - lines, - vec!["↳ Interacted with background terminal", " └ (waited)"], - ); - } - #[test] fn mcp_tools_output_masks_sensitive_values() { let mut config = test_config();