use std::collections::HashMap;
use std::time::Instant;
use crate::formatters::tool_line::format_elapsed;
fn format_token_count(tokens: u64) -> String {
if tokens >= 1_000_000 {
format!("{:.1}M tokens", tokens as f64 / 1_000_000.0)
} else if tokens >= 1_000 {
format!("{:.1}k tokens", tokens as f64 / 1_000.0)
} else {
format!("{tokens} tokens")
}
}
#[derive(Debug, Clone)]
pub struct SubagentDisplayState {
pub subagent_id: String,
pub name: String,
pub task: String,
pub started_at: Instant,
pub finished: bool,
pub success: bool,
pub result_summary: String,
pub tool_call_count: usize,
pub active_tools: HashMap<String, NestedToolCallState>,
pub completed_tools: Vec<CompletedToolCall>,
pub token_count: u64,
pub tick: usize,
pub shallow_warning: Option<String>,
pub finished_at: Option<Instant>,
pub parent_tool_id: Option<String>,
pub description: Option<String>,
pub backgrounded: bool,
}
impl SubagentDisplayState {
pub fn new(subagent_id: String, name: String, task: String) -> Self {
Self {
subagent_id,
name,
task,
started_at: Instant::now(),
finished: false,
success: false,
result_summary: String::new(),
tool_call_count: 0,
active_tools: HashMap::new(),
completed_tools: Vec::new(),
token_count: 0,
tick: 0,
shallow_warning: None,
finished_at: None,
parent_tool_id: None,
description: None,
backgrounded: false,
}
}
pub fn display_label(&self) -> &str {
self.description.as_deref().unwrap_or(&self.task)
}
pub fn add_tool_call(
&mut self,
tool_name: String,
tool_id: String,
args: HashMap<String, serde_json::Value>,
) {
self.tool_call_count += 1;
self.active_tools.insert(
tool_id.clone(),
NestedToolCallState {
tool_name,
tool_id,
args,
started_at: Instant::now(),
tick: 0,
},
);
}
pub fn add_tokens(&mut self, input_tokens: u64, output_tokens: u64) {
self.token_count += input_tokens + output_tokens;
}
pub fn complete_tool_call(&mut self, tool_id: &str, success: bool) {
if let Some(state) = self.active_tools.remove(tool_id) {
self.completed_tools.push(CompletedToolCall {
tool_name: state.tool_name,
args: state.args,
elapsed: state.started_at.elapsed(),
success,
});
if self.completed_tools.len() > 100 {
self.completed_tools
.drain(..self.completed_tools.len() - 100);
}
}
}
pub fn finish(
&mut self,
success: bool,
result_summary: String,
tool_call_count: usize,
shallow_warning: Option<String>,
) {
self.finished = true;
self.finished_at = Some(Instant::now());
self.success = success;
self.result_summary = result_summary;
self.tool_call_count = tool_call_count.max(self.tool_call_count);
self.shallow_warning = shallow_warning;
self.active_tools.clear();
self.completed_tools.clear();
}
pub fn advance_tick(&mut self) {
self.tick += 1;
for tool in self.active_tools.values_mut() {
tool.tick += 1;
}
}
pub fn elapsed_secs(&self) -> u64 {
self.started_at.elapsed().as_secs()
}
pub fn completion_summary(&self) -> String {
let mut parts = Vec::new();
let tc = self.tool_call_count;
parts.push(if tc == 1 {
"1 tool use".to_string()
} else {
format!("{tc} tool uses")
});
let elapsed = self.started_at.elapsed().as_secs();
parts.push(format_elapsed(elapsed));
if self.token_count > 0 {
parts.push(format_token_count(self.token_count));
}
format!("Done ({})", parts.join(", "))
}
pub fn activity_summary(&self) -> String {
if self.active_tools.is_empty() {
if self.finished {
return "Done".to_string();
}
return "Running...".to_string();
}
let mut read_count = 0usize;
let mut search_count = 0usize;
let mut other_name = None;
for tool in self.active_tools.values() {
match tool.tool_name.as_str() {
"read_file" | "read_pdf" => read_count += 1,
"grep"
| "ast_grep"
| "search"
| "list_files"
| "find_symbol"
| "find_referencing_symbols" => search_count += 1,
name => other_name = Some(name.to_string()),
}
}
if read_count > 1 {
format!("Reading {} files...", read_count)
} else if search_count > 1 {
format!("Searching for {} patterns...", search_count)
} else if let Some(name) = other_name {
format!("{name}...")
} else if read_count == 1 {
"Reading...".to_string()
} else {
"Running...".to_string()
}
}
}
#[derive(Debug, Clone)]
pub struct NestedToolCallState {
pub tool_name: String,
pub tool_id: String,
pub args: HashMap<String, serde_json::Value>,
pub started_at: Instant,
pub tick: usize,
}
#[derive(Debug, Clone)]
pub struct CompletedToolCall {
pub tool_name: String,
pub args: HashMap<String, serde_json::Value>,
pub elapsed: std::time::Duration,
pub success: bool,
}
#[cfg(test)]
#[path = "state_tests.rs"]
mod tests;