use std::time::Instant;
use ratatui::{
style::{Modifier, Style},
text::{Line, Span},
};
use rho_providers::model::ModelMetadata;
use crate::app::context_report::{ContextBasis, ContextReport};
use super::{
command_block::CommandBlock,
overlay_panel::{PanelBody, PanelState},
render::{display_width, truncate_one_line},
statusline::path::shorten_path_display,
theme::Theme,
usage_cost::{context_fill_percent, format_token_count},
App, ComposerMode, InteractiveRuntime, PanelOverlay,
};
const TITLE: &str = "Context";
const FOOTER: &str = "c copy Enter/Esc close";
const TOKENS_WIDTH: usize = 8;
const PERCENT_WIDTH: usize = 8;
const MIN_LABEL_WIDTH: usize = 12;
const COPY_WIDTH: usize = 160;
#[derive(Clone, Debug)]
pub(super) struct ContextOverlay {
report: ContextReport,
window: Option<u64>,
panel: PanelState,
}
impl PanelBody for ContextOverlay {
fn state(&self) -> &PanelState {
&self.panel
}
fn state_mut(&mut self) -> &mut PanelState {
&mut self.panel
}
fn title(&self) -> &str {
TITLE
}
fn footer(&self) -> &str {
FOOTER
}
fn body_lines(&self, width: usize, _now: Instant) -> Vec<Line<'static>> {
context_lines(&self.report, self.window, width)
}
fn copy_text(&self) -> Option<String> {
Some(
context_lines(&self.report, self.window, COPY_WIDTH)
.iter()
.map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
.trim_end()
.to_string()
})
.collect::<Vec<_>>()
.join("\n"),
)
}
}
impl App {
pub(super) fn execute_context_command(
&mut self,
agent: &InteractiveRuntime,
) -> anyhow::Result<()> {
let report = agent.context_report();
let window = report
.window
.filter(|window| *window > 0)
.or_else(|| {
self.model_metadata
.as_ref()
.and_then(ModelMetadata::display_context_window)
})
.filter(|window| *window > 0);
self.input_ui
.set_composer(ComposerMode::Panel(PanelOverlay::Context(Box::new(
ContextOverlay {
report,
window,
panel: PanelState::default(),
},
))));
self.set_status_quiet("context");
Ok(())
}
}
fn context_lines(report: &ContextReport, window: Option<u64>, width: usize) -> Vec<Line<'static>> {
let mut header = CommandBlock::on_surface(width);
let total = format_token_count(report.tokens);
let title = match window {
Some(window) => format!(
"≈{total} / {} tokens ({:.1}%)",
format_token_count(window),
context_fill_percent(report.tokens, window)
),
None => format!("≈{total} tokens"),
};
header.push_header(
&title,
match report.basis {
ContextBasis::ProviderCalibrated => "calibrated to the last provider-reported prompt",
ContextBasis::LocalEstimate => "local estimate",
},
);
header
.push_note("Rows are local estimates (about 4 characters per token) scaled to the total.");
let mut lines = header.finish();
if report.groups.is_empty() {
lines.push(Line::default());
lines.push(Line::from(Span::styled(
truncate_one_line(" Nothing in context yet.", width),
Theme::dim(),
)));
return lines;
}
let table = Table {
width,
total: report.tokens,
};
for group in &report.groups {
lines.push(Line::default());
lines.extend(table.row(
group.label,
None,
group.tokens,
Theme::text().add_modifier(Modifier::BOLD),
));
for row in &group.rows {
lines.extend(table.row(
&format!(" {}", row.label),
row.detail.as_deref(),
row.tokens,
Theme::text(),
));
}
}
if let Some(window) = window {
let table = Table {
width,
total: window,
};
lines.push(Line::default());
lines.extend(table.row(
"Free space",
None,
window.saturating_sub(report.tokens),
Theme::dim(),
));
}
lines
}
struct Table {
width: usize,
total: u64,
}
impl Table {
fn row(
&self,
label: &str,
detail: Option<&str>,
tokens: u64,
style: Style,
) -> Vec<Line<'static>> {
let percent = if self.total == 0 {
0.0
} else {
context_fill_percent(tokens, self.total)
};
let tokens = format_token_count(tokens);
let percent = format!("{percent:.1}%");
let numbers = format!("{tokens:>TOKENS_WIDTH$}{percent:>PERCENT_WIDTH$}");
let label_width = self.width.saturating_sub(display_width(&numbers) + 1);
if label_width < MIN_LABEL_WIDTH {
let indent = label.len() - label.trim_start().len() + 2;
let numbers = format!("{}{tokens} {percent}", " ".repeat(indent));
return vec![
Line::from(Span::styled(truncate_one_line(label, self.width), style)),
Line::from(Span::styled(truncate_one_line(&numbers, self.width), style)),
];
}
let label = truncate_one_line(label, label_width);
let mut spans = vec![Span::styled(label.clone(), style)];
let mut used = display_width(&label);
if let Some(detail) = detail {
let room = label_width.saturating_sub(used + 2);
if room >= 4 {
let detail = shorten_path_display(detail, room);
used += 2 + display_width(&detail);
spans.push(Span::styled(format!(" {detail}"), Theme::dim()));
}
}
if used + display_width(&numbers) < self.width {
let padding = self.width - used - display_width(&numbers);
spans.push(Span::styled(" ".repeat(padding), style));
spans.push(Span::styled(numbers, style));
}
vec![Line::from(spans)]
}
}