From bdae0035ec884f18eea11c11f26bb3f30e61ab34 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 14 Jan 2026 08:11:12 -0800 Subject: [PATCH] Render exec output deltas inline (#9194) --- codex-rs/tui/src/chatwidget.rs | 19 ++++++++++++++----- codex-rs/tui/src/exec_cell/model.rs | 12 ++++++++++++ codex-rs/tui2/src/chatwidget.rs | 19 ++++++++++++++----- codex-rs/tui2/src/exec_cell/model.rs | 12 ++++++++++++ 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 450d96918..984a31969 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -54,6 +54,7 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::ExecApprovalRequestEvent; use codex_core::protocol::ExecCommandBeginEvent; use codex_core::protocol::ExecCommandEndEvent; +use codex_core::protocol::ExecCommandOutputDeltaEvent; use codex_core::protocol::ExecCommandSource; use codex_core::protocol::ExitedReviewModeEvent; use codex_core::protocol::ListCustomPromptsResponseEvent; @@ -1017,11 +1018,19 @@ impl ChatWidget { self.defer_or_handle(|q| q.push_exec_begin(ev), |s| s.handle_exec_begin_now(ev2)); } - fn on_exec_command_output_delta( - &mut self, - _ev: codex_core::protocol::ExecCommandOutputDeltaEvent, - ) { - // TODO: Handle streaming exec output if/when implemented + fn on_exec_command_output_delta(&mut self, ev: ExecCommandOutputDeltaEvent) { + let Some(cell) = self + .active_cell + .as_mut() + .and_then(|c| c.as_any_mut().downcast_mut::()) + else { + return; + }; + + if cell.append_output(&ev.call_id, std::str::from_utf8(&ev.chunk).unwrap_or("")) { + self.bump_active_cell_revision(); + self.request_redraw(); + } } fn on_terminal_interaction(&mut self, ev: TerminalInteractionEvent) { diff --git a/codex-rs/tui/src/exec_cell/model.rs b/codex-rs/tui/src/exec_cell/model.rs index 76316968c..21799c619 100644 --- a/codex-rs/tui/src/exec_cell/model.rs +++ b/codex-rs/tui/src/exec_cell/model.rs @@ -125,6 +125,18 @@ impl ExecCell { self.calls.iter() } + pub(crate) fn append_output(&mut self, call_id: &str, chunk: &str) -> bool { + if chunk.is_empty() { + return false; + } + let Some(call) = self.calls.iter_mut().rev().find(|c| c.call_id == call_id) else { + return false; + }; + let output = call.output.get_or_insert_with(CommandOutput::default); + output.aggregated_output.push_str(chunk); + true + } + pub(super) fn is_exploring_call(call: &ExecCall) -> bool { !matches!(call.source, ExecCommandSource::UserShell) && !call.parsed.is_empty() diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index 02855551a..f5f7eef2a 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -53,6 +53,7 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::ExecApprovalRequestEvent; use codex_core::protocol::ExecCommandBeginEvent; use codex_core::protocol::ExecCommandEndEvent; +use codex_core::protocol::ExecCommandOutputDeltaEvent; use codex_core::protocol::ExecCommandSource; use codex_core::protocol::ExitedReviewModeEvent; use codex_core::protocol::ListCustomPromptsResponseEvent; @@ -924,11 +925,19 @@ impl ChatWidget { self.defer_or_handle(|q| q.push_exec_begin(ev), |s| s.handle_exec_begin_now(ev2)); } - fn on_exec_command_output_delta( - &mut self, - _ev: codex_core::protocol::ExecCommandOutputDeltaEvent, - ) { - // TODO: Handle streaming exec output if/when implemented + fn on_exec_command_output_delta(&mut self, ev: ExecCommandOutputDeltaEvent) { + let Some(cell) = self + .active_cell + .as_mut() + .and_then(|c| c.as_any_mut().downcast_mut::()) + else { + return; + }; + + if cell.append_output(&ev.call_id, std::str::from_utf8(&ev.chunk).unwrap_or("")) { + self.bump_active_cell_revision(); + self.request_redraw(); + } } fn on_terminal_interaction(&mut self, _ev: TerminalInteractionEvent) { diff --git a/codex-rs/tui2/src/exec_cell/model.rs b/codex-rs/tui2/src/exec_cell/model.rs index 76316968c..21799c619 100644 --- a/codex-rs/tui2/src/exec_cell/model.rs +++ b/codex-rs/tui2/src/exec_cell/model.rs @@ -125,6 +125,18 @@ impl ExecCell { self.calls.iter() } + pub(crate) fn append_output(&mut self, call_id: &str, chunk: &str) -> bool { + if chunk.is_empty() { + return false; + } + let Some(call) = self.calls.iter_mut().rev().find(|c| c.call_id == call_id) else { + return false; + }; + let output = call.output.get_or_insert_with(CommandOutput::default); + output.aggregated_output.push_str(chunk); + true + } + pub(super) fn is_exploring_call(call: &ExecCall) -> bool { !matches!(call.source, ExecCommandSource::UserShell) && !call.parsed.is_empty()