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 Table, Tabled,
12 settings::{Modify, Style, Width, object::Columns},
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::new(..1)).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_key(|(ts, _, _)| *ts);
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_key(|(_, count)| std::cmp::Reverse(*count));
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_key(|(_, stat)| std::cmp::Reverse(stat.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::new(0..1)).with(Width::wrap(20)))
473 .with(Modify::new(Columns::new(3..4)).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 #[allow(clippy::unnecessary_sort_by)]
549 tool_rows.sort_by(|a, b| {
550 b.count
551 .parse::<u32>()
552 .unwrap_or(0)
553 .cmp(&a.count.parse::<u32>().unwrap_or(0))
554 });
555
556 let table = Table::new(tool_rows)
557 .with(Style::modern())
558 .with(Modify::new(Columns::new(0..1)).with(Width::wrap(20)))
559 .with(Modify::new(Columns::new(3..4)).with(Width::wrap(30)))
560 .to_string();
561 println!("{}", table);
562 println!();
563
564 println!("{}", "📂 Category Breakdown:".bold());
566 let mut category_rows: Vec<_> = analysis
567 .category_breakdown
568 .iter()
569 .map(|(cat, count)| (format!("{:?}", cat), *count))
570 .collect();
571 category_rows.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
572
573 for (category, count) in category_rows {
574 #[allow(clippy::cast_precision_loss)]
575 let percentage = (count as f32 / analysis.total_tool_invocations as f32) * 100.0;
576 println!(
577 " {} {} ({:.1}%)",
578 category.cyan(),
579 count.to_string().yellow(),
580 percentage
581 );
582 }
583 println!();
584
585 if show_correlation && !analysis.agent_tool_correlations.is_empty() {
587 self.print_correlation_matrix(&analysis.agent_tool_correlations);
588 }
589
590 Ok(())
591 }
592
593 pub fn print_correlation_matrix(&self, correlations: &[AgentToolCorrelation]) {
595 println!("{}", "🔗 Agent-Tool Correlation Matrix:".bold());
596 println!();
597
598 let mut agents: Vec<String> = correlations
600 .iter()
601 .map(|c| c.agent_type.clone())
602 .collect::<HashSet<_>>()
603 .into_iter()
604 .collect();
605 agents.sort();
606
607 let mut tools: Vec<String> = correlations
608 .iter()
609 .map(|c| c.tool_name.clone())
610 .collect::<HashSet<_>>()
611 .into_iter()
612 .collect();
613 tools.sort();
614
615 let mut correlation_map: HashMap<(String, String), &AgentToolCorrelation> = HashMap::new();
617 for corr in correlations {
618 correlation_map.insert((corr.agent_type.clone(), corr.tool_name.clone()), corr);
619 }
620
621 print!("{:15}", "");
623 for tool in &tools {
624 print!("{:12}", self.truncate_text(tool, 10));
625 }
626 println!();
627
628 print!("{:15}", "");
630 for _ in &tools {
631 print!("{:12}", "─".repeat(10));
632 }
633 println!();
634
635 for agent in &agents {
637 print!("{:15}", self.truncate_text(agent, 13));
638
639 for tool in &tools {
640 let block = if let Some(corr) = correlation_map.get(&(agent.clone(), tool.clone()))
641 {
642 self.get_correlation_block(corr.average_invocations_per_session)
643 } else {
644 "-".to_string()
645 };
646
647 if self.show_colors {
648 print!("{:12}", block.cyan());
649 } else {
650 print!("{:12}", block);
651 }
652 }
653 println!();
654 }
655 println!();
656
657 println!("{}", "Legend:".dimmed());
659 println!("{}", " █████ = High usage (8+ per session)".dimmed());
660 println!("{}", " ████ = Medium-high (6-8 per session)".dimmed());
661 println!("{}", " ███ = Medium (4-6 per session)".dimmed());
662 println!("{}", " ██ = Low-medium (2-4 per session)".dimmed());
663 println!("{}", " █ = Low (1-2 per session)".dimmed());
664 println!("{}", " - = None".dimmed());
665 println!();
666 }
667
668 fn get_correlation_block(&self, avg_invocations: f32) -> String {
670 if avg_invocations >= 8.0 {
671 "█████".to_string()
672 } else if avg_invocations >= 6.0 {
673 "████".to_string()
674 } else if avg_invocations >= 4.0 {
675 "███".to_string()
676 } else if avg_invocations >= 2.0 {
677 "██".to_string()
678 } else if avg_invocations >= 1.0 {
679 "█".to_string()
680 } else if avg_invocations > 0.0 {
681 "▒".to_string()
682 } else {
683 "-".to_string()
684 }
685 }
686
687 pub fn tool_analysis_to_json(&self, analysis: &ToolAnalysis) -> Result<String> {
689 let json = serde_json::to_string_pretty(analysis)?;
690 Ok(json)
691 }
692
693 pub fn tool_analysis_to_csv(&self, analysis: &ToolAnalysis) -> Result<String> {
695 let mut csv_data = Vec::new();
696
697 csv_data.push(vec![
699 "tool_name".to_string(),
700 "category".to_string(),
701 "count".to_string(),
702 "agents_using".to_string(),
703 "success_rate".to_string(),
704 "sessions".to_string(),
705 ]);
706
707 for (tool_name, stat) in &analysis.tool_statistics {
709 let agents_str = stat.agents_using.join(";");
710
711 let success_rate = if stat.total_invocations > 0 {
712 #[allow(clippy::cast_precision_loss)]
713 let rate = (stat.success_count as f32 / stat.total_invocations as f32) * 100.0;
714 format!("{:.2}", rate)
715 } else {
716 "0".to_string()
717 };
718
719 csv_data.push(vec![
720 tool_name.clone(),
721 format!("{:?}", stat.category),
722 stat.total_invocations.to_string(),
723 agents_str,
724 success_rate,
725 stat.sessions.len().to_string(),
726 ]);
727 }
728
729 let mut csv_output = String::new();
730 for row in csv_data {
731 writeln!(csv_output, "{}", row.join(","))?;
732 }
733
734 Ok(csv_output)
735 }
736
737 pub fn tool_analysis_to_markdown(&self, analysis: &ToolAnalysis) -> Result<String> {
739 let mut md = String::new();
740
741 writeln!(md, "# Tool Usage Analysis Report\n")?;
742
743 writeln!(md, "## Summary\n")?;
745 writeln!(
746 md,
747 "- **Total Tool Invocations**: {}",
748 analysis.total_tool_invocations
749 )?;
750 writeln!(md, "- **Unique Tools**: {}", analysis.tool_statistics.len())?;
751 writeln!(
752 md,
753 "- **Tool Categories**: {}\n",
754 analysis.category_breakdown.len()
755 )?;
756
757 writeln!(md, "## Category Breakdown\n")?;
759 let mut category_rows: Vec<_> = analysis
760 .category_breakdown
761 .iter()
762 .map(|(cat, count)| (format!("{:?}", cat), *count))
763 .collect();
764 category_rows.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
765
766 for (category, count) in category_rows {
767 #[allow(clippy::cast_precision_loss)]
768 let percentage = (count as f32 / analysis.total_tool_invocations as f32) * 100.0;
769 writeln!(md, "- **{}**: {} ({:.1}%)", category, count, percentage)?;
770 }
771 writeln!(md)?;
772
773 writeln!(md, "## Tool Frequency\n")?;
775 writeln!(
776 md,
777 "| Tool | Category | Count | Agents | Success Rate | Sessions |"
778 )?;
779 writeln!(
780 md,
781 "|------|----------|-------|--------|--------------|----------|"
782 )?;
783
784 let mut tool_list: Vec<_> = analysis.tool_statistics.iter().collect();
785 tool_list.sort_by_key(|(_, stat)| std::cmp::Reverse(stat.total_invocations));
786
787 for (tool_name, stat) in tool_list {
788 let agents_str = if stat.agents_using.is_empty() {
789 "-".to_string()
790 } else {
791 stat.agents_using.join(", ")
792 };
793
794 let success_rate = if stat.total_invocations > 0 {
795 #[allow(clippy::cast_precision_loss)]
796 let rate = (stat.success_count as f32 / stat.total_invocations as f32) * 100.0;
797 format!("{:.1}%", rate)
798 } else {
799 "-".to_string()
800 };
801
802 writeln!(
803 md,
804 "| {} | {:?} | {} | {} | {} | {} |",
805 tool_name,
806 stat.category,
807 stat.total_invocations,
808 agents_str,
809 success_rate,
810 stat.sessions.len()
811 )?;
812 }
813 writeln!(md)?;
814
815 if !analysis.agent_tool_correlations.is_empty() {
817 writeln!(md, "## Agent-Tool Correlations\n")?;
818 writeln!(
819 md,
820 "| Agent | Tool | Usage Count | Success Rate | Avg/Session |"
821 )?;
822 writeln!(
823 md,
824 "|-------|------|-------------|--------------|-------------|"
825 )?;
826
827 for corr in &analysis.agent_tool_correlations {
828 writeln!(
829 md,
830 "| {} | {} | {} | {:.1}% | {:.2} |",
831 corr.agent_type,
832 corr.tool_name,
833 corr.usage_count,
834 corr.success_rate * 100.0,
835 corr.average_invocations_per_session
836 )?;
837 }
838 writeln!(md)?;
839 }
840
841 if !analysis.tool_chains.is_empty() {
843 writeln!(md, "## Common Tool Chains\n")?;
844 writeln!(md, "| Tools | Frequency | Success Rate | Typical Agent |")?;
845 writeln!(md, "|-------|-----------|--------------|---------------|")?;
846
847 for chain in &analysis.tool_chains {
848 let agent_str = chain.typical_agent.as_ref().map_or("-", |a| a.as_str());
849 writeln!(
850 md,
851 "| {} | {} | {:.1}% | {} |",
852 chain.tools.join(" → "),
853 chain.frequency,
854 chain.success_rate * 100.0,
855 agent_str
856 )?;
857 }
858 writeln!(md)?;
859 }
860
861 Ok(md)
862 }
863}
864
865impl Default for Reporter {
866 fn default() -> Self {
867 Self::new()
868 }
869}
870
871#[derive(Tabled)]
872struct FileRow {
873 #[tabled(rename = "File")]
874 file: String,
875 #[tabled(rename = "Agent")]
876 agent: String,
877 #[tabled(rename = "Contribution")]
878 contribution: String,
879 #[tabled(rename = "Confidence")]
880 confidence: String,
881 #[tabled(rename = "Ops")]
882 operations: String,
883}
884
885#[derive(Tabled)]
886#[allow(dead_code)] struct ToolRow {
888 #[tabled(rename = "Tool")]
889 tool: String,
890 #[tabled(rename = "Count")]
891 count: String,
892 #[tabled(rename = "Category")]
893 category: String,
894 #[tabled(rename = "Agents")]
895 agents: String,
896 #[tabled(rename = "Sessions")]
897 sessions: String,
898}
899
900#[derive(Tabled)]
901struct DetailedToolRow {
902 #[tabled(rename = "Tool")]
903 tool: String,
904 #[tabled(rename = "Count")]
905 count: String,
906 #[tabled(rename = "Category")]
907 category: String,
908 #[tabled(rename = "Agents")]
909 agents: String,
910 #[tabled(rename = "Success Rate")]
911 success_rate: String,
912 #[tabled(rename = "Sessions")]
913 sessions: String,
914}
915
916#[derive(Tabled)]
917struct AgentRow {
918 #[tabled(rename = "Agent")]
919 agent: String,
920 #[tabled(rename = "Invocations")]
921 invocations: String,
922 #[tabled(rename = "Duration")]
923 duration: String,
924 #[tabled(rename = "Files")]
925 files: String,
926 #[tabled(rename = "Tools")]
927 tools: String,
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933 use crate::models::{AgentInvocation, ToolCategory, ToolStatistics};
934
935 fn create_test_analysis() -> SessionAnalysis {
936 let timestamp = jiff::Timestamp::now();
937
938 SessionAnalysis {
939 session_id: "test-session".to_string(),
940 project_path: "/test/project".to_string(),
941 start_time: timestamp,
942 end_time: timestamp,
943 duration_ms: 5000,
944 agents: vec![AgentInvocation {
945 timestamp,
946 agent_type: "architect".to_string(),
947 task_description: "Design system".to_string(),
948 prompt: "Design the architecture".to_string(),
949 files_modified: vec![],
950 tools_used: vec![],
951 duration_ms: Some(2000),
952 parent_message_id: "msg-1".to_string(),
953 session_id: "test-session".to_string(),
954 }],
955 file_operations: vec![],
956 file_to_agents: IndexMap::new(),
957 agent_stats: IndexMap::new(),
958 collaboration_patterns: vec![],
959 }
960 }
961
962 fn create_test_tool_analysis() -> ToolAnalysis {
963 let timestamp = jiff::Timestamp::now();
964 let mut tool_statistics = IndexMap::new();
965
966 tool_statistics.insert(
967 "npm".to_string(),
968 ToolStatistics {
969 tool_name: "npm".to_string(),
970 category: ToolCategory::PackageManager,
971 total_invocations: 10,
972 agents_using: vec!["developer".to_string()],
973 success_count: 9,
974 failure_count: 1,
975 first_seen: timestamp,
976 last_seen: timestamp,
977 command_patterns: vec!["npm install".to_string()],
978 sessions: vec!["session-1".to_string()],
979 },
980 );
981
982 tool_statistics.insert(
983 "cargo".to_string(),
984 ToolStatistics {
985 tool_name: "cargo".to_string(),
986 category: ToolCategory::BuildTool,
987 total_invocations: 5,
988 agents_using: vec!["developer".to_string()],
989 success_count: 5,
990 failure_count: 0,
991 first_seen: timestamp,
992 last_seen: timestamp,
993 command_patterns: vec!["cargo build".to_string()],
994 sessions: vec!["session-1".to_string()],
995 },
996 );
997
998 let mut category_breakdown = IndexMap::new();
999 category_breakdown.insert(ToolCategory::PackageManager, 10);
1000 category_breakdown.insert(ToolCategory::BuildTool, 5);
1001
1002 ToolAnalysis {
1003 session_id: "test-session".to_string(),
1004 total_tool_invocations: 15,
1005 tool_statistics,
1006 agent_tool_correlations: vec![
1007 AgentToolCorrelation {
1008 agent_type: "developer".to_string(),
1009 tool_name: "npm".to_string(),
1010 usage_count: 10,
1011 success_rate: 0.9,
1012 average_invocations_per_session: 5.0,
1013 },
1014 AgentToolCorrelation {
1015 agent_type: "developer".to_string(),
1016 tool_name: "cargo".to_string(),
1017 usage_count: 5,
1018 success_rate: 1.0,
1019 average_invocations_per_session: 2.5,
1020 },
1021 ],
1022 tool_chains: vec![],
1023 category_breakdown,
1024 }
1025 }
1026
1027 #[test]
1028 fn test_format_agent_icon() {
1029 let reporter = Reporter::new();
1030 assert_eq!(reporter.format_agent_icon("architect"), "🏗️");
1031 assert_eq!(reporter.format_agent_icon("developer"), "💻");
1032 assert_eq!(reporter.format_agent_icon("unknown"), "🔧");
1033 }
1034
1035 #[test]
1036 fn test_format_duration() {
1037 let reporter = Reporter::new();
1038 assert_eq!(reporter.format_duration(500), "500ms");
1039 assert_eq!(reporter.format_duration(1500), "1.5s");
1040 assert_eq!(reporter.format_duration(65000), "1.1m");
1041 }
1042
1043 #[test]
1044 fn test_truncate_path() {
1045 let reporter = Reporter::new();
1046 let long_path = "/very/long/path/to/some/file/deep/in/directory/structure/file.rs";
1047 let truncated = reporter.truncate_path(long_path, 20);
1048 assert!(truncated.len() <= 20);
1049 assert!(truncated.contains("..."));
1050 }
1051
1052 #[test]
1053 fn test_to_markdown() {
1054 let reporter = Reporter::new();
1055 let analysis = create_test_analysis();
1056 let result = reporter.to_markdown(&[analysis]);
1057 assert!(result.is_ok());
1058
1059 let markdown = result.unwrap();
1060 assert!(markdown.contains("# Claude Session Analysis Report"));
1061 assert!(markdown.contains("test-session"));
1062 }
1063
1064 #[test]
1065 fn test_to_json() {
1066 let reporter = Reporter::new();
1067 let analysis = create_test_analysis();
1068 let result = reporter.to_json(&[analysis]);
1069 assert!(result.is_ok());
1070
1071 let json = result.unwrap();
1072 assert!(json.contains("test-session"));
1073 assert!(json.contains("architect"));
1074 }
1075
1076 #[test]
1077 fn test_get_correlation_block() {
1078 let reporter = Reporter::new();
1079 assert_eq!(reporter.get_correlation_block(10.0), "█████");
1080 assert_eq!(reporter.get_correlation_block(7.0), "████");
1081 assert_eq!(reporter.get_correlation_block(5.0), "███");
1082 assert_eq!(reporter.get_correlation_block(3.0), "██");
1083 assert_eq!(reporter.get_correlation_block(1.5), "█");
1084 assert_eq!(reporter.get_correlation_block(0.5), "▒");
1085 assert_eq!(reporter.get_correlation_block(0.0), "-");
1086 }
1087
1088 #[test]
1089 fn test_tool_analysis_to_json() {
1090 let reporter = Reporter::new();
1091 let analysis = create_test_tool_analysis();
1092 let result = reporter.tool_analysis_to_json(&analysis);
1093 assert!(result.is_ok());
1094
1095 let json = result.unwrap();
1096 assert!(json.contains("npm"));
1097 assert!(json.contains("cargo"));
1098 assert!(json.contains("PackageManager"));
1099 assert!(json.contains("developer"));
1100 }
1101
1102 #[test]
1103 fn test_tool_analysis_to_csv() {
1104 let reporter = Reporter::new();
1105 let analysis = create_test_tool_analysis();
1106 let result = reporter.tool_analysis_to_csv(&analysis);
1107 assert!(result.is_ok());
1108
1109 let csv = result.unwrap();
1110 assert!(csv.contains("tool_name,category,count,agents_using,success_rate,sessions"));
1111 assert!(csv.contains("npm"));
1112 assert!(csv.contains("cargo"));
1113 assert!(csv.contains("PackageManager"));
1114 }
1115
1116 #[test]
1117 fn test_tool_analysis_to_markdown() {
1118 let reporter = Reporter::new();
1119 let analysis = create_test_tool_analysis();
1120 let result = reporter.tool_analysis_to_markdown(&analysis);
1121 assert!(result.is_ok());
1122
1123 let markdown = result.unwrap();
1124 assert!(markdown.contains("# Tool Usage Analysis Report"));
1125 assert!(markdown.contains("## Summary"));
1126 assert!(markdown.contains("## Category Breakdown"));
1127 assert!(markdown.contains("## Tool Frequency"));
1128 assert!(markdown.contains("npm"));
1129 assert!(markdown.contains("cargo"));
1130 }
1131
1132 #[test]
1133 fn test_print_tool_analysis_detailed() {
1134 let reporter = Reporter::new();
1135 let analysis = create_test_tool_analysis();
1136 let result = reporter.print_tool_analysis_detailed(&analysis, false);
1137 assert!(result.is_ok());
1138 }
1139
1140 #[test]
1141 fn test_print_tool_analysis_detailed_with_correlation() {
1142 let reporter = Reporter::new();
1143 let analysis = create_test_tool_analysis();
1144 let result = reporter.print_tool_analysis_detailed(&analysis, true);
1145 assert!(result.is_ok());
1146 }
1147
1148 #[test]
1149 fn test_print_correlation_matrix() {
1150 let reporter = Reporter::new();
1151 let correlations = vec![
1152 AgentToolCorrelation {
1153 agent_type: "developer".to_string(),
1154 tool_name: "npm".to_string(),
1155 usage_count: 10,
1156 success_rate: 0.9,
1157 average_invocations_per_session: 5.0,
1158 },
1159 AgentToolCorrelation {
1160 agent_type: "architect".to_string(),
1161 tool_name: "git".to_string(),
1162 usage_count: 3,
1163 success_rate: 1.0,
1164 average_invocations_per_session: 1.5,
1165 },
1166 ];
1167
1168 reporter.print_correlation_matrix(&correlations);
1169 }
1170
1171 #[test]
1172 fn test_print_tool_analysis_detailed_empty() {
1173 let reporter = Reporter::new();
1174 let analysis = ToolAnalysis {
1175 session_id: "test".to_string(),
1176 total_tool_invocations: 0,
1177 tool_statistics: IndexMap::new(),
1178 agent_tool_correlations: vec![],
1179 tool_chains: vec![],
1180 category_breakdown: IndexMap::new(),
1181 };
1182
1183 let result = reporter.print_tool_analysis_detailed(&analysis, false);
1184 assert!(result.is_ok());
1185 }
1186
1187 #[test]
1188 fn test_tool_analysis_csv_with_semicolons() {
1189 let reporter = Reporter::new();
1190 let timestamp = jiff::Timestamp::now();
1191 let mut tool_statistics = IndexMap::new();
1192
1193 tool_statistics.insert(
1194 "npm".to_string(),
1195 ToolStatistics {
1196 tool_name: "npm".to_string(),
1197 category: ToolCategory::PackageManager,
1198 total_invocations: 10,
1199 agents_using: vec!["developer".to_string(), "architect".to_string()],
1200 success_count: 9,
1201 failure_count: 1,
1202 first_seen: timestamp,
1203 last_seen: timestamp,
1204 command_patterns: vec![],
1205 sessions: vec!["session-1".to_string()],
1206 },
1207 );
1208
1209 let analysis = ToolAnalysis {
1210 session_id: "test".to_string(),
1211 total_tool_invocations: 10,
1212 tool_statistics,
1213 agent_tool_correlations: vec![],
1214 tool_chains: vec![],
1215 category_breakdown: IndexMap::new(),
1216 };
1217
1218 let result = reporter.tool_analysis_to_csv(&analysis);
1219 assert!(result.is_ok());
1220
1221 let csv = result.unwrap();
1222 assert!(csv.contains("developer;architect"));
1223 }
1224}