1use crate::agent::ui::colors::{ansi, icons};
7use colored::Colorize;
8use std::io::{self, Write};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ToolCallStatus {
13 Pending,
14 Executing,
15 Success,
16 Error,
17 Canceled,
18}
19
20impl ToolCallStatus {
21 pub fn icon(&self) -> &'static str {
23 match self {
24 ToolCallStatus::Pending => icons::PENDING,
25 ToolCallStatus::Executing => icons::EXECUTING,
26 ToolCallStatus::Success => icons::SUCCESS,
27 ToolCallStatus::Error => icons::ERROR,
28 ToolCallStatus::Canceled => icons::CANCELED,
29 }
30 }
31
32 pub fn color(&self) -> &'static str {
34 match self {
35 ToolCallStatus::Pending => ansi::GRAY,
36 ToolCallStatus::Executing => ansi::CYAN,
37 ToolCallStatus::Success => "\x1b[32m", ToolCallStatus::Error => "\x1b[31m", ToolCallStatus::Canceled => ansi::GRAY,
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct ToolCallInfo {
47 pub name: String,
48 pub description: String,
49 pub status: ToolCallStatus,
50 pub result: Option<String>,
51 pub error: Option<String>,
52}
53
54impl ToolCallInfo {
55 pub fn new(name: &str, description: &str) -> Self {
56 Self {
57 name: name.to_string(),
58 description: description.to_string(),
59 status: ToolCallStatus::Pending,
60 result: None,
61 error: None,
62 }
63 }
64
65 pub fn executing(mut self) -> Self {
66 self.status = ToolCallStatus::Executing;
67 self
68 }
69
70 pub fn success(mut self, result: Option<String>) -> Self {
71 self.status = ToolCallStatus::Success;
72 self.result = result;
73 self
74 }
75
76 pub fn error(mut self, error: String) -> Self {
77 self.status = ToolCallStatus::Error;
78 self.error = Some(error);
79 self
80 }
81}
82
83pub struct ToolCallDisplay;
85
86impl ToolCallDisplay {
87 pub fn print_start(name: &str, description: &str) {
89 println!(
90 "\n{} {} {}",
91 icons::TOOL.cyan(),
92 name.cyan().bold(),
93 description.dimmed()
94 );
95 let _ = io::stdout().flush();
96 }
97
98 pub fn print_status(info: &ToolCallInfo) {
100 let status_icon = info.status.icon();
101 let color = info.status.color();
102
103 print!(
104 "{}{}{} {} {} {}{}",
105 ansi::CLEAR_LINE,
106 color,
107 status_icon,
108 ansi::RESET,
109 info.name.cyan().bold(),
110 info.description.dimmed(),
111 ansi::RESET
112 );
113
114 match info.status {
115 ToolCallStatus::Success => {
116 println!(" {}", "[done]".green());
117 }
118 ToolCallStatus::Error => {
119 if let Some(ref err) = info.error {
120 println!(" {} {}", "[error]".red(), err.red());
121 } else {
122 println!(" {}", "[error]".red());
123 }
124 }
125 ToolCallStatus::Canceled => {
126 println!(" {}", "[canceled]".yellow());
127 }
128 _ => {
129 println!();
130 }
131 }
132
133 let _ = io::stdout().flush();
134 }
135
136 pub fn print_result(name: &str, result: &str, truncate: bool) {
138 let display_result = if truncate && result.chars().count() > 200 {
139 let truncated: String = result.chars().take(200).collect();
140 format!("{}... (truncated)", truncated)
141 } else {
142 result.to_string()
143 };
144
145 println!(
146 " {} {} {}",
147 icons::ARROW.dimmed(),
148 name.cyan(),
149 display_result.dimmed()
150 );
151 let _ = io::stdout().flush();
152 }
153
154 pub fn print_summary(tools: &[ToolCallInfo]) {
156 if tools.is_empty() {
157 return;
158 }
159
160 let success_count = tools
161 .iter()
162 .filter(|t| t.status == ToolCallStatus::Success)
163 .count();
164 let error_count = tools
165 .iter()
166 .filter(|t| t.status == ToolCallStatus::Error)
167 .count();
168
169 println!();
170 if error_count == 0 {
171 println!(
172 "{} {} tool{} executed successfully",
173 icons::SUCCESS.green(),
174 success_count,
175 if success_count == 1 { "" } else { "s" }
176 );
177 } else {
178 println!(
179 "{} {}/{} tools succeeded, {} failed",
180 icons::ERROR.red(),
181 success_count,
182 tools.len(),
183 error_count
184 );
185 }
186 }
187}
188
189pub fn print_tool_inline(status: ToolCallStatus, name: &str, description: &str) {
191 let icon = status.icon();
192 let color = status.color();
193
194 print!(
195 "{}{}{} {} {} {}{}",
196 ansi::CLEAR_LINE,
197 color,
198 icon,
199 ansi::RESET,
200 name,
201 description,
202 ansi::RESET
203 );
204 let _ = io::stdout().flush();
205}
206
207pub fn print_tool_group_header(count: usize) {
209 println!(
210 "\n{} {} tool{}:",
211 icons::TOOL,
212 count,
213 if count == 1 { "" } else { "s" }
214 );
215}
216
217pub struct ForgeToolDisplay;
227
228impl ForgeToolDisplay {
229 pub fn format_args(args: &serde_json::Value) -> String {
234 match args {
235 serde_json::Value::Object(map) => {
236 let formatted: Vec<String> = map
237 .iter()
238 .map(|(key, value)| {
239 let val_str = Self::format_value(value);
240 format!("{}={}", key, val_str)
241 })
242 .collect();
243 formatted.join(", ")
244 }
245 _ => args.to_string(),
246 }
247 }
248
249 fn format_value(value: &serde_json::Value) -> String {
251 match value {
252 serde_json::Value::String(s) => {
253 let line_count = s.lines().count();
254 if line_count > 1 {
255 format!("<{} lines>", line_count)
256 } else if s.chars().count() > 50 {
257 let truncated: String = s.chars().take(47).collect();
258 format!("{}...", truncated)
259 } else {
260 s.clone()
261 }
262 }
263 serde_json::Value::Bool(b) => b.to_string(),
264 serde_json::Value::Number(n) => n.to_string(),
265 serde_json::Value::Array(arr) => {
266 format!("[{} items]", arr.len())
267 }
268 serde_json::Value::Object(map) => {
269 format!("{{{} keys}}", map.len())
270 }
271 serde_json::Value::Null => "null".to_string(),
272 }
273 }
274
275 pub fn start(name: &str, args: &serde_json::Value) {
281 let formatted_args = Self::format_args(args);
282 println!(
283 "{} {}({})",
284 "●".cyan(),
285 name.cyan().bold(),
286 formatted_args.dimmed()
287 );
288 println!(" {} Running...", "└".dimmed());
289 let _ = io::stdout().flush();
290 }
291
292 pub fn update_status(status: &str) {
294 print!("\x1b[1A\x1b[2K");
296 println!(" {} {}", "└".dimmed(), status);
297 let _ = io::stdout().flush();
298 }
299
300 pub fn complete(result_summary: &str) {
302 print!("\x1b[1A\x1b[2K");
304 println!(" {} {}", "└".green(), result_summary.green());
305 let _ = io::stdout().flush();
306 }
307
308 pub fn error(error_msg: &str) {
310 print!("\x1b[1A\x1b[2K");
312 println!(" {} {}", "└".red(), error_msg.red());
313 let _ = io::stdout().flush();
314 }
315
316 pub fn print_inline(name: &str, args: &serde_json::Value) {
318 let formatted_args = Self::format_args(args);
319 println!(
320 "{} {}({})",
321 "●".cyan(),
322 name.cyan().bold(),
323 formatted_args.dimmed()
324 );
325 let _ = io::stdout().flush();
326 }
327
328 pub fn summarize_result(name: &str, result: &str) -> String {
331 if let Ok(json) = serde_json::from_str::<serde_json::Value>(result) {
333 if let Some(success) = json.get("success").and_then(|v| v.as_bool())
335 && !success
336 {
337 if let Some(err) = json.get("error").and_then(|v| v.as_str()) {
338 return format!("Error: {}", truncate_str(err, 50));
339 }
340 return "Failed".to_string();
341 }
342
343 if let Some(issues) = json.get("issues").and_then(|v| v.as_array()) {
345 return format!("{} issues found", issues.len());
346 }
347
348 if let Some(files) = json.get("files_written").and_then(|v| v.as_u64()) {
350 let lines = json
351 .get("total_lines")
352 .and_then(|v| v.as_u64())
353 .unwrap_or(0);
354 return format!("wrote {} file(s) ({} lines)", files, lines);
355 }
356
357 if let Some(lines) = json.get("total_lines").and_then(|v| v.as_u64()) {
359 return format!("read {} lines", lines);
360 }
361
362 if let Some(count) = json.get("total_count").and_then(|v| v.as_u64()) {
364 return format!("{} entries", count);
365 }
366
367 if let Some(action) = json.get("action").and_then(|v| v.as_str()) {
369 if let Some(path) = json.get("path").and_then(|v| v.as_str()) {
370 return format!("{} {}", action.to_lowercase(), path);
371 }
372 return action.to_lowercase();
373 }
374 }
375
376 format!("{} completed", name)
378 }
379}
380
381fn truncate_str(s: &str, max: usize) -> String {
383 let char_count = s.chars().count();
384 if char_count <= max {
385 s.to_string()
386 } else {
387 let truncate_to = max.saturating_sub(3);
388 let truncated: String = s.chars().take(truncate_to).collect();
389 format!("{}...", truncated)
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use serde_json::json;
397
398 #[test]
399 fn test_tool_call_info() {
400 let info = ToolCallInfo::new("read_file", "reading src/main.rs");
401 assert_eq!(info.status, ToolCallStatus::Pending);
402
403 let info = info.executing();
404 assert_eq!(info.status, ToolCallStatus::Executing);
405
406 let info = info.success(Some("file contents".to_string()));
407 assert_eq!(info.status, ToolCallStatus::Success);
408 assert!(info.result.is_some());
409 }
410
411 #[test]
412 fn test_status_icons() {
413 assert_eq!(ToolCallStatus::Pending.icon(), icons::PENDING);
414 assert_eq!(ToolCallStatus::Success.icon(), icons::SUCCESS);
415 assert_eq!(ToolCallStatus::Error.icon(), icons::ERROR);
416 }
417
418 #[test]
419 fn test_forge_format_args() {
420 let args = json!({"path": "src/main.rs", "check": true});
422 let formatted = ForgeToolDisplay::format_args(&args);
423 assert!(formatted.contains("path=src/main.rs"));
424 assert!(formatted.contains("check=true"));
425
426 let args = json!({"content": "line1\nline2\nline3"});
428 let formatted = ForgeToolDisplay::format_args(&args);
429 assert!(formatted.contains("<3 lines>"));
430
431 let long_str = "a".repeat(100);
433 let args = json!({"data": long_str});
434 let formatted = ForgeToolDisplay::format_args(&args);
435 assert!(formatted.contains("..."));
436 }
437
438 #[test]
439 fn test_forge_summarize_result() {
440 let result = r#"{"success": true, "files_written": 3, "total_lines": 150}"#;
442 let summary = ForgeToolDisplay::summarize_result("write_files", result);
443 assert!(summary.contains("3 file"));
444 assert!(summary.contains("150 lines"));
445
446 let result = r#"{"issues": [1, 2, 3]}"#;
448 let summary = ForgeToolDisplay::summarize_result("hadolint", result);
449 assert!(summary.contains("3 issues"));
450
451 let result = r#"{"total_count": 25}"#;
453 let summary = ForgeToolDisplay::summarize_result("list_directory", result);
454 assert!(summary.contains("25 entries"));
455 }
456
457 #[test]
458 fn test_truncate_str() {
459 assert_eq!(truncate_str("short", 10), "short");
460 assert_eq!(truncate_str("this is a longer string", 10), "this is...");
461 }
462}