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(
278 &mut task.output,
279 &output,
280 "swarm output",
281 );
282 }
283 }
284 SwarmEvent::AgentError { subtask_id, error } => {
285 if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
286 task.error = Some(error);
287 }
288 }
289 SwarmEvent::StageComplete { stage, .. } => {
290 self.current_stage = stage + 1;
291 }
292 SwarmEvent::Complete { success: _, stats } => {
293 self.stats = Some(stats);
294 self.complete = true;
295 }
296 SwarmEvent::Error(err) => {
297 self.error = Some(err);
298 }
299 }
300 }
301
302 pub fn select_prev(&mut self) {
304 if self.subtasks.is_empty() {
305 return;
306 }
307 self.selected_index = self.selected_index.saturating_sub(1);
308 self.list_state.select(Some(self.selected_index));
309 }
310
311 pub fn select_next(&mut self) {
313 if self.subtasks.is_empty() {
314 return;
315 }
316 self.selected_index = (self.selected_index + 1).min(self.subtasks.len() - 1);
317 self.list_state.select(Some(self.selected_index));
318 }
319
320 pub fn enter_detail(&mut self) {
322 if !self.subtasks.is_empty() {
323 self.detail_mode = true;
324 self.detail_scroll = 0;
325 }
326 }
327
328 pub fn exit_detail(&mut self) {
330 self.detail_mode = false;
331 self.detail_scroll = 0;
332 }
333
334 pub fn detail_scroll_down(&mut self, amount: usize) {
336 self.detail_scroll = self.detail_scroll.saturating_add(amount);
337 }
338
339 pub fn detail_scroll_up(&mut self, amount: usize) {
341 self.detail_scroll = self.detail_scroll.saturating_sub(amount);
342 }
343
344 pub fn selected_subtask(&self) -> Option<&SubTaskInfo> {
346 self.subtasks.get(self.selected_index)
347 }
348
349 pub fn status_counts(&self) -> (usize, usize, usize, usize) {
351 let mut pending = 0;
352 let mut running = 0;
353 let mut completed = 0;
354 let mut failed = 0;
355
356 for task in &self.subtasks {
357 match task.status {
358 SubTaskStatus::Pending | SubTaskStatus::Blocked => pending += 1,
359 SubTaskStatus::Running => running += 1,
360 SubTaskStatus::Completed => completed += 1,
361 SubTaskStatus::Failed | SubTaskStatus::Cancelled | SubTaskStatus::TimedOut => {
362 failed += 1
363 }
364 }
365 }
366
367 (pending, running, completed, failed)
368 }
369
370 pub fn progress(&self) -> f64 {
372 if self.subtasks.is_empty() {
373 return 0.0;
374 }
375 let (_, _, completed, failed) = self.status_counts();
376 ((completed + failed) as f64 / self.subtasks.len() as f64) * 100.0
377 }
378}
379
380pub fn render_swarm_view(f: &mut Frame, state: &mut SwarmViewState, area: Rect) {
382 if state.detail_mode {
384 render_agent_detail(f, state, area);
385 return;
386 }
387
388 let chunks = Layout::default()
389 .direction(Direction::Vertical)
390 .constraints([
391 Constraint::Length(3), Constraint::Length(3), Constraint::Min(1), Constraint::Length(3), ])
396 .split(area);
397
398 render_header(f, state, chunks[0]);
400
401 render_stage_progress(f, state, chunks[1]);
403
404 render_subtask_list(f, state, chunks[2]);
406
407 render_stats(f, state, chunks[3]);
409}
410
411fn render_header(f: &mut Frame, state: &SwarmViewState, area: Rect) {
412 let (pending, running, completed, failed) = state.status_counts();
413 let total = state.subtasks.len();
414
415 let title = if state.complete {
416 if state.error.is_some() {
417 " Swarm [ERROR] "
418 } else {
419 " Swarm [COMPLETE] "
420 }
421 } else if state.active {
422 " Swarm [ACTIVE] "
423 } else {
424 " Swarm [IDLE] "
425 };
426
427 let status_line = Line::from(vec![
428 Span::styled("Task: ", Style::default().fg(Color::DarkGray)),
429 Span::styled(
430 truncate_str(&state.task, 47),
431 Style::default().fg(Color::White),
432 ),
433 Span::raw(" "),
434 Span::styled(format!("⏳{}", pending), Style::default().fg(Color::Yellow)),
435 Span::raw(" "),
436 Span::styled(format!("⚡{}", running), Style::default().fg(Color::Cyan)),
437 Span::raw(" "),
438 Span::styled(format!("✓{}", completed), Style::default().fg(Color::Green)),
439 Span::raw(" "),
440 Span::styled(format!("✗{}", failed), Style::default().fg(Color::Red)),
441 Span::raw(" "),
442 Span::styled(format!("/{}", total), Style::default().fg(Color::DarkGray)),
443 ]);
444
445 let paragraph = Paragraph::new(status_line).block(
446 Block::default()
447 .borders(Borders::ALL)
448 .title(title)
449 .border_style(Style::default().fg(if state.complete {
450 if state.error.is_some() {
451 Color::Red
452 } else {
453 Color::Green
454 }
455 } else {
456 Color::Cyan
457 })),
458 );
459
460 f.render_widget(paragraph, area);
461}
462
463fn render_stage_progress(f: &mut Frame, state: &SwarmViewState, area: Rect) {
464 let progress = state.progress();
465 let label = format!(
466 "Stage {}/{} - {:.0}%",
467 state.current_stage.min(state.total_stages),
468 state.total_stages,
469 progress
470 );
471
472 let gauge = Gauge::default()
473 .block(Block::default().borders(Borders::ALL).title(" Progress "))
474 .gauge_style(Style::default().fg(Color::Cyan).bg(Color::DarkGray))
475 .percent(progress as u16)
476 .label(label);
477
478 f.render_widget(gauge, area);
479}
480
481fn render_subtask_list(f: &mut Frame, state: &mut SwarmViewState, area: Rect) {
482 state.list_state.select(Some(state.selected_index));
484
485 let items: Vec<ListItem> = state
486 .subtasks
487 .iter()
488 .map(|task| {
489 let (icon, color) = match task.status {
490 SubTaskStatus::Pending => ("○", Color::DarkGray),
491 SubTaskStatus::Blocked => ("⊘", Color::Yellow),
492 SubTaskStatus::Running => ("●", Color::Cyan),
493 SubTaskStatus::Completed => ("✓", Color::Green),
494 SubTaskStatus::Failed => ("✗", Color::Red),
495 SubTaskStatus::Cancelled => ("⊗", Color::DarkGray),
496 SubTaskStatus::TimedOut => ("⏱", Color::Red),
497 };
498
499 let mut spans = vec![
500 Span::styled(format!("{} ", icon), Style::default().fg(color)),
501 Span::styled(
502 format!("[S{}] ", task.stage),
503 Style::default().fg(Color::DarkGray),
504 ),
505 Span::styled(&task.name, Style::default().fg(Color::White)),
506 ];
507
508 if task.status == SubTaskStatus::Running {
510 if let Some(ref agent) = task.agent_name {
511 spans.push(Span::styled(
512 format!(" → {}", agent),
513 Style::default().fg(Color::Cyan),
514 ));
515 }
516 if let Some(ref tool) = task.current_tool {
517 spans.push(Span::styled(
518 format!(" [{}]", tool),
519 Style::default()
520 .fg(Color::Yellow)
521 .add_modifier(Modifier::DIM),
522 ));
523 }
524 }
525
526 if task.steps > 0 {
528 spans.push(Span::styled(
529 format!(" ({}/{})", task.steps, task.max_steps),
530 Style::default().fg(Color::DarkGray),
531 ));
532 }
533
534 ListItem::new(Line::from(spans))
535 })
536 .collect();
537
538 let title = if state.subtasks.is_empty() {
539 " SubTasks (none yet) "
540 } else {
541 " SubTasks (↑↓:select Enter:detail) "
542 };
543
544 let list = List::new(items)
545 .block(Block::default().borders(Borders::ALL).title(title))
546 .highlight_style(
547 Style::default()
548 .add_modifier(Modifier::BOLD)
549 .bg(Color::DarkGray),
550 )
551 .highlight_symbol("▶ ");
552
553 f.render_stateful_widget(list, area, &mut state.list_state);
554}
555
556fn render_agent_detail(f: &mut Frame, state: &SwarmViewState, area: Rect) {
558 let task = match state.selected_subtask() {
559 Some(t) => t,
560 None => {
561 let p = Paragraph::new("No subtask selected").block(
562 Block::default()
563 .borders(Borders::ALL)
564 .title(" Agent Detail "),
565 );
566 f.render_widget(p, area);
567 return;
568 }
569 };
570
571 let chunks = Layout::default()
572 .direction(Direction::Vertical)
573 .constraints([
574 Constraint::Length(5), Constraint::Min(1), Constraint::Length(1), ])
578 .split(area);
579
580 let (status_icon, status_color) = match task.status {
582 SubTaskStatus::Pending => ("○ Pending", Color::DarkGray),
583 SubTaskStatus::Blocked => ("⊘ Blocked", Color::Yellow),
584 SubTaskStatus::Running => ("● Running", Color::Cyan),
585 SubTaskStatus::Completed => ("✓ Completed", Color::Green),
586 SubTaskStatus::Failed => ("✗ Failed", Color::Red),
587 SubTaskStatus::Cancelled => ("⊗ Cancelled", Color::DarkGray),
588 SubTaskStatus::TimedOut => ("⏱ Timed Out", Color::Red),
589 };
590
591 let agent_label = task.agent_name.as_deref().unwrap_or("(unassigned)");
592 let header_lines = vec![
593 Line::from(vec![
594 Span::styled("Task: ", Style::default().fg(Color::DarkGray)),
595 Span::styled(
596 &task.name,
597 Style::default()
598 .fg(Color::White)
599 .add_modifier(Modifier::BOLD),
600 ),
601 ]),
602 Line::from(vec![
603 Span::styled("Agent: ", Style::default().fg(Color::DarkGray)),
604 Span::styled(agent_label, Style::default().fg(Color::Cyan)),
605 Span::raw(" "),
606 Span::styled("Status: ", Style::default().fg(Color::DarkGray)),
607 Span::styled(status_icon, Style::default().fg(status_color)),
608 Span::raw(" "),
609 Span::styled("Stage: ", Style::default().fg(Color::DarkGray)),
610 Span::styled(format!("{}", task.stage), Style::default().fg(Color::White)),
611 Span::raw(" "),
612 Span::styled("Steps: ", Style::default().fg(Color::DarkGray)),
613 Span::styled(
614 format!("{}/{}", task.steps, task.max_steps),
615 Style::default().fg(Color::White),
616 ),
617 ]),
618 Line::from(vec![
619 Span::styled("Deps: ", Style::default().fg(Color::DarkGray)),
620 Span::styled(
621 if task.dependencies.is_empty() {
622 "none".to_string()
623 } else {
624 task.dependencies.join(", ")
625 },
626 Style::default().fg(Color::DarkGray),
627 ),
628 ]),
629 ];
630
631 let title = format!(" Agent Detail: {} ", task.id);
632 let header = Paragraph::new(header_lines).block(
633 Block::default()
634 .borders(Borders::ALL)
635 .title(title)
636 .border_style(Style::default().fg(status_color)),
637 );
638 f.render_widget(header, chunks[0]);
639
640 let mut content_lines: Vec<Line> = Vec::new();
642
643 if !task.tool_call_history.is_empty() {
645 content_lines.push(Line::from(Span::styled(
646 "─── Tool Call History ───",
647 Style::default()
648 .fg(Color::Cyan)
649 .add_modifier(Modifier::BOLD),
650 )));
651 content_lines.push(Line::from(""));
652
653 for (i, tc) in task.tool_call_history.iter().enumerate() {
654 let icon = if tc.success { "✓" } else { "✗" };
655 let icon_color = if tc.success { Color::Green } else { Color::Red };
656 content_lines.push(Line::from(vec![
657 Span::styled(format!(" {icon} "), Style::default().fg(icon_color)),
658 Span::styled(format!("#{} ", i + 1), Style::default().fg(Color::DarkGray)),
659 Span::styled(
660 &tc.tool_name,
661 Style::default()
662 .fg(Color::Yellow)
663 .add_modifier(Modifier::BOLD),
664 ),
665 ]));
666 if !tc.input_preview.is_empty() {
667 content_lines.push(Line::from(vec![
668 Span::raw(" "),
669 Span::styled("in: ", Style::default().fg(Color::DarkGray)),
670 Span::styled(
671 truncate_str(&tc.input_preview, 80),
672 Style::default().fg(Color::White),
673 ),
674 ]));
675 }
676 if !tc.output_preview.is_empty() {
677 content_lines.push(Line::from(vec![
678 Span::raw(" "),
679 Span::styled("out: ", Style::default().fg(Color::DarkGray)),
680 Span::styled(
681 truncate_str(&tc.output_preview, 80),
682 Style::default().fg(Color::White),
683 ),
684 ]));
685 }
686 }
687 content_lines.push(Line::from(""));
688 } else if task.steps > 0 {
689 content_lines.push(Line::from(Span::styled(
691 format!("─── {} tool calls (no detail captured) ───", task.steps),
692 Style::default().fg(Color::DarkGray),
693 )));
694 content_lines.push(Line::from(""));
695 }
696
697 if !task.messages.is_empty() {
699 content_lines.push(Line::from(Span::styled(
700 "─── Conversation ───",
701 Style::default()
702 .fg(Color::Cyan)
703 .add_modifier(Modifier::BOLD),
704 )));
705 content_lines.push(Line::from(""));
706
707 for msg in &task.messages {
708 let (role_color, role_label) = match msg.role.as_str() {
709 "user" => (Color::White, "USER"),
710 "assistant" => (Color::Cyan, "ASST"),
711 "tool" => (Color::Yellow, "TOOL"),
712 "system" => (Color::DarkGray, "SYS "),
713 _ => (Color::White, " "),
714 };
715 content_lines.push(Line::from(vec![Span::styled(
716 format!(" [{role_label}] "),
717 Style::default().fg(role_color).add_modifier(Modifier::BOLD),
718 )]));
719 for line in msg.content.lines().take(10) {
721 content_lines.push(Line::from(vec![
722 Span::raw(" "),
723 Span::styled(line, Style::default().fg(Color::White)),
724 ]));
725 }
726 if msg.content.lines().count() > 10 {
727 content_lines.push(Line::from(vec![
728 Span::raw(" "),
729 Span::styled("... (truncated)", Style::default().fg(Color::DarkGray)),
730 ]));
731 }
732 content_lines.push(Line::from(""));
733 }
734 }
735
736 if let Some(ref output) = task.output {
738 content_lines.push(Line::from(Span::styled(
739 "─── Output ───",
740 Style::default()
741 .fg(Color::Green)
742 .add_modifier(Modifier::BOLD),
743 )));
744 content_lines.push(Line::from(""));
745 for line in output.lines().take(20) {
746 content_lines.push(Line::from(Span::styled(
747 line,
748 Style::default().fg(Color::White),
749 )));
750 }
751 if output.lines().count() > 20 {
752 content_lines.push(Line::from(Span::styled(
753 "... (truncated)",
754 Style::default().fg(Color::DarkGray),
755 )));
756 }
757 content_lines.push(Line::from(""));
758 }
759
760 if let Some(ref err) = task.error {
762 content_lines.push(Line::from(Span::styled(
763 "─── Error ───",
764 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
765 )));
766 content_lines.push(Line::from(""));
767 for line in err.lines() {
768 content_lines.push(Line::from(Span::styled(
769 line,
770 Style::default().fg(Color::Red),
771 )));
772 }
773 content_lines.push(Line::from(""));
774 }
775
776 if content_lines.is_empty() {
778 content_lines.push(Line::from(Span::styled(
779 " Waiting for agent activity...",
780 Style::default().fg(Color::DarkGray),
781 )));
782 }
783
784 let content = Paragraph::new(content_lines)
785 .block(Block::default().borders(Borders::ALL))
786 .wrap(Wrap { trim: false })
787 .scroll((state.detail_scroll as u16, 0));
788 f.render_widget(content, chunks[1]);
789
790 let hints = Paragraph::new(Line::from(vec![
792 Span::styled(" Esc", Style::default().fg(Color::Yellow)),
793 Span::raw(": Back "),
794 Span::styled("PgUp/PgDn", Style::default().fg(Color::Yellow)),
795 Span::raw(": Scroll "),
796 Span::styled("↑/↓", Style::default().fg(Color::Yellow)),
797 Span::raw(": Prev/Next agent"),
798 ]));
799 f.render_widget(hints, chunks[2]);
800}
801
802fn truncate_str(s: &str, max: usize) -> String {
804 if s.len() <= max {
805 s.replace('\n', " ")
806 } else {
807 let mut end = max;
808 while end > 0 && !s.is_char_boundary(end) {
809 end -= 1;
810 }
811 format!("{}...", s[..end].replace('\n', " "))
812 }
813}
814
815fn render_stats(f: &mut Frame, state: &SwarmViewState, area: Rect) {
816 let content = if let Some(ref stats) = state.stats {
817 Line::from(vec![
818 Span::styled("Time: ", Style::default().fg(Color::DarkGray)),
819 Span::styled(
820 format!("{:.1}s", stats.execution_time_ms as f64 / 1000.0),
821 Style::default().fg(Color::White),
822 ),
823 Span::raw(" "),
824 Span::styled("Speedup: ", Style::default().fg(Color::DarkGray)),
825 Span::styled(
826 format!("{:.1}x", stats.speedup_factor),
827 Style::default().fg(Color::Green),
828 ),
829 Span::raw(" "),
830 Span::styled("Tool calls: ", Style::default().fg(Color::DarkGray)),
831 Span::styled(
832 format!("{}", stats.total_tool_calls),
833 Style::default().fg(Color::White),
834 ),
835 Span::raw(" "),
836 Span::styled("Critical path: ", Style::default().fg(Color::DarkGray)),
837 Span::styled(
838 format!("{}", stats.critical_path_length),
839 Style::default().fg(Color::White),
840 ),
841 ])
842 } else if let Some(ref err) = state.error {
843 Line::from(vec![Span::styled(
844 format!("Error: {}", err),
845 Style::default().fg(Color::Red),
846 )])
847 } else {
848 Line::from(vec![Span::styled(
849 "Executing...",
850 Style::default().fg(Color::DarkGray),
851 )])
852 };
853
854 let paragraph = Paragraph::new(content)
855 .block(Block::default().borders(Borders::ALL).title(" Stats "))
856 .wrap(Wrap { trim: true });
857
858 f.render_widget(paragraph, area);
859}