1use crate::prelude::*;
2use crate::process::start_process::SESSION_MANAGER;
3
4#[derive(Debug, Deserialize, JsonSchema)]
6pub struct ListSessionsInput {}
7
8pub struct ListSessionsTool;
10
11impl Tool for ListSessionsTool {
12 type Input = ListSessionsInput;
13
14 fn name(&self) -> &str {
15 "list_sessions"
16 }
17
18 fn description(&self) -> &str {
19 "List all active process sessions with their PIDs, commands, status, and runtime."
20 }
21
22 async fn execute(&self, _input: Self::Input) -> std::result::Result<ToolResult, ToolError> {
23 let manager = SESSION_MANAGER.lock().await;
24 let sessions = manager.list_sessions().await;
25
26 if sessions.is_empty() {
27 return Ok("No active sessions".into());
28 }
29
30 let mut content = String::from("Active Sessions:\n\n");
31 content.push_str("PID | STATUS | RUNTIME | COMMAND\n");
32 content.push_str("-------|---------------------|---------|------------------\n");
33
34 for (pid, command, status, elapsed_ms) in sessions {
35 let runtime = if elapsed_ms < 1000 {
36 format!("{}ms", elapsed_ms)
37 } else if elapsed_ms < 60_000 {
38 format!("{:.1}s", elapsed_ms as f64 / 1000.0)
39 } else {
40 format!("{:.1}m", elapsed_ms as f64 / 60_000.0)
41 };
42
43 let status_str = format!("{:?}", status);
44 let cmd_preview = if command.len() > 30 {
45 format!("{}...", &command[..27])
46 } else {
47 command
48 };
49
50 content.push_str(&format!(
51 "{:<6} | {:<19} | {:<7} | {}\n",
52 pid, status_str, runtime, cmd_preview
53 ));
54 }
55
56 Ok(content.into())
57 }
58
59 fn format_output_plain(&self, result: &ToolResult) -> String {
60 let output = result.as_text();
61 if output == "No active sessions" {
62 return output.to_string();
63 }
64
65 let lines: Vec<&str> = output.lines().collect();
66 let mut out = String::from("Sessions\n");
67 out.push_str(&"─".repeat(60));
68 out.push('\n');
69
70 for line in lines.iter().skip(4) {
71 let parts: Vec<&str> = line.split('|').collect();
72 if parts.len() >= 4 {
73 let (pid, status, runtime, command) = (
74 parts[0].trim(),
75 parts[1].trim(),
76 parts[2].trim(),
77 parts[3].trim(),
78 );
79 let status_icon = if status.contains("Running") {
80 "●"
81 } else if status.contains("Completed") {
82 "✓"
83 } else {
84 "○"
85 };
86 out.push_str(&format!(
87 "{} [{}] {} - {} ({})\n",
88 status_icon,
89 pid,
90 command,
91 status,
92 format_runtime_nice(runtime)
93 ));
94 }
95 }
96 out
97 }
98
99 fn format_output_ansi(&self, result: &ToolResult) -> String {
100 let output = result.as_text();
101 if output == "No active sessions" {
102 return format!("\x1b[2m{}\x1b[0m", output);
103 }
104
105 let lines: Vec<&str> = output.lines().collect();
106 let mut out = String::from("\x1b[1mSessions\x1b[0m\n");
107 out.push_str(&format!("\x1b[2m{}\x1b[0m\n", "─".repeat(60)));
108
109 for line in lines.iter().skip(4) {
110 let parts: Vec<&str> = line.split('|').collect();
111 if parts.len() >= 4 {
112 let (pid, status, runtime, command) = (
113 parts[0].trim(),
114 parts[1].trim(),
115 parts[2].trim(),
116 parts[3].trim(),
117 );
118 let (status_icon, status_color) = if status.contains("Running") {
119 ("\x1b[32m●\x1b[0m", "\x1b[32m")
120 } else if status.contains("Completed") {
121 ("\x1b[34m✓\x1b[0m", "\x1b[34m")
122 } else if status.contains("Failed") || status.contains("Error") {
123 ("\x1b[31m✗\x1b[0m", "\x1b[31m")
124 } else {
125 ("\x1b[33m○\x1b[0m", "\x1b[33m")
126 };
127 out.push_str(&format!(
128 "{} \x1b[36m[{}]\x1b[0m {} {}{}\x1b[0m \x1b[2m({})\x1b[0m\n",
129 status_icon,
130 pid,
131 command,
132 status_color,
133 status,
134 format_runtime_nice(runtime)
135 ));
136 }
137 }
138 out
139 }
140
141 fn format_output_markdown(&self, result: &ToolResult) -> String {
142 let output = result.as_text();
143 if output == "No active sessions" {
144 return format!("*{}*", output);
145 }
146
147 let lines: Vec<&str> = output.lines().collect();
148 let mut out = String::from("### Sessions\n\n| Status | PID | Command | Runtime |\n|--------|-----|---------|--------|\n");
149
150 for line in lines.iter().skip(4) {
151 let parts: Vec<&str> = line.split('|').collect();
152 if parts.len() >= 4 {
153 let (pid, status, runtime, command) = (
154 parts[0].trim(),
155 parts[1].trim(),
156 parts[2].trim(),
157 parts[3].trim(),
158 );
159 let status_emoji = if status.contains("Running") {
160 "🟢"
161 } else if status.contains("Completed") {
162 "🔵"
163 } else if status.contains("Failed") || status.contains("Error") {
164 "🔴"
165 } else {
166 "🟡"
167 };
168 out.push_str(&format!(
169 "| {} {} | {} | `{}` | {} |\n",
170 status_emoji,
171 status,
172 pid,
173 command,
174 format_runtime_nice(runtime)
175 ));
176 }
177 }
178 out
179 }
180}
181
182fn format_runtime_nice(runtime_str: &str) -> String {
184 let s = runtime_str.trim();
186 if s.ends_with("ms") {
187 s.to_string()
188 } else if s.ends_with('s') {
189 let secs: f64 = s.trim_end_matches('s').parse().unwrap_or(0.0);
190 if secs < 60.0 {
191 format!("{:.0}s", secs)
192 } else {
193 let mins = (secs / 60.0).floor();
194 let remaining_secs = secs % 60.0;
195 format!("{}m {:02.0}s", mins as u32, remaining_secs)
196 }
197 } else if s.ends_with('m') {
198 let mins: f64 = s.trim_end_matches('m').parse().unwrap_or(0.0);
199 if mins < 60.0 {
200 format!("{:.0}m", mins)
201 } else {
202 let hours = (mins / 60.0).floor();
203 let remaining_mins = mins % 60.0;
204 format!("{}h {:02.0}m", hours as u32, remaining_mins)
205 }
206 } else {
207 s.to_string()
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::process::start_process::{StartProcessInput, StartProcessTool};
215 use mixtape_core::ToolResult;
216
217 #[tokio::test]
218 async fn test_list_sessions_empty() {
219 let tool = ListSessionsTool;
220 let input = ListSessionsInput {};
221
222 let result = tool.execute(input).await;
223 assert!(result.is_ok());
224
225 let output = result.unwrap().as_text();
226 assert!(!output.is_empty());
228 }
229
230 #[tokio::test]
231 async fn test_list_sessions_with_processes() {
232 let start_tool = StartProcessTool;
234
235 let input1 = StartProcessInput {
236 command: "echo 'session 1'".to_string(),
237 timeout_ms: Some(5000),
238 shell: None,
239 };
240
241 let input2 = StartProcessInput {
242 command: "sleep 5".to_string(),
243 timeout_ms: Some(10000),
244 shell: None,
245 };
246
247 let result1 = start_tool.execute(input1).await;
249 if result1.is_err() {
250 return;
252 }
253
254 let _ = start_tool.execute(input2).await;
256
257 let list_tool = ListSessionsTool;
259 let list_input = ListSessionsInput {};
260
261 let result = list_tool.execute(list_input).await;
262 assert!(result.is_ok());
263
264 let output = result.unwrap().as_text();
265 assert!(output.contains("PID"));
267 assert!(output.contains("STATUS"));
268 assert!(output.contains("RUNTIME"));
269 assert!(output.contains("COMMAND"));
270 }
271
272 #[tokio::test]
273 async fn test_list_sessions_shows_runtime() {
274 let start_tool = StartProcessTool;
275 let input = StartProcessInput {
276 command: "sleep 2".to_string(),
277 timeout_ms: Some(5000),
278 shell: None,
279 };
280
281 let start_result = start_tool.execute(input).await;
282 if start_result.is_err() {
283 return;
284 }
285
286 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
288
289 let list_tool = ListSessionsTool;
290 let list_input = ListSessionsInput {};
291
292 let result = list_tool.execute(list_input).await;
293 assert!(result.is_ok());
294
295 let output = result.unwrap().as_text();
296 assert!(output.contains("ms") || output.contains("s") || output.contains("m"));
298 }
299
300 #[test]
303 fn test_format_runtime_nice_milliseconds() {
304 assert_eq!(format_runtime_nice("500ms"), "500ms");
305 assert_eq!(format_runtime_nice("100ms"), "100ms");
306 }
307
308 #[test]
309 fn test_format_runtime_nice_seconds() {
310 assert_eq!(format_runtime_nice("5.0s"), "5s");
311 assert_eq!(format_runtime_nice("30.5s"), "30s");
313 assert_eq!(format_runtime_nice("30.6s"), "31s");
314 }
315
316 #[test]
317 fn test_format_runtime_nice_seconds_to_minutes() {
318 assert_eq!(format_runtime_nice("90.0s"), "1m 30s");
320 assert_eq!(format_runtime_nice("120.0s"), "2m 00s");
322 }
323
324 #[test]
325 fn test_format_runtime_nice_minutes() {
326 assert_eq!(format_runtime_nice("5.0m"), "5m");
327 assert_eq!(format_runtime_nice("45.0m"), "45m");
328 }
329
330 #[test]
331 fn test_format_runtime_nice_minutes_to_hours() {
332 assert_eq!(format_runtime_nice("90.0m"), "1h 30m");
334 assert_eq!(format_runtime_nice("120.0m"), "2h 00m");
336 }
337
338 #[test]
339 fn test_format_runtime_nice_unknown_format() {
340 assert_eq!(format_runtime_nice("unknown"), "unknown");
341 assert_eq!(format_runtime_nice("5h"), "5h");
342 }
343
344 #[test]
345 fn test_format_runtime_nice_trimming() {
346 assert_eq!(format_runtime_nice(" 500ms "), "500ms");
347 }
348
349 #[test]
352 fn test_format_output_plain_no_sessions() {
353 let tool = ListSessionsTool;
354 let result: ToolResult = "No active sessions".into();
355
356 let formatted = tool.format_output_plain(&result);
357 assert_eq!(formatted, "No active sessions");
358 }
359
360 #[test]
361 fn test_format_output_plain_with_sessions() {
362 let tool = ListSessionsTool;
363 let result: ToolResult = "Active Sessions:\n\nPID | STATUS | RUNTIME | COMMAND\n-------|---------------------|---------|------------------\n12345 | Running | 500ms | echo hello".into();
364
365 let formatted = tool.format_output_plain(&result);
366
367 assert!(formatted.contains("Sessions"));
368 assert!(formatted.contains("12345"));
369 assert!(formatted.contains("●") || formatted.contains("✓") || formatted.contains("○"));
370 }
371
372 #[test]
373 fn test_format_output_ansi_no_sessions() {
374 let tool = ListSessionsTool;
375 let result: ToolResult = "No active sessions".into();
376
377 let formatted = tool.format_output_ansi(&result);
378 assert!(formatted.contains("\x1b[2m")); assert!(formatted.contains("No active sessions"));
380 }
381
382 #[test]
383 fn test_format_output_ansi_with_sessions() {
384 let tool = ListSessionsTool;
385 let result: ToolResult = "Active Sessions:\n\nPID | STATUS | RUNTIME | COMMAND\n-------|---------------------|---------|------------------\n12345 | Running | 500ms | sleep 10".into();
386
387 let formatted = tool.format_output_ansi(&result);
388
389 assert!(formatted.contains("\x1b[")); assert!(formatted.contains("\x1b[1m")); assert!(formatted.contains("\x1b[32m")); }
393
394 #[test]
395 fn test_format_output_ansi_status_colors() {
396 let tool = ListSessionsTool;
397
398 let running: ToolResult = "Active Sessions:\n\nPID | STATUS | RUNTIME | COMMAND\n-------|---------------------|---------|------------------\n1 | Running | 1ms | cmd".into();
400 let formatted = tool.format_output_ansi(&running);
401 assert!(formatted.contains("\x1b[32m")); let completed: ToolResult = "Active Sessions:\n\nPID | STATUS | RUNTIME | COMMAND\n-------|---------------------|---------|------------------\n1 | Completed | 1ms | cmd".into();
405 let formatted = tool.format_output_ansi(&completed);
406 assert!(formatted.contains("\x1b[34m")); }
408
409 #[test]
410 fn test_format_output_markdown_no_sessions() {
411 let tool = ListSessionsTool;
412 let result: ToolResult = "No active sessions".into();
413
414 let formatted = tool.format_output_markdown(&result);
415 assert_eq!(formatted, "*No active sessions*");
416 }
417
418 #[test]
419 fn test_format_output_markdown_with_sessions() {
420 let tool = ListSessionsTool;
421 let result: ToolResult = "Active Sessions:\n\nPID | STATUS | RUNTIME | COMMAND\n-------|---------------------|---------|------------------\n12345 | Running | 500ms | echo hello".into();
422
423 let formatted = tool.format_output_markdown(&result);
424
425 assert!(formatted.contains("### Sessions"));
426 assert!(formatted.contains("| Status |"));
427 assert!(formatted.contains("🟢 Running")); }
429
430 #[test]
431 fn test_format_output_markdown_status_emojis() {
432 let tool = ListSessionsTool;
433
434 let running: ToolResult = "Active Sessions:\n\nPID | STATUS | RUNTIME | COMMAND\n-------|---------------------|---------|------------------\n1 | Running | 1ms | cmd".into();
436 assert!(tool.format_output_markdown(&running).contains("🟢"));
437
438 let completed: ToolResult = "Active Sessions:\n\nPID | STATUS | RUNTIME | COMMAND\n-------|---------------------|---------|------------------\n1 | Completed | 1ms | cmd".into();
440 assert!(tool.format_output_markdown(&completed).contains("🔵"));
441
442 let error: ToolResult = "Active Sessions:\n\nPID | STATUS | RUNTIME | COMMAND\n-------|---------------------|---------|------------------\n1 | Failed | 1ms | cmd".into();
444 assert!(tool.format_output_markdown(&error).contains("🔴"));
445 }
446
447 #[test]
450 fn test_tool_name() {
451 let tool = ListSessionsTool;
452 assert_eq!(tool.name(), "list_sessions");
453 }
454
455 #[test]
456 fn test_tool_description() {
457 let tool = ListSessionsTool;
458 assert!(!tool.description().is_empty());
459 assert!(tool.description().contains("session"));
460 }
461}