1use console::{style, Emoji, Term};
6use indicatif::{ProgressBar, ProgressStyle};
7use std::time::Duration;
8
9pub static ROBOT: Emoji<'_, '_> = Emoji("🤖 ", "");
11pub static THINKING: Emoji<'_, '_> = Emoji("💭 ", "");
12pub static TOOL: Emoji<'_, '_> = Emoji("🔧 ", "");
13pub static SUCCESS: Emoji<'_, '_> = Emoji("✅ ", "[OK] ");
14pub static ERROR: Emoji<'_, '_> = Emoji("❌ ", "[ERR] ");
15pub static SEARCH: Emoji<'_, '_> = Emoji("🔍 ", "");
16pub static SECURITY: Emoji<'_, '_> = Emoji("🛡️ ", "");
17pub static FILE: Emoji<'_, '_> = Emoji("📄 ", "");
18pub static FOLDER: Emoji<'_, '_> = Emoji("📁 ", "");
19pub static SPARKLES: Emoji<'_, '_> = Emoji("✨ ", "");
20pub static ARROW: Emoji<'_, '_> = Emoji("➜ ", "> ");
21
22pub fn print_logo() {
24 let purple = "\x1b[38;5;141m"; let orange = "\x1b[38;5;216m"; let pink = "\x1b[38;5;212m"; let magenta = "\x1b[38;5;207m"; let reset = "\x1b[0m";
35
36 println!();
37 println!(
38 "{} ███████╗{}{} ██╗ ██╗{}{}███╗ ██╗{}{} ██████╗{}{} █████╗ {}{}██████╗ {}{}██╗ {}{}███████╗{}",
39 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
40 );
41 println!(
42 "{} ██╔════╝{}{} ╚██╗ ██╔╝{}{}████╗ ██║{}{} ██╔════╝{}{} ██╔══██╗{}{}██╔══██╗{}{}██║ {}{}██╔════╝{}",
43 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
44 );
45 println!(
46 "{} ███████╗{}{} ╚████╔╝ {}{}██╔██╗ ██║{}{} ██║ {}{} ███████║{}{}██████╔╝{}{}██║ {}{}█████╗ {}",
47 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
48 );
49 println!(
50 "{} ╚════██║{}{} ╚██╔╝ {}{}██║╚██╗██║{}{} ██║ {}{} ██╔══██║{}{}██╔══██╗{}{}██║ {}{}██╔══╝ {}",
51 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
52 );
53 println!(
54 "{} ███████║{}{} ██║ {}{}██║ ╚████║{}{} ╚██████╗{}{} ██║ ██║{}{}██████╔╝{}{}███████╗{}{}███████╗{}",
55 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
56 );
57 println!(
58 "{} ╚══════╝{}{} ╚═╝ {}{}╚═╝ ╚═══╝{}{} ╚═════╝{}{} ╚═╝ ╚═╝{}{}╚═════╝ {}{}╚══════╝{}{}╚══════╝{}",
59 purple, reset, purple, reset, orange, reset, orange, reset, pink, reset, pink, reset, magenta, reset, magenta, reset
60 );
61 println!();
62}
63
64pub struct AgentUI {
66 #[allow(dead_code)]
67 term: Term,
68 spinner: Option<ProgressBar>,
69}
70
71impl AgentUI {
72 pub fn new() -> Self {
73 Self {
74 term: Term::stderr(),
75 spinner: None,
76 }
77 }
78
79 pub fn pause_spinner(&mut self) {
81 if let Some(ref spinner) = self.spinner {
82 spinner.finish_and_clear();
83 }
84 self.spinner = None;
85 }
86
87 pub fn print_welcome(&self, provider: &str, model: &str) {
89 print_logo();
91
92 println!(
94 " {} {} powered by {}: {}",
95 ROBOT,
96 style("Syncable Agent").white().bold(),
97 style(provider).cyan(),
98 style(model).cyan()
99 );
100 println!(
101 " {}",
102 style("Your AI-powered code analysis assistant").dim()
103 );
104 println!();
105 println!(
106 " {} Type your questions. Use {} to exit.\n",
107 style("→").cyan(),
108 style("exit").yellow().bold()
109 );
110 }
111
112 pub fn print_prompt(&self) {
114 print!(
115 "\n{} {} ",
116 style("you").green().bold(),
117 style("›").green()
118 );
119 use std::io::Write;
120 std::io::stdout().flush().ok();
121 }
122
123 pub fn start_thinking(&mut self) {
125 let spinner = ProgressBar::new_spinner();
126 spinner.set_style(
127 ProgressStyle::default_spinner()
128 .tick_strings(&[
129 "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
130 ])
131 .template("{spinner:.cyan} {msg}")
132 .unwrap(),
133 );
134 spinner.set_message(format!("{} Thinking...", THINKING));
135 spinner.enable_steady_tick(Duration::from_millis(80));
136 self.spinner = Some(spinner);
137 }
138
139 pub fn show_tool_call(&mut self, tool_name: &str) {
141 let emoji = match tool_name {
142 "analyze_project" => SEARCH,
143 "security_scan" => SECURITY,
144 "check_vulnerabilities" => SECURITY,
145 "read_file" => FILE,
146 "list_directory" => FOLDER,
147 _ => TOOL,
148 };
149
150 let action = match tool_name {
151 "analyze_project" => "Analyzing project structure...",
152 "security_scan" => "Scanning for security issues...",
153 "check_vulnerabilities" => "Checking dependencies for vulnerabilities...",
154 "read_file" => "Reading file contents...",
155 "list_directory" => "Listing directory...",
156 _ => "Running tool...",
157 };
158
159 if let Some(ref spinner) = self.spinner {
160 spinner.set_message(format!("{} {}", emoji, style(action).cyan()));
161 }
162 }
163
164 pub fn stop_thinking(&mut self) {
166 if let Some(spinner) = self.spinner.take() {
167 spinner.finish_and_clear();
168 }
169 }
170
171 pub fn print_assistant_header(&self) {
173 println!();
174 println!(
175 "{} {} ",
176 style("assistant").magenta().bold(),
177 style("›").magenta()
178 );
179 }
180
181 pub fn start_streaming(&mut self) {
183 let spinner = ProgressBar::new_spinner();
184 spinner.set_style(
185 ProgressStyle::default_spinner()
186 .tick_strings(&["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▂"])
187 .template(" {spinner:.magenta} {msg}")
188 .unwrap(),
189 );
190 spinner.set_message(style("Generating response...").dim().to_string());
191 spinner.enable_steady_tick(Duration::from_millis(80));
192 self.spinner = Some(spinner);
193 }
194
195 pub fn update_streaming(&mut self, char_count: usize) {
197 if let Some(ref spinner) = self.spinner {
198 spinner.set_message(
199 style(format!("Generating... ({} chars)", char_count)).dim().to_string()
200 );
201 }
202 }
203
204 pub fn finish_streaming_and_render(&mut self, response: &str) {
206 if let Some(spinner) = self.spinner.take() {
207 spinner.finish_and_clear();
208 }
209 println!();
210 self.render_markdown(response);
211 println!();
212 }
213
214 pub fn print_stream_chunk(&self, text: &str) {
216 print!("{}", text);
217 use std::io::Write;
218 std::io::stdout().flush().ok();
219 }
220
221 pub fn print_tool_call_notification(&self, tool_name: &str) {
223 let emoji = match tool_name {
224 "analyze_project" => SEARCH,
225 "security_scan" => SECURITY,
226 "check_vulnerabilities" => SECURITY,
227 "read_file" => FILE,
228 "list_directory" => FOLDER,
229 _ => TOOL,
230 };
231
232 let action = match tool_name {
233 "analyze_project" => "Analyzing project structure",
234 "security_scan" => "Scanning for security issues",
235 "check_vulnerabilities" => "Checking dependencies for vulnerabilities",
236 "read_file" => "Reading file contents",
237 "list_directory" => "Listing directory",
238 _ => tool_name,
239 };
240
241 println!();
242 println!(
243 " {} {} {}",
244 style("┌─").dim(),
245 emoji,
246 style(format!("Calling: {}", action)).cyan().bold()
247 );
248 }
249
250 pub fn print_tool_call_complete(&self, tool_name: &str) {
252 let emoji = match tool_name {
253 "analyze_project" => SEARCH,
254 "security_scan" => SECURITY,
255 "check_vulnerabilities" => SECURITY,
256 "read_file" => FILE,
257 "list_directory" => FOLDER,
258 _ => TOOL,
259 };
260
261 println!(
262 " {} {} {}",
263 style("└─").dim(),
264 emoji,
265 style(format!("{} completed", tool_name)).green()
266 );
267 println!();
268 }
269
270 pub fn end_stream(&self) {
272 println!();
273 println!();
274 }
275
276 pub fn print_response(&self, response: &str) {
278 println!();
279 println!(
280 "{} {} ",
281 style("assistant").magenta().bold(),
282 style("›").magenta()
283 );
284 println!();
285
286 self.render_markdown(response);
288
289 println!();
290 }
291
292 fn render_markdown(&self, content: &str) {
294 use termimad::MadSkin;
295 use termimad::crossterm::style::Color;
296
297 let mut skin = MadSkin::default();
298
299 skin.set_headers_fg(Color::Cyan);
301 skin.bold.set_fg(Color::White);
302 skin.italic.set_fg(Color::Magenta);
303 skin.inline_code.set_bg(Color::DarkGrey);
304 skin.inline_code.set_fg(Color::Yellow);
305 skin.code_block.set_bg(Color::DarkGrey);
306 skin.code_block.set_fg(Color::Green);
307
308 skin.print_text(content);
310 }
311
312 pub fn print_error(&self, message: &str) {
314 println!(
315 "\n {} {}",
316 ERROR,
317 style(message).red()
318 );
319 }
320
321 pub fn print_success(&self, message: &str) {
323 println!(
324 "\n {} {}",
325 SUCCESS,
326 style(message).green()
327 );
328 }
329
330 pub fn print_tool_result(&self, tool_name: &str, success: bool) {
332 let emoji = if success { SUCCESS } else { ERROR };
333 let status = if success {
334 style("completed").green()
335 } else {
336 style("failed").red()
337 };
338
339 println!(
340 " {} {} {}",
341 style("│").dim(),
342 emoji,
343 style(format!("{} {}", tool_name, status)).dim()
344 );
345 }
346}
347
348impl Default for AgentUI {
349 fn default() -> Self {
350 Self::new()
351 }
352}
353
354pub fn format_tool_summary(tools_called: &[&str]) -> String {
356 if tools_called.is_empty() {
357 return String::new();
358 }
359
360 let mut summary = String::from("\n ");
361 summary.push_str(&style("Tools used: ").dim().to_string());
362
363 for (i, tool) in tools_called.iter().enumerate() {
364 if i > 0 {
365 summary.push_str(", ");
366 }
367 summary.push_str(&style(*tool).cyan().to_string());
368 }
369
370 summary
371}
372
373pub fn create_progress_bar(len: u64, message: &str) -> ProgressBar {
375 let pb = ProgressBar::new(len);
376 pb.set_style(
377 ProgressStyle::default_bar()
378 .template(" {spinner:.cyan} [{bar:40.cyan/dim}] {pos}/{len} {msg}")
379 .unwrap()
380 .progress_chars("━━╸"),
381 );
382 pb.set_message(message.to_string());
383 pb
384}