claude_code_acp/mcp/tools/
kill_shell.rs

1//! KillShell tool for terminating background shell processes
2//!
3//! This tool kills a running background shell process started with
4//! `run_in_background=true`.
5//!
6//! Supports two execution modes:
7//! - Direct process execution: shell IDs starting with "shell-"
8//! - Terminal API: shell IDs starting with "term-" (Client-side PTY)
9
10use async_trait::async_trait;
11use serde::Deserialize;
12use serde_json::{Value, json};
13
14use super::base::Tool;
15use crate::mcp::registry::{ToolContext, ToolResult};
16use crate::session::{BackgroundTerminal, TerminalExitStatus};
17use crate::terminal::TerminalId;
18
19/// Prefix for Terminal API shell IDs
20const TERMINAL_API_PREFIX: &str = "term-";
21
22/// KillShell tool implementation
23#[derive(Debug, Default)]
24pub struct KillShellTool;
25
26/// Input parameters for KillShell
27#[derive(Debug, Deserialize)]
28struct KillShellInput {
29    /// The ID of the background shell to kill
30    shell_id: String,
31}
32
33#[async_trait]
34impl Tool for KillShellTool {
35    fn name(&self) -> &str {
36        "KillShell"
37    }
38
39    fn description(&self) -> &str {
40        "Kills a running background bash shell. Use this to terminate long-running \
41         commands that were started with run_in_background=true."
42    }
43
44    fn input_schema(&self) -> Value {
45        json!({
46            "type": "object",
47            "properties": {
48                "shell_id": {
49                    "type": "string",
50                    "description": "The ID of the background shell to kill"
51                }
52            },
53            "required": ["shell_id"]
54        })
55    }
56
57    async fn execute(&self, input: Value, context: &ToolContext) -> ToolResult {
58        // Parse input
59        let params: KillShellInput = match serde_json::from_value(input) {
60            Ok(p) => p,
61            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
62        };
63
64        // Check if this is a Terminal API shell
65        if let Some(terminal_id) = params.shell_id.strip_prefix(TERMINAL_API_PREFIX) {
66            return Self::kill_terminal(terminal_id, context).await;
67        }
68
69        // Fall back to background process manager
70        Self::kill_background_process(&params.shell_id, context).await
71    }
72}
73
74impl KillShellTool {
75    /// Kill a terminal via Terminal API
76    async fn kill_terminal(terminal_id: &str, context: &ToolContext) -> ToolResult {
77        let Some(terminal_client) = context.terminal_client() else {
78            return ToolResult::error("Terminal API not available");
79        };
80
81        let tid = TerminalId::new(terminal_id.to_string());
82
83        // Kill the terminal
84        match terminal_client.kill(tid.clone()).await {
85            Ok(_) => {
86                // Get final output
87                let output = match terminal_client.output(tid.clone()).await {
88                    Ok(resp) => resp.output,
89                    Err(_) => String::new(),
90                };
91
92                // Release the terminal
93                drop(terminal_client.release(tid).await);
94
95                ToolResult::success(format!(
96                    "Terminal command killed successfully.\n\nFinal output:\n{}",
97                    if output.is_empty() {
98                        "(No output)".to_string()
99                    } else {
100                        output
101                    }
102                ))
103                .with_metadata(json!({
104                    "terminal_id": terminal_id,
105                    "terminal_api": true
106                }))
107            }
108            Err(e) => ToolResult::error(format!("Failed to kill terminal: {}", e)),
109        }
110    }
111
112    /// Kill a background process via process manager
113    async fn kill_background_process(shell_id: &str, context: &ToolContext) -> ToolResult {
114        // Get the background process manager from context
115        let Some(manager) = context.background_processes() else {
116            return ToolResult::error("Background process manager not available");
117        };
118
119        // Get the terminal
120        // Use get() because BackgroundTerminal contains ChildHandle
121        // We only need a shared reference to clone the ChildHandle
122        let Some(terminal) = manager.get(shell_id) else {
123            return ToolResult::error(format!("Unknown shell ID: {}", shell_id));
124        };
125
126        // Check the terminal state and kill if running
127        match &*terminal {
128            BackgroundTerminal::Running {
129                child,
130                output_buffer,
131                ..
132            } => {
133                // Clone ChildHandle to hold it across await points
134                let mut child_handle = child.clone();
135                let output_buffer_clone = output_buffer.clone();
136                drop(terminal); // Release DashMap read lock before await
137
138                // Clone output and immediately release lock
139                let final_output = {
140                    let buffer_guard = output_buffer_clone.lock().await;
141                    buffer_guard.clone()
142                }; // Lock released here
143
144                // Kill the process (ChildHandle::kill() handles locking internally)
145                match child_handle.kill().await {
146                    Ok(()) => {
147                        // Update terminal to finished state
148                        manager
149                            .finish_terminal(shell_id, TerminalExitStatus::Killed)
150                            .await;
151
152                        ToolResult::success(format!(
153                            "Command killed successfully.\n\nFinal output:\n{}",
154                            if final_output.is_empty() {
155                                "(No output)".to_string()
156                            } else {
157                                final_output
158                            }
159                        ))
160                    }
161                    Err(e) => ToolResult::error(format!("Failed to kill process: {}", e)),
162                }
163            }
164            BackgroundTerminal::Finished {
165                status,
166                final_output,
167            } => {
168                let message = match status {
169                    TerminalExitStatus::Exited(code) => {
170                        format!("Command had already exited with code {}.", code)
171                    }
172                    TerminalExitStatus::Killed => "Command was already killed.".to_string(),
173                    TerminalExitStatus::TimedOut => "Command was killed by timeout.".to_string(),
174                    TerminalExitStatus::Aborted => "Command was aborted by user.".to_string(),
175                };
176
177                ToolResult::success(format!(
178                    "{}\n\nFinal output:\n{}",
179                    message,
180                    if final_output.is_empty() {
181                        "(No output)".to_string()
182                    } else {
183                        final_output.clone()
184                    }
185                ))
186            }
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_kill_shell_tool_properties() {
197        let tool = KillShellTool;
198        assert_eq!(tool.name(), "KillShell");
199        assert!(tool.description().contains("Kill"));
200    }
201
202    #[test]
203    fn test_kill_shell_input_schema() {
204        let tool = KillShellTool;
205        let schema = tool.input_schema();
206
207        assert_eq!(schema["type"], "object");
208        assert!(schema["properties"]["shell_id"].is_object());
209        assert!(
210            schema["required"]
211                .as_array()
212                .unwrap()
213                .contains(&json!("shell_id"))
214        );
215    }
216}