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