From 03e16b94032c98533e78948628aa058210164f94 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 13 Sep 2025 03:23:19 +0000 Subject: [PATCH] vibe coded --- codex-rs/core/src/codex.rs | 105 ++++++++++++++---- codex-rs/core/src/rollout/policy.rs | 1 + .../src/event_processor_with_human_output.rs | 44 ++++++++ codex-rs/mcp-server/src/codex_tool_runner.rs | 1 + codex-rs/protocol/src/protocol.rs | 18 +++ codex-rs/tui/src/chatwidget.rs | 16 +++ codex-rs/tui/src/chatwidget/interrupts.rs | 7 ++ codex-rs/tui/src/history_cell.rs | 70 ++++++++++++ 8 files changed, 242 insertions(+), 20 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9aa8486a0d..410921b985 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -106,6 +106,7 @@ use crate::protocol::TaskCompleteEvent; use crate::protocol::TokenUsage; use crate::protocol::TokenUsageInfo; use crate::protocol::TurnDiffEvent; +use crate::protocol::UnifiedExecCallEvent; use crate::protocol::WebSearchBeginEvent; use crate::rollout::RolloutRecorder; use crate::rollout::RolloutRecorderParams; @@ -2286,19 +2287,37 @@ async fn handle_response_item( async fn handle_unified_exec_tool_call( sess: &Session, + sub_id: String, call_id: String, session_id: Option, arguments: Vec, timeout_ms: Option, ) -> ResponseInputItem { - let parsed_session_id = if let Some(session_id) = session_id { - match session_id.parse::() { + let input_chunks = arguments; + let requested_session_id = session_id; + let parsed_session_id = if let Some(session_id_value) = requested_session_id.as_deref() { + match session_id_value.parse::() { Ok(parsed) => Some(parsed), Err(output) => { + let message = + format!("invalid session_id: {session_id_value} due to error {output}"); + let event_call_id = call_id.clone(); + let event = Event { + id: sub_id.clone(), + msg: EventMsg::UnifiedExecCall(UnifiedExecCallEvent { + call_id: event_call_id, + requested_session_id: requested_session_id.clone(), + session_id: None, + input_chunks: input_chunks.clone(), + output: message.clone(), + success: false, + }), + }; + sess.send_event(event).await; return ResponseInputItem::FunctionCallOutput { - call_id: call_id.to_string(), + call_id, output: FunctionCallOutputPayload { - content: format!("invalid session_id: {session_id} due to error {output}"), + content: message, success: Some(false), }, }; @@ -2310,13 +2329,13 @@ async fn handle_unified_exec_tool_call( let request = crate::unified_exec::UnifiedExecRequest { session_id: parsed_session_id, - input_chunks: &arguments, + input_chunks: &input_chunks, timeout_ms, }; let result = sess.unified_exec_manager.handle_request(request).await; - let output_payload = match result { + let (session_id_for_event, output_for_event, success, output_payload) = match result { Ok(value) => { #[derive(Serialize)] struct SerializedUnifiedExecResult<'a> { @@ -2324,26 +2343,71 @@ async fn handle_unified_exec_tool_call( output: &'a str, } + let crate::unified_exec::UnifiedExecResult { + session_id: value_session_id, + output, + } = value; + let session_id_string = value_session_id.map(|id| id.to_string()); match serde_json::to_string(&SerializedUnifiedExecResult { - session_id: value.session_id.map(|id| id.to_string()), - output: &value.output, + session_id: session_id_string.clone(), + output: &output, }) { - Ok(serialized) => FunctionCallOutputPayload { - content: serialized, - success: Some(true), - }, - Err(err) => FunctionCallOutputPayload { - content: format!("failed to serialize unified exec output: {err}"), - success: Some(false), - }, + Ok(serialized) => ( + session_id_string, + output, + true, + FunctionCallOutputPayload { + content: serialized, + success: Some(true), + }, + ), + Err(err) => { + let message = format!("failed to serialize unified exec output: {err}"); + let combined_output = if output.is_empty() { + message.clone() + } else { + format!("{message}\n{output}") + }; + ( + session_id_string, + combined_output, + false, + FunctionCallOutputPayload { + content: message, + success: Some(false), + }, + ) + } } } - Err(err) => FunctionCallOutputPayload { - content: format!("unified exec failed: {err}"), - success: Some(false), - }, + Err(err) => { + let message = format!("unified exec failed: {err}"); + ( + requested_session_id.clone(), + message.clone(), + false, + FunctionCallOutputPayload { + content: message, + success: Some(false), + }, + ) + } }; + let event_call_id = call_id.clone(); + let event = Event { + id: sub_id, + msg: EventMsg::UnifiedExecCall(UnifiedExecCallEvent { + call_id: event_call_id, + requested_session_id, + session_id: session_id_for_event, + input_chunks, + output: output_for_event, + success, + }), + }; + sess.send_event(event).await; + ResponseInputItem::FunctionCallOutput { call_id, output: output_payload, @@ -2402,6 +2466,7 @@ async fn handle_function_call( handle_unified_exec_tool_call( sess, + sub_id, call_id, args.session_id, args.input, diff --git a/codex-rs/core/src/rollout/policy.rs b/codex-rs/core/src/rollout/policy.rs index f46fa7cab5..02f69dd567 100644 --- a/codex-rs/core/src/rollout/policy.rs +++ b/codex-rs/core/src/rollout/policy.rs @@ -60,6 +60,7 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool { | EventMsg::ApplyPatchApprovalRequest(_) | EventMsg::BackgroundEvent(_) | EventMsg::StreamError(_) + | EventMsg::UnifiedExecCall(_) | EventMsg::PatchApplyBegin(_) | EventMsg::PatchApplyEnd(_) | EventMsg::TurnDiff(_) diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index c126208fad..e515d4f500 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -370,6 +370,50 @@ impl EventProcessor for EventProcessorWithHumanOutput { } } } + EventMsg::UnifiedExecCall(unified_exec_event) => { + let requested_id = unified_exec_event.requested_session_id.as_deref(); + let returned_id = unified_exec_event.session_id.as_deref(); + let session_label = match (requested_id, returned_id) { + (None, Some(ret)) => format!("session {ret} (new)"), + (None, None) => "session ".to_string(), + (Some(req), Some(ret)) if req == ret => format!("session {ret}"), + (Some(req), Some(ret)) => format!("session {req} -> {ret}"), + (Some(req), None) => format!("session {req} (ended)"), + }; + + let input_summary = if unified_exec_event.input_chunks.is_empty() { + "".to_string() + } else { + escape_command(&unified_exec_event.input_chunks) + }; + + let status_text = if unified_exec_event.success { + "succeeded" + } else { + "failed" + }; + let status_style = if unified_exec_event.success { + self.green + } else { + self.red + }; + + let title = format!("{session_label} {status_text}: {input_summary}"); + ts_println!( + self, + "{} {}", + "unified_exec".style(self.magenta), + title.style(status_style) + ); + + for line in unified_exec_event + .output + .lines() + .take(MAX_OUTPUT_LINES_FOR_EXEC_TOOL_CALL) + { + println!("{}", line.style(self.dimmed)); + } + } EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id: _ }) => {} EventMsg::WebSearchEnd(WebSearchEndEvent { call_id: _, query }) => { ts_println!(self, "🌐 Searched: {query}"); diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index db48da28e2..4aec51426b 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -264,6 +264,7 @@ async fn run_codex_tool_session_inner( | EventMsg::McpToolCallEnd(_) | EventMsg::McpListToolsResponse(_) | EventMsg::ListCustomPromptsResponse(_) + | EventMsg::UnifiedExecCall(_) | EventMsg::ExecCommandBegin(_) | EventMsg::ExecCommandOutputDelta(_) | EventMsg::ExecCommandEnd(_) diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 6f102cdeb1..f6b95c64ce 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -460,6 +460,8 @@ pub enum EventMsg { McpToolCallEnd(McpToolCallEndEvent), + UnifiedExecCall(UnifiedExecCallEvent), + WebSearchBegin(WebSearchBeginEvent), WebSearchEnd(WebSearchEndEvent), @@ -821,6 +823,22 @@ impl McpToolCallEndEvent { } } +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +pub struct UnifiedExecCallEvent { + /// Identifier for the corresponding `FunctionCall` from the model. + pub call_id: String, + /// Session identifier requested by the model when invoking the tool. + pub requested_session_id: Option, + /// Session identifier returned by the tool after handling the request. + pub session_id: Option, + /// Input chunks provided to the tool. For a new session, this represents the command line. + pub input_chunks: Vec, + /// Aggregated output captured from the subprocess or an error message when the tool failed. + pub output: String, + /// Whether the tool invocation succeeded. + pub success: bool, +} + #[derive(Debug, Clone, Deserialize, Serialize, TS)] pub struct WebSearchBeginEvent { pub call_id: String, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b53f87e664..8040141213 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -32,6 +32,7 @@ use codex_core::protocol::TokenUsage; use codex_core::protocol::TokenUsageInfo; use codex_core::protocol::TurnAbortReason; use codex_core::protocol::TurnDiffEvent; +use codex_core::protocol::UnifiedExecCallEvent; use codex_core::protocol::UserMessageEvent; use codex_core::protocol::WebSearchBeginEvent; use codex_core::protocol::WebSearchEndEvent; @@ -378,6 +379,15 @@ impl ChatWidget { self.defer_or_handle(|q| q.push_mcp_end(ev), |s| s.handle_mcp_end_now(ev2)); } + fn on_unified_exec_call(&mut self, ev: UnifiedExecCallEvent) { + self.flush_answer_stream_with_separator(); + let ev2 = ev.clone(); + self.defer_or_handle( + |q| q.push_unified_exec_call(ev), + |s| s.handle_unified_exec_call_now(ev2), + ); + } + fn on_web_search_begin(&mut self, _ev: WebSearchBeginEvent) { self.flush_answer_stream_with_separator(); } @@ -609,6 +619,11 @@ impl ChatWidget { )); } + pub(crate) fn handle_unified_exec_call_now(&mut self, ev: UnifiedExecCallEvent) { + self.add_to_history(history_cell::new_unified_exec_call(ev)); + self.request_redraw(); + } + fn layout_areas(&self, area: Rect) -> [Rect; 2] { Layout::vertical([ Constraint::Max( @@ -1069,6 +1084,7 @@ impl ChatWidget { EventMsg::ExecCommandEnd(ev) => self.on_exec_command_end(ev), EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev), EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev), + EventMsg::UnifiedExecCall(ev) => self.on_unified_exec_call(ev), EventMsg::WebSearchBegin(ev) => self.on_web_search_begin(ev), EventMsg::WebSearchEnd(ev) => self.on_web_search_end(ev), EventMsg::GetHistoryEntryResponse(ev) => self.on_get_history_entry_response(ev), diff --git a/codex-rs/tui/src/chatwidget/interrupts.rs b/codex-rs/tui/src/chatwidget/interrupts.rs index 531de3e646..9c137174c1 100644 --- a/codex-rs/tui/src/chatwidget/interrupts.rs +++ b/codex-rs/tui/src/chatwidget/interrupts.rs @@ -7,6 +7,7 @@ use codex_core::protocol::ExecCommandEndEvent; use codex_core::protocol::McpToolCallBeginEvent; use codex_core::protocol::McpToolCallEndEvent; use codex_core::protocol::PatchApplyEndEvent; +use codex_core::protocol::UnifiedExecCallEvent; use super::ChatWidget; @@ -19,6 +20,7 @@ pub(crate) enum QueuedInterrupt { McpBegin(McpToolCallBeginEvent), McpEnd(McpToolCallEndEvent), PatchEnd(PatchApplyEndEvent), + UnifiedExec(UnifiedExecCallEvent), } #[derive(Default)] @@ -71,6 +73,10 @@ impl InterruptManager { self.queue.push_back(QueuedInterrupt::PatchEnd(ev)); } + pub(crate) fn push_unified_exec_call(&mut self, ev: UnifiedExecCallEvent) { + self.queue.push_back(QueuedInterrupt::UnifiedExec(ev)); + } + pub(crate) fn flush_all(&mut self, chat: &mut ChatWidget) { while let Some(q) = self.queue.pop_front() { match q { @@ -83,6 +89,7 @@ impl InterruptManager { QueuedInterrupt::McpBegin(ev) => chat.handle_mcp_begin_now(ev), QueuedInterrupt::McpEnd(ev) => chat.handle_mcp_end_now(ev), QueuedInterrupt::PatchEnd(ev) => chat.handle_patch_apply_end_now(ev), + QueuedInterrupt::UnifiedExec(ev) => chat.handle_unified_exec_call_now(ev), } } } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index e4405d7b93..bf49c9dcf1 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1,4 +1,5 @@ use crate::diff_render::create_diff_summary; +use crate::exec_command::escape_command; use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; use crate::markdown::append_markdown; @@ -27,6 +28,7 @@ use codex_core::protocol::McpInvocation; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionConfiguredEvent; use codex_core::protocol::TokenUsage; +use codex_core::protocol::UnifiedExecCallEvent; use codex_protocol::mcp_protocol::ConversationId; use codex_protocol::num_format::format_with_separators; use codex_protocol::parse_command::ParsedCommand; @@ -821,6 +823,74 @@ pub(crate) fn new_completed_mcp_tool_call( Box::new(PlainHistoryCell { lines }) } +pub(crate) fn new_unified_exec_call(event: UnifiedExecCallEvent) -> PlainHistoryCell { + let UnifiedExecCallEvent { + requested_session_id, + session_id, + input_chunks, + output, + success, + .. + } = event; + + let session_label = match (requested_session_id.as_deref(), session_id.as_deref()) { + (None, Some(ret)) => format!("session {ret} (new)"), + (None, None) => "session ".to_string(), + (Some(req), Some(ret)) if req == ret => format!("session {ret}"), + (Some(req), Some(ret)) => format!("session {req} -> {ret}"), + (Some(req), None) => format!("session {req} (ended)"), + }; + + let input_summary = if input_chunks.is_empty() { + "".to_string() + } else { + escape_command(&input_chunks) + }; + + let status_text = if success { "succeeded" } else { "failed" }; + let title = format!("{session_label} {status_text}: {input_summary}"); + let mut lines: Vec> = Vec::new(); + lines.push(Line::from(vec![ + "tool".magenta(), + " ".into(), + "unified_exec".bold(), + " ".into(), + if success { title.green() } else { title.red() }, + ])); + + if !output.trim().is_empty() { + let command_output = if success { + CommandOutput { + exit_code: 0, + stdout: output.clone(), + stderr: String::new(), + formatted_output: String::new(), + } + } else { + CommandOutput { + exit_code: 1, + stdout: String::new(), + stderr: output.clone(), + formatted_output: String::new(), + } + }; + let nested_output = prefix_lines( + output_lines(Some(&command_output), false, false, false), + " ".into(), + " ".into(), + ); + if !nested_output.is_empty() { + let mut labeled_output: Vec> = + Vec::with_capacity(nested_output.len() + 1); + labeled_output.push(Line::from(vec!["output".dim(), ":".dim()])); + labeled_output.extend(nested_output); + lines.extend(prefix_lines(labeled_output, " └ ".dim(), " ".into())); + } + } + + PlainHistoryCell { lines } +} + pub(crate) fn new_status_output( config: &Config, usage: &TokenUsage,