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 { .. } => (), 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 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}