cc_token_usage/analysis/
project.rs1use std::collections::HashMap;
2
3use crate::data::models::SessionData;
4use crate::pricing::calculator::PricingCalculator;
5
6use super::{AggregatedTokens, ProjectResult, ProjectSummary};
7
8pub fn analyze_projects(
9 sessions: &[SessionData],
10 calc: &PricingCalculator,
11 top_n: usize,
12) -> ProjectResult {
13 let mut project_map: HashMap<String, ProjectAccumulator> = HashMap::new();
14
15 for session in sessions {
16 let project_name = session
17 .project
18 .clone()
19 .unwrap_or_else(|| "(unknown)".to_string());
20
21 let acc = project_map
22 .entry(project_name.clone())
23 .or_insert_with(|| ProjectAccumulator {
24 name: project_name,
25 session_count: 0,
26 total_turns: 0,
27 agent_turns: 0,
28 tokens: AggregatedTokens::default(),
29 cost: 0.0,
30 });
31
32 acc.session_count += 1;
33
34 for turn in &session.turns {
35 acc.tokens.add_usage(&turn.usage);
36 acc.total_turns += 1;
37 let cost = calc.calculate_turn_cost(&turn.model, &turn.usage);
38 acc.cost += cost.total;
39 }
40
41 for turn in &session.agent_turns {
42 acc.tokens.add_usage(&turn.usage);
43 acc.total_turns += 1;
44 acc.agent_turns += 1;
45 let cost = calc.calculate_turn_cost(&turn.model, &turn.usage);
46 acc.cost += cost.total;
47 }
48 }
49
50 let mut projects: Vec<ProjectSummary> = project_map
51 .into_values()
52 .map(|acc| ProjectSummary {
53 display_name: project_display_name(&acc.name),
54 name: acc.name,
55 session_count: acc.session_count,
56 total_turns: acc.total_turns,
57 agent_turns: acc.agent_turns,
58 tokens: acc.tokens,
59 cost: acc.cost,
60 })
61 .collect();
62
63 projects.sort_by(|a, b| b.cost.partial_cmp(&a.cost).unwrap_or(std::cmp::Ordering::Equal));
65
66 if top_n > 0 {
68 projects.truncate(top_n);
69 }
70
71 ProjectResult { projects }
72}
73
74pub fn project_display_name(name: &str) -> String {
78 if let Some(rest) = name.strip_prefix("-Users-") {
81 if let Some(after_user) = rest.find('-') {
83 let path_part = &rest[after_user..];
84 let display = path_part.replace('-', "/");
86 return format!("~{display}");
87 }
88 }
89
90 name.to_string()
91}
92
93struct ProjectAccumulator {
94 name: String,
95 session_count: usize,
96 total_turns: usize,
97 agent_turns: usize,
98 tokens: AggregatedTokens,
99 cost: f64,
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn test_project_display_name() {
108 assert_eq!(
109 project_display_name("-Users-testuser-cc-web3"),
110 "~/cc/web3"
111 );
112 assert_eq!(
113 project_display_name("-Users-alice-projects-my-app"),
114 "~/projects/my/app"
115 );
116 assert_eq!(project_display_name("simple-project"), "simple-project");
117 assert_eq!(project_display_name("-Users-bob"), "-Users-bob");
118 }
119}