Add current session statistics
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
use super::*;
|
||||
use crate::app::generation;
|
||||
use iced::widget::column;
|
||||
|
||||
impl App {
|
||||
pub(super) fn stats_dashboard(&self) -> Element<'_, Message> {
|
||||
let stats = &self.metrics_snapshot;
|
||||
let session = SessionStats::from_messages(&self.conversation, self.config.model);
|
||||
let context_fraction = if stats.context_limit == 0 {
|
||||
0.0
|
||||
} else {
|
||||
@@ -97,6 +99,51 @@ impl App {
|
||||
]
|
||||
.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(
|
||||
"MODEL ACTIVITY · LAST 24 SECONDS",
|
||||
column![
|
||||
@@ -528,6 +575,7 @@ impl App {
|
||||
column![
|
||||
heading,
|
||||
headline,
|
||||
current_session,
|
||||
throughput,
|
||||
ssd_activity,
|
||||
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.
|
||||
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 {
|
||||
((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<|DSML|tool_calls><|DSML|invoke name=\"read\"><|DSML|parameter name=\"path\" string=\"true\">src/main.rs</|DSML|parameter></|DSML|invoke><|DSML|invoke name=\"list\"><|DSML|parameter name=\"path\" string=\"true\">src</|DSML|parameter></|DSML|invoke></|DSML|tool_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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user