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.iter().filter(|t| t.status == ToolCallStatus::Success).count();
160 let error_count = tools.iter().filter(|t| t.status == ToolCallStatus::Error).count();
161
162 println!();
163 if error_count == 0 {
164 println!(
165 "{} {} tool{} executed successfully",
166 icons::SUCCESS.green(),
167 success_count,
168 if success_count == 1 { "" } else { "s" }
169 );
170 } else {
171 println!(
172 "{} {}/{} tools succeeded, {} failed",
173 icons::ERROR.red(),
174 success_count,
175 tools.len(),
176 error_count
177 );
178 }
179 }
180}
181
182pub fn print_tool_inline(status: ToolCallStatus, name: &str, description: &str) {
184 let icon = status.icon();
185 let color = status.color();
186
187 print!(
188 "{}{}{} {} {} {}{}",
189 ansi::CLEAR_LINE,
190 color,
191 icon,
192 ansi::RESET,
193 name,
194 description,
195 ansi::RESET
196 );
197 let _ = io::stdout().flush();
198}
199
200pub fn print_tool_group_header(count: usize) {
202 println!("\n{} {} tool{}:", icons::TOOL, count, if count == 1 { "" } else { "s" });
203}
204
205pub struct ForgeToolDisplay;
215
216impl ForgeToolDisplay {
217 pub fn format_args(args: &serde_json::Value) -> String {
222 match args {
223 serde_json::Value::Object(map) => {
224 let formatted: Vec<String> = map
225 .iter()
226 .map(|(key, value)| {
227 let val_str = Self::format_value(value);
228 format!("{}={}", key, val_str)
229 })
230 .collect();
231 formatted.join(", ")
232 }
233 _ => args.to_string(),
234 }
235 }
236
237 fn format_value(value: &serde_json::Value) -> String {
239 match value {
240 serde_json::Value::String(s) => {
241 let line_count = s.lines().count();
242 if line_count > 1 {
243 format!("<{} lines>", line_count)
244 } else if s.len() > 50 {
245 format!("{}...", &s[..47])
246 } else {
247 s.clone()
248 }
249 }
250 serde_json::Value::Bool(b) => b.to_string(),
251 serde_json::Value::Number(n) => n.to_string(),
252 serde_json::Value::Array(arr) => {
253 format!("[{} items]", arr.len())
254 }
255 serde_json::Value::Object(map) => {
256 format!("{{{} keys}}", map.len())
257 }
258 serde_json::Value::Null => "null".to_string(),
259 }
260 }
261
262 pub fn start(name: &str, args: &serde_json::Value) {
268 let formatted_args = Self::format_args(args);
269 println!(
270 "{} {}({})",
271 "●".cyan(),
272 name.cyan().bold(),
273 formatted_args.dimmed()
274 );
275 println!(" {} Running...", "└".dimmed());
276 let _ = io::stdout().flush();
277 }
278
279 pub fn update_status(status: &str) {
281 print!("\x1b[1A\x1b[2K");
283 println!(" {} {}", "└".dimmed(), status);
284 let _ = io::stdout().flush();
285 }
286
287 pub fn complete(result_summary: &str) {
289 print!("\x1b[1A\x1b[2K");
291 println!(" {} {}", "└".green(), result_summary.green());
292 let _ = io::stdout().flush();
293 }
294
295 pub fn error(error_msg: &str) {
297 print!("\x1b[1A\x1b[2K");
299 println!(" {} {}", "└".red(), error_msg.red());
300 let _ = io::stdout().flush();
301 }
302
303 pub fn print_inline(name: &str, args: &serde_json::Value) {
305 let formatted_args = Self::format_args(args);
306 println!(
307 "{} {}({})",
308 "●".cyan(),
309 name.cyan().bold(),
310 formatted_args.dimmed()
311 );
312 let _ = io::stdout().flush();
313 }
314
315 pub fn summarize_result(name: &str, result: &str) -> String {
318 if let Ok(json) = serde_json::from_str::<serde_json::Value>(result) {
320 if let Some(success) = json.get("success").and_then(|v| v.as_bool()) {
322 if !success {
323 if let Some(err) = json.get("error").and_then(|v| v.as_str()) {
324 return format!("Error: {}", truncate_str(err, 50));
325 }
326 return "Failed".to_string();
327 }
328 }
329
330 if let Some(issues) = json.get("issues").and_then(|v| v.as_array()) {
332 return format!("{} issues found", issues.len());
333 }
334
335 if let Some(files) = json.get("files_written").and_then(|v| v.as_u64()) {
337 let lines = json.get("total_lines").and_then(|v| v.as_u64()).unwrap_or(0);
338 return format!("wrote {} file(s) ({} lines)", files, lines);
339 }
340
341 if let Some(lines) = json.get("total_lines").and_then(|v| v.as_u64()) {
343 return format!("read {} lines", lines);
344 }
345
346 if let Some(count) = json.get("total_count").and_then(|v| v.as_u64()) {
348 return format!("{} entries", count);
349 }
350
351 if let Some(action) = json.get("action").and_then(|v| v.as_str()) {
353 if let Some(path) = json.get("path").and_then(|v| v.as_str()) {
354 return format!("{} {}", action.to_lowercase(), path);
355 }
356 return action.to_lowercase();
357 }
358 }
359
360 format!("{} completed", name)
362 }
363}
364
365fn truncate_str(s: &str, max: usize) -> String {
367 if s.len() <= max {
368 s.to_string()
369 } else {
370 format!("{}...", &s[..max.saturating_sub(3)])
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use serde_json::json;
378
379 #[test]
380 fn test_tool_call_info() {
381 let info = ToolCallInfo::new("read_file", "reading src/main.rs");
382 assert_eq!(info.status, ToolCallStatus::Pending);
383
384 let info = info.executing();
385 assert_eq!(info.status, ToolCallStatus::Executing);
386
387 let info = info.success(Some("file contents".to_string()));
388 assert_eq!(info.status, ToolCallStatus::Success);
389 assert!(info.result.is_some());
390 }
391
392 #[test]
393 fn test_status_icons() {
394 assert_eq!(ToolCallStatus::Pending.icon(), icons::PENDING);
395 assert_eq!(ToolCallStatus::Success.icon(), icons::SUCCESS);
396 assert_eq!(ToolCallStatus::Error.icon(), icons::ERROR);
397 }
398
399 #[test]
400 fn test_forge_format_args() {
401 let args = json!({"path": "src/main.rs", "check": true});
403 let formatted = ForgeToolDisplay::format_args(&args);
404 assert!(formatted.contains("path=src/main.rs"));
405 assert!(formatted.contains("check=true"));
406
407 let args = json!({"content": "line1\nline2\nline3"});
409 let formatted = ForgeToolDisplay::format_args(&args);
410 assert!(formatted.contains("<3 lines>"));
411
412 let long_str = "a".repeat(100);
414 let args = json!({"data": long_str});
415 let formatted = ForgeToolDisplay::format_args(&args);
416 assert!(formatted.contains("..."));
417 }
418
419 #[test]
420 fn test_forge_summarize_result() {
421 let result = r#"{"success": true, "files_written": 3, "total_lines": 150}"#;
423 let summary = ForgeToolDisplay::summarize_result("write_files", result);
424 assert!(summary.contains("3 file"));
425 assert!(summary.contains("150 lines"));
426
427 let result = r#"{"issues": [1, 2, 3]}"#;
429 let summary = ForgeToolDisplay::summarize_result("hadolint", result);
430 assert!(summary.contains("3 issues"));
431
432 let result = r#"{"total_count": 25}"#;
434 let summary = ForgeToolDisplay::summarize_result("list_directory", result);
435 assert!(summary.contains("25 entries"));
436 }
437
438 #[test]
439 fn test_truncate_str() {
440 assert_eq!(truncate_str("short", 10), "short");
441 assert_eq!(truncate_str("this is a longer string", 10), "this is...");
442 }
443}