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() {
261 let mut lines: Vec<String> = Vec::new();
262 lines.push(String::new()); let call_lines = renderer.render_call(&self.args, width, theme, &ctx);
266
267 let mut all_lines: Vec<String> = Vec::new();
268 if !call_lines.is_empty() {
269 all_lines.extend(call_lines);
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_lines.is_empty() {
276 all_lines.push(String::new());
277 }
278 all_lines.extend(result_lines);
279 }
280 }
281
282 if !all_lines.is_empty() {
283 lines.extend(all_lines);
285 }
286
287 return lines;
288 }
289
290 let mut lines: Vec<String> = Vec::new();
292 lines.push(String::new()); let bg_key = self.compute_bg_key(Some(renderer));
295 let bg_ansi = theme.bg_ansi(bg_key).to_string();
296 let theme_clone = theme.clone();
297
298 let padding_x = 1;
299 let content_width = width.saturating_sub(2 * padding_x).max(1);
300 let mut msg_box = TuiBox::new(1, 1, Some(crate::tui::Style::new().bg(bg_ansi)));
301
302 let call_lines = renderer.render_call(&self.args, content_width, &theme_clone, &ctx);
303 let header_text = Text::new(call_lines.join("\n"), 0, 0, None);
304 msg_box.add_child(std::boxed::Box::new(header_text));
305
306 if let Some(ref output) = self.output {
307 let result_lines = renderer.render_result(output, content_width, &theme_clone, &ctx);
308 if !result_lines.is_empty() {
309 let result_text = Text::new(result_lines.join("\n"), 0, 0, None);
310 msg_box.add_child(std::boxed::Box::new(result_text));
311 }
312 }
313
314 lines.extend(msg_box.render(width));
315 lines
316 }
317
318 fn render_generic(&self, theme: &RabTheme, width: usize) -> Vec<String> {
321 let mut lines: Vec<String> = Vec::new();
322 lines.push(String::new()); let bg_key = self.compute_bg_key(None);
325 let bg_ansi = theme.bg_ansi(bg_key).to_string();
326 let mut msg_box = TuiBox::new(1, 1, Some(crate::tui::Style::new().bg(bg_ansi)));
327
328 let args_str = serde_json::to_string(&self.args).unwrap_or_default();
330 let header = if args_str.is_empty() || args_str == "{}" {
331 theme.fg("toolTitle", &theme.bold(&self.name))
332 } else {
333 format!(
334 "{} {}",
335 theme.fg("toolTitle", &theme.bold(&self.name)),
336 theme.fg("muted", &args_str),
337 )
338 };
339 let header_text = Text::new(header, 0, 0, None);
340 msg_box.add_child(std::boxed::Box::new(header_text));
341
342 if let Some(ref output) = self.output {
344 let display_text = if self.expanded {
345 output.clone()
346 } else {
347 let lines: Vec<&str> = output.lines().collect();
348 if lines.len() > PREVIEW_LINES {
349 let preview = lines[..PREVIEW_LINES].join("\n");
350 format!(
351 "{}\n{}",
352 preview,
353 theme.fg(
354 "muted",
355 &format!("... ({} more lines)", lines.len() - PREVIEW_LINES)
356 ),
357 )
358 } else {
359 output.clone()
360 }
361 };
362
363 let fg_key = if self.is_error { "error" } else { "toolOutput" };
364 let styled = display_text
365 .lines()
366 .map(|line| {
367 if line.is_empty() {
368 String::new()
369 } else {
370 theme.fg(fg_key, line)
371 }
372 })
373 .collect::<Vec<_>>()
374 .join("\n");
375 let result_text = Text::new(styled, 0, 0, None);
376 msg_box.add_child(std::boxed::Box::new(result_text));
377 }
378
379 lines.extend(msg_box.render(width));
380 lines
381 }
382
383 fn compute_bg_key(&self, renderer: Option<&dyn ToolRenderer>) -> &'static str {
384 if let Some(r) = renderer
385 && let Some(hint) = r.render_bg_key()
386 {
387 return hint;
388 }
389 if !self.is_complete {
390 "toolPendingBg"
391 } else if self.is_error {
392 "toolErrorBg"
393 } else {
394 "toolSuccessBg"
395 }
396 }
397}
398
399fn format_key_hint(action_id: &str) -> String {
401 let keys = keybindings::get_keybindings().get_keys(action_id);
402 if keys.is_empty() {
403 return String::new();
404 }
405 keys[0].clone()
406}
407
408pub struct RcToolExec(pub Rc<RefCell<ToolExecComponent>>);
413
414impl Clone for RcToolExec {
415 fn clone(&self) -> Self {
416 Self(self.0.clone())
417 }
418}
419
420impl Component for RcToolExec {
421 fn render(&mut self, width: usize) -> Vec<String> {
422 self.0.borrow_mut().render(width)
423 }
424
425 fn set_expanded(&mut self, expanded: bool) {
426 self.0.borrow_mut().set_expanded(expanded);
427 }
428
429 fn invalidate(&mut self) {
430 self.0.borrow_mut().invalidate();
431 }
432
433 fn is_dirty(&self) -> bool {
434 self.0.borrow().is_dirty()
435 }
436
437 fn clear_dirty(&mut self) {
438 self.0.borrow_mut().clear_dirty();
439 }
440
441 fn cache_key(&self, width: usize) -> Option<RenderCacheKey> {
442 self.0.borrow().cache_key(width)
443 }
444
445 fn get_cached_render(&self) -> Option<&RenderCache> {
446 None
447 }
448
449 fn set_cached_render(&mut self, cache: RenderCache) {
450 self.0.borrow_mut().set_cached_render(cache);
451 }
452}