opendev_tui/widgets/nested_tool/
state.rs1use std::collections::HashMap;
4use std::time::Instant;
5
6use crate::formatters::tool_line::format_elapsed;
7
8fn 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#[derive(Debug, Clone)]
21pub struct SubagentDisplayState {
22 pub subagent_id: String,
24 pub name: String,
26 pub task: String,
28 pub started_at: Instant,
30 pub finished: bool,
32 pub success: bool,
34 pub result_summary: String,
36 pub tool_call_count: usize,
38 pub active_tools: HashMap<String, NestedToolCallState>,
40 pub completed_tools: Vec<CompletedToolCall>,
42 pub token_count: u64,
44 pub tick: usize,
46 pub shallow_warning: Option<String>,
48 pub finished_at: Option<Instant>,
50 pub parent_tool_id: Option<String>,
52 pub description: Option<String>,
54 pub backgrounded: bool,
56}
57
58impl SubagentDisplayState {
59 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 pub fn display_label(&self) -> &str {
84 self.description.as_deref().unwrap_or(&self.task)
85 }
86
87 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 pub fn add_tokens(&mut self, input_tokens: u64, output_tokens: u64) {
109 self.token_count += input_tokens + output_tokens;
110 }
111
112 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 if self.completed_tools.len() > 100 {
123 self.completed_tools
124 .drain(..self.completed_tools.len() - 100);
125 }
126 }
127 }
128
129 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 self.active_tools.clear();
145 self.completed_tools.clear();
146 }
147
148 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 pub fn elapsed_secs(&self) -> u64 {
158 self.started_at.elapsed().as_secs()
159 }
160
161 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 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 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#[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#[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;