1use owo_colors::OwoColorize;
7use std::sync::{Arc, Mutex};
8use indicatif::ProgressBar;
9
10use super::schema_agentic::{ToolCall, EvaluationReport};
11use super::tools::ToolResult;
12
13pub trait AgenticReporter: Send + Sync {
15 fn report_assessment(&self, reasoning: &str, needs_context: bool, tools: &[ToolCall]);
17
18 fn report_tool_start(&self, idx: usize, tool: &ToolCall);
20
21 fn report_tool_complete(&self, idx: usize, result: &ToolResult);
23
24 fn report_generation(&self, reasoning: Option<&str>, query_count: usize, confidence: f32);
26
27 fn report_evaluation(&self, evaluation: &EvaluationReport);
29
30 fn report_refinement_start(&self);
32
33 fn report_phase(&self, phase_num: usize, phase_name: &str);
35
36 fn report_reindex_progress(&self, current: usize, total: usize, message: String);
38
39 fn clear_all(&self);
41}
42
43pub struct ConsoleReporter {
45 show_reasoning: bool,
47
48 verbose: bool,
50
51 debug: bool,
53
54 lines_printed: Mutex<usize>,
56
57 spinner: Option<Arc<Mutex<ProgressBar>>>,
59}
60
61impl ConsoleReporter {
62 pub fn new(show_reasoning: bool, verbose: bool, debug: bool, spinner: Option<Arc<Mutex<ProgressBar>>>) -> Self {
64 Self {
65 show_reasoning,
66 verbose,
67 debug,
68 lines_printed: Mutex::new(0),
69 spinner,
70 }
71 }
72
73 fn clear_last_output(&self) {
75 if self.debug {
77 return;
78 }
79
80 let lines = *self.lines_printed.lock().unwrap();
81 if lines > 0 {
82 for _ in 0..lines {
83 eprint!("\x1b[1A\x1b[2K");
85 }
86 *self.lines_printed.lock().unwrap() = 0;
87 }
88 }
89
90 fn add_lines(&self, count: usize) {
92 *self.lines_printed.lock().unwrap() += count;
93 }
94
95 fn count_lines(text: &str) -> usize {
97 if text.is_empty() {
98 0
99 } else {
100 text.lines().count()
101 }
102 }
103
104 fn display_reasoning_block(&self, reasoning: &str) {
106 let mut line_count = 0;
107 for line in reasoning.lines() {
108 if line.trim().is_empty() {
109 println!();
110 } else {
111 println!(" \x1b[90m{}\x1b[0m", line);
113 }
114 line_count += 1;
115 }
116 self.add_lines(line_count);
117 }
118
119 fn describe_tool(&self, tool: &ToolCall) -> String {
121 match tool {
122 ToolCall::GatherContext { params } => {
123 let mut parts = Vec::new();
124 if params.structure { parts.push("structure"); }
125 if params.file_types { parts.push("file types"); }
126 if params.project_type { parts.push("project type"); }
127 if params.framework { parts.push("frameworks"); }
128 if params.entry_points { parts.push("entry points"); }
129 if params.test_layout { parts.push("test layout"); }
130 if params.config_files { parts.push("config files"); }
131
132 if parts.is_empty() {
133 "gather_context: General codebase context".to_string()
134 } else {
135 format!("gather_context: {}", parts.join(", "))
136 }
137 }
138 ToolCall::ExploreCodebase { description, command } => {
139 format!("explore_codebase: {} ({})", description, command)
140 }
141 ToolCall::AnalyzeStructure { analysis_type } => {
142 format!("analyze_structure: {:?}", analysis_type)
143 }
144 ToolCall::SearchDocumentation { query, files } => {
145 if let Some(file_list) = files {
146 format!("search_documentation: '{}' in files {:?}", query, file_list)
147 } else {
148 format!("search_documentation: '{}'", query)
149 }
150 }
151 ToolCall::GetStatistics => {
152 "get_statistics: Retrieve index statistics".to_string()
153 }
154 ToolCall::GetDependencies { file_path, reverse } => {
155 if *reverse {
156 format!("get_dependencies: Reverse deps for '{}'", file_path)
157 } else {
158 format!("get_dependencies: Dependencies of '{}'", file_path)
159 }
160 }
161 ToolCall::GetAnalysisSummary { min_dependents } => {
162 format!("get_analysis_summary: Dependency analysis (min_dependents={})", min_dependents)
163 }
164 ToolCall::FindIslands { min_size, max_size } => {
165 format!("find_islands: Disconnected components (size {}-{})", min_size, max_size)
166 }
167 }
168 }
169
170 fn truncate(&self, text: &str, max_len: usize) -> String {
172 if text.len() <= max_len {
173 return text.to_string();
174 }
175
176 let truncated = &text[..max_len];
177 format!("{}...", truncated)
178 }
179
180 fn with_suspended_spinner<F, R>(&self, f: F) -> R
183 where
184 F: FnOnce() -> R,
185 {
186 if let Some(ref spinner) = self.spinner {
187 if let Ok(spinner_guard) = spinner.lock() {
188 return spinner_guard.suspend(f);
189 }
190 }
191 f()
193 }
194}
195
196impl AgenticReporter for ConsoleReporter {
197 fn report_phase(&self, phase_num: usize, phase_name: &str) {
198 if let Some(ref spinner) = self.spinner {
199 if let Ok(spinner_guard) = spinner.lock() {
201 spinner_guard.suspend(|| {
203 let line = format!("\n━━━ Phase {}: {} ━━━", phase_num, phase_name);
204 println!("{}", line.bold().cyan());
205 self.add_lines(2); });
207 spinner_guard.finish_and_clear();
210 }
211 } else {
212 let line = format!("\n━━━ Phase {}: {} ━━━", phase_num, phase_name);
214 println!("{}", line.bold().cyan());
215 self.add_lines(2); }
217 }
218
219 fn report_assessment(&self, reasoning: &str, needs_context: bool, tools: &[ToolCall]) {
220 self.report_phase(1, "Assessment");
221
222 self.with_suspended_spinner(|| {
223 if self.show_reasoning && !reasoning.is_empty() {
224 println!("\n{}", "💭 Reasoning:".dimmed());
225 self.add_lines(2); self.display_reasoning_block(reasoning);
227 }
228
229 println!();
230 self.add_lines(1);
231
232 if needs_context && !tools.is_empty() {
233 println!("{} {}", "→".bright_green(), "Needs additional context".bold());
234 println!(" {} tool(s) to execute:", tools.len());
235 self.add_lines(2);
236 for (i, tool) in tools.iter().enumerate() {
237 println!(" {}. {}", (i + 1).to_string().bright_white(), self.describe_tool(tool).dimmed());
238 self.add_lines(1);
239 }
240 } else {
241 println!("{} {}", "→".bright_green(), "Has sufficient context".bold());
242 println!(" Proceeding directly to query generation");
243 self.add_lines(2);
244 }
245 });
246 }
247
248 fn report_tool_start(&self, idx: usize, tool: &ToolCall) {
249 if idx == 1 {
250 self.report_phase(2, "Context Gathering");
251 self.with_suspended_spinner(|| {
252 println!();
253 self.add_lines(1);
254 });
255 }
256
257 if self.verbose {
258 self.with_suspended_spinner(|| {
259 println!(" {} Executing: {}", "⋯".dimmed(), self.describe_tool(tool).dimmed());
260 self.add_lines(1);
261 });
262 }
263 }
264
265 fn report_tool_complete(&self, idx: usize, result: &ToolResult) {
266 self.with_suspended_spinner(|| {
267 if result.success {
268 println!(" {} {} {}",
269 "✓".bright_green(),
270 format!("[{}]", idx).dimmed(),
271 result.description
272 );
273 self.add_lines(1);
274
275 if self.verbose && !result.output.is_empty() {
276 let preview = self.truncate(&result.output, 150);
278 let lines_shown = preview.lines().take(3);
279 for line in lines_shown {
280 println!(" {}", line.dimmed());
281 self.add_lines(1);
282 }
283 if result.output.lines().count() > 3 {
284 println!(" {}", "...".dimmed());
285 self.add_lines(1);
286 }
287 }
288 } else {
289 println!(" {} {} {} - {}",
290 "✗".bright_red(),
291 format!("[{}]", idx).dimmed(),
292 result.description,
293 "failed".red()
294 );
295 self.add_lines(1);
296 }
297 });
298 }
299
300 fn report_generation(&self, reasoning: Option<&str>, query_count: usize, confidence: f32) {
301 self.clear_last_output();
303
304 self.report_phase(3, "Query Generation");
305
306 self.with_suspended_spinner(|| {
307 if self.show_reasoning {
308 if let Some(reasoning_text) = reasoning {
309 if !reasoning_text.is_empty() {
310 println!("\n{}", "💭 Reasoning:".dimmed());
311 self.add_lines(2);
312 self.display_reasoning_block(reasoning_text);
313 }
314 }
315 }
316
317 println!();
318 self.add_lines(1);
319
320 let confidence_pct = (confidence * 100.0) as u8;
321
322 print!("{} Generated {} {} (confidence: ",
323 "→".bright_green(),
324 query_count,
325 if query_count == 1 { "query" } else { "queries" }
326 );
327
328 if confidence >= 0.8 {
329 println!("{}%)", confidence_pct.to_string().bright_green());
330 } else if confidence >= 0.6 {
331 println!("{}%)", confidence_pct.to_string().yellow());
332 } else {
333 println!("{}%)", confidence_pct.to_string().bright_red());
334 }
335 self.add_lines(1);
336 });
337 }
338
339 fn report_evaluation(&self, evaluation: &EvaluationReport) {
340 self.clear_last_output();
342
343 self.report_phase(5, "Evaluation");
344
345 self.with_suspended_spinner(|| {
346 println!();
347 self.add_lines(1);
348
349 if evaluation.success {
350 println!("{} {} (score: {}/1.0)",
351 "✓".bright_green(),
352 "Success".bold().bright_green(),
353 format!("{:.2}", evaluation.score).bright_white()
354 );
355 self.add_lines(1);
356
357 if self.verbose && !evaluation.issues.is_empty() {
358 println!("\n Minor issues noted:");
359 self.add_lines(2);
360 for issue in &evaluation.issues {
361 println!(" - {} (severity: {:.2})",
362 issue.description.dimmed(),
363 issue.severity
364 );
365 self.add_lines(1);
366 }
367 }
368 } else {
369 println!("{} {} (score: {}/1.0)",
370 "⚠".yellow(),
371 "Results need refinement".bold().yellow(),
372 format!("{:.2}", evaluation.score).bright_white()
373 );
374 self.add_lines(1);
375
376 if !evaluation.issues.is_empty() {
377 println!("\n Issues found:");
378 self.add_lines(2);
379 for (idx, issue) in evaluation.issues.iter().enumerate().take(3) {
380 println!(" {}. {}",
381 (idx + 1).to_string().dimmed(),
382 issue.description
383 );
384 self.add_lines(1);
385 }
386 }
387
388 if !evaluation.suggestions.is_empty() {
389 println!("\n Suggestions:");
390 self.add_lines(2);
391 for (idx, suggestion) in evaluation.suggestions.iter().enumerate().take(3) {
392 println!(" {}. {}",
393 (idx + 1).to_string().dimmed(),
394 suggestion.dimmed()
395 );
396 self.add_lines(1);
397 }
398 }
399 }
400 });
401 }
402
403 fn report_refinement_start(&self) {
404 self.clear_last_output();
406
407 self.report_phase(6, "Refinement");
408
409 self.with_suspended_spinner(|| {
410 println!();
411 println!("{} Refining queries based on evaluation feedback...", "→".yellow());
412 self.add_lines(2);
413 });
414 }
415
416 fn report_reindex_progress(&self, current: usize, total: usize, message: String) {
417 self.with_suspended_spinner(|| {
418 if current > 0 {
420 eprint!("\r\x1b[2K");
422 }
423
424 let percentage = if total > 0 {
425 (current as f32 / total as f32 * 100.0) as u8
426 } else {
427 0
428 };
429
430 eprint!(" {} Reindexing cache: [{}/{}] {}% - {}",
431 "⋯".yellow(),
432 current,
433 total,
434 percentage,
435 message.dimmed()
436 );
437
438 use std::io::Write;
440 let _ = std::io::stderr().flush();
441
442 if current >= total {
444 eprintln!();
445 self.add_lines(1);
446 }
447 });
448 }
449
450 fn clear_all(&self) {
451 self.clear_last_output();
454 }
455}
456
457pub struct QuietReporter;
459
460impl AgenticReporter for QuietReporter {
461 fn report_assessment(&self, _reasoning: &str, _needs_context: bool, _tools: &[ToolCall]) {}
462 fn report_tool_start(&self, _idx: usize, _tool: &ToolCall) {}
463 fn report_tool_complete(&self, _idx: usize, _result: &ToolResult) {}
464 fn report_generation(&self, _reasoning: Option<&str>, _query_count: usize, _confidence: f32) {}
465 fn report_evaluation(&self, _evaluation: &EvaluationReport) {}
466 fn report_refinement_start(&self) {}
467 fn report_phase(&self, _phase_num: usize, _phase_name: &str) {}
468 fn report_reindex_progress(&self, _current: usize, _total: usize, _message: String) {}
469 fn clear_all(&self) {}
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use crate::semantic::schema_agentic::*;
476
477 #[test]
478 fn test_console_reporter_creation() {
479 let reporter = ConsoleReporter::new(true, false, false, None);
480 assert!(reporter.show_reasoning);
481 assert!(!reporter.verbose);
482 assert!(!reporter.debug);
483 }
484
485 #[test]
486 fn test_truncate() {
487 let reporter = ConsoleReporter::new(false, false, false, None);
488 let text = "a".repeat(300);
489 let truncated = reporter.truncate(&text, 100);
490 assert!(truncated.len() <= 103); }
492
493 #[test]
494 fn test_describe_gather_context_tool() {
495 let reporter = ConsoleReporter::new(false, false, false, None);
496 let tool = ToolCall::GatherContext {
497 params: ContextGatheringParams {
498 structure: true,
499 file_types: true,
500 ..Default::default()
501 },
502 };
503
504 let desc = reporter.describe_tool(&tool);
505 assert!(desc.contains("gather_context"));
506 assert!(desc.contains("structure"));
507 }
508}