1use crate::exceptions::AicoError;
2use crate::llm::tokens::count_heuristic;
3use crate::model_registry::get_model_info;
4use crate::models::TokenInfo;
5use crate::session::Session;
6use comfy_table::ColumnConstraint;
7use comfy_table::Row;
8use comfy_table::Width;
9use comfy_table::{Attribute, Cell, CellAlignment, Table};
10use crossterm::style::Stylize;
11use std::fmt::Write as _;
12use std::io::Write;
13
14use crate::historystore::reconstruct::reconstruct_history;
15
16pub async fn run(json_output: bool) -> Result<(), AicoError> {
17 let session = Session::load_active()?;
18 let model_id = &session.view.model;
19 let model_info = get_model_info(model_id).await;
20 let width = crate::console::get_terminal_width();
21
22 let components = vec![
24 TokenInfo {
25 description: "system prompt".into(),
26 tokens: crate::llm::tokens::SYSTEM_TOKEN_COUNT,
27 cost: None,
28 },
29 TokenInfo {
30 description: "alignment prompts (worst-case)".into(),
31 tokens: crate::llm::tokens::MAX_ALIGNMENT_TOKENS,
32 cost: None,
33 },
34 ];
35
36 let history_vec = reconstruct_history(&session.store, &session.view, true)?;
38 let mut history_counter = crate::llm::tokens::HeuristicCounter::new();
39
40 for item in &history_vec {
41 if item.is_excluded {
42 continue;
43 }
44
45 let rec = &item.record;
46 if rec.role == crate::models::Role::User && !rec.passthrough {
47 if let Some(ref piped) = rec.piped_content {
48 let _ = write!(
49 history_counter,
50 "<stdin_content>\n{}\n</stdin_content>\n<prompt>\n{}\n</prompt>",
51 piped.trim(),
52 rec.content.trim()
53 );
54 } else {
55 history_counter.add_str(&rec.content);
56 }
57 } else {
58 history_counter.add_str(&rec.content);
59 }
60 }
61
62 let history_comp = TokenInfo {
63 description: "chat history".into(),
64 tokens: history_counter.count(),
65 cost: None,
66 };
67
68 let mut sorted_keys: Vec<_> = session.context_content.keys().collect();
70 sorted_keys.sort();
71
72 let mut file_components: Vec<TokenInfo> = sorted_keys
73 .into_iter()
74 .map(|rel_path| {
75 let content = &session.context_content[rel_path];
76 let mut buffer = String::new();
77 crate::llm::executor::append_file_context_xml(&mut buffer, rel_path, content);
78 TokenInfo {
79 description: rel_path.clone(),
80 tokens: count_heuristic(&buffer),
81 cost: None,
82 }
83 })
84 .collect();
85
86 let has_context_files = !file_components.is_empty();
88 let mut all_included = components;
89 all_included.push(history_comp);
90 all_included.append(&mut file_components);
91
92 let mut total_tokens = 0;
93 let mut total_cost = 0.0;
94 let mut has_cost_info = false;
95
96 for comp in &mut all_included {
97 total_tokens += comp.tokens;
98 if let Some(ref info) = model_info {
99 let usage = crate::models::TokenUsage {
100 prompt_tokens: comp.tokens,
101 completion_tokens: 0,
102 total_tokens: comp.tokens,
103 cached_tokens: None,
104 reasoning_tokens: None,
105 cost: None,
106 };
107 if let Some(cost) = crate::llm::tokens::calculate_cost_prefetched(info, &usage) {
108 comp.cost = Some(cost);
109 total_cost += cost;
110 has_cost_info = true;
111 }
112 }
113 }
114
115 if json_output {
116 let mut files = session.view.context_files.clone();
117 files.sort();
118
119 let session_name = session
120 .view_path
121 .file_stem()
122 .and_then(|s| s.to_str())
123 .unwrap_or("main")
124 .to_string();
125
126 let resp = crate::models::StatusResponse {
127 session_name,
128 model: model_id.clone(),
129 context_files: files,
130 total_tokens: if total_tokens > 0 {
131 Some(total_tokens)
132 } else {
133 None
134 },
135 total_cost: if has_cost_info {
136 Some(total_cost)
137 } else {
138 None
139 },
140 };
141 {
142 let mut stdout = std::io::stdout();
143 if let Err(e) = serde_json::to_writer(&mut stdout, &resp)
144 && !e.is_io()
145 {
146 return Err(AicoError::Session(e.to_string()));
147 }
148 let _ = writeln!(stdout);
149 }
150 return Ok(());
151 }
152
153 let session_name = session
155 .view_path
156 .file_stem()
157 .and_then(|s| s.to_str())
158 .unwrap_or("main");
159
160 session.warn_missing_files();
161
162 crate::console::draw_panel(
163 &format!("Session '{}'", session_name),
164 std::slice::from_ref(model_id),
165 width,
166 );
167 println!();
168
169 let mut table = Table::new();
171 table
172 .load_preset(comfy_table::presets::NOTHING)
173 .set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth)
174 .set_style(comfy_table::TableComponent::HeaderLines, '─')
175 .set_style(comfy_table::TableComponent::MiddleHeaderIntersections, ' ')
176 .set_width(width as u16)
177 .set_truncation_indicator("");
178
179 table.set_header(vec![
180 Cell::new("Tokens\n(approx.)").add_attribute(Attribute::Bold),
181 Cell::new("Cost").add_attribute(Attribute::Bold),
182 Cell::new("Component").add_attribute(Attribute::Bold),
183 ]);
184
185 table
187 .column_mut(0)
188 .unwrap()
189 .set_constraint(ColumnConstraint::UpperBoundary(Width::Fixed(10)));
190 table
191 .column_mut(1)
192 .unwrap()
193 .set_constraint(ColumnConstraint::UpperBoundary(Width::Fixed(10)));
194
195 for comp in all_included.iter().take(2) {
197 let cost_str = comp.cost.map(|c| format!("${:.5}", c)).unwrap_or_default();
198 table.add_row(vec![
199 Cell::new(crate::console::format_thousands(comp.tokens)),
200 Cell::new(cost_str),
201 Cell::new(&comp.description),
202 ]);
203 }
204
205 let history_cost_str = all_included[2]
207 .cost
208 .map(|c| format!("${:.5}", c))
209 .unwrap_or_default();
210 table.add_row(vec![
211 Cell::new(crate::console::format_thousands(all_included[2].tokens)),
212 Cell::new(history_cost_str),
213 Cell::new(&all_included[2].description),
214 ]);
215
216 if let Ok(Some(summary)) = session.summarize_active_window(&history_vec) {
218 if summary.active_pairs > 0 {
219 let mut line1 = format!(
220 "└─ Active window: {} pair{} (IDs {}-{}), {} sent",
221 summary.active_pairs,
222 if summary.active_pairs == 1 { "" } else { "s" },
223 summary.active_start_id,
224 summary.active_end_id,
225 summary.pairs_sent
226 );
227
228 if summary.excluded_in_window > 0 {
229 line1.push_str(&format!(" ({} excluded)", summary.excluded_in_window));
230 }
231 line1.push('.');
232
233 let line2 = "(Use `aico log`, `undo`, and `set-history` to manage)";
234
235 let (fmt1, fmt2) = if crate::console::is_stdout_terminal() {
236 (
237 format!(" {}", line1).dim().to_string(),
238 format!(" {}", line2).dim().italic().to_string(),
239 )
240 } else {
241 (format!(" {}", line1), format!(" {}", line2))
242 };
243
244 table.add_row(vec![Cell::new(""), Cell::new(""), Cell::new(fmt1)]);
245 table.add_row(vec![Cell::new(""), Cell::new(""), Cell::new(fmt2)]);
246 }
247
248 if summary.has_dangling {
249 table.add_row(vec![
250 Cell::new(""),
251 Cell::new(""),
252 Cell::new(
253 "Active context contains partial/dangling messages."
254 .yellow()
255 .to_string(),
256 ),
257 ]);
258 }
259 }
260
261 if has_context_files {
263 let mut separator = Row::from(vec![
264 Cell::new("─".repeat(width)),
265 Cell::new("─".repeat(width)),
266 Cell::new("─".repeat(width)),
267 ]);
268 separator.max_height(1);
269 table.add_row(separator);
270
271 for comp in all_included.iter().skip(3) {
272 let cost_str = comp.cost.map(|c| format!("${:.5}", c)).unwrap_or_default();
273 table.add_row(vec![
274 Cell::new(crate::console::format_thousands(comp.tokens)),
275 Cell::new(cost_str),
276 Cell::new(&comp.description),
277 ]);
278 }
279 }
280
281 let mut separator = Row::from(vec![
283 Cell::new("─".repeat(width)),
284 Cell::new("─".repeat(width)),
285 Cell::new("─".repeat(width)),
286 ]);
287 separator.max_height(1);
288 table.add_row(separator);
289
290 let total_cost_str = if has_cost_info {
291 format!("${:.5}", total_cost)
292 } else {
293 "".to_string()
294 };
295 table.add_row(vec![
296 Cell::new(format!(
297 "~{}",
298 crate::console::format_thousands(total_tokens)
299 ))
300 .add_attribute(Attribute::Bold),
301 Cell::new(total_cost_str).add_attribute(Attribute::Bold),
302 Cell::new("Total").add_attribute(Attribute::Bold),
303 ]);
304
305 table
306 .column_mut(0)
307 .unwrap()
308 .set_padding((0, 0))
309 .set_cell_alignment(CellAlignment::Right);
310 table
311 .column_mut(1)
312 .unwrap()
313 .set_padding((0, 0))
314 .set_cell_alignment(CellAlignment::Right);
315 table
316 .column_mut(2)
317 .unwrap()
318 .set_padding((0, 0))
319 .set_cell_alignment(CellAlignment::Left);
320
321 println!("{}", table);
322
323 if let Some(info) = model_info
325 && let Some(max) = info.max_input_tokens
326 {
327 println!();
328 let pct = (total_tokens as f64 / max as f64).min(1.0);
329 let bar_max_width = width.saturating_sub(4);
330 let filled = (pct * bar_max_width as f64) as usize;
331
332 let bar_filled = "━".repeat(filled);
333 let bar = format!(
334 "{}{}",
335 if crate::console::is_stdout_terminal() {
336 bar_filled.cyan().bold().to_string()
337 } else {
338 bar_filled
339 },
340 " ".repeat(bar_max_width.saturating_sub(filled))
341 );
342
343 let summary = format!(
344 "({} of {} used - {:.0}% remaining)",
345 crate::console::format_thousands(total_tokens),
346 crate::console::format_thousands(max),
347 (1.0 - pct) * 100.0
348 );
349
350 crate::console::draw_panel("Context Window", &[summary, bar], width);
351 }
352
353 Ok(())
354}