1use crate::config;
2use crate::store::{self, TaskStore};
3use std::sync::Arc;
4
5pub async fn show_task(id: &str) -> anyhow::Result<()> {
7 let store: Option<Arc<TaskStore>> = crate::cli::init_store().await.ok().map(Arc::new);
8 let repo = config::get_current_repo().unwrap_or_default();
9
10 let usage = store::get_token_usage(&store, &repo, id).await;
11 let cost_estimate = store::get_cost_estimate(&store, &repo, id).await;
12 let model = store::opt_store_get_task(&store, &repo, id)
13 .await
14 .and_then(|t| t.model)
15 .unwrap_or_default();
16
17 if usage.total_tokens() == 0 {
18 println!("No token data available for task #{}", id);
19 println!("Run the task first to collect token usage.");
20 return Ok(());
21 }
22
23 let pricing = store::pricing_for_model(&model);
24
25 println!("Task #{}: cost breakdown", id);
26 println!(
27 "├── Input tokens: {:>12}",
28 format_tokens(usage.input_tokens)
29 );
30 println!(
31 "├── Output tokens: {:>12}",
32 format_tokens(usage.output_tokens)
33 );
34 println!(
35 "├── Input cost: {:>12}",
36 format!("${:.4}", cost_estimate.input_cost_usd)
37 );
38 println!(
39 "├── Output cost: {:>12}",
40 format!("${:.4}", cost_estimate.output_cost_usd)
41 );
42 println!(
43 "└── Total cost: {:>12}",
44 format!("${:.4}", cost_estimate.total_cost_usd)
45 );
46 println!();
47 println!(
48 "Model: {}",
49 if model.is_empty() { "unknown" } else { &model }
50 );
51 println!(
52 "Pricing: ${:.2}/${:.2} per 1M tokens",
53 pricing.input_per_million_usd, pricing.output_per_million_usd
54 );
55
56 Ok(())
57}
58
59pub async fn show_summary() -> anyhow::Result<()> {
61 let store = crate::cli::init_store().await?;
62
63 let summary = store.get_cost_summary().await?;
64
65 println!();
66 println!("Cost Summary");
67 println!("{}", "=".repeat(60));
68 println!();
69 println!(
70 "{:<8} {:>10} {:>10} {:>10} {:>8}",
71 "PERIOD", "INPUT", "OUTPUT", "COST", "TASKS"
72 );
73 println!("{}", "-".repeat(60));
74
75 for period in &summary.periods {
76 println!(
77 "{:<8} {:>10} {:>10} {:>10} {:>8}",
78 period.label,
79 format_tokens(period.input_tokens as u64),
80 format_tokens(period.output_tokens as u64),
81 format!("${:.4}", period.total_cost_usd),
82 period.task_count,
83 );
84 }
85 println!();
86
87 Ok(())
88}
89
90pub async fn show_by_agent() -> anyhow::Result<()> {
92 let store = crate::cli::init_store().await?;
93
94 let groups = store.get_cost_by_agent().await?;
95
96 if groups.is_empty() {
97 println!("No cost data available yet.");
98 return Ok(());
99 }
100
101 println!();
102 println!("Cost by Agent");
103 println!("{}", "=".repeat(60));
104 println!();
105 println!(
106 "{:<12} {:>10} {:>10} {:>10} {:>8}",
107 "AGENT", "INPUT", "OUTPUT", "COST", "TASKS"
108 );
109 println!("{}", "-".repeat(60));
110
111 let mut total_cost = 0.0;
112 for group in &groups {
113 total_cost += group.total_cost_usd;
114 println!(
115 "{:<12} {:>10} {:>10} {:>10} {:>8}",
116 group.name,
117 format_tokens(group.input_tokens as u64),
118 format_tokens(group.output_tokens as u64),
119 format!("${:.4}", group.total_cost_usd),
120 group.task_count,
121 );
122 }
123 println!("{}", "-".repeat(60));
124 println!(
125 "{:<12} {:>10} {:>10} {:>10}",
126 "",
127 "",
128 "",
129 format!("${:.4}", total_cost)
130 );
131 println!();
132
133 Ok(())
134}
135
136pub async fn show_by_model() -> anyhow::Result<()> {
138 let store = crate::cli::init_store().await?;
139
140 let groups = store.get_cost_by_model().await?;
141
142 if groups.is_empty() {
143 println!("No cost data available yet.");
144 return Ok(());
145 }
146
147 println!();
148 println!("Cost by Model");
149 println!("{}", "=".repeat(60));
150 println!();
151 println!(
152 "{:<20} {:>10} {:>10} {:>10} {:>8}",
153 "MODEL", "INPUT", "OUTPUT", "COST", "TASKS"
154 );
155 println!("{}", "-".repeat(60));
156
157 let mut total_cost = 0.0;
158 for group in &groups {
159 total_cost += group.total_cost_usd;
160 println!(
161 "{:<20} {:>10} {:>10} {:>10} {:>8}",
162 group.name,
163 format_tokens(group.input_tokens as u64),
164 format_tokens(group.output_tokens as u64),
165 format!("${:.4}", group.total_cost_usd),
166 group.task_count,
167 );
168 }
169 println!("{}", "-".repeat(60));
170 println!(
171 "{:<20} {:>10} {:>10} {:>10}",
172 "",
173 "",
174 "",
175 format!("${:.4}", total_cost)
176 );
177 println!();
178
179 Ok(())
180}
181
182fn format_tokens(n: u64) -> String {
184 if n == 0 {
185 return "0".to_string();
186 }
187 let s = n.to_string();
188 let mut result = String::new();
189 for (i, c) in s.chars().rev().enumerate() {
190 if i > 0 && i % 3 == 0 {
191 result.push(',');
192 }
193 result.push(c);
194 }
195 result.chars().rev().collect()
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn format_tokens_zero() {
204 assert_eq!(format_tokens(0), "0");
205 }
206
207 #[test]
208 fn format_tokens_small() {
209 assert_eq!(format_tokens(123), "123");
210 }
211
212 #[test]
213 fn format_tokens_thousands() {
214 assert_eq!(format_tokens(1_234), "1,234");
215 }
216
217 #[test]
218 fn format_tokens_millions() {
219 assert_eq!(format_tokens(1_234_567), "1,234,567");
220 }
221}