syncable_cli/agent/ui/
tool_display.rs

1//! Tool call display for visible tool execution feedback
2//!
3//! Shows tool calls with status indicators, names, descriptions, and results.
4
5use crate::agent::ui::colors::{ansi, icons};
6use colored::Colorize;
7use std::io::{self, Write};
8
9/// Status of a tool call
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ToolCallStatus {
12    Pending,
13    Executing,
14    Success,
15    Error,
16    Canceled,
17}
18
19impl ToolCallStatus {
20    /// Get the icon for this status
21    pub fn icon(&self) -> &'static str {
22        match self {
23            ToolCallStatus::Pending => icons::PENDING,
24            ToolCallStatus::Executing => icons::EXECUTING,
25            ToolCallStatus::Success => icons::SUCCESS,
26            ToolCallStatus::Error => icons::ERROR,
27            ToolCallStatus::Canceled => icons::CANCELED,
28        }
29    }
30
31    /// Get the color code for this status
32    pub fn color(&self) -> &'static str {
33        match self {
34            ToolCallStatus::Pending => ansi::GRAY,
35            ToolCallStatus::Executing => ansi::CYAN,
36            ToolCallStatus::Success => "\x1b[32m", // Green
37            ToolCallStatus::Error => "\x1b[31m",   // Red
38            ToolCallStatus::Canceled => ansi::GRAY,
39        }
40    }
41}
42
43/// Represents a tool call for display
44#[derive(Debug, Clone)]
45pub struct ToolCallInfo {
46    pub name: String,
47    pub description: String,
48    pub status: ToolCallStatus,
49    pub result: Option<String>,
50    pub error: Option<String>,
51}
52
53impl ToolCallInfo {
54    pub fn new(name: &str, description: &str) -> Self {
55        Self {
56            name: name.to_string(),
57            description: description.to_string(),
58            status: ToolCallStatus::Pending,
59            result: None,
60            error: None,
61        }
62    }
63
64    pub fn executing(mut self) -> Self {
65        self.status = ToolCallStatus::Executing;
66        self
67    }
68
69    pub fn success(mut self, result: Option<String>) -> Self {
70        self.status = ToolCallStatus::Success;
71        self.result = result;
72        self
73    }
74
75    pub fn error(mut self, error: String) -> Self {
76        self.status = ToolCallStatus::Error;
77        self.error = Some(error);
78        self
79    }
80}
81
82/// Display manager for tool calls
83pub struct ToolCallDisplay;
84
85impl ToolCallDisplay {
86    /// Print a tool call start message
87    pub fn print_start(name: &str, description: &str) {
88        println!(
89            "\n{} {} {}",
90            icons::TOOL.cyan(),
91            name.cyan().bold(),
92            description.dimmed()
93        );
94        let _ = io::stdout().flush();
95    }
96
97    /// Print a tool call with status
98    pub fn print_status(info: &ToolCallInfo) {
99        let status_icon = info.status.icon();
100        let color = info.status.color();
101
102        print!(
103            "{}{}{} {} {} {}{}",
104            ansi::CLEAR_LINE,
105            color,
106            status_icon,
107            ansi::RESET,
108            info.name.cyan().bold(),
109            info.description.dimmed(),
110            ansi::RESET
111        );
112
113        match info.status {
114            ToolCallStatus::Success => {
115                println!(" {}", "[done]".green());
116            }
117            ToolCallStatus::Error => {
118                if let Some(ref err) = info.error {
119                    println!(" {} {}", "[error]".red(), err.red());
120                } else {
121                    println!(" {}", "[error]".red());
122                }
123            }
124            ToolCallStatus::Canceled => {
125                println!(" {}", "[canceled]".yellow());
126            }
127            _ => {
128                println!();
129            }
130        }
131
132        let _ = io::stdout().flush();
133    }
134
135    /// Print a tool call result (for verbose output)
136    pub fn print_result(name: &str, result: &str, truncate: bool) {
137        let display_result = if truncate && result.len() > 200 {
138            format!("{}... (truncated)", &result[..200])
139        } else {
140            result.to_string()
141        };
142
143        println!(
144            "  {} {} {}",
145            icons::ARROW.dimmed(),
146            name.cyan(),
147            display_result.dimmed()
148        );
149        let _ = io::stdout().flush();
150    }
151
152    /// Print a summary of tool calls
153    pub fn print_summary(tools: &[ToolCallInfo]) {
154        if tools.is_empty() {
155            return;
156        }
157
158        let success_count = tools.iter().filter(|t| t.status == ToolCallStatus::Success).count();
159        let error_count = tools.iter().filter(|t| t.status == ToolCallStatus::Error).count();
160
161        println!();
162        if error_count == 0 {
163            println!(
164                "{} {} tool{} executed successfully",
165                icons::SUCCESS.green(),
166                success_count,
167                if success_count == 1 { "" } else { "s" }
168            );
169        } else {
170            println!(
171                "{} {}/{} tools succeeded, {} failed",
172                icons::ERROR.red(),
173                success_count,
174                tools.len(),
175                error_count
176            );
177        }
178    }
179}
180
181/// Print a tool call inline (single line, updating)
182pub fn print_tool_inline(status: ToolCallStatus, name: &str, description: &str) {
183    let icon = status.icon();
184    let color = status.color();
185
186    print!(
187        "{}{}{} {} {} {}{}",
188        ansi::CLEAR_LINE,
189        color,
190        icon,
191        ansi::RESET,
192        name,
193        description,
194        ansi::RESET
195    );
196    let _ = io::stdout().flush();
197}
198
199/// Print a tool group header
200pub fn print_tool_group_header(count: usize) {
201    println!("\n{} {} tool{}:", icons::TOOL, count, if count == 1 { "" } else { "s" });
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_tool_call_info() {
210        let info = ToolCallInfo::new("read_file", "reading src/main.rs");
211        assert_eq!(info.status, ToolCallStatus::Pending);
212
213        let info = info.executing();
214        assert_eq!(info.status, ToolCallStatus::Executing);
215
216        let info = info.success(Some("file contents".to_string()));
217        assert_eq!(info.status, ToolCallStatus::Success);
218        assert!(info.result.is_some());
219    }
220
221    #[test]
222    fn test_status_icons() {
223        assert_eq!(ToolCallStatus::Pending.icon(), icons::PENDING);
224        assert_eq!(ToolCallStatus::Success.icon(), icons::SUCCESS);
225        assert_eq!(ToolCallStatus::Error.icon(), icons::ERROR);
226    }
227}