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