Skip to main content

tycode_core/formatter/
verbose.rs

1use super::EventFormatter;
2use crate::ai::model::Model;
3use crate::ai::TokenUsage;
4use crate::chat::events::{ChatMessage, ToolExecutionResult, ToolRequest, ToolRequestType};
5use crate::chat::ModelInfo;
6use crate::modules::task_list::{TaskList, TaskStatus};
7use serde_json::Value;
8use similar::{ChangeTag, TextDiff};
9use std::io::Write;
10
11#[derive(Clone)]
12pub struct VerboseFormatter {
13    use_colors: bool,
14    spinner_state: usize,
15    thinking_shown: bool,
16    last_tool_request: Option<ToolRequest>,
17}
18
19impl Default for VerboseFormatter {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl VerboseFormatter {
26    pub fn new() -> Self {
27        Self {
28            use_colors: true,
29            spinner_state: 0,
30            thinking_shown: false,
31            last_tool_request: None,
32        }
33    }
34
35    fn get_spinner_char(&mut self) -> char {
36        const SPINNER_CHARS: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
37        let c = SPINNER_CHARS[self.spinner_state % SPINNER_CHARS.len()];
38        self.spinner_state += 1;
39        c
40    }
41
42    fn clear_thinking_if_shown(&mut self) {
43        if self.thinking_shown {
44            print!("\r\x1b[2K");
45            self.thinking_shown = false;
46        }
47    }
48
49    fn print_line(&mut self, line: &str) {
50        self.clear_thinking_if_shown();
51        println!("{line}");
52    }
53
54    fn eprint_line(&mut self, line: &str) {
55        self.clear_thinking_if_shown();
56        eprintln!("{line}");
57    }
58
59    fn print_tool_call(&mut self, name: &str, arguments: &serde_json::Value) {
60        if self.use_colors {
61            self.print_line(&format!("\x1b[36m🔧 Tool:\x1b[0m \x1b[1;36m{name}\x1b[0m \x1b[36mwith args:\x1b[0m \x1b[90m{arguments}\x1b[0m"));
62        } else {
63            self.print_line(&format!("🔧 Tool: {name} with args: {arguments}"));
64        }
65    }
66
67    fn print_formatted_tool_call(&mut self, name: &str, args: &Value) {
68        match name {
69            "write_file" => {
70                if let Some(path) = args.get("file_path").and_then(|v| v.as_str()) {
71                    let content_len = args
72                        .get("content")
73                        .and_then(|v| v.as_str())
74                        .unwrap_or("")
75                        .len();
76                    self.print_system(&format!("💾 Writing file {path} ({content_len} chars)"));
77                } else {
78                    self.print_tool_call(name, args);
79                }
80            }
81            _ => {
82                self.print_tool_call(name, args);
83            }
84        }
85    }
86
87    fn print_file_diff(&mut self, before: &str, after: &str, use_colors: bool) {
88        let diff = TextDiff::from_lines(before, after);
89        let mut diff = diff.unified_diff();
90        let unified = diff.context_radius(7);
91
92        for hunk in unified.iter_hunks() {
93            self.print_line(&hunk.header().to_string());
94            for change in hunk.iter_changes() {
95                let line = change.value().trim_end_matches('\n');
96                match change.tag() {
97                    ChangeTag::Equal => self.print_line(&format!(" {line}")),
98                    ChangeTag::Delete => {
99                        if use_colors {
100                            self.print_line(&format!("\x1b[91m-{line}\x1b[0m"));
101                        } else {
102                            self.print_line(&format!("-{line}"));
103                        }
104                    }
105                    ChangeTag::Insert => {
106                        if use_colors {
107                            self.print_line(&format!("\x1b[92m+{line}\x1b[0m"));
108                        } else {
109                            self.print_line(&format!("+{line}"));
110                        }
111                    }
112                }
113            }
114        }
115    }
116
117    fn format_bytes(&self, bytes: usize) -> String {
118        const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
119        let mut size = bytes as f64;
120        let mut unit_index = 0;
121
122        while size >= 1024.0 && unit_index < UNITS.len() - 1 {
123            size /= 1024.0;
124            unit_index += 1;
125        }
126
127        if unit_index == 0 {
128            format!("{} {}", bytes, UNITS[unit_index])
129        } else {
130            format!("{:.1} {}", size, UNITS[unit_index])
131        }
132    }
133}
134
135impl EventFormatter for VerboseFormatter {
136    fn print_system(&mut self, msg: &str) {
137        if self.use_colors {
138            self.print_line(&format!("\x1b[33m[System]\x1b[0m {msg}"));
139        } else {
140            self.print_line(&format!("[System] {msg}"));
141        }
142    }
143
144    fn print_ai(
145        &mut self,
146        msg: &str,
147        agent: &str,
148        model_info: &Option<ModelInfo>,
149        token_usage: &Option<TokenUsage>,
150    ) {
151        let model_name = model_info
152            .as_ref()
153            .map(|m| m.model.name())
154            .unwrap_or_default();
155
156        let usage_text = token_usage
157            .as_ref()
158            .map(|usage| {
159                let display_input =
160                    usage.input_tokens + usage.cache_creation_input_tokens.unwrap_or(0);
161                let input_part = if let Some(cached) = usage.cached_prompt_tokens {
162                    if cached > 0 {
163                        format!("{} ({} cached)", display_input, cached)
164                    } else {
165                        format!("{}", display_input)
166                    }
167                } else {
168                    format!("{}", display_input)
169                };
170
171                let display_output = usage.output_tokens + usage.reasoning_tokens.unwrap_or(0);
172                let output_part = if let Some(reasoning) = usage.reasoning_tokens {
173                    if reasoning > 0 {
174                        format!("{} ({} reasoning)", display_output, reasoning)
175                    } else {
176                        format!("{}", display_output)
177                    }
178                } else {
179                    format!("{}", display_output)
180                };
181
182                format!(" (usage: {}/{})", input_part, output_part)
183            })
184            .unwrap_or_default();
185
186        if self.use_colors {
187            self.print_line(&format!(
188                "\x1b[32m[{agent}]\x1b[0m \x1b[90m({model_name}){usage_text}\x1b[0m {msg}"
189            ));
190        } else {
191            self.print_line(&format!("[{agent}] ({model_name}){usage_text} {msg}"));
192        }
193    }
194
195    fn print_warning(&mut self, msg: &str) {
196        if self.use_colors {
197            self.eprint_line(&format!("\x1b[33m[Warning]\x1b[0m {msg}"));
198        } else {
199            self.eprint_line(&format!("[Warning] {msg}"));
200        }
201    }
202
203    fn print_error(&mut self, msg: &str) {
204        if self.use_colors {
205            self.eprint_line(&format!("\x1b[31m[Error]\x1b[0m {msg}"));
206        } else {
207            self.eprint_line(&format!("[Error] {msg}"));
208        }
209    }
210
211    fn print_retry_attempt(&mut self, attempt: u32, max_retries: u32, error: &str) {
212        if self.use_colors {
213            self.print_line(&format!(
214                "\x1b[33m🔄 Retry attempt {}/{}: {}\x1b[0m",
215                attempt, max_retries, error
216            ));
217        } else {
218            self.print_line(&format!(
219                "🔄 Retry attempt {}/{}: {}",
220                attempt, max_retries, error
221            ));
222        }
223    }
224
225    fn print_tool_request(&mut self, tool_request: &ToolRequest) {
226        self.last_tool_request = Some(tool_request.clone());
227        match &tool_request.tool_type {
228            ToolRequestType::ModifyFile {
229                file_path,
230                before,
231                after,
232            } => {
233                self.print_system(&format!("📝 Modifying file {file_path}"));
234                self.print_file_diff(before, after, self.use_colors);
235            }
236            ToolRequestType::RunCommand {
237                command,
238                working_directory,
239            } => self.print_system(&format!(
240                "💻 Running command `{command}` (in directory {working_directory})"
241            )),
242            ToolRequestType::ReadFiles { .. } => (), //handled in execute result
243            ToolRequestType::Other { args } => {
244                self.print_formatted_tool_call(&tool_request.tool_name, args);
245            }
246            ToolRequestType::SearchTypes {
247                type_name,
248                workspace_root,
249                ..
250            } => {
251                self.print_system(&format!(
252                    "🔍 Searching types for '{type_name}' in {workspace_root}"
253                ));
254            }
255            ToolRequestType::GetTypeDocs {
256                type_path,
257                workspace_root,
258                ..
259            } => {
260                self.print_system(&format!(
261                    "📚 Getting docs for '{type_path}' in {workspace_root}"
262                ));
263            }
264        }
265    }
266
267    fn print_tool_result(
268        &mut self,
269        name: &str,
270        success: bool,
271        result: ToolExecutionResult,
272        verbose: bool,
273    ) {
274        if success {
275            self.print_system(&format!("✅ {name} completed"));
276        }
277
278        // This is an awful hack to not have complete a task show up with a json
279        // string in addition to the system emssage the actor sends. This should
280        // be a strongly typed tool result and have UI specific rendoring...
281        if name == "complete_task" {
282            self.last_tool_request = None;
283            return;
284        }
285
286        match result {
287            ToolExecutionResult::RunCommand {
288                exit_code,
289                stdout,
290                stderr,
291            } => {
292                let status = if exit_code == 0 {
293                    if self.use_colors {
294                        "\x1b[32mSuccess\x1b[0m"
295                    } else {
296                        "Success"
297                    }
298                } else {
299                    if self.use_colors {
300                        "\x1b[31mFailed\x1b[0m"
301                    } else {
302                        "Failed"
303                    }
304                };
305
306                self.print_system(&format!("💻 Command completed with status: {status}"));
307                if self.use_colors {
308                    self.print_line(&format!("  \x1b[36mExit Code:\x1b[0m {exit_code}"));
309                } else {
310                    self.print_line(&format!("  Exit Code: {exit_code}"));
311                }
312
313                if !stdout.is_empty() {
314                    if self.use_colors {
315                        self.print_line("  \x1b[32mStdout:\x1b[0m");
316                    } else {
317                        self.print_line("  Stdout:");
318                    }
319                    for line in stdout.lines() {
320                        self.print_line(&format!("    {line}"));
321                    }
322                }
323
324                if !stderr.is_empty() {
325                    if self.use_colors {
326                        self.print_line("  \x1b[31mStderr:\x1b[0m");
327                    } else {
328                        self.print_line("  Stderr:");
329                    }
330                    for line in stderr.lines() {
331                        self.print_line(&format!("    {line}"));
332                    }
333                }
334            }
335            ToolExecutionResult::ReadFiles { files } => {
336                self.print_system(&format!("📁 Tracked {} files", files.len()));
337                for file in files {
338                    let formatted_size = self.format_bytes(file.bytes);
339                    self.print_system(&format!("  📁 {} ({})", file.path, formatted_size));
340                }
341            }
342            ToolExecutionResult::ModifyFile {
343                lines_added,
344                lines_removed,
345            } => {
346                self.print_system(&format!(
347                    "📝 File modified: {lines_added} additions, {lines_removed} deletions"
348                ));
349            }
350            ToolExecutionResult::Error {
351                short_message,
352                detailed_message,
353            } => {
354                let message = if verbose {
355                    detailed_message
356                } else {
357                    short_message
358                };
359                self.print_error(&format!("❌ Tool failed: {message}"));
360            }
361            ToolExecutionResult::SearchTypes { types } => {
362                self.print_system(&format!("🔍 Found {} types", types.len()));
363                for type_path in types {
364                    self.print_line(&format!("  📦 {}", type_path));
365                }
366            }
367            ToolExecutionResult::GetTypeDocs { documentation } => {
368                self.print_system("📚 Documentation retrieved");
369                for line in documentation.lines().take(20) {
370                    self.print_line(&format!("  {}", line));
371                }
372                if documentation.lines().count() > 20 {
373                    self.print_line("  ...(truncated)");
374                }
375            }
376            ToolExecutionResult::Other { result } => {
377                if let Ok(pretty) = serde_json::to_string_pretty(&result) {
378                    self.print_line(&format!("  {}", pretty.replace("\n", "\n  ")));
379                }
380            }
381        }
382        self.last_tool_request = None;
383    }
384
385    fn print_thinking(&mut self) {
386        let spinner = self.get_spinner_char();
387        let text = if let Some(ref tool_request) = self.last_tool_request {
388            match &tool_request.tool_type {
389                ToolRequestType::ModifyFile { file_path, .. } => {
390                    format!("Modifying {}...", file_path)
391                }
392                ToolRequestType::RunCommand { command, .. } => {
393                    format!("Running `{}`...", command)
394                }
395                ToolRequestType::ReadFiles { file_paths } => {
396                    if file_paths.is_empty() {
397                        "Reading files...".to_string()
398                    } else if file_paths.len() == 1 {
399                        format!("Reading {}...", file_paths[0])
400                    } else {
401                        format!("Reading {} files...", file_paths.len())
402                    }
403                }
404                ToolRequestType::Other { .. } => {
405                    format!("Executing {}...", tool_request.tool_name)
406                }
407                ToolRequestType::SearchTypes { type_name, .. } => {
408                    format!("Searching types for '{}'...", type_name)
409                }
410                ToolRequestType::GetTypeDocs { type_path, .. } => {
411                    format!("Getting docs for '{}'...", type_path)
412                }
413            }
414        } else {
415            "Thinking...".to_string()
416        };
417
418        if self.use_colors {
419            print!("\r\x1b[2K\x1b[36m{} {}\x1b[0m", spinner, text);
420        } else {
421            print!("\r{} {}", spinner, text);
422        }
423        let _ = std::io::stdout().flush();
424        self.thinking_shown = true;
425    }
426
427    fn print_stream_start(&mut self, _message_id: &str, agent: &str, model: &Model) {
428        self.clear_thinking_if_shown();
429        let model_name = model.name();
430        if self.use_colors {
431            print!("\r\x1b[2K\x1b[32m[{agent}]\x1b[0m \x1b[90m({model_name})\x1b[0m ");
432        } else {
433            print!("\r[{agent}] ({model_name}) ");
434        }
435        let _ = std::io::stdout().flush();
436    }
437
438    fn print_stream_delta(&mut self, _message_id: &str, text: &str) {
439        print!("{text}");
440        let _ = std::io::stdout().flush();
441    }
442
443    fn print_stream_end(&mut self, message: &ChatMessage) {
444        println!();
445        if let Some(ref usage) = message.token_usage {
446            let display_input = usage.input_tokens + usage.cache_creation_input_tokens.unwrap_or(0);
447            let input_part = if let Some(cached) = usage.cached_prompt_tokens {
448                if cached > 0 {
449                    format!("{} ({} cached)", display_input, cached)
450                } else {
451                    format!("{}", display_input)
452                }
453            } else {
454                format!("{}", display_input)
455            };
456
457            let display_output = usage.output_tokens + usage.reasoning_tokens.unwrap_or(0);
458            let output_part = if let Some(reasoning) = usage.reasoning_tokens {
459                if reasoning > 0 {
460                    format!("{} ({} reasoning)", display_output, reasoning)
461                } else {
462                    format!("{}", display_output)
463                }
464            } else {
465                format!("{}", display_output)
466            };
467
468            let usage_text = format!("(usage: {}/{})", input_part, output_part);
469            if self.use_colors {
470                self.print_line(&format!("  \x1b[90m{usage_text}\x1b[0m"));
471            } else {
472                self.print_line(&format!("  {usage_text}"));
473            }
474        }
475    }
476
477    fn print_task_update(&mut self, task_list: &TaskList) {
478        self.print_system("Task List:");
479        for task in &task_list.tasks {
480            let (status_text, color_code) = match task.status {
481                TaskStatus::Pending => ("Pending", "\x1b[37m"),
482                TaskStatus::InProgress => ("InProgress", "\x1b[33m"),
483                TaskStatus::Completed => ("Completed", "\x1b[32m"),
484                TaskStatus::Failed => ("Failed", "\x1b[31m"),
485            };
486            let status_display = format!("{color_code}[{status_text}]\x1b[0m");
487            self.print_system(&format!(
488                "  - {} Task {}: {}",
489                status_display, task.id, task.description
490            ));
491        }
492    }
493
494    fn clone_box(&self) -> Box<dyn EventFormatter> {
495        Box::new(self.clone())
496    }
497}