use crate::{ cursor::{ interaction, proto::agent::v1 as pb, tools::{ edit, result::{self, ToolCompletion}, runtime::{CursorToolRuntime, ExecStage, PendingExec}, }, }, model::ToolCall, Error, Result, }; use super::request::edit_write_request; pub enum ClientExecEvent { Delta(Box), Message(Box), Completed(Box), Pending, } pub async fn client_event( message: &pb::ExecClientMessage, pending: &CursorToolRuntime, ) -> Result { if pending.is_interrupted(message.id).await { if message.message.as_ref().is_some_and(is_terminal) { pending.discard_exec(message.id).await; } return Ok(ClientExecEvent::Pending); } let call = match pending.exec_call(message.id).await { Some(call) => call, None if pending.completed_call(message.id).await.is_some() => { return Err(Error::Protocol(format!( "duplicate terminal ExecClientMessage id: {}", message.id ))) } None => { return Err(Error::Protocol(format!( "unknown ExecClientMessage id: {}", message.id ))) } }; let Some(wire_result) = &message.message else { return Ok(ClientExecEvent::Pending); }; let pb::exec_client_message::Message::ShellStream(stream) = wire_result else { let entry = take(message.id, pending).await?; return match entry.stage { ExecStage::EditRead => advance_edit(entry, wire_result, pending).await, ExecStage::Direct | ExecStage::DynamicMcp(_) | ExecStage::EditWrite(_) => { completed(entry, wire_result.clone()) } }; }; use pb::shell_stream::Event; let event = match &stream.event { Some(Event::Stdout(stdout)) => { if pending.append_stdout(message.id, &stdout.data).await { ClientExecEvent::Delta(Box::new(shell_delta(&call, true, &stdout.data))) } else { ClientExecEvent::Pending } } Some(Event::Stderr(stderr)) => { if pending.append_stderr(message.id, &stderr.data).await { ClientExecEvent::Delta(Box::new(shell_delta(&call, false, &stderr.data))) } else { ClientExecEvent::Pending } } Some(Event::Start(_)) | Some(Event::HookContext(_)) => ClientExecEvent::Pending, Some(Event::Exit(exit)) => { let entry = take(message.id, pending).await?; let result = shell_exit_result(message, exit, &entry.stdout, &entry.stderr); completed(entry, pb::exec_client_message::Message::ShellResult(result))? } Some(Event::Backgrounded(backgrounded)) => { let entry = take(message.id, pending).await?; let result = shell_backgrounded_result( backgrounded, &entry.stdout, &entry.stderr, &entry.context.terminals_folder, ); completed(entry, pb::exec_client_message::Message::ShellResult(result))? } Some(Event::Rejected(value)) => { let result = pb::ShellResult { result: Some(pb::shell_result::Result::Rejected(value.clone())), ..Default::default() }; complete( message.id, pending, pb::exec_client_message::Message::ShellResult(result), ) .await? } Some(Event::PermissionDenied(value)) => { let result = pb::ShellResult { result: Some(pb::shell_result::Result::PermissionDenied(value.clone())), ..Default::default() }; complete( message.id, pending, pb::exec_client_message::Message::ShellResult(result), ) .await? } Some(Event::SandboxUnsupported(value)) => { let result = pb::ShellResult { result: Some(pb::shell_result::Result::SpawnError(pb::ShellSpawnError { command: value.command.clone(), working_directory: value.working_directory.clone(), error: value.reason.clone(), })), ..Default::default() }; complete( message.id, pending, pb::exec_client_message::Message::ShellResult(result), ) .await? } None => ClientExecEvent::Pending, }; Ok(event) } pub async fn stream_closed(id: u32, pending: &CursorToolRuntime) -> Result> { if pending.is_interrupted(id).await { pending.discard_exec(id).await; return Ok(None); } let Some(entry) = pending.take_exec(id).await else { return Ok(None); }; let error = "Cursor Exec stream closed before returning a terminal result"; if entry.call.name.eq_ignore_ascii_case("Shell") { let command = entry .call .arguments .get("command") .and_then(serde_json::Value::as_str) .unwrap_or_default() .to_string(); let working_directory = entry .call .arguments .get("working_directory") .and_then(serde_json::Value::as_str) .unwrap_or_default() .to_string(); return Ok(Some(result::from_exec( entry, &pb::exec_client_message::Message::ShellResult(pb::ShellResult { result: Some(pb::shell_result::Result::SpawnError(pb::ShellSpawnError { command, working_directory, error: error.into(), })), ..Default::default() }), )?)); } let rendered = match &entry.stage { ExecStage::DynamicMcp(definition) => { interaction::render_dynamic_mcp(&entry.call, definition, false) } _ => interaction::render_tool_call(&entry.call, false)?, }; Ok(Some(ToolCompletion::from_rendered( &entry.call, entry.started_at_ms, error.into(), true, rendered, )?)) } fn is_terminal(message: &pb::exec_client_message::Message) -> bool { use pb::{exec_client_message::Message, shell_stream::Event}; match message { Message::ShellStream(stream) => matches!( stream.event.as_ref(), Some(Event::Exit(_)) | Some(Event::Backgrounded(_)) | Some(Event::Rejected(_)) | Some(Event::PermissionDenied(_)) | Some(Event::SandboxUnsupported(_)) ), _ => true, } } async fn advance_edit( entry: PendingExec, result: &pb::exec_client_message::Message, registry: &CursorToolRuntime, ) -> Result { let read = match result { pb::exec_client_message::Message::ReadResult(result) | pb::exec_client_message::Message::RedactedReadResult(result) => result, _ => { return Err(Error::Protocol(format!( "expected ReadResult for edit tool {}", entry.call.name ))) } }; let write = match edit::after_read(&entry.call, read) { Ok(write) => write, Err(error) => { return Ok(ClientExecEvent::Completed(Box::new(result::edit_failure( entry, error, )?))) } }; let id = registry .reserve_edit_write( &entry.call, &entry.context, write.clone(), entry.started_at_ms, ) .await?; Ok(ClientExecEvent::Message(Box::new(edit_write_request( id, &entry.call, &write, )?))) } async fn complete( id: u32, pending: &CursorToolRuntime, result: pb::exec_client_message::Message, ) -> Result { completed(take(id, pending).await?, result) } async fn take(id: u32, pending: &CursorToolRuntime) -> Result { pending .take_exec(id) .await .ok_or_else(|| Error::Protocol(format!("unknown terminal Exec id: {id}"))) } fn completed( pending: PendingExec, result: pb::exec_client_message::Message, ) -> Result { Ok(ClientExecEvent::Completed(Box::new(result::from_exec( pending, &result, )?))) } fn shell_exit_result( message: &pb::ExecClientMessage, exit: &pb::ShellStreamExit, stdout: &str, stderr: &str, ) -> pb::ShellResult { let result = if exit.code == 0 && !exit.aborted { pb::shell_result::Result::Success(pb::ShellSuccess { working_directory: exit.cwd.clone(), exit_code: exit.code as i32, stdout: stdout.into(), stderr: stderr.into(), interleaved_output: Some(format!("{stdout}{stderr}")), local_execution_time_ms: exit .local_execution_time_ms .or(message.local_execution_time_ms), ..Default::default() }) } else { pb::shell_result::Result::Failure(pb::ShellFailure { working_directory: exit.cwd.clone(), exit_code: exit.code as i32, stdout: stdout.into(), stderr: stderr.into(), interleaved_output: Some(format!("{stdout}{stderr}")), abort_reason: exit.abort_reason, aborted: exit.aborted, local_execution_time_ms: exit .local_execution_time_ms .or(message.local_execution_time_ms), ..Default::default() }) }; pb::ShellResult { result: Some(result), is_background: Some(false), ..Default::default() } } fn shell_backgrounded_result( backgrounded: &pb::ShellStreamBackgrounded, stdout: &str, stderr: &str, terminals_folder: &str, ) -> pb::ShellResult { pb::ShellResult { result: Some(pb::shell_result::Result::Success(pb::ShellSuccess { command: backgrounded.command.clone(), working_directory: backgrounded.working_directory.clone(), stdout: stdout.into(), stderr: stderr.into(), shell_id: Some(backgrounded.shell_id), pid: backgrounded.pid, ms_to_wait: backgrounded.ms_to_wait, background_reason: backgrounded.reason, interleaved_output: Some(format!("{stdout}{stderr}")), ..Default::default() })), is_background: Some(true), terminals_folder: (!terminals_folder.is_empty()).then(|| terminals_folder.into()), pid: backgrounded.pid, ..Default::default() } } fn shell_delta(call: &ToolCall, stdout: bool, content: &str) -> pb::AgentServerMessage { let delta = if stdout { pb::shell_tool_call_delta::Delta::Stdout(pb::ShellToolCallStdoutDelta { content: content.into(), }) } else { pb::shell_tool_call_delta::Delta::Stderr(pb::ShellToolCallStderrDelta { content: content.into(), }) }; interaction::server_interaction(pb::interaction_update::Message::ToolCallDelta(Box::new( pb::ToolCallDeltaUpdate { call_id: call.call_id.clone(), tool_call_delta: Some(Box::new(pb::ToolCallDelta { delta: Some(pb::tool_call_delta::Delta::ShellToolCallDelta( pb::ShellToolCallDelta { delta: Some(delta) }, )), })), model_call_id: call.model_call_id.clone(), }, ))) }