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 = store::get_reported_cost(&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 println!("Task #{id}: cost breakdown");
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 "└── Cost: {:>12}",
38 format_cost(cost.unwrap_or(0.0))
39 );
40 println!();
41 println!(
42 "Model: {}",
43 if model.is_empty() { "unknown" } else { &model }
44 );
45
46 Ok(())
47}
48
49pub async fn show_summary() -> anyhow::Result<()> {
51 let store = cli::init_store().await?;
52
53 let summary = store.get_cost_summary().await?;
54
55 println!();
56 println!("Cost Summary");
57 println!("{}", "=".repeat(60));
58 println!();
59 println!(
60 "{:<8} {:>10} {:>10} {:>10} {:>8}",
61 "PERIOD", "INPUT", "OUTPUT", "COST", "TASKS"
62 );
63 println!("{}", "-".repeat(60));
64
65 for period in &summary.periods {
66 println!(
67 "{:<8} {:>10} {:>10} {:>10} {:>8}",
68 period.label,
69 format_tokens(u64::try_from(period.input_tokens).unwrap_or(0)),
70 format_tokens(u64::try_from(period.output_tokens).unwrap_or(0)),
71 format_cost(period.total_cost_usd),
72 period.task_count,
73 );
74 }
75 println!();
76
77 Ok(())
78}
79
80pub async fn show_by_agent() -> anyhow::Result<()> {
82 let store = cli::init_store().await?;
83
84 let groups = store.get_cost_by_agent().await?;
85
86 if groups.is_empty() {
87 println!("No cost data available yet.");
88 return Ok(());
89 }
90
91 println!();
92 println!("Cost by Agent");
93 println!("{}", "=".repeat(60));
94 println!();
95 println!(
96 "{:<12} {:>10} {:>10} {:>10} {:>8}",
97 "AGENT", "INPUT", "OUTPUT", "COST", "TASKS"
98 );
99 println!("{}", "-".repeat(60));
100
101 let mut total_cost = 0.0;
102 for group in &groups {
103 total_cost += group.total_cost_usd;
104 println!(
105 "{:<12} {:>10} {:>10} {:>10} {:>8}",
106 group.name,
107 format_tokens(u64::try_from(group.input_tokens).unwrap_or(0)),
108 format_tokens(u64::try_from(group.output_tokens).unwrap_or(0)),
109 format_cost(group.total_cost_usd),
110 group.task_count,
111 );
112 }
113 println!("{}", "-".repeat(60));
114 println!(
115 "{:<12} {:>10} {:>10} {:>10}",
116 "",
117 "",
118 "",
119 format_cost(total_cost)
120 );
121 println!();
122
123 Ok(())
124}
125
126pub async fn show_by_model() -> anyhow::Result<()> {
128 let store = cli::init_store().await?;
129
130 let groups = store.get_cost_by_model().await?;
131
132 if groups.is_empty() {
133 println!("No cost data available yet.");
134 return Ok(());
135 }
136
137 println!();
138 println!("Cost by Model");
139 println!("{}", "=".repeat(60));
140 println!();
141 println!(
142 "{:<20} {:>10} {:>10} {:>10} {:>8}",
143 "MODEL", "INPUT", "OUTPUT", "COST", "TASKS"
144 );
145 println!("{}", "-".repeat(60));
146
147 let mut total_cost = 0.0;
148 for group in &groups {
149 total_cost += group.total_cost_usd;
150 println!(
151 "{:<20} {:>10} {:>10} {:>10} {:>8}",
152 group.name,
153 format_tokens(u64::try_from(group.input_tokens).unwrap_or(0)),
154 format_tokens(u64::try_from(group.output_tokens).unwrap_or(0)),
155 format_cost(group.total_cost_usd),
156 group.task_count,
157 );
158 }
159 println!("{}", "-".repeat(60));
160 println!(
161 "{:<20} {:>10} {:>10} {:>10}",
162 "",
163 "",
164 "",
165 format_cost(total_cost)
166 );
167 println!();
168
169 Ok(())
170}
171
172pub(crate) fn format_cost(cost_usd: f64) -> String {
175 if cost_usd > 0.0 {
176 format!("${cost_usd:.4}")
177 } else {
178 "not reported".to_string()
179 }
180}
181
182fn format_tokens(n: u64) -> String {
183 if n == 0 {
184 return "0".to_string();
185 }
186 let s = n.to_string();
187 let mut result = String::new();
188 for (i, c) in s.chars().rev().enumerate() {
189 if i > 0 && i % 3 == 0 {
190 result.push(',');
191 }
192 result.push(c);
193 }
194 result.chars().rev().collect()
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn format_tokens_zero() {
203 assert_eq!(format_tokens(0), "0");
204 }
205
206 #[test]
207 fn format_tokens_small() {
208 assert_eq!(format_tokens(123), "123");
209 }
210
211 #[test]
212 fn format_tokens_thousands() {
213 assert_eq!(format_tokens(1_234), "1,234");
214 }
215
216 #[test]
217 fn format_tokens_millions() {
218 assert_eq!(format_tokens(1_234_567), "1,234,567");
219 }
220}