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.len() > 200 {
139 format!("{}... (truncated)", &result[..200])
140 } else {
141 result.to_string()
142 };
143
144 println!(
145 " {} {} {}",
146 icons::ARROW.dimmed(),
147 name.cyan(),
148 display_result.dimmed()
149 );
150 let _ = io::stdout().flush();
151 }
152
153 pub fn print_summary(tools: &[ToolCallInfo]) {
155 if tools.is_empty() {
156 return;
157 }
158
159 let success_count = tools
160 .iter()
161 .filter(|t| t.status == ToolCallStatus::Success)
162 .count();
163 let error_count = tools
164 .iter()
165 .filter(|t| t.status == ToolCallStatus::Error)
166 .count();
167
168 println!();
169 if error_count == 0 {
170 println!(
171 "{} {} tool{} executed successfully",
172 icons::SUCCESS.green(),
173 success_count,
174 if success_count == 1 { "" } else { "s" }
175 );
176 } else {
177 println!(
178 "{} {}/{} tools succeeded, {} failed",
179 icons::ERROR.red(),
180 success_count,
181 tools.len(),
182 error_count
183 );
184 }
185 }
186}
187
188pub fn print_tool_inline(status: ToolCallStatus, name: &str, description: &str) {
190 let icon = status.icon();
191 let color = status.color();
192
193 print!(
194 "{}{}{} {} {} {}{}",
195 ansi::CLEAR_LINE,
196 color,
197 icon,
198 ansi::RESET,
199 name,
200 description,
201 ansi::RESET
202 );
203 let _ = io::stdout().flush();
204}
205
206pub fn print_tool_group_header(count: usize) {
208 println!(
209 "\n{} {} tool{}:",
210 icons::TOOL,
211 count,
212 if count == 1 { "" } else { "s" }
213 );
214}
215
216pub struct ForgeToolDisplay;
226
227impl ForgeToolDisplay {
228 pub fn format_args(args: &serde_json::Value) -> String {
233 match args {
234 serde_json::Value::Object(map) => {
235 let formatted: Vec<String> = map
236 .iter()
237 .map(|(key, value)| {
238 let val_str = Self::format_value(value);
239 format!("{}={}", key, val_str)
240 })
241 .collect();
242 formatted.join(", ")
243 }
244 _ => args.to_string(),
245 }
246 }
247
248 fn format_value(value: &serde_json::Value) -> String {
250 match value {
251 serde_json::Value::String(s) => {
252 let line_count = s.lines().count();
253 if line_count > 1 {
254 format!("<{} lines>", line_count)
255 } else if s.len() > 50 {
256 format!("{}...", &s[..47])
257 } else {
258 s.clone()
259 }
260 }
261 serde_json::Value::Bool(b) => b.to_string(),
262 serde_json::Value::Number(n) => n.to_string(),
263 serde_json::Value::Array(arr) => {
264 format!("[{} items]", arr.len())
265 }
266 serde_json::Value::Object(map) => {
267 format!("{{{} keys}}", map.len())
268 }
269 serde_json::Value::Null => "null".to_string(),
270 }
271 }
272
273 pub fn start(name: &str, args: &serde_json::Value) {
279 let formatted_args = Self::format_args(args);
280 println!(
281 "{} {}({})",
282 "●".cyan(),
283 name.cyan().bold(),
284 formatted_args.dimmed()
285 );
286 println!(" {} Running...", "└".dimmed());
287 let _ = io::stdout().flush();
288 }
289
290 pub fn update_status(status: &str) {
292 print!("\x1b[1A\x1b[2K");
294 println!(" {} {}", "└".dimmed(), status);
295 let _ = io::stdout().flush();
296 }
297
298 pub fn complete(result_summary: &str) {
300 print!("\x1b[1A\x1b[2K");
302 println!(" {} {}", "└".green(), result_summary.green());
303 let _ = io::stdout().flush();
304 }
305
306 pub fn error(error_msg: &str) {
308 print!("\x1b[1A\x1b[2K");
310 println!(" {} {}", "└".red(), error_msg.red());
311 let _ = io::stdout().flush();
312 }
313
314 pub fn print_inline(name: &str, args: &serde_json::Value) {
316 let formatted_args = Self::format_args(args);
317 println!(
318 "{} {}({})",
319 "●".cyan(),
320 name.cyan().bold(),
321 formatted_args.dimmed()
322 );
323 let _ = io::stdout().flush();
324 }
325
326 pub fn summarize_result(name: &str, result: &str) -> String {
329 if let Ok(json) = serde_json::from_str::<serde_json::Value>(result) {
331 if let Some(success) = json.get("success").and_then(|v| v.as_bool()) {
333 if !success {
334 if let Some(err) = json.get("error").and_then(|v| v.as_str()) {
335 return format!("Error: {}", truncate_str(err, 50));
336 }
337 return "Failed".to_string();
338 }
339 }
340
341 if let Some(issues) = json.get("issues").and_then(|v| v.as_array()) {
343 return format!("{} issues found", issues.len());
344 }
345
346 if let Some(files) = json.get("files_written").and_then(|v| v.as_u64()) {
348 let lines = json
349 .get("total_lines")
350 .and_then(|v| v.as_u64())
351 .unwrap_or(0);
352 return format!("wrote {} file(s) ({} lines)", files, lines);
353 }
354
355 if let Some(lines) = json.get("total_lines").and_then(|v| v.as_u64()) {
357 return format!("read {} lines", lines);
358 }
359
360 if let Some(count) = json.get("total_count").and_then(|v| v.as_u64()) {
362 return format!("{} entries", count);
363 }
364
365 if let Some(action) = json.get("action").and_then(|v| v.as_str()) {
367 if let Some(path) = json.get("path").and_then(|v| v.as_str()) {
368 return format!("{} {}", action.to_lowercase(), path);
369 }
370 return action.to_lowercase();
371 }
372 }
373
374 format!("{} completed", name)
376 }
377}
378
379fn truncate_str(s: &str, max: usize) -> String {
381 if s.len() <= max {
382 s.to_string()
383 } else {
384 format!("{}...", &s[..max.saturating_sub(3)])
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use serde_json::json;
392
393 #[test]
394 fn test_tool_call_info() {
395 let info = ToolCallInfo::new("read_file", "reading src/main.rs");
396 assert_eq!(info.status, ToolCallStatus::Pending);
397
398 let info = info.executing();
399 assert_eq!(info.status, ToolCallStatus::Executing);
400
401 let info = info.success(Some("file contents".to_string()));
402 assert_eq!(info.status, ToolCallStatus::Success);
403 assert!(info.result.is_some());
404 }
405
406 #[test]
407 fn test_status_icons() {
408 assert_eq!(ToolCallStatus::Pending.icon(), icons::PENDING);
409 assert_eq!(ToolCallStatus::Success.icon(), icons::SUCCESS);
410 assert_eq!(ToolCallStatus::Error.icon(), icons::ERROR);
411 }
412
413 #[test]
414 fn test_forge_format_args() {
415 let args = json!({"path": "src/main.rs", "check": true});
417 let formatted = ForgeToolDisplay::format_args(&args);
418 assert!(formatted.contains("path=src/main.rs"));
419 assert!(formatted.contains("check=true"));
420
421 let args = json!({"content": "line1\nline2\nline3"});
423 let formatted = ForgeToolDisplay::format_args(&args);
424 assert!(formatted.contains("<3 lines>"));
425
426 let long_str = "a".repeat(100);
428 let args = json!({"data": long_str});
429 let formatted = ForgeToolDisplay::format_args(&args);
430 assert!(formatted.contains("..."));
431 }
432
433 #[test]
434 fn test_forge_summarize_result() {
435 let result = r#"{"success": true, "files_written": 3, "total_lines": 150}"#;
437 let summary = ForgeToolDisplay::summarize_result("write_files", result);
438 assert!(summary.contains("3 file"));
439 assert!(summary.contains("150 lines"));
440
441 let result = r#"{"issues": [1, 2, 3]}"#;
443 let summary = ForgeToolDisplay::summarize_result("hadolint", result);
444 assert!(summary.contains("3 issues"));
445
446 let result = r#"{"total_count": 25}"#;
448 let summary = ForgeToolDisplay::summarize_result("list_directory", result);
449 assert!(summary.contains("25 entries"));
450 }
451
452 #[test]
453 fn test_truncate_str() {
454 assert_eq!(truncate_str("short", 10), "short");
455 assert_eq!(truncate_str("this is a longer string", 10), "this is...");
456 }
457}