use owo_colors::OwoColorize;
use std::sync::{Arc, Mutex};
use indicatif::ProgressBar;
use super::schema_agentic::{ToolCall, EvaluationReport};
use super::tools::ToolResult;
pub trait AgenticReporter: Send + Sync {
fn report_assessment(&self, reasoning: &str, needs_context: bool, tools: &[ToolCall]);
fn report_tool_start(&self, idx: usize, tool: &ToolCall);
fn report_tool_complete(&self, idx: usize, result: &ToolResult);
fn report_generation(&self, reasoning: Option<&str>, query_count: usize, confidence: f32);
fn report_evaluation(&self, evaluation: &EvaluationReport);
fn report_refinement_start(&self);
fn report_phase(&self, phase_num: usize, phase_name: &str);
fn report_reindex_progress(&self, current: usize, total: usize, message: String);
fn clear_all(&self);
}
pub struct ConsoleReporter {
show_reasoning: bool,
verbose: bool,
debug: bool,
lines_printed: Mutex<usize>,
spinner: Option<Arc<Mutex<ProgressBar>>>,
}
impl ConsoleReporter {
pub fn new(show_reasoning: bool, verbose: bool, debug: bool, spinner: Option<Arc<Mutex<ProgressBar>>>) -> Self {
Self {
show_reasoning,
verbose,
debug,
lines_printed: Mutex::new(0),
spinner,
}
}
fn clear_last_output(&self) {
if self.debug {
return;
}
let lines = *self.lines_printed.lock().unwrap();
if lines > 0 {
for _ in 0..lines {
eprint!("\x1b[1A\x1b[2K");
}
*self.lines_printed.lock().unwrap() = 0;
}
}
fn add_lines(&self, count: usize) {
*self.lines_printed.lock().unwrap() += count;
}
fn count_lines(text: &str) -> usize {
if text.is_empty() {
0
} else {
text.lines().count()
}
}
fn display_reasoning_block(&self, reasoning: &str) {
let mut line_count = 0;
for line in reasoning.lines() {
if line.trim().is_empty() {
println!();
} else {
println!(" \x1b[90m{}\x1b[0m", line);
}
line_count += 1;
}
self.add_lines(line_count);
}
fn describe_tool(&self, tool: &ToolCall) -> String {
match tool {
ToolCall::GatherContext { params } => {
let mut parts = Vec::new();
if params.structure { parts.push("structure"); }
if params.file_types { parts.push("file types"); }
if params.project_type { parts.push("project type"); }
if params.framework { parts.push("frameworks"); }
if params.entry_points { parts.push("entry points"); }
if params.test_layout { parts.push("test layout"); }
if params.config_files { parts.push("config files"); }
if parts.is_empty() {
"gather_context: General codebase context".to_string()
} else {
format!("gather_context: {}", parts.join(", "))
}
}
ToolCall::ExploreCodebase { description, command } => {
format!("explore_codebase: {} ({})", description, command)
}
ToolCall::AnalyzeStructure { analysis_type } => {
format!("analyze_structure: {:?}", analysis_type)
}
ToolCall::SearchDocumentation { query, files } => {
if let Some(file_list) = files {
format!("search_documentation: '{}' in files {:?}", query, file_list)
} else {
format!("search_documentation: '{}'", query)
}
}
ToolCall::GetStatistics => {
"get_statistics: Retrieve index statistics".to_string()
}
ToolCall::GetDependencies { file_path, reverse } => {
if *reverse {
format!("get_dependencies: Reverse deps for '{}'", file_path)
} else {
format!("get_dependencies: Dependencies of '{}'", file_path)
}
}
ToolCall::GetAnalysisSummary { min_dependents } => {
format!("get_analysis_summary: Dependency analysis (min_dependents={})", min_dependents)
}
ToolCall::FindIslands { min_size, max_size } => {
format!("find_islands: Disconnected components (size {}-{})", min_size, max_size)
}
}
}
fn truncate(&self, text: &str, max_len: usize) -> String {
if text.len() <= max_len {
return text.to_string();
}
let truncated = &text[..max_len];
format!("{}...", truncated)
}
fn with_suspended_spinner<F, R>(&self, f: F) -> R
where
F: FnOnce() -> R,
{
if let Some(ref spinner) = self.spinner {
if let Ok(spinner_guard) = spinner.lock() {
return spinner_guard.suspend(f);
}
}
f()
}
}
impl AgenticReporter for ConsoleReporter {
fn report_phase(&self, phase_num: usize, phase_name: &str) {
if let Some(ref spinner) = self.spinner {
if let Ok(spinner_guard) = spinner.lock() {
spinner_guard.suspend(|| {
let line = format!("\n━━━ Phase {}: {} ━━━", phase_num, phase_name);
println!("{}", line.bold().cyan());
self.add_lines(2); });
spinner_guard.finish_and_clear();
}
} else {
let line = format!("\n━━━ Phase {}: {} ━━━", phase_num, phase_name);
println!("{}", line.bold().cyan());
self.add_lines(2); }
}
fn report_assessment(&self, reasoning: &str, needs_context: bool, tools: &[ToolCall]) {
self.report_phase(1, "Assessment");
self.with_suspended_spinner(|| {
if self.show_reasoning && !reasoning.is_empty() {
println!("\n{}", "💭 Reasoning:".dimmed());
self.add_lines(2); self.display_reasoning_block(reasoning);
}
println!();
self.add_lines(1);
if needs_context && !tools.is_empty() {
println!("{} {}", "→".bright_green(), "Needs additional context".bold());
println!(" {} tool(s) to execute:", tools.len());
self.add_lines(2);
for (i, tool) in tools.iter().enumerate() {
println!(" {}. {}", (i + 1).to_string().bright_white(), self.describe_tool(tool).dimmed());
self.add_lines(1);
}
} else {
println!("{} {}", "→".bright_green(), "Has sufficient context".bold());
println!(" Proceeding directly to query generation");
self.add_lines(2);
}
});
}
fn report_tool_start(&self, idx: usize, tool: &ToolCall) {
if idx == 1 {
self.report_phase(2, "Context Gathering");
self.with_suspended_spinner(|| {
println!();
self.add_lines(1);
});
}
if self.verbose {
self.with_suspended_spinner(|| {
println!(" {} Executing: {}", "⋯".dimmed(), self.describe_tool(tool).dimmed());
self.add_lines(1);
});
}
}
fn report_tool_complete(&self, idx: usize, result: &ToolResult) {
self.with_suspended_spinner(|| {
if result.success {
println!(" {} {} {}",
"✓".bright_green(),
format!("[{}]", idx).dimmed(),
result.description
);
self.add_lines(1);
if self.verbose && !result.output.is_empty() {
let preview = self.truncate(&result.output, 150);
let lines_shown = preview.lines().take(3);
for line in lines_shown {
println!(" {}", line.dimmed());
self.add_lines(1);
}
if result.output.lines().count() > 3 {
println!(" {}", "...".dimmed());
self.add_lines(1);
}
}
} else {
println!(" {} {} {} - {}",
"✗".bright_red(),
format!("[{}]", idx).dimmed(),
result.description,
"failed".red()
);
self.add_lines(1);
}
});
}
fn report_generation(&self, reasoning: Option<&str>, query_count: usize, confidence: f32) {
self.clear_last_output();
self.report_phase(3, "Query Generation");
self.with_suspended_spinner(|| {
if self.show_reasoning {
if let Some(reasoning_text) = reasoning {
if !reasoning_text.is_empty() {
println!("\n{}", "💭 Reasoning:".dimmed());
self.add_lines(2);
self.display_reasoning_block(reasoning_text);
}
}
}
println!();
self.add_lines(1);
let confidence_pct = (confidence * 100.0) as u8;
print!("{} Generated {} {} (confidence: ",
"→".bright_green(),
query_count,
if query_count == 1 { "query" } else { "queries" }
);
if confidence >= 0.8 {
println!("{}%)", confidence_pct.to_string().bright_green());
} else if confidence >= 0.6 {
println!("{}%)", confidence_pct.to_string().yellow());
} else {
println!("{}%)", confidence_pct.to_string().bright_red());
}
self.add_lines(1);
});
}
fn report_evaluation(&self, evaluation: &EvaluationReport) {
self.clear_last_output();
self.report_phase(5, "Evaluation");
self.with_suspended_spinner(|| {
println!();
self.add_lines(1);
if evaluation.success {
println!("{} {} (score: {}/1.0)",
"✓".bright_green(),
"Success".bold().bright_green(),
format!("{:.2}", evaluation.score).bright_white()
);
self.add_lines(1);
if self.verbose && !evaluation.issues.is_empty() {
println!("\n Minor issues noted:");
self.add_lines(2);
for issue in &evaluation.issues {
println!(" - {} (severity: {:.2})",
issue.description.dimmed(),
issue.severity
);
self.add_lines(1);
}
}
} else {
println!("{} {} (score: {}/1.0)",
"⚠".yellow(),
"Results need refinement".bold().yellow(),
format!("{:.2}", evaluation.score).bright_white()
);
self.add_lines(1);
if !evaluation.issues.is_empty() {
println!("\n Issues found:");
self.add_lines(2);
for (idx, issue) in evaluation.issues.iter().enumerate().take(3) {
println!(" {}. {}",
(idx + 1).to_string().dimmed(),
issue.description
);
self.add_lines(1);
}
}
if !evaluation.suggestions.is_empty() {
println!("\n Suggestions:");
self.add_lines(2);
for (idx, suggestion) in evaluation.suggestions.iter().enumerate().take(3) {
println!(" {}. {}",
(idx + 1).to_string().dimmed(),
suggestion.dimmed()
);
self.add_lines(1);
}
}
}
});
}
fn report_refinement_start(&self) {
self.clear_last_output();
self.report_phase(6, "Refinement");
self.with_suspended_spinner(|| {
println!();
println!("{} Refining queries based on evaluation feedback...", "→".yellow());
self.add_lines(2);
});
}
fn report_reindex_progress(&self, current: usize, total: usize, message: String) {
self.with_suspended_spinner(|| {
if current > 0 {
eprint!("\r\x1b[2K");
}
let percentage = if total > 0 {
(current as f32 / total as f32 * 100.0) as u8
} else {
0
};
eprint!(" {} Reindexing cache: [{}/{}] {}% - {}",
"⋯".yellow(),
current,
total,
percentage,
message.dimmed()
);
use std::io::Write;
let _ = std::io::stderr().flush();
if current >= total {
eprintln!();
self.add_lines(1);
}
});
}
fn clear_all(&self) {
self.clear_last_output();
}
}
pub struct QuietReporter;
impl AgenticReporter for QuietReporter {
fn report_assessment(&self, _reasoning: &str, _needs_context: bool, _tools: &[ToolCall]) {}
fn report_tool_start(&self, _idx: usize, _tool: &ToolCall) {}
fn report_tool_complete(&self, _idx: usize, _result: &ToolResult) {}
fn report_generation(&self, _reasoning: Option<&str>, _query_count: usize, _confidence: f32) {}
fn report_evaluation(&self, _evaluation: &EvaluationReport) {}
fn report_refinement_start(&self) {}
fn report_phase(&self, _phase_num: usize, _phase_name: &str) {}
fn report_reindex_progress(&self, _current: usize, _total: usize, _message: String) {}
fn clear_all(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::semantic::schema_agentic::*;
#[test]
fn test_console_reporter_creation() {
let reporter = ConsoleReporter::new(true, false, false, None);
assert!(reporter.show_reasoning);
assert!(!reporter.verbose);
assert!(!reporter.debug);
}
#[test]
fn test_truncate() {
let reporter = ConsoleReporter::new(false, false, false, None);
let text = "a".repeat(300);
let truncated = reporter.truncate(&text, 100);
assert!(truncated.len() <= 103); }
#[test]
fn test_describe_gather_context_tool() {
let reporter = ConsoleReporter::new(false, false, false, None);
let tool = ToolCall::GatherContext {
params: ContextGatheringParams {
structure: true,
file_types: true,
..Default::default()
},
};
let desc = reporter.describe_tool(&tool);
assert!(desc.contains("gather_context"));
assert!(desc.contains("structure"));
}
}