1use crate::prelude::*;
2use crate::process::start_process::SESSION_MANAGER;
3
4#[derive(Debug, Deserialize, JsonSchema)]
6pub struct ReadProcessOutputInput {
7 pub pid: u32,
9
10 #[serde(default)]
12 pub clear_buffer: bool,
13
14 #[serde(default = "default_timeout")]
16 pub timeout_ms: u64,
17}
18
19fn default_timeout() -> u64 {
20 5000
21}
22
23pub struct ReadProcessOutputTool;
25
26impl Tool for ReadProcessOutputTool {
27 type Input = ReadProcessOutputInput;
28
29 fn name(&self) -> &str {
30 "read_process_output"
31 }
32
33 fn description(&self) -> &str {
34 "Read accumulated output from a running process. Can optionally clear the buffer after reading."
35 }
36
37 fn format_output_plain(&self, result: &ToolResult) -> String {
38 let text = result.as_text();
39 let (pid, status, lines) = parse_process_output(&text);
40
41 let mut out = String::new();
42 out.push_str(&"─".repeat(50));
43 out.push('\n');
44 if let Some(p) = pid {
45 out.push_str(&format!(" Process {}", p));
46 }
47 if let Some(s) = status {
48 out.push_str(&format!(" [{}]", s));
49 }
50 out.push_str(&format!("\n{}\n", "─".repeat(50)));
51
52 if lines.is_empty() {
53 out.push_str(" (no output)\n");
54 } else {
55 let width = lines.len().to_string().len().max(3);
56 for (i, line) in lines.iter().enumerate() {
57 out.push_str(&format!(" {:>width$} │ {}\n", i + 1, line, width = width));
58 }
59 }
60 out
61 }
62
63 fn format_output_ansi(&self, result: &ToolResult) -> String {
64 let text = result.as_text();
65 let (pid, status, lines) = parse_process_output(&text);
66
67 let mut out = String::new();
68 out.push_str(&format!("\x1b[2m{}\x1b[0m\n", "─".repeat(50)));
69
70 let (icon, status_color) = match status {
71 Some(s) if s.contains("Running") => ("\x1b[32m●\x1b[0m", "\x1b[32m"),
72 Some(s) if s.contains("Completed") => ("\x1b[34m●\x1b[0m", "\x1b[34m"),
73 Some(s) if s.contains("Waiting") => ("\x1b[33m●\x1b[0m", "\x1b[33m"),
74 _ => ("\x1b[2m●\x1b[0m", "\x1b[2m"),
75 };
76
77 out.push_str(&format!(" {} ", icon));
78 if let Some(p) = pid {
79 out.push_str(&format!("\x1b[1mProcess {}\x1b[0m", p));
80 }
81 if let Some(s) = status {
82 out.push_str(&format!(" {}{}\x1b[0m", status_color, s));
83 }
84 out.push_str(&format!("\n\x1b[2m{}\x1b[0m\n", "─".repeat(50)));
85
86 if lines.is_empty() {
87 out.push_str(" \x1b[2m(no output)\x1b[0m\n");
88 } else {
89 let width = lines.len().to_string().len().max(3);
90 for (i, line) in lines.iter().enumerate() {
91 out.push_str(&format!(
92 " \x1b[36m{:>width$}\x1b[0m \x1b[2m│\x1b[0m {}\n",
93 i + 1,
94 line,
95 width = width
96 ));
97 }
98 }
99 out
100 }
101
102 fn format_output_markdown(&self, result: &ToolResult) -> String {
103 let text = result.as_text();
104 let (pid, status, lines) = parse_process_output(&text);
105
106 let mut out = String::new();
107 let status_emoji = match status {
108 Some(s) if s.contains("Running") => "🟢",
109 Some(s) if s.contains("Completed") => "🔵",
110 Some(s) if s.contains("Waiting") => "🟡",
111 _ => "⚪",
112 };
113
114 if let Some(p) = pid {
115 out.push_str(&format!("### {} Process {}", status_emoji, p));
116 }
117 if let Some(s) = status {
118 out.push_str(&format!(" - {}", s));
119 }
120 out.push_str("\n\n");
121
122 if lines.is_empty() {
123 out.push_str("*No output*\n");
124 } else {
125 out.push_str("```\n");
126 for line in lines {
127 out.push_str(line);
128 out.push('\n');
129 }
130 out.push_str("```\n");
131 }
132 out
133 }
134
135 async fn execute(&self, input: Self::Input) -> std::result::Result<ToolResult, ToolError> {
136 let manager = SESSION_MANAGER.lock().await;
137
138 if manager.get_session(input.pid).await.is_none() {
140 return Err(format!("Process {} not found", input.pid).into());
141 }
142
143 drop(manager);
145 tokio::time::sleep(tokio::time::Duration::from_millis(
146 input.timeout_ms.min(10000),
147 ))
148 .await;
149 let manager = SESSION_MANAGER.lock().await;
150
151 let output = manager.read_output(input.pid, input.clear_buffer).await?;
152 let status = manager.check_status(input.pid).await?;
153
154 let content = if output.is_empty() {
155 format!("Process {}\nStatus: {:?}\nNo new output", input.pid, status)
156 } else {
157 let mut result = format!(
158 "Process {}\nStatus: {:?}\n\nOutput ({} lines):\n",
159 input.pid,
160 status,
161 output.len()
162 );
163
164 for line in &output {
165 result.push_str(&format!("{}\n", line));
166 }
167
168 result
169 };
170
171 Ok(content.into())
172 }
173}
174
175fn parse_process_output(output: &str) -> (Option<&str>, Option<&str>, Vec<&str>) {
177 let mut pid = None;
178 let mut status = None;
179 let mut lines = Vec::new();
180 let mut in_output = false;
181
182 for line in output.lines() {
183 if line.starts_with("Process ") && !line.contains("Output") {
184 pid = line.split_whitespace().nth(1);
185 } else if line.starts_with("Status:") {
186 status = Some(line.trim_start_matches("Status:").trim());
187 } else if line.contains("Output (") || line == "No new output" {
188 in_output = true;
189 if line == "No new output" {
190 }
192 } else if in_output {
193 lines.push(line);
194 }
195 }
196
197 (pid, status, lines)
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::process::start_process::{StartProcessInput, StartProcessTool};
204 use mixtape_core::ToolResult;
205
206 #[tokio::test]
207 async fn test_read_process_output_nonexistent() {
208 let tool = ReadProcessOutputTool;
209
210 let input = ReadProcessOutputInput {
211 pid: 99999999,
212 clear_buffer: false,
213 timeout_ms: 100,
214 };
215
216 let result = tool.execute(input).await;
217 assert!(result.is_err());
218 assert!(result.unwrap_err().to_string().contains("not found"));
219 }
220
221 #[tokio::test]
222 async fn test_read_process_output_basic() {
223 let start_tool = StartProcessTool;
225 let start_input = StartProcessInput {
226 command: "echo 'test output'".to_string(),
227 timeout_ms: Some(5000),
228 shell: None,
229 };
230
231 let start_result = start_tool.execute(start_input).await;
232 if start_result.is_err() {
233 return;
235 }
236
237 let start_output = start_result.unwrap().as_text();
238 if let Some(pid_line) = start_output.lines().find(|l| l.contains("PID:")) {
240 if let Some(pid_str) = pid_line.split(':').nth(1) {
241 if let Ok(pid) = pid_str.trim().parse::<u32>() {
242 let read_tool = ReadProcessOutputTool;
244 let read_input = ReadProcessOutputInput {
245 pid,
246 clear_buffer: false,
247 timeout_ms: 100,
248 };
249
250 let result = read_tool.execute(read_input).await;
251 assert!(result.is_ok());
252 return;
253 }
254 }
255 }
256
257 }
259
260 #[test]
263 fn test_parse_process_output_complete() {
264 let output = "Process 12345\nStatus: Running\n\nOutput (3 lines):\nline1\nline2\nline3";
265 let (pid, status, lines) = parse_process_output(output);
266
267 assert_eq!(pid, Some("12345"));
268 assert_eq!(status, Some("Running"));
269 assert_eq!(lines, vec!["line1", "line2", "line3"]);
270 }
271
272 #[test]
273 fn test_parse_process_output_no_output() {
274 let output = "Process 12345\nStatus: Running\nNo new output";
275 let (pid, status, lines) = parse_process_output(output);
276
277 assert_eq!(pid, Some("12345"));
278 assert_eq!(status, Some("Running"));
279 assert!(lines.is_empty());
280 }
281
282 #[test]
283 fn test_parse_process_output_empty() {
284 let output = "";
285 let (pid, status, lines) = parse_process_output(output);
286
287 assert_eq!(pid, None);
288 assert_eq!(status, None);
289 assert!(lines.is_empty());
290 }
291
292 #[test]
293 fn test_parse_process_output_completed_status() {
294 let output =
295 "Process 999\nStatus: Completed { exit_code: Some(0) }\n\nOutput (1 lines):\ndone";
296 let (pid, status, lines) = parse_process_output(output);
297
298 assert_eq!(pid, Some("999"));
299 assert_eq!(status, Some("Completed { exit_code: Some(0) }"));
300 assert_eq!(lines, vec!["done"]);
301 }
302
303 #[test]
304 fn test_parse_process_output_multiline() {
305 let output = "Process 1\nStatus: Running\n\nOutput (5 lines):\na\nb\nc\nd\ne";
306 let (_, _, lines) = parse_process_output(output);
307
308 assert_eq!(lines.len(), 5);
309 }
310
311 #[test]
314 fn test_format_output_plain_with_output() {
315 let tool = ReadProcessOutputTool;
316 let result: ToolResult =
317 "Process 12345\nStatus: Running\n\nOutput (2 lines):\nHello\nWorld".into();
318
319 let formatted = tool.format_output_plain(&result);
320
321 assert!(formatted.contains("Process 12345"));
322 assert!(formatted.contains("Running"));
323 assert!(formatted.contains("Hello"));
324 assert!(formatted.contains("World"));
325 assert!(formatted.contains("│")); }
327
328 #[test]
329 fn test_format_output_plain_no_output() {
330 let tool = ReadProcessOutputTool;
331 let result: ToolResult = "Process 12345\nStatus: Running\nNo new output".into();
332
333 let formatted = tool.format_output_plain(&result);
334
335 assert!(formatted.contains("(no output)"));
336 }
337
338 #[test]
339 fn test_format_output_ansi_running() {
340 let tool = ReadProcessOutputTool;
341 let result: ToolResult = "Process 12345\nStatus: Running\n\nOutput (1 lines):\ntest".into();
342
343 let formatted = tool.format_output_ansi(&result);
344
345 assert!(formatted.contains("\x1b[")); assert!(formatted.contains("\x1b[32m")); }
348
349 #[test]
350 fn test_format_output_ansi_completed() {
351 let tool = ReadProcessOutputTool;
352 let result: ToolResult =
353 "Process 12345\nStatus: Completed\n\nOutput (1 lines):\ndone".into();
354
355 let formatted = tool.format_output_ansi(&result);
356
357 assert!(formatted.contains("\x1b[34m")); }
359
360 #[test]
361 fn test_format_output_ansi_waiting() {
362 let tool = ReadProcessOutputTool;
363 let result: ToolResult =
364 "Process 12345\nStatus: WaitingForInput\n\nOutput (1 lines):\n>>> ".into();
365
366 let formatted = tool.format_output_ansi(&result);
367
368 assert!(formatted.contains("\x1b[33m")); }
370
371 #[test]
372 fn test_format_output_markdown_with_output() {
373 let tool = ReadProcessOutputTool;
374 let result: ToolResult =
375 "Process 12345\nStatus: Running\n\nOutput (2 lines):\nline1\nline2".into();
376
377 let formatted = tool.format_output_markdown(&result);
378
379 assert!(formatted.contains("### 🟢 Process 12345")); assert!(formatted.contains("```"));
381 assert!(formatted.contains("line1"));
382 }
383
384 #[test]
385 fn test_format_output_markdown_no_output() {
386 let tool = ReadProcessOutputTool;
387 let result: ToolResult = "Process 12345\nStatus: Running\nNo new output".into();
388
389 let formatted = tool.format_output_markdown(&result);
390
391 assert!(formatted.contains("*No output*"));
392 }
393
394 #[test]
395 fn test_format_output_markdown_status_emojis() {
396 let tool = ReadProcessOutputTool;
397
398 let running: ToolResult = "Process 1\nStatus: Running\nNo new output".into();
400 assert!(tool.format_output_markdown(&running).contains("🟢"));
401
402 let completed: ToolResult = "Process 1\nStatus: Completed\nNo new output".into();
404 assert!(tool.format_output_markdown(&completed).contains("🔵"));
405
406 let waiting: ToolResult = "Process 1\nStatus: WaitingForInput\nNo new output".into();
408 assert!(tool.format_output_markdown(&waiting).contains("🟡"));
409 }
410
411 #[test]
414 fn test_default_timeout() {
415 assert_eq!(default_timeout(), 5000);
416 }
417
418 #[test]
421 fn test_tool_name() {
422 let tool = ReadProcessOutputTool;
423 assert_eq!(tool.name(), "read_process_output");
424 }
425
426 #[test]
427 fn test_tool_description() {
428 let tool = ReadProcessOutputTool;
429 assert!(!tool.description().is_empty());
430 assert!(tool.description().contains("output"));
431 }
432}