1use crate::prelude::*;
2use crate::process::start_process::SESSION_MANAGER;
3
4#[derive(Debug, Deserialize, JsonSchema)]
6pub struct InteractWithProcessInput {
7 pub pid: u32,
9
10 pub input: String,
12
13 #[serde(default = "default_wait")]
15 pub wait_for_response: bool,
16
17 #[serde(default = "default_response_timeout")]
19 pub response_timeout_ms: u64,
20}
21
22fn default_wait() -> bool {
23 true
24}
25
26fn default_response_timeout() -> u64 {
27 5000
28}
29
30pub struct InteractWithProcessTool;
32
33impl Tool for InteractWithProcessTool {
34 type Input = InteractWithProcessInput;
35
36 fn name(&self) -> &str {
37 "interact_with_process"
38 }
39
40 fn description(&self) -> &str {
41 "Send input to a running process and optionally wait for its response. Useful for interactive programs."
42 }
43
44 fn format_output_plain(&self, result: &ToolResult) -> String {
45 let text = result.as_text();
46 let (pid, input_sent, status, response) = parse_interact_output(&text);
47
48 let mut out = String::new();
49 out.push_str(&"─".repeat(50));
50 out.push('\n');
51 if let Some(p) = pid {
52 out.push_str(&format!(" Process {} ", p));
53 }
54 if let Some(s) = status {
55 out.push_str(&format!("[{}]", s));
56 }
57 out.push_str(&format!("\n{}\n", "─".repeat(50)));
58 if let Some(cmd) = input_sent {
59 out.push_str(&format!(" >>> {}\n", cmd));
60 }
61 if !response.is_empty() {
62 out.push_str(&"─".repeat(50));
63 out.push('\n');
64 for line in response {
65 out.push_str(&format!(" {}\n", line));
66 }
67 }
68 out
69 }
70
71 fn format_output_ansi(&self, result: &ToolResult) -> String {
72 let text = result.as_text();
73 let (pid, input_sent, status, response) = parse_interact_output(&text);
74
75 let mut out = String::new();
76 out.push_str(&format!("\x1b[2m{}\x1b[0m\n", "─".repeat(50)));
77
78 let (icon, status_color) = match status {
79 Some(s) if s.contains("Running") => ("\x1b[32m●\x1b[0m", "\x1b[32m"),
80 Some(s) if s.contains("Completed") => ("\x1b[34m●\x1b[0m", "\x1b[34m"),
81 Some(s) if s.contains("Waiting") => ("\x1b[33m●\x1b[0m", "\x1b[33m"),
82 _ => ("\x1b[2m●\x1b[0m", "\x1b[2m"),
83 };
84
85 out.push_str(&format!(" {} ", icon));
86 if let Some(p) = pid {
87 out.push_str(&format!("\x1b[1mProcess {}\x1b[0m ", p));
88 }
89 if let Some(s) = status {
90 out.push_str(&format!("{}{}\x1b[0m", status_color, s));
91 }
92 out.push_str(&format!("\n\x1b[2m{}\x1b[0m\n", "─".repeat(50)));
93 if let Some(cmd) = input_sent {
94 out.push_str(&format!(" \x1b[33m>>>\x1b[0m \x1b[36m{}\x1b[0m\n", cmd));
95 }
96 if !response.is_empty() {
97 out.push_str(&format!("\x1b[2m{}\x1b[0m\n", "─".repeat(50)));
98 for line in response {
99 out.push_str(&format!(" \x1b[2m│\x1b[0m {}\n", line));
100 }
101 }
102 out
103 }
104
105 fn format_output_markdown(&self, result: &ToolResult) -> String {
106 let text = result.as_text();
107 let (pid, input_sent, status, response) = parse_interact_output(&text);
108
109 let mut out = String::new();
110 let status_emoji = match status {
111 Some(s) if s.contains("Running") => "🟢",
112 Some(s) if s.contains("Completed") => "🔵",
113 Some(s) if s.contains("Waiting") => "🟡",
114 _ => "⚪",
115 };
116
117 if let Some(p) = pid {
118 out.push_str(&format!("### {} Process {}", status_emoji, p));
119 }
120 if let Some(s) = status {
121 out.push_str(&format!(" - {}", s));
122 }
123 out.push_str("\n\n");
124 if let Some(cmd) = input_sent {
125 out.push_str(&format!("**Input:** `{}`\n\n", cmd));
126 }
127 if !response.is_empty() {
128 out.push_str("**Response:**\n```\n");
129 for line in response {
130 out.push_str(line);
131 out.push('\n');
132 }
133 out.push_str("```\n");
134 }
135 out
136 }
137
138 async fn execute(&self, input: Self::Input) -> std::result::Result<ToolResult, ToolError> {
139 use crate::process::session_manager::ProcessState;
140
141 let manager = SESSION_MANAGER.lock().await;
142
143 manager.send_input(input.pid, &input.input).await?;
145
146 if !input.wait_for_response {
147 return Ok(format!("Sent input to process {}: {}", input.pid, input.input).into());
148 }
149
150 let _ = manager.read_output(input.pid, true).await;
152
153 drop(manager);
154
155 let timeout_ms = input.response_timeout_ms.min(10000);
157 let poll_interval_ms = 50;
158 let max_polls = timeout_ms / poll_interval_ms;
159 let mut exit_reason = "timeout";
160
161 for _ in 0..max_polls {
162 tokio::time::sleep(tokio::time::Duration::from_millis(poll_interval_ms)).await;
163
164 let manager = SESSION_MANAGER.lock().await;
165 let status = manager.check_status(input.pid).await?;
166
167 match status {
168 ProcessState::WaitingForInput => {
169 exit_reason = "prompt_detected";
170 break;
171 }
172 ProcessState::Completed { .. } => {
173 exit_reason = "process_exited";
174 break;
175 }
176 ProcessState::TimedOut => {
177 exit_reason = "process_timeout";
178 break;
179 }
180 ProcessState::Running => {
181 }
183 }
184 }
185
186 let manager = SESSION_MANAGER.lock().await;
187 let output = manager.read_output(input.pid, false).await?;
188 let status = manager.check_status(input.pid).await?;
189
190 let content = format!(
191 "Sent to process {}: {}\nStatus: {:?} ({})\n\nResponse ({} lines):\n{}",
192 input.pid,
193 input.input,
194 status,
195 exit_reason,
196 output.len(),
197 output.join("\n")
198 );
199
200 Ok(content.into())
201 }
202}
203
204fn parse_interact_output(output: &str) -> (Option<&str>, Option<&str>, Option<&str>, Vec<&str>) {
206 let mut pid = None;
207 let mut input_sent = None;
208 let mut status = None;
209 let mut response_lines = Vec::new();
210 let mut in_response = false;
211
212 for line in output.lines() {
213 if line.starts_with("Sent to process ") {
214 let rest = line.trim_start_matches("Sent to process ");
216 if let Some(colon_idx) = rest.find(':') {
217 pid = Some(&rest[..colon_idx]);
218 input_sent = Some(rest[colon_idx + 1..].trim());
219 }
220 } else if line.starts_with("Sent input to process ") {
221 let rest = line.trim_start_matches("Sent input to process ");
223 if let Some(colon_idx) = rest.find(':') {
224 pid = Some(&rest[..colon_idx]);
225 input_sent = Some(rest[colon_idx + 1..].trim());
226 }
227 } else if line.starts_with("Status:") {
228 status = Some(line.trim_start_matches("Status:").trim());
229 } else if line.starts_with("Response (") {
230 in_response = true;
231 } else if in_response {
232 response_lines.push(line);
233 }
234 }
235
236 (pid, input_sent, status, response_lines)
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::process::start_process::{StartProcessInput, StartProcessTool};
243 use mixtape_core::ToolResult;
244
245 #[tokio::test]
246 async fn test_interact_with_process_nonexistent() {
247 let tool = InteractWithProcessTool;
248
249 let input = InteractWithProcessInput {
250 pid: 99999999,
251 input: "test".to_string(),
252 wait_for_response: false,
253 response_timeout_ms: 100,
254 };
255
256 let result = tool.execute(input).await;
257 assert!(result.is_err());
258 }
259
260 #[tokio::test]
261 async fn test_interact_with_process_no_wait() {
262 let start_tool = StartProcessTool;
264 let start_input = StartProcessInput {
265 command: "cat".to_string(), timeout_ms: Some(5000),
267 shell: None,
268 };
269
270 let start_result = start_tool.execute(start_input).await;
271 if start_result.is_err() {
272 return;
274 }
275
276 let start_output = start_result.unwrap().as_text();
277 if let Some(pid_line) = start_output.lines().find(|l| l.contains("PID:")) {
279 if let Some(pid_str) = pid_line.split(':').nth(1) {
280 if let Ok(pid) = pid_str.trim().parse::<u32>() {
281 let interact_tool = InteractWithProcessTool;
283 let interact_input = InteractWithProcessInput {
284 pid,
285 input: "hello".to_string(),
286 wait_for_response: false,
287 response_timeout_ms: 100,
288 };
289
290 let result = interact_tool.execute(interact_input).await;
291 assert!(result.is_ok());
292 let output = result.unwrap().as_text();
293 assert!(output.contains("Sent input to process"));
294 return;
295 }
296 }
297 }
298 }
299
300 #[tokio::test]
301 async fn test_interact_with_process_with_wait() {
302 let start_tool = StartProcessTool;
304 let start_input = StartProcessInput {
305 command: "cat".to_string(),
306 timeout_ms: Some(5000),
307 shell: None,
308 };
309
310 let start_result = start_tool.execute(start_input).await;
311 if start_result.is_err() {
312 return;
313 }
314
315 let start_output = start_result.unwrap().as_text();
316 if let Some(pid_line) = start_output.lines().find(|l| l.contains("PID:")) {
317 if let Some(pid_str) = pid_line.split(':').nth(1) {
318 if let Ok(pid) = pid_str.trim().parse::<u32>() {
319 let interact_tool = InteractWithProcessTool;
321 let interact_input = InteractWithProcessInput {
322 pid,
323 input: "echo test".to_string(),
324 wait_for_response: true,
325 response_timeout_ms: 500,
326 };
327
328 let result = interact_tool.execute(interact_input).await;
329 assert!(result.is_ok());
330 let output = result.unwrap().as_text();
331 assert!(output.contains("Sent to process"));
332 assert!(output.contains("Response"));
333 return;
334 }
335 }
336 }
337 }
338
339 #[test]
342 fn test_parse_interact_output_complete() {
343 let output = "Sent to process 12345: hello\nStatus: Running (prompt_detected)\n\nResponse (2 lines):\nworld\nmore";
344 let (pid, input_sent, status, lines) = parse_interact_output(output);
345
346 assert_eq!(pid, Some("12345"));
347 assert_eq!(input_sent, Some("hello"));
348 assert_eq!(status, Some("Running (prompt_detected)"));
349 assert_eq!(lines, vec!["world", "more"]);
350 }
351
352 #[test]
353 fn test_parse_interact_output_short_form() {
354 let output = "Sent input to process 12345: test command";
355 let (pid, input_sent, status, lines) = parse_interact_output(output);
356
357 assert_eq!(pid, Some("12345"));
358 assert_eq!(input_sent, Some("test command"));
359 assert_eq!(status, None);
360 assert!(lines.is_empty());
361 }
362
363 #[test]
364 fn test_parse_interact_output_empty() {
365 let output = "";
366 let (pid, input_sent, status, lines) = parse_interact_output(output);
367
368 assert_eq!(pid, None);
369 assert_eq!(input_sent, None);
370 assert_eq!(status, None);
371 assert!(lines.is_empty());
372 }
373
374 #[test]
375 fn test_parse_interact_output_with_multiline_response() {
376 let output = "Sent to process 1: cmd\nStatus: Completed\n\nResponse (3 lines):\na\nb\nc";
377 let (_, _, _, lines) = parse_interact_output(output);
378
379 assert_eq!(lines.len(), 3);
380 assert_eq!(lines, vec!["a", "b", "c"]);
381 }
382
383 #[test]
386 fn test_format_output_plain_basic() {
387 let tool = InteractWithProcessTool;
388 let result: ToolResult =
389 "Sent to process 12345: hello\nStatus: Running\n\nResponse (1 lines):\nworld".into();
390
391 let formatted = tool.format_output_plain(&result);
392
393 assert!(formatted.contains("Process 12345"));
394 assert!(formatted.contains(">>> hello"));
395 assert!(formatted.contains("world"));
396 }
397
398 #[test]
399 fn test_format_output_plain_no_response() {
400 let tool = InteractWithProcessTool;
401 let result: ToolResult = "Sent input to process 12345: test".into();
402
403 let formatted = tool.format_output_plain(&result);
404
405 assert!(formatted.contains("Process 12345"));
406 assert!(formatted.contains(">>> test"));
407 }
408
409 #[test]
410 fn test_format_output_ansi_running() {
411 let tool = InteractWithProcessTool;
412 let result: ToolResult =
413 "Sent to process 12345: hello\nStatus: Running\n\nResponse (1 lines):\nworld".into();
414
415 let formatted = tool.format_output_ansi(&result);
416
417 assert!(formatted.contains("\x1b[")); assert!(formatted.contains("\x1b[32m")); assert!(formatted.contains("\x1b[33m")); assert!(formatted.contains("\x1b[36m")); }
422
423 #[test]
424 fn test_format_output_ansi_completed() {
425 let tool = InteractWithProcessTool;
426 let result: ToolResult =
427 "Sent to process 12345: hello\nStatus: Completed\n\nResponse (1 lines):\ndone".into();
428
429 let formatted = tool.format_output_ansi(&result);
430
431 assert!(formatted.contains("\x1b[34m")); }
433
434 #[test]
435 fn test_format_output_markdown_with_response() {
436 let tool = InteractWithProcessTool;
437 let result: ToolResult =
438 "Sent to process 12345: hello\nStatus: Running\n\nResponse (2 lines):\nline1\nline2"
439 .into();
440
441 let formatted = tool.format_output_markdown(&result);
442
443 assert!(formatted.contains("### 🟢 Process 12345"));
444 assert!(formatted.contains("**Input:** `hello`"));
445 assert!(formatted.contains("**Response:**"));
446 assert!(formatted.contains("```"));
447 }
448
449 #[test]
450 fn test_format_output_markdown_status_emojis() {
451 let tool = InteractWithProcessTool;
452
453 let running: ToolResult =
455 "Sent to process 1: x\nStatus: Running\n\nResponse (0 lines):".into();
456 assert!(tool.format_output_markdown(&running).contains("🟢"));
457
458 let completed: ToolResult =
460 "Sent to process 1: x\nStatus: Completed\n\nResponse (0 lines):".into();
461 assert!(tool.format_output_markdown(&completed).contains("🔵"));
462
463 let waiting: ToolResult =
465 "Sent to process 1: x\nStatus: WaitingForInput\n\nResponse (0 lines):".into();
466 assert!(tool.format_output_markdown(&waiting).contains("🟡"));
467 }
468
469 #[test]
472 fn test_default_wait() {
473 assert!(default_wait());
474 }
475
476 #[test]
477 fn test_default_response_timeout() {
478 assert_eq!(default_response_timeout(), 5000);
479 }
480
481 #[test]
484 fn test_tool_name() {
485 let tool = InteractWithProcessTool;
486 assert_eq!(tool.name(), "interact_with_process");
487 }
488
489 #[test]
490 fn test_tool_description() {
491 let tool = InteractWithProcessTool;
492 assert!(!tool.description().is_empty());
493 assert!(tool.description().contains("input"));
494 }
495}