Execute a shell command in the session context (matching TS `SessionPrompt.shell`). Creates a user message + assistant message with a tool call part recording the shell execution and its output. The command is provided by the user through the session UI and is intentionally executed as-is.
(input: &ShellInput, session: &mut Session)
| 3308 | /// the shell execution and its output. The command is provided by the user |
| 3309 | /// through the session UI and is intentionally executed as-is. |
| 3310 | pub async fn shell_exec(input: &ShellInput, session: &mut Session) -> anyhow::Result<String> { |
| 3311 | // Create synthetic user message |
| 3312 | let _user_msg = session.add_user_message("The following tool was executed by the user"); |
| 3313 | |
| 3314 | // Create assistant message with tool call |
| 3315 | let assistant_msg = session.add_assistant_message(); |
| 3316 | let call_id = format!("call_{}", uuid::Uuid::new_v4()); |
| 3317 | assistant_msg.add_tool_call( |
| 3318 | &call_id, |
| 3319 | "bash", |
| 3320 | serde_json::json!({ "command": input.command_str }), |
| 3321 | ); |
| 3322 | |
| 3323 | let invocation = |
| 3324 | resolve_shell_invocation(std::env::var("SHELL").ok().as_deref(), &input.command_str); |
| 3325 | let abort = input.abort.clone().unwrap_or_else(CancellationToken::new); |
| 3326 | |
| 3327 | let mut command = tokio::process::Command::new(&invocation.program); |
| 3328 | command |
| 3329 | .args(&invocation.args) |
| 3330 | .stdout(std::process::Stdio::piped()) |
| 3331 | .stderr(std::process::Stdio::piped()); |
| 3332 | |
| 3333 | let mut child = command.spawn()?; |
| 3334 | let mut stdout = String::new(); |
| 3335 | let mut stderr = String::new(); |
| 3336 | |
| 3337 | let stdout_task = child.stdout.take().map(|mut pipe| { |
| 3338 | tokio::spawn(async move { |
| 3339 | let mut buf = Vec::new(); |
| 3340 | let _ = pipe.read_to_end(&mut buf).await; |
| 3341 | String::from_utf8_lossy(&buf).to_string() |
| 3342 | }) |
| 3343 | }); |
| 3344 | let stderr_task = child.stderr.take().map(|mut pipe| { |
| 3345 | tokio::spawn(async move { |
| 3346 | let mut buf = Vec::new(); |
| 3347 | let _ = pipe.read_to_end(&mut buf).await; |
| 3348 | String::from_utf8_lossy(&buf).to_string() |
| 3349 | }) |
| 3350 | }); |
| 3351 | |
| 3352 | let mut aborted = false; |
| 3353 | tokio::select! { |
| 3354 | _ = child.wait() => {} |
| 3355 | _ = abort.cancelled() => { |
| 3356 | aborted = true; |
| 3357 | let _ = child.kill().await; |
| 3358 | let _ = child.wait().await; |
| 3359 | } |
| 3360 | } |
| 3361 | |
| 3362 | if let Some(task) = stdout_task { |
| 3363 | if let Ok(out) = task.await { |
| 3364 | stdout = out; |
| 3365 | } |
| 3366 | } |
| 3367 | if let Some(task) = stderr_task { |
nothing calls this directly
no test coverage detected