use crate::parser::ExecutionNode;
use chrono::{Local, TimeZone};
#[derive(Debug, Default)]
pub struct WatchStats {
pub total_nodes: usize,
pub tools_used: Vec<String>,
pub errors: usize,
pub start_time: Option<i64>,
pub last_update: Option<i64>,
}
impl WatchStats {
pub fn update(&mut self, node: &ExecutionNode) {
self.total_nodes += 1;
if let Some(ref tool_use) = node.tool_use {
if !self.tools_used.contains(&tool_use.name) {
self.tools_used.push(tool_use.name.clone());
}
}
if let Some(ref tool_result) = node.tool_result {
if tool_result.is_error == Some(true) {
self.errors += 1;
}
}
if let Some(ts) = node.timestamp {
if self.start_time.is_none() {
self.start_time = Some(ts);
}
self.last_update = Some(ts);
}
}
pub fn format_duration(&self) -> String {
match (self.start_time, self.last_update) {
(Some(start), Some(end)) => {
let duration_ms = end - start;
let duration_s = duration_ms / 1000;
if duration_s < 60 {
format!("{}s", duration_s)
} else if duration_s < 3600 {
format!("{}m {}s", duration_s / 60, duration_s % 60)
} else {
format!("{}h {}m", duration_s / 3600, (duration_s % 3600) / 60)
}
}
_ => "0s".to_string(),
}
}
}
pub fn render_node(node: &ExecutionNode) {
let timestamp = format_timestamp(node.timestamp);
match node.node_type.as_str() {
"user" => {
println!(" [{}] USER", timestamp);
if let Some(ref message) = node.message {
let text = message.text_content();
if !text.is_empty() {
let preview = truncate(&text, 80);
println!(" {}", preview);
}
}
}
"assistant" => {
println!(" [{}] ASSISTANT", timestamp);
if let Some(ref message) = node.message {
let tool_names: Vec<&str> = message
.content_blocks()
.iter()
.filter_map(|b| match b {
crate::parser::models::ContentBlock::ToolUse { name, .. } => {
Some(name.as_str())
}
_ => None,
})
.collect();
if !tool_names.is_empty() {
println!(" [Tools: {}]", tool_names.join(", "));
} else {
let text = message.text_content();
if !text.is_empty() {
let preview = truncate(&text, 80);
println!(" {}", preview);
}
}
}
}
"tool_use" => {
if let Some(ref tool_use) = node.tool_use {
println!(" [{}] TOOL: {}", timestamp, tool_use.name);
} else {
println!(" [{}] TOOL_USE", timestamp);
}
}
"tool_result" => {
if let Some(ref tool_result) = node.tool_result {
if tool_result.is_error == Some(true) {
println!(" [{}] TOOL RESULT: \x1b[31mERROR\x1b[0m", timestamp);
if let Some(ref error) = tool_result.error {
println!(" {}", truncate(error, 80));
}
} else {
println!(" [{}] TOOL RESULT: \x1b[32mOK\x1b[0m", timestamp);
}
if let Some(duration) = tool_result.duration_ms {
println!(" Duration: {}ms", duration);
}
}
}
"thinking" => {
println!(" [{}] 💭 THINKING...", timestamp);
if let Some(ref thinking) = node.thinking {
let preview = truncate(thinking, 80);
println!(" {}", preview);
}
}
"progress" => {
if let Some(ref progress) = node.progress {
if let Some(ref message) = progress.message {
println!(" [{}] ⏳ {}", timestamp, message);
}
}
}
_ => {
println!(" [{}] {}", timestamp, node.node_type.to_uppercase());
}
}
}
pub fn render_stats(stats: &WatchStats) {
println!("\n{}", "=".repeat(80));
println!(
" Nodes: {} | Tools: {} | Errors: {} | Duration: {}",
stats.total_nodes,
stats.tools_used.len(),
stats.errors,
stats.format_duration()
);
println!("{}", "=".repeat(80));
}
fn format_timestamp(timestamp: Option<i64>) -> String {
match timestamp {
Some(ts_ms) => {
let ts_s = ts_ms / 1000;
let dt = Local.timestamp_opt(ts_s, 0);
match dt.single() {
Some(datetime) => datetime.format("%H:%M:%S").to_string(),
None => "??:??:??".to_string(),
}
}
None => "??:??:??".to_string(),
}
}
fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len - 3])
}
}