syncable_cli/agent/ui/
tool_display.rs1use crate::agent::ui::colors::{ansi, icons};
6use colored::Colorize;
7use std::io::{self, Write};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ToolCallStatus {
12 Pending,
13 Executing,
14 Success,
15 Error,
16 Canceled,
17}
18
19impl ToolCallStatus {
20 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 pub fn color(&self) -> &'static str {
33 match self {
34 ToolCallStatus::Pending => ansi::GRAY,
35 ToolCallStatus::Executing => ansi::CYAN,
36 ToolCallStatus::Success => "\x1b[32m", ToolCallStatus::Error => "\x1b[31m", ToolCallStatus::Canceled => ansi::GRAY,
39 }
40 }
41}
42
43#[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
82pub struct ToolCallDisplay;
84
85impl ToolCallDisplay {
86 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 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 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 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
181pub 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
199pub 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}