diff --git a/src/tui.rs b/src/tui.rs index 8657d15..cbe1ee7 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1018,7 +1018,7 @@ pub(crate) fn build_display_stats( display_stats } -/// Column width for all token count columns (Cached, Input, Output, Reasoning). +/// Column width for all token count columns (Cached, Input, Output, Total, Reasoning). /// /// Width of 12 accommodates: /// - All u32 per-day values without commas (max 10 digits: "4294967295") @@ -2660,6 +2660,8 @@ fn draw_aggregate_stats_table( let mut best_input_tokens_period = None; let mut best_output_tokens: u64 = 0; let mut best_output_tokens_period = None; + let mut best_total_tokens: u64 = 0; + let mut best_total_tokens_period = None; let mut best_reasoning_tokens: u64 = 0; let mut best_reasoning_tokens_period = None; let mut best_conversations = 0; @@ -2689,6 +2691,13 @@ fn draw_aggregate_stats_table( best_output_tokens = period_stats.stats.output_tokens; best_output_tokens_period = Some(period.as_str()); } + let total_tokens = period_stats.stats.cached_tokens + + period_stats.stats.input_tokens + + period_stats.stats.output_tokens; + if best_total_tokens_period.is_none() || total_tokens > best_total_tokens { + best_total_tokens = total_tokens; + best_total_tokens_period = Some(period.as_str()); + } if best_reasoning_tokens_period.is_none() || period_stats.stats.reasoning_tokens > best_reasoning_tokens { @@ -2755,8 +2764,8 @@ fn draw_aggregate_stats_table( .max(terminal_text_width(&width_all_models_text)) .clamp(MODELS_COL_MIN_WIDTH, MODELS_COL_MAX_WIDTH); - let mut fixed_width = 1usize + period_width as usize + 10; - let mut column_count = 3usize; + let mut fixed_width = 1usize + period_width as usize + 10 + TOKEN_COL_WIDTH as usize; + let mut column_count = 4usize; for (column, width) in [ ("cached", TOKEN_COL_WIDTH), ("input", TOKEN_COL_WIDTH), @@ -2930,6 +2939,28 @@ fn draw_aggregate_stats_table( } .right_aligned(); + let total_tokens = period_stats.stats.cached_tokens + + period_stats.stats.input_tokens + + period_stats.stats.output_tokens; + let total_cell = if is_empty_row { + Line::from(Span::styled( + format_number_fit(total_tokens, format_options, tw), + Style::default().add_modifier(Modifier::DIM), + )) + } else if best_total_tokens_period == Some(period.as_str()) { + Line::from(Span::styled( + format_number_fit(total_tokens, format_options, tw), + Style::default().fg(Color::Red), + )) + } else { + Line::from(Span::raw(format_number_fit( + total_tokens, + format_options, + tw, + ))) + } + .right_aligned(); + let reasoning_cell = if is_empty_row { Line::from(Span::styled( format_number_fit(period_stats.stats.reasoning_tokens, format_options, tw), @@ -3021,6 +3052,7 @@ fn draw_aggregate_stats_table( if show("output") { row_cells.push(Cell::new(output_cell)); } + row_cells.push(Cell::new(total_cell)); if show("reason") { row_cells.push(Cell::new(reasoning_cell)); } @@ -3062,6 +3094,7 @@ fn draw_aggregate_stats_table( if show("output") { header_cells.push(Cell::new(Text::from("Outp Tks").right_aligned())); } + header_cells.push(Cell::new(Text::from("Total Tks").right_aligned())); if show("reason") { header_cells.push(Cell::new(Text::from("Reason Tks").right_aligned())); } @@ -3112,6 +3145,7 @@ fn draw_aggregate_stats_table( if show("output") { sep_cells.push(dim(token_sep.clone())); } + sep_cells.push(dim(token_sep.clone())); if show("reason") { sep_cells.push(dim(token_sep.clone())); } @@ -3196,6 +3230,17 @@ fn draw_aggregate_stats_table( .right_aligned(), )); } + totals_cells.push(Cell::new( + Line::from(Span::styled( + format_number_fit( + total_cached + total_input + total_output, + format_options, + tw, + ), + Style::default().add_modifier(Modifier::BOLD), + )) + .right_aligned(), + )); if show("reason") { totals_cells.push(Cell::new( Line::from(Span::styled( @@ -3262,6 +3307,7 @@ fn draw_aggregate_stats_table( if show("output") { widths.push(Constraint::Length(TOKEN_COL_WIDTH)); } + widths.push(Constraint::Length(TOKEN_COL_WIDTH)); if show("reason") { widths.push(Constraint::Length(TOKEN_COL_WIDTH)); } diff --git a/src/tui/tests.rs b/src/tui/tests.rs index 9491209..6c3f1b5 100644 --- a/src/tui/tests.rs +++ b/src/tui/tests.rs @@ -278,6 +278,89 @@ fn test_update_window_offsets_and_period_filters_resize() { assert_eq!(filters, vec![Some(PeriodFilter::Day(day))]); } +#[test] +fn aggregate_table_shows_total_tokens_for_each_period_and_footer() { + let date = "2025-01-01"; + let view = AnalyzerStatsView { + daily_stats: BTreeMap::from([( + date.to_string(), + DailyStats { + date: CompactDate::from_str(date).unwrap(), + stats: TuiStats { + cached_tokens: 100, + input_tokens: 200, + output_tokens: 30, + reasoning_tokens: 10, + ..TuiStats::default() + }, + ..DailyStats::default() + }, + )]), + session_aggregates: Vec::new(), + num_conversations: 0, + analyzer_name: Arc::from("Test"), + }; + let format_options = crate::utils::NumberFormatOptions { + use_comma: false, + use_human: false, + locale: "en".to_string(), + currency_symbol: "$".to_string(), + cost_decimal_places: 2, + decimal_places: 2, + }; + let width = 160; + let mut terminal = Terminal::new(TestBackend::new(width, 8)).unwrap(); + let mut table_state = TableState::default(); + + terminal + .draw(|frame| { + draw_aggregate_stats_table( + frame, + Rect::new(0, 0, width, 8), + &view, + &format_options, + &mut table_state, + AggregateViewMode::Daily, + "", + false, + false, + Color::Cyan, + &HashSet::new(), + false, + ModelUsageShareMetric::Tokens, + ); + }) + .unwrap(); + + let rows: Vec = terminal + .backend() + .buffer() + .content + .chunks(width as usize) + .map(|row| row.iter().map(|cell| cell.symbol()).collect()) + .collect(); + let header = rows.iter().find(|row| row.contains("Total Tks")).unwrap(); + let total_column = header.find("Total Tks").unwrap(); + let period = rows.iter().find(|row| row.contains("1/1/2025")).unwrap(); + let footer = rows.iter().find(|row| row.contains("Total (1d)")).unwrap(); + assert!( + period + .chars() + .skip(total_column) + .take(9) + .collect::() + .contains("330") + ); + assert!( + footer + .chars() + .skip(total_column) + .take(9) + .collect::() + .contains("330") + ); +} + #[test] fn aggregate_table_preserves_leading_digit_in_large_tool_total() { let mut daily_stats = BTreeMap::new(); @@ -369,7 +452,7 @@ fn aggregate_table_highlights_best_value_when_sort_is_reversed() { cost_decimal_places: 2, decimal_places: 2, }; - let width = 120; + let width = 150; let backend = TestBackend::new(width, 8); let mut terminal = Terminal::new(backend).unwrap(); let mut table_state = TableState::default(); @@ -1155,7 +1238,7 @@ fn aggregate_table_wraps_model_column_on_narrow_terminal() { cost_decimal_places: 2, decimal_places: 2, }; - let backend = TestBackend::new(115, 12); + let backend = TestBackend::new(145, 12); let mut terminal = Terminal::new(backend).unwrap(); let mut table_state = TableState::default(); @@ -1163,7 +1246,7 @@ fn aggregate_table_wraps_model_column_on_narrow_terminal() { .draw(|frame| { draw_aggregate_stats_table( frame, - Rect::new(0, 0, 115, 12), + Rect::new(0, 0, 145, 12), &view, &format_options, &mut table_state,