1use std::cell::RefCell;
2use std::rc::Rc;
3use std::sync::Arc;
4use std::time::Instant;
5
6use crate::agent::extension::{ToolRenderContext, ToolRenderer};
7use crate::agent::ui::theme::{RabTheme, current_theme};
8use crate::tui::Component;
9use crate::tui::component::{RenderCache, RenderCacheKey};
10use crate::tui::components::Text;
11use crate::tui::components::r#box::TuiBox;
12use crate::tui::keybindings;
13
14const PREVIEW_LINES: usize = 10;
16
17pub struct ToolExecComponent {
25 name: String,
26 renderer: Option<Arc<dyn ToolRenderer>>,
27 args: serde_json::Value,
28 output: Option<String>,
29 is_error: bool,
30 is_complete: bool,
31 expanded: bool,
32 started_at: Option<Instant>,
34 final_duration: Option<f64>,
37 last_timer_tick: Option<Instant>,
39 tool_call_id: String,
41 details: Option<serde_json::Value>,
43 state: Rc<RefCell<serde_json::Value>>,
45 cwd: String,
47 invalidate_tx: Option<tokio::sync::mpsc::UnboundedSender<()>>,
49 dirty: bool,
51 cache: Option<RenderCache>,
53}
54
55impl ToolExecComponent {
56 pub fn new(
57 name: impl Into<String>,
58 renderer: Option<Arc<dyn ToolRenderer>>,
59 args: serde_json::Value,
60 cwd: String,
61 tool_call_id: String,
62 ) -> Self {
63 Self {
64 name: name.into(),
65 renderer,
66 args,
67 output: None,
68 is_error: false,
69 is_complete: false,
70 expanded: false,
71 started_at: None,
72 final_duration: None,
73 last_timer_tick: None,
74 tool_call_id,
75 details: None,
76 state: Rc::new(RefCell::new(serde_json::Value::Object(Default::default()))),
77 cwd,
78 invalidate_tx: None,
79 dirty: true,
80 cache: None,
81 }
82 }
83
84 pub fn set_started_at(&mut self, instant: Instant) {
86 self.started_at = Some(instant);
87 self.last_timer_tick = Some(instant);
88 self.mark_dirty();
89 }
90
91 pub fn set_invalidate_tx(&mut self, tx: tokio::sync::mpsc::UnboundedSender<()>) {
93 self.invalidate_tx = Some(tx);
94 }
95
96 pub fn append_output(&mut self, text: &str) {
99 let output = self.output.get_or_insert_with(String::new);
100 output.push_str(text);
101 self.mark_dirty();
102 }
103
104 pub fn set_result_with_details(
105 &mut self,
106 output: impl Into<String>,
107 is_error: bool,
108 details: Option<serde_json::Value>,
109 ) {
110 self.output = Some(output.into());
111 self.is_error = is_error;
112 self.is_complete = true;
113 self.details = details;
114 if self.final_duration.is_none()
115 && let Some(start) = self.started_at
116 {
117 self.final_duration = Some(start.elapsed().as_secs_f64());
118 }
119 self.mark_dirty();
120 }
121
122 pub fn set_result(&mut self, output: impl Into<String>, is_error: bool) {
123 self.set_result_with_details(output, is_error, None);
124 }
125
126 pub fn make_invalidation_channel() -> (
128 tokio::sync::mpsc::UnboundedSender<()>,
129 tokio::sync::mpsc::UnboundedReceiver<()>,
130 ) {
131 tokio::sync::mpsc::unbounded_channel()
132 }
133
134 fn mark_dirty(&mut self) {
135 self.dirty = true;
136 self.cache = None;
137 }
138
139 fn live_duration(&self) -> Option<f64> {
140 if let Some(dur) = self.final_duration {
141 return Some(dur);
142 }
143 self.started_at.map(|t| t.elapsed().as_secs_f64())
144 }
145
146 pub fn tick_timer(&mut self) -> bool {
148 if self.is_complete || self.started_at.is_none() {
149 return false;
150 }
151 let now = Instant::now();
152 let should_invalidate = self
153 .last_timer_tick
154 .is_none_or(|last| now.duration_since(last) >= std::time::Duration::from_secs(1));
155 if should_invalidate {
156 self.last_timer_tick = Some(now);
157 self.mark_dirty();
158 return true;
159 }
160 false
161 }
162
163 fn state_hash(&self) -> u64 {
164 use std::collections::hash_map::DefaultHasher;
165 use std::hash::{Hash, Hasher};
166 let mut hasher = DefaultHasher::new();
167 self.name.hash(&mut hasher);
168 self.args.to_string().hash(&mut hasher);
169 self.is_error.hash(&mut hasher);
170 self.is_complete.hash(&mut hasher);
171 self.live_duration().map(|s| s.to_bits()).hash(&mut hasher);
172 self.output.hash(&mut hasher);
173 hasher.finish()
174 }
175}
176
177impl Component for ToolExecComponent {
178 fn set_expanded(&mut self, expanded: bool) {
179 self.expanded = expanded;
180 self.mark_dirty();
181 }
182
183 fn render(&mut self, width: usize) -> Vec<String> {
184 let theme = current_theme();
185
186 if let Some(ref renderer) = self.renderer {
188 return self.render_with_renderer(renderer.as_ref(), &theme, width);
189 }
190
191 self.render_generic(&theme, width)
193 }
194
195 fn invalidate(&mut self) {
196 self.mark_dirty();
197 }
198
199 fn is_dirty(&self) -> bool {
200 self.dirty
201 }
202
203 fn clear_dirty(&mut self) {
204 self.dirty = false;
205 }
206
207 fn cache_key(&self, width: usize) -> Option<RenderCacheKey> {
208 Some(RenderCacheKey {
209 width,
210 expanded: self.expanded,
211 state_hash: self.state_hash(),
212 })
213 }
214
215 fn get_cached_render(&self) -> Option<&RenderCache> {
216 self.cache.as_ref()
217 }
218
219 fn set_cached_render(&mut self, cache: RenderCache) {
220 self.cache = Some(cache);
221 self.dirty = false;
222 }
223}
224
225impl ToolExecComponent {
226 fn render_with_renderer(
228 &self,
229 renderer: &dyn ToolRenderer,
230 theme: &RabTheme,
231 width: usize,
232 ) -> Vec<String> {
233 let is_partial = !self.is_complete;
234
235 let expand_key = format_key_hint(crate::tui::keybindings::ACTION_APP_TOOLS_EXPAND);
236 let ctx = ToolRenderContext {
237 expanded: self.expanded,
238 args_complete: self.is_complete,
239 is_partial,
240 is_error: self.is_error,
241 tool_call_id: self.tool_call_id.clone(),
242 execution_started: self.started_at.is_some(),
243 cwd: self.cwd.clone(),
244 duration_secs: self.live_duration(),
245 exit_code: None,
246 cancelled: false,
247 was_truncated: false,
248 full_output_path: None,
249 file_path: None,
250 expand_key,
251 details: self.details.clone(),
252 state: self.state.clone(),
253 invalidate: self.invalidate_tx.clone(),
254 };
255
256 if renderer.render_self() {
258 let mut lines: Vec<String> = Vec::new();
259 lines.push(String::new());
260
261 let bg_key = self.compute_bg_key(Some(renderer));
262 let bg_ansi = theme.bg_ansi(bg_key).to_string();
263 let mut call_box = TuiBox::new(1, 1, Some(crate::tui::Style::new().bg(bg_ansi)));
264
265 let mut all_content = String::new();
266
267 let call_lines = renderer.render_call(&self.args, width, theme, &ctx);
268 if !call_lines.is_empty() {
269 all_content.push_str(&call_lines.join("\n"));
270 }
271
272 if let Some(ref output) = self.output {
273 let result_lines = renderer.render_result(output, width, theme, &ctx);
274 if !result_lines.is_empty() {
275 if !all_content.is_empty() {
276 all_content.push('\n');
277 all_content.push('\n');
278 }
279 all_content.push_str(&result_lines.join("\n"));
280 }
281 }
282
283 if !all_content.is_empty() {
284 let call_text = Text::new(all_content, 0, 0, None);
285 call_box.add_child(std::boxed::Box::new(call_text));
286 lines.extend(call_box.render(width));
287 }
288 return lines;
289 }
290
291 let bg_key = self.compute_bg_key(Some(renderer));
293 let bg_ansi = theme.bg_ansi(bg_key).to_string();
294 let theme_clone = theme.clone();
295
296 let padding_x = 1;
297 let content_width = width.saturating_sub(2 * padding_x).max(1);
298 let mut msg_box = TuiBox::new(1, 1, Some(crate::tui::Style::new().bg(bg_ansi)));
299
300 let call_lines = renderer.render_call(&self.args, content_width, &theme_clone, &ctx);
301 let header_text = Text::new(call_lines.join("\n"), 0, 0, None);
302 msg_box.add_child(std::boxed::Box::new(header_text));
303
304 if let Some(ref output) = self.output {
305 let result_lines = renderer.render_result(output, content_width, &theme_clone, &ctx);
306 if !result_lines.is_empty() {
307 let result_text = Text::new(result_lines.join("\n"), 0, 0, None);
308 msg_box.add_child(std::boxed::Box::new(result_text));
309 }
310 }
311
312 msg_box.render(width)
313 }
314
315 fn render_generic(&self, theme: &RabTheme, width: usize) -> Vec<String> {
318 let bg_key = self.compute_bg_key(None);
319 let bg_ansi = theme.bg_ansi(bg_key).to_string();
320 let mut msg_box = TuiBox::new(1, 1, Some(crate::tui::Style::new().bg(bg_ansi)));
321
322 let args_str = serde_json::to_string(&self.args).unwrap_or_default();
324 let header = if args_str.is_empty() || args_str == "{}" {
325 theme.fg("toolTitle", &theme.bold(&self.name))
326 } else {
327 format!(
328 "{} {}",
329 theme.fg("toolTitle", &theme.bold(&self.name)),
330 theme.fg("muted", &args_str),
331 )
332 };
333 let header_text = Text::new(header, 0, 0, None);
334 msg_box.add_child(std::boxed::Box::new(header_text));
335
336 if let Some(ref output) = self.output {
338 let display_text = if self.expanded {
339 output.clone()
340 } else {
341 let lines: Vec<&str> = output.lines().collect();
342 if lines.len() > PREVIEW_LINES {
343 let preview = lines[..PREVIEW_LINES].join("\n");
344 format!(
345 "{}\n{}",
346 preview,
347 theme.fg(
348 "muted",
349 &format!("... ({} more lines)", lines.len() - PREVIEW_LINES)
350 ),
351 )
352 } else {
353 output.clone()
354 }
355 };
356
357 let fg_key = if self.is_error { "error" } else { "toolOutput" };
358 let styled = display_text
359 .lines()
360 .map(|line| {
361 if line.is_empty() {
362 String::new()
363 } else {
364 theme.fg(fg_key, line)
365 }
366 })
367 .collect::<Vec<_>>()
368 .join("\n");
369 let result_text = Text::new(styled, 0, 0, None);
370 msg_box.add_child(std::boxed::Box::new(result_text));
371 }
372
373 msg_box.render(width)
374 }
375
376 fn compute_bg_key(&self, renderer: Option<&dyn ToolRenderer>) -> &'static str {
377 if let Some(r) = renderer
378 && let Some(hint) = r.render_bg_key()
379 {
380 return hint;
381 }
382 if !self.is_complete {
383 "toolPendingBg"
384 } else if self.is_error {
385 "toolErrorBg"
386 } else {
387 "toolSuccessBg"
388 }
389 }
390}
391
392fn format_key_hint(action_id: &str) -> String {
394 let keys = keybindings::get_keybindings().get_keys(action_id);
395 if keys.is_empty() {
396 return String::new();
397 }
398 keys[0].clone()
399}
400
401pub struct RcToolExec(pub Rc<RefCell<ToolExecComponent>>);
406
407impl Clone for RcToolExec {
408 fn clone(&self) -> Self {
409 Self(self.0.clone())
410 }
411}
412
413impl Component for RcToolExec {
414 fn render(&mut self, width: usize) -> Vec<String> {
415 self.0.borrow_mut().render(width)
416 }
417
418 fn set_expanded(&mut self, expanded: bool) {
419 self.0.borrow_mut().set_expanded(expanded);
420 }
421
422 fn invalidate(&mut self) {
423 self.0.borrow_mut().invalidate();
424 }
425
426 fn is_dirty(&self) -> bool {
427 self.0.borrow().is_dirty()
428 }
429
430 fn clear_dirty(&mut self) {
431 self.0.borrow_mut().clear_dirty();
432 }
433
434 fn cache_key(&self, width: usize) -> Option<RenderCacheKey> {
435 self.0.borrow().cache_key(width)
436 }
437
438 fn get_cached_render(&self) -> Option<&RenderCache> {
439 None
440 }
441
442 fn set_cached_render(&mut self, cache: RenderCache) {
443 self.0.borrow_mut().set_cached_render(cache);
444 }
445}