Skip to main content

opendev_tui/widgets/nested_tool/
state.rs

1//! State types for nested subagent tool display.
2
3use std::collections::HashMap;
4use std::time::Instant;
5
6use crate::formatters::tool_line::format_elapsed;
7
8/// Format a token count as human-readable (e.g., "1.2k tokens", "3.5M tokens").
9fn format_token_count(tokens: u64) -> String {
10    if tokens >= 1_000_000 {
11        format!("{:.1}M tokens", tokens as f64 / 1_000_000.0)
12    } else if tokens >= 1_000 {
13        format!("{:.1}k tokens", tokens as f64 / 1_000.0)
14    } else {
15        format!("{tokens} tokens")
16    }
17}
18
19/// State tracking for a single subagent execution.
20#[derive(Debug, Clone)]
21pub struct SubagentDisplayState {
22    /// Unique identifier for this subagent instance.
23    pub subagent_id: String,
24    /// Subagent name.
25    pub name: String,
26    /// Task description.
27    pub task: String,
28    /// When the subagent started.
29    pub started_at: Instant,
30    /// Whether the subagent has finished.
31    pub finished: bool,
32    /// Whether the subagent succeeded (only valid when finished).
33    pub success: bool,
34    /// Final result summary (only valid when finished).
35    pub result_summary: String,
36    /// Total tool calls made.
37    pub tool_call_count: usize,
38    /// Active tool calls (tool_id -> NestedToolCallState).
39    pub active_tools: HashMap<String, NestedToolCallState>,
40    /// Completed tool calls (for display).
41    pub completed_tools: Vec<CompletedToolCall>,
42    /// Accumulated token count (input + output).
43    pub token_count: u64,
44    /// Animation tick counter for spinner.
45    pub tick: usize,
46    /// Optional shallow subagent warning.
47    pub shallow_warning: Option<String>,
48    /// When the subagent finished (for cleanup timing).
49    pub finished_at: Option<Instant>,
50    /// The parent spawn_subagent tool_id (set when SubagentStarted is linked to ToolStarted).
51    pub parent_tool_id: Option<String>,
52    /// Short display label (from `description` param), used instead of full task in spinner.
53    pub description: Option<String>,
54    /// Whether this subagent's parent was sent to background (Ctrl+B).
55    pub backgrounded: bool,
56}
57
58impl SubagentDisplayState {
59    /// Create a new subagent display state.
60    pub fn new(subagent_id: String, name: String, task: String) -> Self {
61        Self {
62            subagent_id,
63            name,
64            task,
65            started_at: Instant::now(),
66            finished: false,
67            success: false,
68            result_summary: String::new(),
69            tool_call_count: 0,
70            active_tools: HashMap::new(),
71            completed_tools: Vec::new(),
72            token_count: 0,
73            tick: 0,
74            shallow_warning: None,
75            finished_at: None,
76            parent_tool_id: None,
77            description: None,
78            backgrounded: false,
79        }
80    }
81
82    /// Returns the short display label if set, otherwise the full task.
83    pub fn display_label(&self) -> &str {
84        self.description.as_deref().unwrap_or(&self.task)
85    }
86
87    /// Record a new tool call starting.
88    pub fn add_tool_call(
89        &mut self,
90        tool_name: String,
91        tool_id: String,
92        args: HashMap<String, serde_json::Value>,
93    ) {
94        self.tool_call_count += 1;
95        self.active_tools.insert(
96            tool_id.clone(),
97            NestedToolCallState {
98                tool_name,
99                tool_id,
100                args,
101                started_at: Instant::now(),
102                tick: 0,
103            },
104        );
105    }
106
107    /// Accumulate token usage from an LLM call.
108    pub fn add_tokens(&mut self, input_tokens: u64, output_tokens: u64) {
109        self.token_count += input_tokens + output_tokens;
110    }
111
112    /// Record a tool call completing.
113    pub fn complete_tool_call(&mut self, tool_id: &str, success: bool) {
114        if let Some(state) = self.active_tools.remove(tool_id) {
115            self.completed_tools.push(CompletedToolCall {
116                tool_name: state.tool_name,
117                args: state.args,
118                elapsed: state.started_at.elapsed(),
119                success,
120            });
121            // Cap completed tools to prevent unbounded growth in long-running subagents
122            if self.completed_tools.len() > 100 {
123                self.completed_tools
124                    .drain(..self.completed_tools.len() - 100);
125            }
126        }
127    }
128
129    /// Mark the subagent as finished.
130    pub fn finish(
131        &mut self,
132        success: bool,
133        result_summary: String,
134        tool_call_count: usize,
135        shallow_warning: Option<String>,
136    ) {
137        self.finished = true;
138        self.finished_at = Some(Instant::now());
139        self.success = success;
140        self.result_summary = result_summary;
141        self.tool_call_count = tool_call_count.max(self.tool_call_count);
142        self.shallow_warning = shallow_warning;
143        // Clear tool lists to reduce visual clutter — keep only header with "Done" status
144        self.active_tools.clear();
145        self.completed_tools.clear();
146    }
147
148    /// Advance the animation tick.
149    pub fn advance_tick(&mut self) {
150        self.tick += 1;
151        for tool in self.active_tools.values_mut() {
152            tool.tick += 1;
153        }
154    }
155
156    /// Elapsed time since start.
157    pub fn elapsed_secs(&self) -> u64 {
158        self.started_at.elapsed().as_secs()
159    }
160
161    /// Generate a persistent completion summary for display after the subagent finishes.
162    /// Format: "Done (N tool uses, Xs, N.Nk tokens)"
163    pub fn completion_summary(&self) -> String {
164        let mut parts = Vec::new();
165
166        let tc = self.tool_call_count;
167        parts.push(if tc == 1 {
168            "1 tool use".to_string()
169        } else {
170            format!("{tc} tool uses")
171        });
172
173        let elapsed = self.started_at.elapsed().as_secs();
174        parts.push(format_elapsed(elapsed));
175
176        if self.token_count > 0 {
177            parts.push(format_token_count(self.token_count));
178        }
179
180        format!("Done ({})", parts.join(", "))
181    }
182
183    /// Generate a summary of current activity.
184    pub fn activity_summary(&self) -> String {
185        if self.active_tools.is_empty() {
186            if self.finished {
187                return "Done".to_string();
188            }
189            return "Running...".to_string();
190        }
191
192        // Count active tool types
193        let mut read_count = 0usize;
194        let mut search_count = 0usize;
195        let mut other_name = None;
196        for tool in self.active_tools.values() {
197            match tool.tool_name.as_str() {
198                "read_file" | "read_pdf" => read_count += 1,
199                "grep"
200                | "ast_grep"
201                | "search"
202                | "list_files"
203                | "find_symbol"
204                | "find_referencing_symbols" => search_count += 1,
205                name => other_name = Some(name.to_string()),
206            }
207        }
208
209        if read_count > 1 {
210            format!("Reading {} files...", read_count)
211        } else if search_count > 1 {
212            format!("Searching for {} patterns...", search_count)
213        } else if let Some(name) = other_name {
214            format!("{name}...")
215        } else if read_count == 1 {
216            "Reading...".to_string()
217        } else {
218            "Running...".to_string()
219        }
220    }
221}
222
223/// State for an active nested tool call.
224#[derive(Debug, Clone)]
225pub struct NestedToolCallState {
226    pub tool_name: String,
227    pub tool_id: String,
228    pub args: HashMap<String, serde_json::Value>,
229    pub started_at: Instant,
230    pub tick: usize,
231}
232
233/// Record of a completed tool call.
234#[derive(Debug, Clone)]
235pub struct CompletedToolCall {
236    pub tool_name: String,
237    pub args: HashMap<String, serde_json::Value>,
238    pub elapsed: std::time::Duration,
239    pub success: bool,
240}
241
242#[cfg(test)]
243#[path = "state_tests.rs"]
244mod tests;