syncable_cli/agent/
ui.rs

1//! Beautiful terminal UI for the agent
2//!
3//! Provides colorful output, markdown rendering, and tool call animations.
4
5use console::{style, Emoji, Term};
6use indicatif::{ProgressBar, ProgressStyle};
7use std::time::Duration;
8
9// Emojis for different states
10pub 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
22/// Print the SYNCABLE ASCII art logo with gradient colors
23pub fn print_logo() {
24    // Colors matching the logo gradient: purple → orange → pink
25    // Using ANSI 256 colors for better gradient
26    
27    // Purple shades for S, y
28    let purple = "\x1b[38;5;141m";  // Light purple
29    // Orange shades for n, c  
30    let orange = "\x1b[38;5;216m";  // Peach/orange
31    // Pink shades for a, b, l, e
32    let pink = "\x1b[38;5;212m";    // Hot pink
33    let magenta = "\x1b[38;5;207m"; // Magenta
34    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
64/// Terminal UI handler for the agent
65pub 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    /// Pause the current spinner temporarily
80    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    /// Print the welcome banner
88    pub fn print_welcome(&self, provider: &str, model: &str) {
89        // Print the gradient ASCII logo
90        print_logo();
91
92        // Print agent info
93        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    /// Print the prompt
113    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    /// Start a thinking spinner
124    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    /// Update spinner with tool call info
140    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    /// Stop the spinner
165    pub fn stop_thinking(&mut self) {
166        if let Some(spinner) = self.spinner.take() {
167            spinner.finish_and_clear();
168        }
169    }
170
171    /// Print the assistant header for streaming response
172    pub fn print_assistant_header(&self) {
173        println!();
174        println!(
175            "{} {} ",
176            style("assistant").magenta().bold(),
177            style("›").magenta()
178        );
179    }
180
181    /// Start a streaming indicator
182    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    /// Update streaming progress
196    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    /// Stop streaming and print the response
205    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    /// Print streaming text chunk (no newline) - real-time output
215    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    /// Print tool call notification during streaming
222    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    /// Print tool call completion
251    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    /// End the streaming response
271    pub fn end_stream(&self) {
272        println!();
273        println!();
274    }
275
276    /// Print the assistant's response with markdown rendering
277    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        // Render markdown
287        self.render_markdown(response);
288
289        println!();
290    }
291
292    /// Render markdown content beautifully
293    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        // Customize colors using crossterm colors
300        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        // Print markdown to terminal
309        skin.print_text(content);
310    }
311
312    /// Print an error message
313    pub fn print_error(&self, message: &str) {
314        println!(
315            "\n  {} {}",
316            ERROR,
317            style(message).red()
318        );
319    }
320
321    /// Print a success message
322    pub fn print_success(&self, message: &str) {
323        println!(
324            "\n  {} {}",
325            SUCCESS,
326            style(message).green()
327        );
328    }
329
330    /// Print tool execution result summary
331    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
354/// Format tool calls for display
355pub 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
373/// Create a simple progress bar for long operations
374pub 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}