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