Add current session statistics

This commit is contained in:
Georg Bauer
2026-07-28 19:04:02 +02:00
parent 44d614378f
commit 07c89094b4

View File

@@ -1,9 +1,11 @@
use super::*; use super::*;
use crate::app::generation;
use iced::widget::column; use iced::widget::column;
impl App { impl App {
pub(super) fn stats_dashboard(&self) -> Element<'_, Message> { pub(super) fn stats_dashboard(&self) -> Element<'_, Message> {
let stats = &self.metrics_snapshot; let stats = &self.metrics_snapshot;
let session = SessionStats::from_messages(&self.conversation, self.config.model);
let context_fraction = if stats.context_limit == 0 { let context_fraction = if stats.context_limit == 0 {
0.0 0.0
} else { } else {
@@ -97,6 +99,51 @@ impl App {
] ]
.spacing(10); .spacing(10);
let session_title = self
.selected_session
.and_then(|id| self.session_title(id))
.unwrap_or_else(|| "No session selected".to_owned());
let session_cache_fraction = if session.input_tokens == 0 {
0.0
} else {
session.cached_tokens as f64 / session.input_tokens as f64
};
let current_session = stats_panel(
"CURRENT SESSION",
column![
metric_row("Session", session_title),
metric_row(
"Messages",
format!(
"{} · {} user · {} assistant · {} tool",
format_count(session.messages),
format_count(session.user_messages),
format_count(session.assistant_messages),
format_count(session.tool_messages),
),
),
metric_row("Tool calls", format_count(session.tool_calls)),
metric_row("Context rebuilds", format_count(session.context_rebuilds)),
metric_row("Completed turns", format_count(session.completed_turns),),
metric_row(
"Generation time",
format_duration(session.duration_ms as f64 / 1_000.0),
),
metric_row("Input tokens", format_count(session.input_tokens)),
metric_row(
"Cached tokens",
format!(
"{} · {:.1}% reused",
format_count(session.cached_tokens),
session_cache_fraction * 100.0,
),
),
metric_row("Output tokens", format_count(session.output_tokens)),
]
.spacing(9)
.into(),
);
let throughput = stats_panel( let throughput = stats_panel(
"MODEL ACTIVITY · LAST 24 SECONDS", "MODEL ACTIVITY · LAST 24 SECONDS",
column![ column![
@@ -528,6 +575,7 @@ impl App {
column![ column![
heading, heading,
headline, headline,
current_session,
throughput, throughput,
ssd_activity, ssd_activity,
kv_io, kv_io,
@@ -681,6 +729,59 @@ impl App {
} }
} }
#[derive(Debug, Default, PartialEq, Eq)]
struct SessionStats {
messages: u64,
user_messages: u64,
assistant_messages: u64,
tool_messages: u64,
tool_calls: u64,
context_rebuilds: u64,
completed_turns: u64,
duration_ms: u64,
input_tokens: u64,
cached_tokens: u64,
output_tokens: u64,
}
impl SessionStats {
fn from_messages(messages: &[generation::ChatMessage], model: ModelChoice) -> Self {
let mut summary = Self::default();
for message in messages {
if let Some(stats) = message.generation_stats {
summary.completed_turns = summary.completed_turns.saturating_add(1);
summary.duration_ms = summary.duration_ms.saturating_add(stats.duration_ms);
summary.input_tokens = summary
.input_tokens
.saturating_add(u64::from(stats.input_tokens));
summary.cached_tokens = summary
.cached_tokens
.saturating_add(u64::from(stats.cached_tokens));
summary.output_tokens = summary
.output_tokens
.saturating_add(u64::from(stats.output_tokens));
}
if message.compaction {
summary.context_rebuilds = summary.context_rebuilds.saturating_add(1);
} else if !message.system {
summary.messages = summary.messages.saturating_add(1);
if message.user {
summary.user_messages = summary.user_messages.saturating_add(1);
} else if message.tool {
summary.tool_messages = summary.tool_messages.saturating_add(1);
} else {
summary.assistant_messages = summary.assistant_messages.saturating_add(1);
let calls = crate::agent::parse_tool_calls(model, &message.content)
.map(|(_, calls)| calls.len() as u64)
.unwrap_or(0);
summary.tool_calls = summary.tool_calls.saturating_add(calls);
}
}
}
summary
}
}
/// Sessions sit outside the age scale: they are not evicted, only deleted. /// Sessions sit outside the age scale: they are not evicted, only deleted.
const SESSION_COLOR: Color = Color::from_rgb(0.85, 0.44, 0.56); const SESSION_COLOR: Color = Color::from_rgb(0.85, 0.44, 0.56);
@@ -705,3 +806,74 @@ fn shorten(title: &str) -> String {
fn portion(bytes: u64, capacity: u64) -> u16 { fn portion(bytes: u64, capacity: u64) -> u16 {
((bytes.saturating_mul(1000) / capacity.max(1)) as u16).max(1) ((bytes.saturating_mul(1000) / capacity.max(1)) as u16).max(1)
} }
#[cfg(test)]
mod tests {
use super::*;
fn message(user: bool, tool: bool, system: bool, content: &str) -> generation::ChatMessage {
generation::ChatMessage {
id: 1,
user,
tool,
system,
compaction: false,
compaction_tail_start: None,
generation_stats: None,
reasoning: None,
reasoning_complete: true,
reasoning_open: false,
content: content.to_owned(),
model_content: None,
markdown: markdown::Content::new(),
transcript: text_editor::Content::new(),
a2ui_lines_processed: 0,
a2ui_errors: Vec::new(),
a2ui_replies: Vec::new(),
a2ui_open_urls: Vec::new(),
}
}
#[test]
fn current_session_summary_uses_persisted_chat_facts() {
let mut user = message(true, false, false, "Run it");
user.generation_stats = Some(generation::GenerationStats {
duration_ms: 1_250,
input_tokens: 2_000,
cached_tokens: 1_500,
output_tokens: 300,
});
let assistant = message(
false,
false,
false,
"Checking<DSMLtool_calls><DSMLinvoke name=\"read\"><DSMLparameter name=\"path\" string=\"true\">src/main.rs</DSMLparameter></DSMLinvoke><DSMLinvoke name=\"list\"><DSMLparameter name=\"path\" string=\"true\">src</DSMLparameter></DSMLinvoke></DSMLtool_calls>",
);
let tool = message(false, true, false, "Tool result 1\n...");
let mut compaction = message(false, false, true, "summary");
compaction.compaction = true;
let system = message(false, false, true, "reminder");
let summary = SessionStats::from_messages(
&[user, assistant, tool, compaction, system],
ModelChoice::DeepSeekV4Flash,
);
assert_eq!(
summary,
SessionStats {
messages: 3,
user_messages: 1,
assistant_messages: 1,
tool_messages: 1,
tool_calls: 2,
context_rebuilds: 1,
completed_turns: 1,
duration_ms: 1_250,
input_tokens: 2_000,
cached_tokens: 1_500,
output_tokens: 300,
}
);
}
}