1use crate::swarm::{SubTaskStatus, SwarmStats};
8use ratatui::{
9 Frame,
10 layout::{Constraint, Direction, Layout, Rect},
11 style::{Color, Modifier, Style},
12 text::{Line, Span},
13 widgets::{Block, Borders, Gauge, List, ListItem, ListState, Paragraph, Wrap},
14};
15use tokio::sync::mpsc;
16
17#[derive(Debug, Clone)]
19pub struct AgentToolCallDetail {
20 pub tool_name: String,
21 pub input_preview: String,
22 pub output_preview: String,
23 pub success: bool,
24}
25
26#[derive(Debug, Clone)]
28pub struct AgentMessageEntry {
29 pub role: String,
30 pub content: String,
31 pub is_tool_call: bool,
32}
33
34#[derive(Debug, Clone)]
36pub enum SwarmEvent {
37 Started { task: String, total_subtasks: usize },
39 Decomposed { subtasks: Vec<SubTaskInfo> },
41 SubTaskUpdate {
43 id: String,
44 name: String,
45 status: SubTaskStatus,
46 agent_name: Option<String>,
47 },
48 AgentStarted {
50 subtask_id: String,
51 agent_name: String,
52 specialty: String,
53 },
54 AgentToolCall {
56 subtask_id: String,
57 tool_name: String,
58 },
59 AgentToolCallDetail {
61 subtask_id: String,
62 detail: AgentToolCallDetail,
63 },
64 AgentMessage {
66 subtask_id: String,
67 entry: AgentMessageEntry,
68 },
69 AgentComplete {
71 subtask_id: String,
72 success: bool,
73 steps: usize,
74 },
75 AgentOutput { subtask_id: String, output: String },
77 AgentError { subtask_id: String, error: String },
79 StageComplete {
81 stage: usize,
82 completed: usize,
83 failed: usize,
84 },
85 Complete { success: bool, stats: SwarmStats },
87 Error(String),
89}
90
91#[derive(Debug, Clone)]
93pub struct SubTaskInfo {
94 pub id: String,
95 pub name: String,
96 pub status: SubTaskStatus,
97 pub stage: usize,
98 pub dependencies: Vec<String>,
99 pub agent_name: Option<String>,
100 pub current_tool: Option<String>,
101 pub steps: usize,
102 pub max_steps: usize,
103 pub tool_call_history: Vec<AgentToolCallDetail>,
105 pub messages: Vec<AgentMessageEntry>,
107 pub output: Option<String>,
109 pub error: Option<String>,
111}
112
113#[derive(Debug, Default)]
115pub struct SwarmViewState {
116 pub active: bool,
118 pub task: String,
120 pub subtasks: Vec<SubTaskInfo>,
122 pub current_stage: usize,
124 pub total_stages: usize,
126 pub stats: Option<SwarmStats>,
128 pub error: Option<String>,
130 pub complete: bool,
132 pub selected_index: usize,
134 pub detail_mode: bool,
136 pub detail_scroll: usize,
138 pub list_state: ListState,
140 event_rx: Option<mpsc::Receiver<SwarmEvent>>,
142}
143
144impl SwarmViewState {
145 pub fn new() -> Self {
146 Self::default()
147 }
148
149 pub fn mark_active(&mut self, task: impl Into<String>) {
150 self.handle_event(SwarmEvent::Started {
151 task: task.into(),
152 total_subtasks: self.subtasks.len(),
153 });
154 }
155
156 pub fn attach_event_rx(&mut self, rx: mpsc::Receiver<SwarmEvent>) {
158 self.event_rx = Some(rx);
159 self.active = true;
160 self.complete = false;
161 self.error = None;
162 }
163
164 pub fn drain_events(&mut self) -> bool {
168 let Some(mut rx) = self.event_rx.take() else {
169 return false;
170 };
171 let mut any = false;
172 loop {
173 match rx.try_recv() {
174 Ok(evt) => {
175 any = true;
176 self.handle_event(evt);
177 }
178 Err(mpsc::error::TryRecvError::Empty) => {
179 self.event_rx = Some(rx);
180 break;
181 }
182 Err(mpsc::error::TryRecvError::Disconnected) => break,
183 }
184 }
185 any
186 }
187
188 pub fn handle_event(&mut self, event: SwarmEvent) {
190 match event {
191 SwarmEvent::Started {
192 task,
193 total_subtasks,
194 } => {
195 self.active = true;
196 self.task = task;
197 self.subtasks.clear();
198 self.current_stage = 0;
199 self.complete = false;
200 self.error = None;
201 self.selected_index = 0;
202 self.detail_mode = false;
203 self.detail_scroll = 0;
204 self.list_state = ListState::default();
205 self.subtasks.reserve(total_subtasks);
207 }
208 SwarmEvent::Decomposed { subtasks } => {
209 self.subtasks = subtasks;
210 self.total_stages = self.subtasks.iter().map(|s| s.stage).max().unwrap_or(0) + 1;
211 if !self.subtasks.is_empty() {
212 self.list_state.select(Some(0));
213 }
214 }
215 SwarmEvent::SubTaskUpdate {
216 id,
217 name,
218 status,
219 agent_name,
220 } => {
221 if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == id) {
222 task.status = status;
223 task.name = name;
224 if agent_name.is_some() {
225 task.agent_name = agent_name;
226 }
227 }
228 }
229 SwarmEvent::AgentStarted {
230 subtask_id,
231 agent_name,
232 ..
233 } => {
234 if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
235 task.status = SubTaskStatus::Running;
236 task.agent_name = Some(agent_name);
237 }
238 }
239 SwarmEvent::AgentToolCall {
240 subtask_id,
241 tool_name,
242 } => {
243 if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
244 task.current_tool = Some(tool_name);
245 task.steps += 1;
246 }
247 }
248 SwarmEvent::AgentToolCallDetail { subtask_id, detail } => {
249 if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
250 task.current_tool = Some(detail.tool_name.clone());
251 task.steps += 1;
252 crate::tui::agent_detail_update::push(&mut task.tool_call_history, detail);
253 }
254 }
255 SwarmEvent::AgentMessage { subtask_id, entry } => {
256 if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
257 crate::tui::agent_detail_update::push(&mut task.messages, entry);
258 }
259 }
260 SwarmEvent::AgentComplete {
261 subtask_id,
262 success,
263 steps,
264 } => {
265 if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
266 task.status = if success {
267 SubTaskStatus::Completed
268 } else {
269 SubTaskStatus::Failed
270 };
271 task.steps = steps;
272 task.current_tool = None;
273 }
274 }
275 SwarmEvent::AgentOutput { subtask_id, output } => {
276 if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
277 crate::tui::agent_detail_update::output(&mut task.output, &output, "swarm output");
278 }
279 }
280 SwarmEvent::AgentError { subtask_id, error } => {
281 if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
282 task.error = Some(error);
283 }
284 }
285 SwarmEvent::StageComplete { stage, .. } => {
286 self.current_stage = stage + 1;
287 }
288 SwarmEvent::Complete { success: _, stats } => {
289 self.stats = Some(stats);
290 self.complete = true;
291 }
292 SwarmEvent::Error(err) => {
293 self.error = Some(err);
294 }
295 }
296 }
297
298 pub fn select_prev(&mut self) {
300 if self.subtasks.is_empty() {
301 return;
302 }
303 self.selected_index = self.selected_index.saturating_sub(1);
304 self.list_state.select(Some(self.selected_index));
305 }
306
307 pub fn select_next(&mut self) {
309 if self.subtasks.is_empty() {
310 return;
311 }
312 self.selected_index = (self.selected_index + 1).min(self.subtasks.len() - 1);
313 self.list_state.select(Some(self.selected_index));
314 }
315
316 pub fn enter_detail(&mut self) {
318 if !self.subtasks.is_empty() {
319 self.detail_mode = true;
320 self.detail_scroll = 0;
321 }
322 }
323
324 pub fn exit_detail(&mut self) {
326 self.detail_mode = false;
327 self.detail_scroll = 0;
328 }
329
330 pub fn detail_scroll_down(&mut self, amount: usize) {
332 self.detail_scroll = self.detail_scroll.saturating_add(amount);
333 }
334
335 pub fn detail_scroll_up(&mut self, amount: usize) {
337 self.detail_scroll = self.detail_scroll.saturating_sub(amount);
338 }
339
340 pub fn selected_subtask(&self) -> Option<&SubTaskInfo> {
342 self.subtasks.get(self.selected_index)
343 }
344
345 pub fn status_counts(&self) -> (usize, usize, usize, usize) {
347 let mut pending = 0;
348 let mut running = 0;
349 let mut completed = 0;
350 let mut failed = 0;
351
352 for task in &self.subtasks {
353 match task.status {
354 SubTaskStatus::Pending | SubTaskStatus::Blocked => pending += 1,
355 SubTaskStatus::Running => running += 1,
356 SubTaskStatus::Completed => completed += 1,
357 SubTaskStatus::Failed | SubTaskStatus::Cancelled | SubTaskStatus::TimedOut => {
358 failed += 1
359 }
360 }
361 }
362
363 (pending, running, completed, failed)
364 }
365
366 pub fn progress(&self) -> f64 {
368 if self.subtasks.is_empty() {
369 return 0.0;
370 }
371 let (_, _, completed, failed) = self.status_counts();
372 ((completed + failed) as f64 / self.subtasks.len() as f64) * 100.0
373 }
374}
375
376pub fn render_swarm_view(f: &mut Frame, state: &mut SwarmViewState, area: Rect) {
378 if state.detail_mode {
380 render_agent_detail(f, state, area);
381 return;
382 }
383
384 let chunks = Layout::default()
385 .direction(Direction::Vertical)
386 .constraints([
387 Constraint::Length(3), Constraint::Length(3), Constraint::Min(1), Constraint::Length(3), ])
392 .split(area);
393
394 render_header(f, state, chunks[0]);
396
397 render_stage_progress(f, state, chunks[1]);
399
400 render_subtask_list(f, state, chunks[2]);
402
403 render_stats(f, state, chunks[3]);
405}
406
407fn render_header(f: &mut Frame, state: &SwarmViewState, area: Rect) {
408 let (pending, running, completed, failed) = state.status_counts();
409 let total = state.subtasks.len();
410
411 let title = if state.complete {
412 if state.error.is_some() {
413 " Swarm [ERROR] "
414 } else {
415 " Swarm [COMPLETE] "
416 }
417 } else if state.active {
418 " Swarm [ACTIVE] "
419 } else {
420 " Swarm [IDLE] "
421 };
422
423 let status_line = Line::from(vec![
424 Span::styled("Task: ", Style::default().fg(Color::DarkGray)),
425 Span::styled(
426 truncate_str(&state.task, 47),
427 Style::default().fg(Color::White),
428 ),
429 Span::raw(" "),
430 Span::styled(format!("⏳{}", pending), Style::default().fg(Color::Yellow)),
431 Span::raw(" "),
432 Span::styled(format!("⚡{}", running), Style::default().fg(Color::Cyan)),
433 Span::raw(" "),
434 Span::styled(format!("✓{}", completed), Style::default().fg(Color::Green)),
435 Span::raw(" "),
436 Span::styled(format!("✗{}", failed), Style::default().fg(Color::Red)),
437 Span::raw(" "),
438 Span::styled(format!("/{}", total), Style::default().fg(Color::DarkGray)),
439 ]);
440
441 let paragraph = Paragraph::new(status_line).block(
442 Block::default()
443 .borders(Borders::ALL)
444 .title(title)
445 .border_style(Style::default().fg(if state.complete {
446 if state.error.is_some() {
447 Color::Red
448 } else {
449 Color::Green
450 }
451 } else {
452 Color::Cyan
453 })),
454 );
455
456 f.render_widget(paragraph, area);
457}
458
459fn render_stage_progress(f: &mut Frame, state: &SwarmViewState, area: Rect) {
460 let progress = state.progress();
461 let label = format!(
462 "Stage {}/{} - {:.0}%",
463 state.current_stage.min(state.total_stages),
464 state.total_stages,
465 progress
466 );
467
468 let gauge = Gauge::default()
469 .block(Block::default().borders(Borders::ALL).title(" Progress "))
470 .gauge_style(Style::default().fg(Color::Cyan).bg(Color::DarkGray))
471 .percent(progress as u16)
472 .label(label);
473
474 f.render_widget(gauge, area);
475}
476
477fn render_subtask_list(f: &mut Frame, state: &mut SwarmViewState, area: Rect) {
478 state.list_state.select(Some(state.selected_index));
480
481 let items: Vec<ListItem> = state
482 .subtasks
483 .iter()
484 .map(|task| {
485 let (icon, color) = match task.status {
486 SubTaskStatus::Pending => ("○", Color::DarkGray),
487 SubTaskStatus::Blocked => ("⊘", Color::Yellow),
488 SubTaskStatus::Running => ("●", Color::Cyan),
489 SubTaskStatus::Completed => ("✓", Color::Green),
490 SubTaskStatus::Failed => ("✗", Color::Red),
491 SubTaskStatus::Cancelled => ("⊗", Color::DarkGray),
492 SubTaskStatus::TimedOut => ("⏱", Color::Red),
493 };
494
495 let mut spans = vec![
496 Span::styled(format!("{} ", icon), Style::default().fg(color)),
497 Span::styled(
498 format!("[S{}] ", task.stage),
499 Style::default().fg(Color::DarkGray),
500 ),
501 Span::styled(&task.name, Style::default().fg(Color::White)),
502 ];
503
504 if task.status == SubTaskStatus::Running {
506 if let Some(ref agent) = task.agent_name {
507 spans.push(Span::styled(
508 format!(" → {}", agent),
509 Style::default().fg(Color::Cyan),
510 ));
511 }
512 if let Some(ref tool) = task.current_tool {
513 spans.push(Span::styled(
514 format!(" [{}]", tool),
515 Style::default()
516 .fg(Color::Yellow)
517 .add_modifier(Modifier::DIM),
518 ));
519 }
520 }
521
522 if task.steps > 0 {
524 spans.push(Span::styled(
525 format!(" ({}/{})", task.steps, task.max_steps),
526 Style::default().fg(Color::DarkGray),
527 ));
528 }
529
530 ListItem::new(Line::from(spans))
531 })
532 .collect();
533
534 let title = if state.subtasks.is_empty() {
535 " SubTasks (none yet) "
536 } else {
537 " SubTasks (↑↓:select Enter:detail) "
538 };
539
540 let list = List::new(items)
541 .block(Block::default().borders(Borders::ALL).title(title))
542 .highlight_style(
543 Style::default()
544 .add_modifier(Modifier::BOLD)
545 .bg(Color::DarkGray),
546 )
547 .highlight_symbol("▶ ");
548
549 f.render_stateful_widget(list, area, &mut state.list_state);
550}
551
552fn render_agent_detail(f: &mut Frame, state: &SwarmViewState, area: Rect) {
554 let task = match state.selected_subtask() {
555 Some(t) => t,
556 None => {
557 let p = Paragraph::new("No subtask selected").block(
558 Block::default()
559 .borders(Borders::ALL)
560 .title(" Agent Detail "),
561 );
562 f.render_widget(p, area);
563 return;
564 }
565 };
566
567 let chunks = Layout::default()
568 .direction(Direction::Vertical)
569 .constraints([
570 Constraint::Length(5), Constraint::Min(1), Constraint::Length(1), ])
574 .split(area);
575
576 let (status_icon, status_color) = match task.status {
578 SubTaskStatus::Pending => ("○ Pending", Color::DarkGray),
579 SubTaskStatus::Blocked => ("⊘ Blocked", Color::Yellow),
580 SubTaskStatus::Running => ("● Running", Color::Cyan),
581 SubTaskStatus::Completed => ("✓ Completed", Color::Green),
582 SubTaskStatus::Failed => ("✗ Failed", Color::Red),
583 SubTaskStatus::Cancelled => ("⊗ Cancelled", Color::DarkGray),
584 SubTaskStatus::TimedOut => ("⏱ Timed Out", Color::Red),
585 };
586
587 let agent_label = task.agent_name.as_deref().unwrap_or("(unassigned)");
588 let header_lines = vec![
589 Line::from(vec![
590 Span::styled("Task: ", Style::default().fg(Color::DarkGray)),
591 Span::styled(
592 &task.name,
593 Style::default()
594 .fg(Color::White)
595 .add_modifier(Modifier::BOLD),
596 ),
597 ]),
598 Line::from(vec![
599 Span::styled("Agent: ", Style::default().fg(Color::DarkGray)),
600 Span::styled(agent_label, Style::default().fg(Color::Cyan)),
601 Span::raw(" "),
602 Span::styled("Status: ", Style::default().fg(Color::DarkGray)),
603 Span::styled(status_icon, Style::default().fg(status_color)),
604 Span::raw(" "),
605 Span::styled("Stage: ", Style::default().fg(Color::DarkGray)),
606 Span::styled(format!("{}", task.stage), Style::default().fg(Color::White)),
607 Span::raw(" "),
608 Span::styled("Steps: ", Style::default().fg(Color::DarkGray)),
609 Span::styled(
610 format!("{}/{}", task.steps, task.max_steps),
611 Style::default().fg(Color::White),
612 ),
613 ]),
614 Line::from(vec![
615 Span::styled("Deps: ", Style::default().fg(Color::DarkGray)),
616 Span::styled(
617 if task.dependencies.is_empty() {
618 "none".to_string()
619 } else {
620 task.dependencies.join(", ")
621 },
622 Style::default().fg(Color::DarkGray),
623 ),
624 ]),
625 ];
626
627 let title = format!(" Agent Detail: {} ", task.id);
628 let header = Paragraph::new(header_lines).block(
629 Block::default()
630 .borders(Borders::ALL)
631 .title(title)
632 .border_style(Style::default().fg(status_color)),
633 );
634 f.render_widget(header, chunks[0]);
635
636 let mut content_lines: Vec<Line> = Vec::new();
638
639 if !task.tool_call_history.is_empty() {
641 content_lines.push(Line::from(Span::styled(
642 "─── Tool Call History ───",
643 Style::default()
644 .fg(Color::Cyan)
645 .add_modifier(Modifier::BOLD),
646 )));
647 content_lines.push(Line::from(""));
648
649 for (i, tc) in task.tool_call_history.iter().enumerate() {
650 let icon = if tc.success { "✓" } else { "✗" };
651 let icon_color = if tc.success { Color::Green } else { Color::Red };
652 content_lines.push(Line::from(vec![
653 Span::styled(format!(" {icon} "), Style::default().fg(icon_color)),
654 Span::styled(format!("#{} ", i + 1), Style::default().fg(Color::DarkGray)),
655 Span::styled(
656 &tc.tool_name,
657 Style::default()
658 .fg(Color::Yellow)
659 .add_modifier(Modifier::BOLD),
660 ),
661 ]));
662 if !tc.input_preview.is_empty() {
663 content_lines.push(Line::from(vec![
664 Span::raw(" "),
665 Span::styled("in: ", Style::default().fg(Color::DarkGray)),
666 Span::styled(
667 truncate_str(&tc.input_preview, 80),
668 Style::default().fg(Color::White),
669 ),
670 ]));
671 }
672 if !tc.output_preview.is_empty() {
673 content_lines.push(Line::from(vec![
674 Span::raw(" "),
675 Span::styled("out: ", Style::default().fg(Color::DarkGray)),
676 Span::styled(
677 truncate_str(&tc.output_preview, 80),
678 Style::default().fg(Color::White),
679 ),
680 ]));
681 }
682 }
683 content_lines.push(Line::from(""));
684 } else if task.steps > 0 {
685 content_lines.push(Line::from(Span::styled(
687 format!("─── {} tool calls (no detail captured) ───", task.steps),
688 Style::default().fg(Color::DarkGray),
689 )));
690 content_lines.push(Line::from(""));
691 }
692
693 if !task.messages.is_empty() {
695 content_lines.push(Line::from(Span::styled(
696 "─── Conversation ───",
697 Style::default()
698 .fg(Color::Cyan)
699 .add_modifier(Modifier::BOLD),
700 )));
701 content_lines.push(Line::from(""));
702
703 for msg in &task.messages {
704 let (role_color, role_label) = match msg.role.as_str() {
705 "user" => (Color::White, "USER"),
706 "assistant" => (Color::Cyan, "ASST"),
707 "tool" => (Color::Yellow, "TOOL"),
708 "system" => (Color::DarkGray, "SYS "),
709 _ => (Color::White, " "),
710 };
711 content_lines.push(Line::from(vec![Span::styled(
712 format!(" [{role_label}] "),
713 Style::default().fg(role_color).add_modifier(Modifier::BOLD),
714 )]));
715 for line in msg.content.lines().take(10) {
717 content_lines.push(Line::from(vec![
718 Span::raw(" "),
719 Span::styled(line, Style::default().fg(Color::White)),
720 ]));
721 }
722 if msg.content.lines().count() > 10 {
723 content_lines.push(Line::from(vec![
724 Span::raw(" "),
725 Span::styled("... (truncated)", Style::default().fg(Color::DarkGray)),
726 ]));
727 }
728 content_lines.push(Line::from(""));
729 }
730 }
731
732 if let Some(ref output) = task.output {
734 content_lines.push(Line::from(Span::styled(
735 "─── Output ───",
736 Style::default()
737 .fg(Color::Green)
738 .add_modifier(Modifier::BOLD),
739 )));
740 content_lines.push(Line::from(""));
741 for line in output.lines().take(20) {
742 content_lines.push(Line::from(Span::styled(
743 line,
744 Style::default().fg(Color::White),
745 )));
746 }
747 if output.lines().count() > 20 {
748 content_lines.push(Line::from(Span::styled(
749 "... (truncated)",
750 Style::default().fg(Color::DarkGray),
751 )));
752 }
753 content_lines.push(Line::from(""));
754 }
755
756 if let Some(ref err) = task.error {
758 content_lines.push(Line::from(Span::styled(
759 "─── Error ───",
760 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
761 )));
762 content_lines.push(Line::from(""));
763 for line in err.lines() {
764 content_lines.push(Line::from(Span::styled(
765 line,
766 Style::default().fg(Color::Red),
767 )));
768 }
769 content_lines.push(Line::from(""));
770 }
771
772 if content_lines.is_empty() {
774 content_lines.push(Line::from(Span::styled(
775 " Waiting for agent activity...",
776 Style::default().fg(Color::DarkGray),
777 )));
778 }
779
780 let content = Paragraph::new(content_lines)
781 .block(Block::default().borders(Borders::ALL))
782 .wrap(Wrap { trim: false })
783 .scroll((state.detail_scroll as u16, 0));
784 f.render_widget(content, chunks[1]);
785
786 let hints = Paragraph::new(Line::from(vec![
788 Span::styled(" Esc", Style::default().fg(Color::Yellow)),
789 Span::raw(": Back "),
790 Span::styled("PgUp/PgDn", Style::default().fg(Color::Yellow)),
791 Span::raw(": Scroll "),
792 Span::styled("↑/↓", Style::default().fg(Color::Yellow)),
793 Span::raw(": Prev/Next agent"),
794 ]));
795 f.render_widget(hints, chunks[2]);
796}
797
798fn truncate_str(s: &str, max: usize) -> String {
800 if s.len() <= max {
801 s.replace('\n', " ")
802 } else {
803 let mut end = max;
804 while end > 0 && !s.is_char_boundary(end) {
805 end -= 1;
806 }
807 format!("{}...", s[..end].replace('\n', " "))
808 }
809}
810
811fn render_stats(f: &mut Frame, state: &SwarmViewState, area: Rect) {
812 let content = if let Some(ref stats) = state.stats {
813 Line::from(vec![
814 Span::styled("Time: ", Style::default().fg(Color::DarkGray)),
815 Span::styled(
816 format!("{:.1}s", stats.execution_time_ms as f64 / 1000.0),
817 Style::default().fg(Color::White),
818 ),
819 Span::raw(" "),
820 Span::styled("Speedup: ", Style::default().fg(Color::DarkGray)),
821 Span::styled(
822 format!("{:.1}x", stats.speedup_factor),
823 Style::default().fg(Color::Green),
824 ),
825 Span::raw(" "),
826 Span::styled("Tool calls: ", Style::default().fg(Color::DarkGray)),
827 Span::styled(
828 format!("{}", stats.total_tool_calls),
829 Style::default().fg(Color::White),
830 ),
831 Span::raw(" "),
832 Span::styled("Critical path: ", Style::default().fg(Color::DarkGray)),
833 Span::styled(
834 format!("{}", stats.critical_path_length),
835 Style::default().fg(Color::White),
836 ),
837 ])
838 } else if let Some(ref err) = state.error {
839 Line::from(vec![Span::styled(
840 format!("Error: {}", err),
841 Style::default().fg(Color::Red),
842 )])
843 } else {
844 Line::from(vec![Span::styled(
845 "Executing...",
846 Style::default().fg(Color::DarkGray),
847 )])
848 };
849
850 let paragraph = Paragraph::new(content)
851 .block(Block::default().borders(Borders::ALL).title(" Stats "))
852 .wrap(Wrap { trim: true });
853
854 f.render_widget(paragraph, area);
855}