use ratatui::prelude::*;
use shuvarie_highlight::theme;
use shuvarie_llm::TokenUsage;
use crate::tui::utils::num::{fmt_cost, fmt_tokens};
pub struct ContextDisplay {
input_tokens: u64,
output_tokens: u64,
reasoning_tokens: u64,
cached_tokens: u64,
cost: f64,
context_length: Option<u64>,
context_tokens: Option<u64>,
read_tokens: Option<u64>,
cached_pct: Option<u64>,
}
impl ContextDisplay {
pub fn new() -> Self {
Self {
input_tokens: 0,
output_tokens: 0,
reasoning_tokens: 0,
cached_tokens: 0,
cost: 0.0,
context_length: None,
context_tokens: None,
read_tokens: None,
cached_pct: None,
}
}
pub fn set_context_length(&mut self, context_length: Option<u64>) {
self.context_length = context_length;
}
pub fn add_usage(&mut self, usage: &TokenUsage, cost: f64, context_tokens: Option<u64>) {
self.input_tokens = self.input_tokens.saturating_add(usage.input_tokens);
self.output_tokens = self.output_tokens.saturating_add(usage.output_tokens);
self.reasoning_tokens = self.reasoning_tokens.saturating_add(usage.reasoning_tokens);
self.cached_tokens = self.cached_tokens.saturating_add(usage.cached_input_tokens);
self.cost += cost;
if context_tokens.is_some_and(|t| t > 0) {
self.context_tokens = Some(context_tokens.unwrap());
self.set_request_metrics(usage);
}
}
pub fn set_usage(&mut self, usage: &TokenUsage, cost: f64) {
self.input_tokens = usage.input_tokens;
self.output_tokens = usage.output_tokens;
self.reasoning_tokens = usage.reasoning_tokens;
self.cached_tokens = usage.cached_input_tokens;
self.cost = cost;
}
pub fn set_request(&mut self, usage: Option<&TokenUsage>) {
let usage = usage.filter(|u| shuvarie_llm::context_footprint(u) > 0);
match usage {
Some(usage) => {
self.context_tokens = Some(shuvarie_llm::context_footprint(usage));
self.set_request_metrics(usage);
}
None => {
self.context_tokens = None;
self.read_tokens = None;
self.cached_pct = None;
}
}
}
fn set_request_metrics(&mut self, usage: &TokenUsage) {
let read = shuvarie_llm::read_tokens(usage);
self.read_tokens = (read > 0).then_some(read);
self.cached_pct = match usage.cached_input_tokens {
cached if cached > 0 && read > 0 => {
Some((cached as f64 / read as f64 * 100.0).round().min(100.0) as u64)
}
_ => None,
};
}
pub fn view(&self, lines: &mut Vec<Line<'static>>) {
lines.push(Line::from("Context").fg(theme::ACCENT).bold());
let window = self.context_length.filter(|&c| c > 0);
lines.push(
Line::from(format!(
" ↑{} ↓{}",
fmt_tokens(self.input_tokens),
fmt_tokens(self.output_tokens),
))
.fg(theme::TEXT_DIM),
);
if let Some(ctx) = window {
let line = match self.context_tokens {
Some(tokens) => {
let pct = (tokens as f64 / ctx as f64 * 100.0).round().min(100.0);
format!(" {}/{} ({pct:.0}%)", fmt_tokens(tokens), fmt_tokens(ctx))
}
None => format!(" {}", fmt_tokens(ctx)),
};
lines.push(Line::from(line).fg(theme::TEXT_DIM));
}
let mut request = Vec::new();
if let Some(read) = self.read_tokens {
request.push(format!("R{}", fmt_tokens(read)));
}
if let Some(pct) = self.cached_pct {
request.push(format!("CH{pct}%"));
}
if !request.is_empty() {
lines.push(Line::from(format!(" {}", request.join(" "))).fg(theme::TEXT_DIM));
}
if self.reasoning_tokens > 0 {
lines.push(
Line::from(format!(" Think {}", fmt_tokens(self.reasoning_tokens)))
.fg(theme::TEXT_DIM),
);
}
if self.cached_tokens > 0 {
lines.push(
Line::from(format!(" Cache {}", fmt_tokens(self.cached_tokens)))
.fg(theme::TEXT_DIM),
);
}
lines.push(Line::from(format!(" Cost {}", fmt_cost(self.cost))).fg(theme::TEXT_DIM));
lines.push(Line::from(""));
}
pub(crate) fn compact_spans(&self) -> Vec<Span<'static>> {
let mut spans = Vec::new();
spans.push(
Span::raw(format!(
"↑{} ↓{}",
fmt_tokens(self.input_tokens),
fmt_tokens(self.output_tokens),
))
.fg(theme::TEXT_DIM),
);
if let Some(window) = self.context_length.filter(|&c| c > 0) {
let window_text = match self.context_tokens {
Some(tokens) => {
let pct = (tokens as f64 / window as f64 * 100.0).round().min(100.0);
format!(" {}/{} ({pct:.0}%)", fmt_tokens(tokens), fmt_tokens(window))
}
None => format!(" {}", fmt_tokens(window)),
};
spans.push(Span::raw(window_text).fg(theme::TEXT_DIM));
}
if let Some(read) = self.read_tokens {
spans.push(Span::raw(format!(" R{}", fmt_tokens(read))).fg(theme::TEXT_DIM));
}
if let Some(pct) = self.cached_pct {
spans.push(Span::raw(format!(" CH{pct}%")).fg(theme::TEXT_DIM));
}
spans.push(Span::raw(format!(" {}", fmt_cost(self.cost))).fg(theme::TEXT_DIM));
spans
}
}