Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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()));
}
Expand Down Expand Up @@ -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()));
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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));
}
Expand Down
89 changes: 86 additions & 3 deletions src/tui/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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::<String>()
.contains("330")
);
assert!(
footer
.chars()
.skip(total_column)
.take(9)
.collect::<String>()
.contains("330")
);
}

#[test]
fn aggregate_table_preserves_leading_digit_in_large_tool_total() {
let mut daily_stats = BTreeMap::new();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -1155,15 +1238,15 @@ 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();

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,
Expand Down