1use crate::models::{
2 AgentAttribution, AgentStatistics, AgentToolCorrelation, CollaborationPattern, SessionAnalysis,
3 ToolAnalysis,
4};
5use anyhow::Result;
6use colored::Colorize;
7use indexmap::IndexMap;
8use std::collections::{HashMap, HashSet};
9use std::fmt::Write as FmtWrite;
10use tabled::{
11 settings::{object::Columns, Modify, Style, Width},
12 Table, Tabled,
13};
14
15pub struct Reporter {
16 show_colors: bool,
17}
18
19impl Reporter {
20 #[must_use]
21 pub fn new() -> Self {
22 Self { show_colors: true }
23 }
24
25 #[must_use]
26 pub fn with_colors(mut self, show_colors: bool) -> Self {
27 self.show_colors = show_colors;
28 self
29 }
30
31 pub fn print_terminal(&self, analyses: &[SessionAnalysis]) {
33 if analyses.is_empty() {
34 println!("{}", "No sessions found to analyze".yellow());
35 return;
36 }
37
38 self.print_header(analyses);
40
41 for (i, analysis) in analyses.iter().enumerate() {
43 if i > 0 {
44 println!();
45 }
46 self.print_session_analysis(analysis);
47 }
48
49 if analyses.len() > 1 {
51 println!();
52 self.print_summary(analyses);
53 }
54 }
55
56 fn print_header(&self, analyses: &[SessionAnalysis]) {
57 let title = if analyses.len() == 1 {
58 "Claude Session Analysis"
59 } else {
60 "Claude Sessions Analysis"
61 };
62
63 println!("{}", format!("═══ {} ═══", title).bold().cyan());
64
65 if analyses.len() > 1 {
66 println!(
67 "{} {}",
68 "Sessions analyzed:".bold(),
69 analyses.len().to_string().yellow()
70 );
71 }
72 println!();
73 }
74
75 fn print_session_analysis(&self, analysis: &SessionAnalysis) {
76 println!("{} {}", "Session:".bold(), analysis.session_id.yellow());
78 println!("{} {}", "Project:".bold(), analysis.project_path.green());
79 println!("{} {}ms", "Duration:".bold(), analysis.duration_ms);
80
81 if !analysis.agents.is_empty() {
82 println!("{} {}", "Agents used:".bold(), analysis.agents.len());
83 }
84
85 if !analysis.file_to_agents.is_empty() {
87 println!("\n{}", "📊 File Contributions:".bold());
88 self.print_file_attributions(&analysis.file_to_agents);
89 }
90
91 if !analysis.agent_stats.is_empty() {
93 println!("\n{}", "👥 Agent Statistics:".bold());
94 self.print_agent_statistics(&analysis.agent_stats);
95 }
96
97 if !analysis.agents.is_empty() {
99 println!("\n{}", "⏱️ Timeline:".bold());
100 self.print_timeline(analysis);
101 }
102
103 if !analysis.collaboration_patterns.is_empty() {
105 println!("\n{}", "🔗 Collaboration Patterns:".bold());
106 self.print_collaboration_patterns(&analysis.collaboration_patterns);
107 }
108 }
109
110 fn print_file_attributions(&self, file_to_agents: &IndexMap<String, Vec<AgentAttribution>>) {
111 let mut table_data = Vec::new();
112
113 for (file_path, attributions) in file_to_agents {
114 let file_display = self.truncate_path(file_path, 40);
115
116 for (i, attr) in attributions.iter().enumerate() {
117 let file_col = if i == 0 {
118 file_display.clone()
119 } else {
120 String::new()
121 };
122
123 table_data.push(FileRow {
124 file: file_col,
125 agent: self.format_agent_display(&attr.agent_type),
126 contribution: format!("{:.1}%", attr.contribution_percent),
127 confidence: format!("{:.0}%", attr.confidence_score * 100.0),
128 operations: attr.operations.len().to_string(),
129 });
130 }
131 }
132
133 if !table_data.is_empty() {
134 let table = Table::new(table_data)
135 .with(Style::modern())
136 .with(Modify::new(Columns::first()).with(Width::wrap(40)))
137 .to_string();
138 println!("{}", table);
139 }
140 }
141
142 fn print_agent_statistics(&self, agent_stats: &IndexMap<String, AgentStatistics>) {
143 let mut table_data = Vec::new();
144
145 for stats in agent_stats.values() {
146 table_data.push(AgentRow {
147 agent: self.format_agent_display(&stats.agent_type),
148 invocations: stats.total_invocations.to_string(),
149 duration: self.format_duration(stats.total_duration_ms),
150 files: stats.files_touched.to_string(),
151 tools: stats.tools_used.len().to_string(),
152 });
153 }
154
155 if !table_data.is_empty() {
156 let table = Table::new(table_data).with(Style::modern()).to_string();
157 println!("{}", table);
158 }
159 }
160
161 fn print_timeline(&self, analysis: &SessionAnalysis) {
162 let mut events: Vec<_> = analysis
163 .agents
164 .iter()
165 .map(|a| (a.timestamp, &a.agent_type, &a.task_description))
166 .collect();
167
168 events.sort_by(|a, b| a.0.cmp(&b.0));
169
170 for (timestamp, agent_type, description) in events.iter().take(10) {
171 let time_str = self.format_timestamp(*timestamp);
172 let agent_display = self.format_agent_display(agent_type);
173 let desc = self.truncate_text(description, 60);
174
175 println!(
176 " {} {} - {}",
177 time_str.dimmed(),
178 agent_display,
179 desc.dimmed()
180 );
181 }
182
183 if events.len() > 10 {
184 println!(
185 " {} {} more events...",
186 "...".dimmed(),
187 (events.len() - 10).to_string().dimmed()
188 );
189 }
190 }
191
192 fn print_collaboration_patterns(&self, patterns: &[CollaborationPattern]) {
193 for pattern in patterns {
194 let agents_display = pattern
195 .agents
196 .iter()
197 .map(|a| self.format_agent_icon(a))
198 .collect::<Vec<_>>()
199 .join(" → ");
200
201 println!(
202 " {} {} ({}% confidence)",
203 agents_display,
204 pattern.description.dimmed(),
205 (pattern.confidence * 100.0) as u32
206 );
207 }
208 }
209
210 fn print_summary(&self, analyses: &[SessionAnalysis]) {
211 println!("{}", "📈 Summary Statistics:".bold());
212
213 let total_agents: usize = analyses.iter().map(|a| a.agents.len()).sum();
215 let total_files: usize = analyses.iter().map(|a| a.file_to_agents.len()).sum();
216 let total_duration: u64 = analyses.iter().map(|a| a.duration_ms).sum();
217
218 let mut agent_counts: IndexMap<String, u32> = IndexMap::new();
220 for analysis in analyses {
221 for agent in &analysis.agents {
222 *agent_counts.entry(agent.agent_type.clone()).or_insert(0) += 1;
223 }
224 }
225
226 let mut sorted_agents: Vec<_> = agent_counts.into_iter().collect();
227 sorted_agents.sort_by(|a, b| b.1.cmp(&a.1));
228
229 println!(" {} {}", "Total agent invocations:".bold(), total_agents);
230 println!(" {} {}", "Total files modified:".bold(), total_files);
231 println!(
232 " {} {}",
233 "Total session time:".bold(),
234 self.format_duration(total_duration)
235 );
236
237 println!("\n{}", "🏆 Most Active Agents:".bold());
238 for (agent, count) in sorted_agents.iter().take(5) {
239 println!(
240 " {} {} ({}x)",
241 self.format_agent_icon(agent),
242 agent.cyan(),
243 count.to_string().yellow()
244 );
245 }
246 }
247
248 pub fn to_markdown(&self, analyses: &[SessionAnalysis]) -> Result<String> {
250 let mut md = String::new();
251
252 writeln!(md, "# Claude Session Analysis Report\n")?;
253
254 if analyses.len() > 1 {
255 writeln!(md, "**Sessions Analyzed**: {}\n", analyses.len())?;
256 }
257
258 for (i, analysis) in analyses.iter().enumerate() {
259 if analyses.len() > 1 {
260 writeln!(md, "## Session {} - {}\n", i + 1, analysis.session_id)?;
261 } else {
262 writeln!(md, "## Session Analysis\n")?;
263 }
264
265 writeln!(md, "- **Session ID**: `{}`", analysis.session_id)?;
266 writeln!(md, "- **Project**: `{}`", analysis.project_path)?;
267 writeln!(md, "- **Duration**: {} ms", analysis.duration_ms)?;
268 writeln!(md, "- **Agents Used**: {}", analysis.agents.len())?;
269 writeln!(
270 md,
271 "- **Files Modified**: {}\n",
272 analysis.file_to_agents.len()
273 )?;
274
275 if !analysis.file_to_agents.is_empty() {
276 writeln!(md, "### Files Created/Modified\n")?;
277
278 for (file_path, attributions) in &analysis.file_to_agents {
279 writeln!(md, "#### `{}`\n", file_path)?;
280 writeln!(md, "| Agent | Contribution | Confidence | Operations |")?;
281 writeln!(md, "|-------|-------------|------------|------------|")?;
282
283 for attr in attributions {
284 writeln!(
285 md,
286 "| {} | {:.1}% | {:.0}% | {} |",
287 attr.agent_type,
288 attr.contribution_percent,
289 attr.confidence_score * 100.0,
290 attr.operations.len()
291 )?;
292 }
293 writeln!(md)?;
294 }
295 }
296
297 if !analysis.collaboration_patterns.is_empty() {
298 writeln!(md, "### Collaboration Patterns\n")?;
299 for pattern in &analysis.collaboration_patterns {
300 writeln!(
301 md,
302 "- **{}**: {} ({:.0}% confidence)",
303 pattern.pattern_type,
304 pattern.description,
305 pattern.confidence * 100.0
306 )?;
307 }
308 writeln!(md)?;
309 }
310 }
311
312 Ok(md)
313 }
314
315 pub fn to_json(&self, analyses: &[SessionAnalysis]) -> Result<String> {
317 let json = if analyses.len() == 1 {
318 serde_json::to_string_pretty(&analyses[0])?
319 } else {
320 serde_json::to_string_pretty(analyses)?
321 };
322 Ok(json)
323 }
324
325 pub fn to_csv(&self, analyses: &[SessionAnalysis]) -> Result<String> {
327 let mut csv_data = Vec::new();
328
329 csv_data.push(vec![
331 "session_id".to_string(),
332 "file_path".to_string(),
333 "agent_type".to_string(),
334 "contribution_percent".to_string(),
335 "confidence_score".to_string(),
336 "operations_count".to_string(),
337 ]);
338
339 for analysis in analyses {
341 for (file_path, attributions) in &analysis.file_to_agents {
342 for attr in attributions {
343 csv_data.push(vec![
344 analysis.session_id.clone(),
345 file_path.clone(),
346 attr.agent_type.clone(),
347 attr.contribution_percent.to_string(),
348 attr.confidence_score.to_string(),
349 attr.operations.len().to_string(),
350 ]);
351 }
352 }
353 }
354
355 let mut csv_output = String::new();
356 for row in csv_data {
357 writeln!(csv_output, "{}", row.join(","))?;
358 }
359
360 Ok(csv_output)
361 }
362
363 fn format_agent_display(&self, agent_type: &str) -> String {
366 if self.show_colors {
367 format!(
368 "{} {}",
369 self.format_agent_icon(agent_type),
370 agent_type.cyan()
371 )
372 } else {
373 format!("{} {}", self.format_agent_icon(agent_type), agent_type)
374 }
375 }
376
377 pub fn format_agent_icon(&self, agent_type: &str) -> String {
378 match agent_type {
379 "architect" => "🏗️".to_string(),
380 "developer" => "💻".to_string(),
381 "backend-architect" => "🔧".to_string(),
382 "frontend-developer" => "🎨".to_string(),
383 "rust-performance-expert" => "🦀".to_string(),
384 "rust-code-reviewer" => "🔍".to_string(),
385 "debugger" => "🐛".to_string(),
386 "technical-writer" => "📝".to_string(),
387 "test-writer-fixer" => "🧪".to_string(),
388 "rapid-prototyper" => "⚡".to_string(),
389 "devops-automator" => "🚀".to_string(),
390 "overseer" => "👁️".to_string(),
391 "ai-engineer" => "🤖".to_string(),
392 "general-purpose" => "🎯".to_string(),
393 _ => "🔧".to_string(),
394 }
395 }
396
397 fn format_timestamp(&self, timestamp: jiff::Timestamp) -> String {
398 timestamp.strftime("%H:%M:%S").to_string()
399 }
400
401 fn format_duration(&self, duration_ms: u64) -> String {
402 if duration_ms < 1000 {
403 format!("{}ms", duration_ms)
404 } else if duration_ms < 60_000 {
405 format!("{:.1}s", duration_ms as f64 / 1000.0)
406 } else if duration_ms < 3_600_000 {
407 format!("{:.1}m", duration_ms as f64 / 60_000.0)
408 } else {
409 format!("{:.1}h", duration_ms as f64 / 3_600_000.0)
410 }
411 }
412
413 fn truncate_path(&self, path: &str, max_len: usize) -> String {
414 if path.len() <= max_len {
415 path.to_string()
416 } else {
417 let start_len = max_len / 3;
418 let end_len = max_len - start_len - 3;
419 format!("{}...{}", &path[..start_len], &path[path.len() - end_len..])
420 }
421 }
422
423 fn truncate_text(&self, text: &str, max_len: usize) -> String {
424 if text.len() <= max_len {
425 text.to_string()
426 } else {
427 format!("{}...", &text[..max_len - 3])
428 }
429 }
430
431 #[allow(dead_code)] pub fn print_tool_analysis(
434 &self,
435 stats: &std::collections::HashMap<String, crate::models::ToolStatistics>,
436 ) {
437 if stats.is_empty() {
438 println!("{}", "No tool usage found".yellow());
439 return;
440 }
441
442 println!("{}", "Tool Usage Analysis".bold().cyan());
443 println!();
444
445 let mut tool_stats: Vec<_> = stats.iter().collect();
447 tool_stats.sort_by(|a, b| b.1.total_invocations.cmp(&a.1.total_invocations));
448
449 let mut rows = Vec::new();
451 for (tool_name, stat) in tool_stats {
452 let agents_str = if stat.agents_using.is_empty() {
453 "-".to_string()
454 } else {
455 stat.agents_using.join(", ")
456 };
457
458 let sessions_str = format!("{} sessions", stat.sessions.len());
459 let category_str = format!("{:?}", stat.category);
460
461 rows.push(ToolRow {
462 tool: tool_name.clone(),
463 count: stat.total_invocations.to_string(),
464 category: category_str,
465 agents: self.truncate_text(&agents_str, 40),
466 sessions: sessions_str,
467 });
468 }
469
470 let table = Table::new(rows)
471 .with(Style::modern())
472 .with(Modify::new(Columns::single(0)).with(Width::wrap(20)))
473 .with(Modify::new(Columns::single(3)).with(Width::wrap(40)))
474 .to_string();
475
476 println!("{table}");
477 println!();
478 println!(
479 "{} {} unique tools found",
480 "Total:".bold(),
481 stats.len().to_string().yellow()
482 );
483 }
484
485 pub fn print_tool_analysis_detailed(
487 &self,
488 analysis: &ToolAnalysis,
489 show_correlation: bool,
490 ) -> Result<()> {
491 if analysis.tool_statistics.is_empty() {
492 println!("{}", "No tool usage found".yellow());
493 return Ok(());
494 }
495
496 println!("{}", "═══ Tool Analysis ═══".bold().cyan());
498 println!();
499
500 println!("{}", "📊 Summary:".bold());
502 println!(
503 " {} {}",
504 "Total Tool Invocations:".bold(),
505 analysis.total_tool_invocations.to_string().yellow()
506 );
507 println!(
508 " {} {}",
509 "Unique Tools:".bold(),
510 analysis.tool_statistics.len().to_string().yellow()
511 );
512 println!(
513 " {} {}",
514 "Tool Categories:".bold(),
515 analysis.category_breakdown.len().to_string().yellow()
516 );
517 println!();
518
519 println!("{}", "🔧 Tool Frequency:".bold());
521 let mut tool_rows = Vec::new();
522 for (tool_name, stat) in &analysis.tool_statistics {
523 let agents_str = if stat.agents_using.is_empty() {
524 "-".to_string()
525 } else {
526 stat.agents_using.join(", ")
527 };
528
529 let success_rate = if stat.total_invocations > 0 {
530 #[allow(clippy::cast_precision_loss)]
531 let rate = (stat.success_count as f32 / stat.total_invocations as f32) * 100.0;
532 format!("{:.1}%", rate)
533 } else {
534 "-".to_string()
535 };
536
537 tool_rows.push(DetailedToolRow {
538 tool: tool_name.clone(),
539 count: stat.total_invocations.to_string(),
540 category: format!("{:?}", stat.category),
541 agents: self.truncate_text(&agents_str, 30),
542 success_rate,
543 sessions: stat.sessions.len().to_string(),
544 });
545 }
546
547 tool_rows.sort_by(|a, b| {
549 b.count
550 .parse::<u32>()
551 .unwrap_or(0)
552 .cmp(&a.count.parse::<u32>().unwrap_or(0))
553 });
554
555 let table = Table::new(tool_rows)
556 .with(Style::modern())
557 .with(Modify::new(Columns::single(0)).with(Width::wrap(20)))
558 .with(Modify::new(Columns::single(3)).with(Width::wrap(30)))
559 .to_string();
560 println!("{}", table);
561 println!();
562
563 println!("{}", "📂 Category Breakdown:".bold());
565 let mut category_rows: Vec<_> = analysis
566 .category_breakdown
567 .iter()
568 .map(|(cat, count)| (format!("{:?}", cat), *count))
569 .collect();
570 category_rows.sort_by(|a, b| b.1.cmp(&a.1));
571
572 for (category, count) in category_rows {
573 #[allow(clippy::cast_precision_loss)]
574 let percentage = (count as f32 / analysis.total_tool_invocations as f32) * 100.0;
575 println!(
576 " {} {} ({:.1}%)",
577 category.cyan(),
578 count.to_string().yellow(),
579 percentage
580 );
581 }
582 println!();
583
584 if show_correlation && !analysis.agent_tool_correlations.is_empty() {
586 self.print_correlation_matrix(&analysis.agent_tool_correlations);
587 }
588
589 Ok(())
590 }
591
592 pub fn print_correlation_matrix(&self, correlations: &[AgentToolCorrelation]) {
594 println!("{}", "🔗 Agent-Tool Correlation Matrix:".bold());
595 println!();
596
597 let mut agents: Vec<String> = correlations
599 .iter()
600 .map(|c| c.agent_type.clone())
601 .collect::<HashSet<_>>()
602 .into_iter()
603 .collect();
604 agents.sort();
605
606 let mut tools: Vec<String> = correlations
607 .iter()
608 .map(|c| c.tool_name.clone())
609 .collect::<HashSet<_>>()
610 .into_iter()
611 .collect();
612 tools.sort();
613
614 let mut correlation_map: HashMap<(String, String), &AgentToolCorrelation> = HashMap::new();
616 for corr in correlations {
617 correlation_map.insert((corr.agent_type.clone(), corr.tool_name.clone()), corr);
618 }
619
620 print!("{:15}", "");
622 for tool in &tools {
623 print!("{:12}", self.truncate_text(tool, 10));
624 }
625 println!();
626
627 print!("{:15}", "");
629 for _ in &tools {
630 print!("{:12}", "─".repeat(10));
631 }
632 println!();
633
634 for agent in &agents {
636 print!("{:15}", self.truncate_text(agent, 13));
637
638 for tool in &tools {
639 let block = if let Some(corr) = correlation_map.get(&(agent.clone(), tool.clone()))
640 {
641 self.get_correlation_block(corr.average_invocations_per_session)
642 } else {
643 "-".to_string()
644 };
645
646 if self.show_colors {
647 print!("{:12}", block.cyan());
648 } else {
649 print!("{:12}", block);
650 }
651 }
652 println!();
653 }
654 println!();
655
656 println!("{}", "Legend:".dimmed());
658 println!("{}", " █████ = High usage (8+ per session)".dimmed());
659 println!("{}", " ████ = Medium-high (6-8 per session)".dimmed());
660 println!("{}", " ███ = Medium (4-6 per session)".dimmed());
661 println!("{}", " ██ = Low-medium (2-4 per session)".dimmed());
662 println!("{}", " █ = Low (1-2 per session)".dimmed());
663 println!("{}", " - = None".dimmed());
664 println!();
665 }
666
667 fn get_correlation_block(&self, avg_invocations: f32) -> String {
669 if avg_invocations >= 8.0 {
670 "█████".to_string()
671 } else if avg_invocations >= 6.0 {
672 "████".to_string()
673 } else if avg_invocations >= 4.0 {
674 "███".to_string()
675 } else if avg_invocations >= 2.0 {
676 "██".to_string()
677 } else if avg_invocations >= 1.0 {
678 "█".to_string()
679 } else if avg_invocations > 0.0 {
680 "▒".to_string()
681 } else {
682 "-".to_string()
683 }
684 }
685
686 pub fn tool_analysis_to_json(&self, analysis: &ToolAnalysis) -> Result<String> {
688 let json = serde_json::to_string_pretty(analysis)?;
689 Ok(json)
690 }
691
692 pub fn tool_analysis_to_csv(&self, analysis: &ToolAnalysis) -> Result<String> {
694 let mut csv_data = Vec::new();
695
696 csv_data.push(vec![
698 "tool_name".to_string(),
699 "category".to_string(),
700 "count".to_string(),
701 "agents_using".to_string(),
702 "success_rate".to_string(),
703 "sessions".to_string(),
704 ]);
705
706 for (tool_name, stat) in &analysis.tool_statistics {
708 let agents_str = stat.agents_using.join(";");
709
710 let success_rate = if stat.total_invocations > 0 {
711 #[allow(clippy::cast_precision_loss)]
712 let rate = (stat.success_count as f32 / stat.total_invocations as f32) * 100.0;
713 format!("{:.2}", rate)
714 } else {
715 "0".to_string()
716 };
717
718 csv_data.push(vec![
719 tool_name.clone(),
720 format!("{:?}", stat.category),
721 stat.total_invocations.to_string(),
722 agents_str,
723 success_rate,
724 stat.sessions.len().to_string(),
725 ]);
726 }
727
728 let mut csv_output = String::new();
729 for row in csv_data {
730 writeln!(csv_output, "{}", row.join(","))?;
731 }
732
733 Ok(csv_output)
734 }
735
736 pub fn tool_analysis_to_markdown(&self, analysis: &ToolAnalysis) -> Result<String> {
738 let mut md = String::new();
739
740 writeln!(md, "# Tool Usage Analysis Report\n")?;
741
742 writeln!(md, "## Summary\n")?;
744 writeln!(
745 md,
746 "- **Total Tool Invocations**: {}",
747 analysis.total_tool_invocations
748 )?;
749 writeln!(md, "- **Unique Tools**: {}", analysis.tool_statistics.len())?;
750 writeln!(
751 md,
752 "- **Tool Categories**: {}\n",
753 analysis.category_breakdown.len()
754 )?;
755
756 writeln!(md, "## Category Breakdown\n")?;
758 let mut category_rows: Vec<_> = analysis
759 .category_breakdown
760 .iter()
761 .map(|(cat, count)| (format!("{:?}", cat), *count))
762 .collect();
763 category_rows.sort_by(|a, b| b.1.cmp(&a.1));
764
765 for (category, count) in category_rows {
766 #[allow(clippy::cast_precision_loss)]
767 let percentage = (count as f32 / analysis.total_tool_invocations as f32) * 100.0;
768 writeln!(md, "- **{}**: {} ({:.1}%)", category, count, percentage)?;
769 }
770 writeln!(md)?;
771
772 writeln!(md, "## Tool Frequency\n")?;
774 writeln!(
775 md,
776 "| Tool | Category | Count | Agents | Success Rate | Sessions |"
777 )?;
778 writeln!(
779 md,
780 "|------|----------|-------|--------|--------------|----------|"
781 )?;
782
783 let mut tool_list: Vec<_> = analysis.tool_statistics.iter().collect();
784 tool_list.sort_by(|a, b| b.1.total_invocations.cmp(&a.1.total_invocations));
785
786 for (tool_name, stat) in tool_list {
787 let agents_str = if stat.agents_using.is_empty() {
788 "-".to_string()
789 } else {
790 stat.agents_using.join(", ")
791 };
792
793 let success_rate = if stat.total_invocations > 0 {
794 #[allow(clippy::cast_precision_loss)]
795 let rate = (stat.success_count as f32 / stat.total_invocations as f32) * 100.0;
796 format!("{:.1}%", rate)
797 } else {
798 "-".to_string()
799 };
800
801 writeln!(
802 md,
803 "| {} | {:?} | {} | {} | {} | {} |",
804 tool_name,
805 stat.category,
806 stat.total_invocations,
807 agents_str,
808 success_rate,
809 stat.sessions.len()
810 )?;
811 }
812 writeln!(md)?;
813
814 if !analysis.agent_tool_correlations.is_empty() {
816 writeln!(md, "## Agent-Tool Correlations\n")?;
817 writeln!(
818 md,
819 "| Agent | Tool | Usage Count | Success Rate | Avg/Session |"
820 )?;
821 writeln!(
822 md,
823 "|-------|------|-------------|--------------|-------------|"
824 )?;
825
826 for corr in &analysis.agent_tool_correlations {
827 writeln!(
828 md,
829 "| {} | {} | {} | {:.1}% | {:.2} |",
830 corr.agent_type,
831 corr.tool_name,
832 corr.usage_count,
833 corr.success_rate * 100.0,
834 corr.average_invocations_per_session
835 )?;
836 }
837 writeln!(md)?;
838 }
839
840 if !analysis.tool_chains.is_empty() {
842 writeln!(md, "## Common Tool Chains\n")?;
843 writeln!(md, "| Tools | Frequency | Success Rate | Typical Agent |")?;
844 writeln!(md, "|-------|-----------|--------------|---------------|")?;
845
846 for chain in &analysis.tool_chains {
847 let agent_str = chain.typical_agent.as_ref().map_or("-", |a| a.as_str());
848 writeln!(
849 md,
850 "| {} | {} | {:.1}% | {} |",
851 chain.tools.join(" → "),
852 chain.frequency,
853 chain.success_rate * 100.0,
854 agent_str
855 )?;
856 }
857 writeln!(md)?;
858 }
859
860 Ok(md)
861 }
862}
863
864impl Default for Reporter {
865 fn default() -> Self {
866 Self::new()
867 }
868}
869
870#[derive(Tabled)]
871struct FileRow {
872 #[tabled(rename = "File")]
873 file: String,
874 #[tabled(rename = "Agent")]
875 agent: String,
876 #[tabled(rename = "Contribution")]
877 contribution: String,
878 #[tabled(rename = "Confidence")]
879 confidence: String,
880 #[tabled(rename = "Ops")]
881 operations: String,
882}
883
884#[derive(Tabled)]
885#[allow(dead_code)] struct ToolRow {
887 #[tabled(rename = "Tool")]
888 tool: String,
889 #[tabled(rename = "Count")]
890 count: String,
891 #[tabled(rename = "Category")]
892 category: String,
893 #[tabled(rename = "Agents")]
894 agents: String,
895 #[tabled(rename = "Sessions")]
896 sessions: String,
897}
898
899#[derive(Tabled)]
900struct DetailedToolRow {
901 #[tabled(rename = "Tool")]
902 tool: String,
903 #[tabled(rename = "Count")]
904 count: String,
905 #[tabled(rename = "Category")]
906 category: String,
907 #[tabled(rename = "Agents")]
908 agents: String,
909 #[tabled(rename = "Success Rate")]
910 success_rate: String,
911 #[tabled(rename = "Sessions")]
912 sessions: String,
913}
914
915#[derive(Tabled)]
916struct AgentRow {
917 #[tabled(rename = "Agent")]
918 agent: String,
919 #[tabled(rename = "Invocations")]
920 invocations: String,
921 #[tabled(rename = "Duration")]
922 duration: String,
923 #[tabled(rename = "Files")]
924 files: String,
925 #[tabled(rename = "Tools")]
926 tools: String,
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932 use crate::models::{AgentInvocation, ToolCategory, ToolStatistics};
933
934 fn create_test_analysis() -> SessionAnalysis {
935 let timestamp = jiff::Timestamp::now();
936
937 SessionAnalysis {
938 session_id: "test-session".to_string(),
939 project_path: "/test/project".to_string(),
940 start_time: timestamp,
941 end_time: timestamp,
942 duration_ms: 5000,
943 agents: vec![AgentInvocation {
944 timestamp,
945 agent_type: "architect".to_string(),
946 task_description: "Design system".to_string(),
947 prompt: "Design the architecture".to_string(),
948 files_modified: vec![],
949 tools_used: vec![],
950 duration_ms: Some(2000),
951 parent_message_id: "msg-1".to_string(),
952 session_id: "test-session".to_string(),
953 }],
954 file_operations: vec![],
955 file_to_agents: IndexMap::new(),
956 agent_stats: IndexMap::new(),
957 collaboration_patterns: vec![],
958 }
959 }
960
961 fn create_test_tool_analysis() -> ToolAnalysis {
962 let timestamp = jiff::Timestamp::now();
963 let mut tool_statistics = IndexMap::new();
964
965 tool_statistics.insert(
966 "npm".to_string(),
967 ToolStatistics {
968 tool_name: "npm".to_string(),
969 category: ToolCategory::PackageManager,
970 total_invocations: 10,
971 agents_using: vec!["developer".to_string()],
972 success_count: 9,
973 failure_count: 1,
974 first_seen: timestamp,
975 last_seen: timestamp,
976 command_patterns: vec!["npm install".to_string()],
977 sessions: vec!["session-1".to_string()],
978 },
979 );
980
981 tool_statistics.insert(
982 "cargo".to_string(),
983 ToolStatistics {
984 tool_name: "cargo".to_string(),
985 category: ToolCategory::BuildTool,
986 total_invocations: 5,
987 agents_using: vec!["developer".to_string()],
988 success_count: 5,
989 failure_count: 0,
990 first_seen: timestamp,
991 last_seen: timestamp,
992 command_patterns: vec!["cargo build".to_string()],
993 sessions: vec!["session-1".to_string()],
994 },
995 );
996
997 let mut category_breakdown = IndexMap::new();
998 category_breakdown.insert(ToolCategory::PackageManager, 10);
999 category_breakdown.insert(ToolCategory::BuildTool, 5);
1000
1001 ToolAnalysis {
1002 session_id: "test-session".to_string(),
1003 total_tool_invocations: 15,
1004 tool_statistics,
1005 agent_tool_correlations: vec![
1006 AgentToolCorrelation {
1007 agent_type: "developer".to_string(),
1008 tool_name: "npm".to_string(),
1009 usage_count: 10,
1010 success_rate: 0.9,
1011 average_invocations_per_session: 5.0,
1012 },
1013 AgentToolCorrelation {
1014 agent_type: "developer".to_string(),
1015 tool_name: "cargo".to_string(),
1016 usage_count: 5,
1017 success_rate: 1.0,
1018 average_invocations_per_session: 2.5,
1019 },
1020 ],
1021 tool_chains: vec![],
1022 category_breakdown,
1023 }
1024 }
1025
1026 #[test]
1027 fn test_format_agent_icon() {
1028 let reporter = Reporter::new();
1029 assert_eq!(reporter.format_agent_icon("architect"), "🏗️");
1030 assert_eq!(reporter.format_agent_icon("developer"), "💻");
1031 assert_eq!(reporter.format_agent_icon("unknown"), "🔧");
1032 }
1033
1034 #[test]
1035 fn test_format_duration() {
1036 let reporter = Reporter::new();
1037 assert_eq!(reporter.format_duration(500), "500ms");
1038 assert_eq!(reporter.format_duration(1500), "1.5s");
1039 assert_eq!(reporter.format_duration(65000), "1.1m");
1040 }
1041
1042 #[test]
1043 fn test_truncate_path() {
1044 let reporter = Reporter::new();
1045 let long_path = "/very/long/path/to/some/file/deep/in/directory/structure/file.rs";
1046 let truncated = reporter.truncate_path(long_path, 20);
1047 assert!(truncated.len() <= 20);
1048 assert!(truncated.contains("..."));
1049 }
1050
1051 #[test]
1052 fn test_to_markdown() {
1053 let reporter = Reporter::new();
1054 let analysis = create_test_analysis();
1055 let result = reporter.to_markdown(&[analysis]);
1056 assert!(result.is_ok());
1057
1058 let markdown = result.unwrap();
1059 assert!(markdown.contains("# Claude Session Analysis Report"));
1060 assert!(markdown.contains("test-session"));
1061 }
1062
1063 #[test]
1064 fn test_to_json() {
1065 let reporter = Reporter::new();
1066 let analysis = create_test_analysis();
1067 let result = reporter.to_json(&[analysis]);
1068 assert!(result.is_ok());
1069
1070 let json = result.unwrap();
1071 assert!(json.contains("test-session"));
1072 assert!(json.contains("architect"));
1073 }
1074
1075 #[test]
1076 fn test_get_correlation_block() {
1077 let reporter = Reporter::new();
1078 assert_eq!(reporter.get_correlation_block(10.0), "█████");
1079 assert_eq!(reporter.get_correlation_block(7.0), "████");
1080 assert_eq!(reporter.get_correlation_block(5.0), "███");
1081 assert_eq!(reporter.get_correlation_block(3.0), "██");
1082 assert_eq!(reporter.get_correlation_block(1.5), "█");
1083 assert_eq!(reporter.get_correlation_block(0.5), "▒");
1084 assert_eq!(reporter.get_correlation_block(0.0), "-");
1085 }
1086
1087 #[test]
1088 fn test_tool_analysis_to_json() {
1089 let reporter = Reporter::new();
1090 let analysis = create_test_tool_analysis();
1091 let result = reporter.tool_analysis_to_json(&analysis);
1092 assert!(result.is_ok());
1093
1094 let json = result.unwrap();
1095 assert!(json.contains("npm"));
1096 assert!(json.contains("cargo"));
1097 assert!(json.contains("PackageManager"));
1098 assert!(json.contains("developer"));
1099 }
1100
1101 #[test]
1102 fn test_tool_analysis_to_csv() {
1103 let reporter = Reporter::new();
1104 let analysis = create_test_tool_analysis();
1105 let result = reporter.tool_analysis_to_csv(&analysis);
1106 assert!(result.is_ok());
1107
1108 let csv = result.unwrap();
1109 assert!(csv.contains("tool_name,category,count,agents_using,success_rate,sessions"));
1110 assert!(csv.contains("npm"));
1111 assert!(csv.contains("cargo"));
1112 assert!(csv.contains("PackageManager"));
1113 }
1114
1115 #[test]
1116 fn test_tool_analysis_to_markdown() {
1117 let reporter = Reporter::new();
1118 let analysis = create_test_tool_analysis();
1119 let result = reporter.tool_analysis_to_markdown(&analysis);
1120 assert!(result.is_ok());
1121
1122 let markdown = result.unwrap();
1123 assert!(markdown.contains("# Tool Usage Analysis Report"));
1124 assert!(markdown.contains("## Summary"));
1125 assert!(markdown.contains("## Category Breakdown"));
1126 assert!(markdown.contains("## Tool Frequency"));
1127 assert!(markdown.contains("npm"));
1128 assert!(markdown.contains("cargo"));
1129 }
1130
1131 #[test]
1132 fn test_print_tool_analysis_detailed() {
1133 let reporter = Reporter::new();
1134 let analysis = create_test_tool_analysis();
1135 let result = reporter.print_tool_analysis_detailed(&analysis, false);
1136 assert!(result.is_ok());
1137 }
1138
1139 #[test]
1140 fn test_print_tool_analysis_detailed_with_correlation() {
1141 let reporter = Reporter::new();
1142 let analysis = create_test_tool_analysis();
1143 let result = reporter.print_tool_analysis_detailed(&analysis, true);
1144 assert!(result.is_ok());
1145 }
1146
1147 #[test]
1148 fn test_print_correlation_matrix() {
1149 let reporter = Reporter::new();
1150 let correlations = vec![
1151 AgentToolCorrelation {
1152 agent_type: "developer".to_string(),
1153 tool_name: "npm".to_string(),
1154 usage_count: 10,
1155 success_rate: 0.9,
1156 average_invocations_per_session: 5.0,
1157 },
1158 AgentToolCorrelation {
1159 agent_type: "architect".to_string(),
1160 tool_name: "git".to_string(),
1161 usage_count: 3,
1162 success_rate: 1.0,
1163 average_invocations_per_session: 1.5,
1164 },
1165 ];
1166
1167 reporter.print_correlation_matrix(&correlations);
1168 }
1169
1170 #[test]
1171 fn test_print_tool_analysis_detailed_empty() {
1172 let reporter = Reporter::new();
1173 let analysis = ToolAnalysis {
1174 session_id: "test".to_string(),
1175 total_tool_invocations: 0,
1176 tool_statistics: IndexMap::new(),
1177 agent_tool_correlations: vec![],
1178 tool_chains: vec![],
1179 category_breakdown: IndexMap::new(),
1180 };
1181
1182 let result = reporter.print_tool_analysis_detailed(&analysis, false);
1183 assert!(result.is_ok());
1184 }
1185
1186 #[test]
1187 fn test_tool_analysis_csv_with_semicolons() {
1188 let reporter = Reporter::new();
1189 let timestamp = jiff::Timestamp::now();
1190 let mut tool_statistics = IndexMap::new();
1191
1192 tool_statistics.insert(
1193 "npm".to_string(),
1194 ToolStatistics {
1195 tool_name: "npm".to_string(),
1196 category: ToolCategory::PackageManager,
1197 total_invocations: 10,
1198 agents_using: vec!["developer".to_string(), "architect".to_string()],
1199 success_count: 9,
1200 failure_count: 1,
1201 first_seen: timestamp,
1202 last_seen: timestamp,
1203 command_patterns: vec![],
1204 sessions: vec!["session-1".to_string()],
1205 },
1206 );
1207
1208 let analysis = ToolAnalysis {
1209 session_id: "test".to_string(),
1210 total_tool_invocations: 10,
1211 tool_statistics,
1212 agent_tool_correlations: vec![],
1213 tool_chains: vec![],
1214 category_breakdown: IndexMap::new(),
1215 };
1216
1217 let result = reporter.tool_analysis_to_csv(&analysis);
1218 assert!(result.is_ok());
1219
1220 let csv = result.unwrap();
1221 assert!(csv.contains("developer;architect"));
1222 }
1223}