1use crate::prelude::*;
2use crate::process::session_manager::{ProcessState, SessionManager};
3use std::sync::Arc;
4use tokio::sync::Mutex;
5
6lazy_static::lazy_static! {
7 pub(crate) static ref SESSION_MANAGER: Arc<Mutex<SessionManager>> = Arc::new(Mutex::new(SessionManager::new()));
8}
9
10#[derive(Debug, Deserialize, JsonSchema)]
12pub struct StartProcessInput {
13 pub command: String,
15
16 #[serde(default)]
18 pub timeout_ms: Option<u64>,
19
20 #[serde(default)]
22 pub shell: Option<String>,
23}
24
25pub struct StartProcessTool;
27
28impl Tool for StartProcessTool {
29 type Input = StartProcessInput;
30
31 fn name(&self) -> &str {
32 "start_process"
33 }
34
35 fn description(&self) -> &str {
36 "Start a new process session. Returns a PID that can be used to interact with the process, read its output, or terminate it."
37 }
38
39 fn format_output_plain(&self, result: &ToolResult) -> String {
40 let text = result.as_text();
41 let (command, pid, status, output_lines) = parse_start_output(&text);
42
43 let mut out = String::new();
44 out.push_str(&"─".repeat(50));
45 out.push_str("\n PROCESS STARTED\n");
46 out.push_str(&"─".repeat(50));
47 out.push('\n');
48
49 if let Some(cmd) = command {
50 out.push_str(&format!(" Command: {}\n", cmd));
51 }
52 if let Some(p) = pid {
53 out.push_str(&format!(" PID: {}\n", p));
54 }
55 if let Some(s) = status {
56 out.push_str(&format!(" Status: {}\n", s));
57 }
58
59 if !output_lines.is_empty() {
60 out.push_str(&"─".repeat(50));
61 out.push('\n');
62 for line in output_lines {
63 out.push_str(&format!(" {}\n", line));
64 }
65 }
66 out
67 }
68
69 fn format_output_ansi(&self, result: &ToolResult) -> String {
70 let text = result.as_text();
71 let (command, pid, status, output_lines) = parse_start_output(&text);
72
73 let mut out = String::new();
74 out.push_str(&format!("\x1b[2m{}\x1b[0m\n \x1b[32m●\x1b[0m \x1b[1mProcess Started\x1b[0m\n\x1b[2m{}\x1b[0m\n", "─".repeat(50), "─".repeat(50)));
75
76 if let Some(cmd) = command {
77 out.push_str(&format!(
78 " \x1b[2mCommand\x1b[0m \x1b[36m{}\x1b[0m\n",
79 cmd
80 ));
81 }
82 if let Some(p) = pid {
83 out.push_str(&format!(" \x1b[2mPID\x1b[0m \x1b[33m{}\x1b[0m\n", p));
84 }
85 if let Some(s) = status {
86 let status_color = if s.contains("Running") {
87 "\x1b[32m"
88 } else if s.contains("Completed") {
89 "\x1b[34m"
90 } else {
91 "\x1b[33m"
92 };
93 out.push_str(&format!(
94 " \x1b[2mStatus\x1b[0m {}{}\x1b[0m\n",
95 status_color, s
96 ));
97 }
98
99 if !output_lines.is_empty() {
100 out.push_str(&format!("\x1b[2m{}\x1b[0m\n", "─".repeat(50)));
101 for line in output_lines {
102 out.push_str(&format!(" \x1b[2m│\x1b[0m {}\n", line));
103 }
104 }
105 out
106 }
107
108 fn format_output_markdown(&self, result: &ToolResult) -> String {
109 let text = result.as_text();
110 let (command, pid, status, output_lines) = parse_start_output(&text);
111
112 let mut out = String::from("### 🚀 Process Started\n\n");
113 if let Some(cmd) = command {
114 out.push_str(&format!("- **Command**: `{}`\n", cmd));
115 }
116 if let Some(p) = pid {
117 out.push_str(&format!("- **PID**: `{}`\n", p));
118 }
119 if let Some(s) = status {
120 out.push_str(&format!("- **Status**: {}\n", s));
121 }
122
123 if !output_lines.is_empty() {
124 out.push_str("\n**Initial Output:**\n```\n");
125 for line in output_lines {
126 out.push_str(line);
127 out.push('\n');
128 }
129 out.push_str("```\n");
130 }
131 out
132 }
133
134 async fn execute(&self, input: Self::Input) -> std::result::Result<ToolResult, ToolError> {
135 let manager = SESSION_MANAGER.lock().await;
136 let pid = manager
137 .create_session(input.command.clone(), input.shell, input.timeout_ms)
138 .await?;
139
140 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
142
143 let initial_output = manager.read_output(pid, false).await.unwrap_or_default();
145 let status = manager
146 .check_status(pid)
147 .await
148 .unwrap_or(ProcessState::Running);
149
150 let mut content = format!(
151 "Started process: {}\nPID: {}\nStatus: {:?}\n",
152 input.command, pid, status
153 );
154
155 if !initial_output.is_empty() {
156 content.push_str("\nInitial output:\n");
157 for line in initial_output.iter().take(20) {
158 content.push_str(&format!("{}\n", line));
159 }
160 if initial_output.len() > 20 {
161 content.push_str(&format!(
162 "... and {} more lines\n",
163 initial_output.len() - 20
164 ));
165 }
166 }
167
168 Ok(content.into())
169 }
170}
171
172fn parse_start_output(output: &str) -> (Option<&str>, Option<&str>, Option<&str>, Vec<&str>) {
174 let mut command = None;
175 let mut pid = None;
176 let mut status = None;
177 let mut output_lines = Vec::new();
178 let mut in_output = false;
179
180 for line in output.lines() {
181 if line.starts_with("Started process:") {
182 command = Some(line.trim_start_matches("Started process:").trim());
183 } else if line.starts_with("PID:") {
184 pid = Some(line.trim_start_matches("PID:").trim());
185 } else if line.starts_with("Status:") {
186 status = Some(line.trim_start_matches("Status:").trim());
187 } else if line.starts_with("Initial output:") {
188 in_output = true;
189 } else if in_output && !line.starts_with("...") {
190 output_lines.push(line);
191 }
192 }
193
194 (command, pid, status, output_lines)
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use mixtape_core::ToolResult;
201
202 #[tokio::test]
203 async fn test_start_process_simple_command() {
204 let tool = StartProcessTool;
205
206 let input = StartProcessInput {
208 command: "echo 'Hello from process'".to_string(),
209 timeout_ms: Some(5000),
210 shell: None,
211 };
212
213 let result = tool.execute(input).await;
214 assert!(result.is_ok());
215
216 let output = result.unwrap().as_text();
217 assert!(output.contains("Started process"));
218 assert!(output.contains("PID:"));
219 }
220
221 #[tokio::test]
222 async fn test_start_process_with_timeout() {
223 let tool = StartProcessTool;
224
225 let input = StartProcessInput {
226 command: "echo 'test'".to_string(),
227 timeout_ms: Some(1000),
228 shell: None,
229 };
230
231 let result = tool.execute(input).await;
232 assert!(result.is_ok());
233 }
234
235 #[tokio::test]
236 async fn test_start_process_empty_command() {
237 let tool = StartProcessTool;
238
239 let input = StartProcessInput {
240 command: String::new(),
241 timeout_ms: Some(5000),
242 shell: None,
243 };
244
245 let result = tool.execute(input).await;
246 assert!(result.is_ok() || result.is_err());
248 }
249
250 #[test]
253 fn test_parse_start_output_complete() {
254 let output = "Started process: echo hello\nPID: 12345\nStatus: Running\nInitial output:\nHello World\nLine 2";
255 let (command, pid, status, lines) = parse_start_output(output);
256
257 assert_eq!(command, Some("echo hello"));
258 assert_eq!(pid, Some("12345"));
259 assert_eq!(status, Some("Running"));
260 assert_eq!(lines, vec!["Hello World", "Line 2"]);
261 }
262
263 #[test]
264 fn test_parse_start_output_no_output() {
265 let output = "Started process: sleep 10\nPID: 12345\nStatus: Running";
266 let (command, pid, status, lines) = parse_start_output(output);
267
268 assert_eq!(command, Some("sleep 10"));
269 assert_eq!(pid, Some("12345"));
270 assert_eq!(status, Some("Running"));
271 assert!(lines.is_empty());
272 }
273
274 #[test]
275 fn test_parse_start_output_empty() {
276 let output = "";
277 let (command, pid, status, lines) = parse_start_output(output);
278
279 assert_eq!(command, None);
280 assert_eq!(pid, None);
281 assert_eq!(status, None);
282 assert!(lines.is_empty());
283 }
284
285 #[test]
286 fn test_parse_start_output_partial() {
287 let output = "PID: 99999";
288 let (command, pid, status, lines) = parse_start_output(output);
289
290 assert_eq!(command, None);
291 assert_eq!(pid, Some("99999"));
292 assert_eq!(status, None);
293 assert!(lines.is_empty());
294 }
295
296 #[test]
297 fn test_parse_start_output_with_more_lines_indicator() {
298 let output = "Started process: cmd\nPID: 1\nStatus: Running\nInitial output:\nline1\n... and 5 more lines";
299 let (_, _, _, lines) = parse_start_output(output);
300
301 assert_eq!(lines, vec!["line1"]);
303 }
304
305 #[test]
308 fn test_format_output_plain_basic() {
309 let tool = StartProcessTool;
310 let result: ToolResult = "Started process: echo test\nPID: 12345\nStatus: Running".into();
311
312 let formatted = tool.format_output_plain(&result);
313
314 assert!(formatted.contains("PROCESS STARTED"));
315 assert!(formatted.contains("Command:"));
316 assert!(formatted.contains("PID:"));
317 assert!(formatted.contains("Status:"));
318 }
319
320 #[test]
321 fn test_format_output_plain_with_output() {
322 let tool = StartProcessTool;
323 let result: ToolResult = "Started process: echo test\nPID: 12345\nStatus: Completed { exit_code: Some(0) }\nInitial output:\nHello".into();
324
325 let formatted = tool.format_output_plain(&result);
326
327 assert!(formatted.contains("Hello"));
328 }
329
330 #[test]
331 fn test_format_output_ansi_colors() {
332 let tool = StartProcessTool;
333 let result: ToolResult = "Started process: echo test\nPID: 12345\nStatus: Running".into();
334
335 let formatted = tool.format_output_ansi(&result);
336
337 assert!(formatted.contains("\x1b["));
339 assert!(formatted.contains("Process Started"));
340 }
341
342 #[test]
343 fn test_format_output_ansi_status_colors() {
344 let tool = StartProcessTool;
345
346 let running: ToolResult = "Started process: test\nPID: 1\nStatus: Running".into();
348 let formatted = tool.format_output_ansi(&running);
349 assert!(formatted.contains("\x1b[32m")); let completed: ToolResult = "Started process: test\nPID: 1\nStatus: Completed".into();
353 let formatted = tool.format_output_ansi(&completed);
354 assert!(formatted.contains("\x1b[34m")); }
356
357 #[test]
358 fn test_format_output_markdown() {
359 let tool = StartProcessTool;
360 let result: ToolResult =
361 "Started process: echo test\nPID: 12345\nStatus: Running\nInitial output:\nHello"
362 .into();
363
364 let formatted = tool.format_output_markdown(&result);
365
366 assert!(formatted.contains("### 🚀 Process Started"));
367 assert!(formatted.contains("**Command**: `echo test`"));
368 assert!(formatted.contains("**PID**: `12345`"));
369 assert!(formatted.contains("**Initial Output:**"));
370 assert!(formatted.contains("```"));
371 }
372
373 #[test]
374 fn test_format_output_markdown_no_output() {
375 let tool = StartProcessTool;
376 let result: ToolResult = "Started process: sleep 10\nPID: 12345\nStatus: Running".into();
377
378 let formatted = tool.format_output_markdown(&result);
379
380 assert!(!formatted.contains("**Initial Output:**"));
382 }
383
384 #[test]
387 fn test_tool_name() {
388 let tool = StartProcessTool;
389 assert_eq!(tool.name(), "start_process");
390 }
391
392 #[test]
393 fn test_tool_description() {
394 let tool = StartProcessTool;
395 assert!(!tool.description().is_empty());
396 assert!(tool.description().contains("process"));
397 }
398}