Skip to main content

codetether_agent/tui/
swarm_view.rs

1//! Swarm mode view for the TUI
2//!
3//! Displays real-time status of parallel sub-agent execution.
4
5use crate::swarm::{SubTaskStatus, SwarmStats};
6use ratatui::{
7    layout::{Constraint, Direction, Layout, Rect},
8    style::{Color, Modifier, Style},
9    text::{Line, Span},
10    widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Wrap},
11    Frame,
12};
13
14/// Events emitted by swarm execution for TUI updates
15#[derive(Debug, Clone)]
16pub enum SwarmEvent {
17    /// Swarm execution started
18    Started {
19        task: String,
20        total_subtasks: usize,
21    },
22    /// Task decomposition complete
23    Decomposed {
24        subtasks: Vec<SubTaskInfo>,
25    },
26    /// SubTask status changed
27    SubTaskUpdate {
28        id: String,
29        name: String,
30        status: SubTaskStatus,
31        agent_name: Option<String>,
32    },
33    /// SubAgent started working
34    AgentStarted {
35        subtask_id: String,
36        agent_name: String,
37        specialty: String,
38    },
39    /// SubAgent made a tool call
40    AgentToolCall {
41        subtask_id: String,
42        tool_name: String,
43    },
44    /// SubAgent completed
45    AgentComplete {
46        subtask_id: String,
47        success: bool,
48        steps: usize,
49    },
50    /// Stage completed
51    StageComplete {
52        stage: usize,
53        completed: usize,
54        failed: usize,
55    },
56    /// Swarm execution complete
57    Complete {
58        success: bool,
59        stats: SwarmStats,
60    },
61    /// Error occurred
62    Error(String),
63}
64
65/// Information about a subtask for display
66#[derive(Debug, Clone)]
67pub struct SubTaskInfo {
68    pub id: String,
69    pub name: String,
70    pub status: SubTaskStatus,
71    pub stage: usize,
72    pub dependencies: Vec<String>,
73    pub agent_name: Option<String>,
74    pub current_tool: Option<String>,
75    pub steps: usize,
76    pub max_steps: usize,
77}
78
79/// State for the swarm view
80#[derive(Debug, Default)]
81pub struct SwarmViewState {
82    /// Whether swarm mode is active
83    pub active: bool,
84    /// The main task being executed
85    pub task: String,
86    /// All subtasks
87    pub subtasks: Vec<SubTaskInfo>,
88    /// Current stage (0-indexed)
89    pub current_stage: usize,
90    /// Total stages
91    pub total_stages: usize,
92    /// Stats from execution
93    pub stats: Option<SwarmStats>,
94    /// Any error message
95    pub error: Option<String>,
96    /// Scroll position in subtask list
97    pub scroll: usize,
98    /// Whether execution is complete
99    pub complete: bool,
100}
101
102impl SwarmViewState {
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    /// Handle a swarm event
108    pub fn handle_event(&mut self, event: SwarmEvent) {
109        match event {
110            SwarmEvent::Started { task, total_subtasks } => {
111                self.active = true;
112                self.task = task;
113                self.subtasks.clear();
114                self.current_stage = 0;
115                self.complete = false;
116                self.error = None;
117                // Pre-allocate
118                self.subtasks.reserve(total_subtasks);
119            }
120            SwarmEvent::Decomposed { subtasks } => {
121                self.subtasks = subtasks;
122                self.total_stages = self.subtasks.iter().map(|s| s.stage).max().unwrap_or(0) + 1;
123            }
124            SwarmEvent::SubTaskUpdate { id, name, status, agent_name } => {
125                if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == id) {
126                    task.status = status;
127                    task.name = name;
128                    if agent_name.is_some() {
129                        task.agent_name = agent_name;
130                    }
131                }
132            }
133            SwarmEvent::AgentStarted { subtask_id, agent_name, .. } => {
134                if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
135                    task.status = SubTaskStatus::Running;
136                    task.agent_name = Some(agent_name);
137                }
138            }
139            SwarmEvent::AgentToolCall { subtask_id, tool_name } => {
140                if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
141                    task.current_tool = Some(tool_name);
142                    task.steps += 1;
143                }
144            }
145            SwarmEvent::AgentComplete { subtask_id, success, steps } => {
146                if let Some(task) = self.subtasks.iter_mut().find(|t| t.id == subtask_id) {
147                    task.status = if success {
148                        SubTaskStatus::Completed
149                    } else {
150                        SubTaskStatus::Failed
151                    };
152                    task.steps = steps;
153                    task.current_tool = None;
154                }
155            }
156            SwarmEvent::StageComplete { stage, .. } => {
157                self.current_stage = stage + 1;
158            }
159            SwarmEvent::Complete { success: _, stats } => {
160                self.stats = Some(stats);
161                self.complete = true;
162            }
163            SwarmEvent::Error(err) => {
164                self.error = Some(err);
165            }
166        }
167    }
168
169    /// Get count of tasks by status
170    pub fn status_counts(&self) -> (usize, usize, usize, usize) {
171        let mut pending = 0;
172        let mut running = 0;
173        let mut completed = 0;
174        let mut failed = 0;
175
176        for task in &self.subtasks {
177            match task.status {
178                SubTaskStatus::Pending | SubTaskStatus::Blocked => pending += 1,
179                SubTaskStatus::Running => running += 1,
180                SubTaskStatus::Completed => completed += 1,
181                SubTaskStatus::Failed | SubTaskStatus::Cancelled | SubTaskStatus::TimedOut => {
182                    failed += 1
183                }
184            }
185        }
186
187        (pending, running, completed, failed)
188    }
189
190    /// Overall progress as percentage
191    pub fn progress(&self) -> f64 {
192        if self.subtasks.is_empty() {
193            return 0.0;
194        }
195        let (_, _, completed, failed) = self.status_counts();
196        ((completed + failed) as f64 / self.subtasks.len() as f64) * 100.0
197    }
198}
199
200/// Render the swarm view
201pub fn render_swarm_view(f: &mut Frame, state: &SwarmViewState, area: Rect) {
202    let chunks = Layout::default()
203        .direction(Direction::Vertical)
204        .constraints([
205            Constraint::Length(3), // Header with task + progress
206            Constraint::Length(3), // Stage progress
207            Constraint::Min(1),    // Subtask list
208            Constraint::Length(3), // Stats
209        ])
210        .split(area);
211
212    // Header
213    render_header(f, state, chunks[0]);
214
215    // Stage progress
216    render_stage_progress(f, state, chunks[1]);
217
218    // Subtask list
219    render_subtask_list(f, state, chunks[2]);
220
221    // Stats footer
222    render_stats(f, state, chunks[3]);
223}
224
225fn render_header(f: &mut Frame, state: &SwarmViewState, area: Rect) {
226    let (pending, running, completed, failed) = state.status_counts();
227    let total = state.subtasks.len();
228
229    let title = if state.complete {
230        if state.error.is_some() {
231            " Swarm [ERROR] "
232        } else {
233            " Swarm [COMPLETE] "
234        }
235    } else {
236        " Swarm [ACTIVE] "
237    };
238
239    let status_line = Line::from(vec![
240        Span::styled("Task: ", Style::default().fg(Color::DarkGray)),
241        Span::styled(
242            if state.task.len() > 50 {
243                format!("{}...", &state.task[..47])
244            } else {
245                state.task.clone()
246            },
247            Style::default().fg(Color::White),
248        ),
249        Span::raw("  "),
250        Span::styled(format!("⏳{}", pending), Style::default().fg(Color::Yellow)),
251        Span::raw(" "),
252        Span::styled(format!("⚡{}", running), Style::default().fg(Color::Cyan)),
253        Span::raw(" "),
254        Span::styled(format!("✓{}", completed), Style::default().fg(Color::Green)),
255        Span::raw(" "),
256        Span::styled(format!("✗{}", failed), Style::default().fg(Color::Red)),
257        Span::raw(" "),
258        Span::styled(
259            format!("/{}", total),
260            Style::default().fg(Color::DarkGray),
261        ),
262    ]);
263
264    let paragraph = Paragraph::new(status_line).block(
265        Block::default()
266            .borders(Borders::ALL)
267            .title(title)
268            .border_style(Style::default().fg(if state.complete {
269                if state.error.is_some() {
270                    Color::Red
271                } else {
272                    Color::Green
273                }
274            } else {
275                Color::Cyan
276            })),
277    );
278
279    f.render_widget(paragraph, area);
280}
281
282fn render_stage_progress(f: &mut Frame, state: &SwarmViewState, area: Rect) {
283    let progress = state.progress();
284    let label = format!(
285        "Stage {}/{} - {:.0}%",
286        state.current_stage.min(state.total_stages),
287        state.total_stages,
288        progress
289    );
290
291    let gauge = Gauge::default()
292        .block(Block::default().borders(Borders::ALL).title(" Progress "))
293        .gauge_style(Style::default().fg(Color::Cyan).bg(Color::DarkGray))
294        .percent(progress as u16)
295        .label(label);
296
297    f.render_widget(gauge, area);
298}
299
300fn render_subtask_list(f: &mut Frame, state: &SwarmViewState, area: Rect) {
301    let items: Vec<ListItem> = state
302        .subtasks
303        .iter()
304        .map(|task| {
305            let (icon, color) = match task.status {
306                SubTaskStatus::Pending => ("○", Color::DarkGray),
307                SubTaskStatus::Blocked => ("⊘", Color::Yellow),
308                SubTaskStatus::Running => ("●", Color::Cyan),
309                SubTaskStatus::Completed => ("✓", Color::Green),
310                SubTaskStatus::Failed => ("✗", Color::Red),
311                SubTaskStatus::Cancelled => ("⊗", Color::DarkGray),
312                SubTaskStatus::TimedOut => ("⏱", Color::Red),
313            };
314
315            let mut spans = vec![
316                Span::styled(format!("{} ", icon), Style::default().fg(color)),
317                Span::styled(
318                    format!("[S{}] ", task.stage),
319                    Style::default().fg(Color::DarkGray),
320                ),
321                Span::styled(&task.name, Style::default().fg(Color::White)),
322            ];
323
324            // Show agent/tool info for running tasks
325            if task.status == SubTaskStatus::Running {
326                if let Some(ref agent) = task.agent_name {
327                    spans.push(Span::styled(
328                        format!(" → {}", agent),
329                        Style::default().fg(Color::Cyan),
330                    ));
331                }
332                if let Some(ref tool) = task.current_tool {
333                    spans.push(Span::styled(
334                        format!(" [{}]", tool),
335                        Style::default()
336                            .fg(Color::Yellow)
337                            .add_modifier(Modifier::DIM),
338                    ));
339                }
340            }
341
342            // Show step count
343            if task.steps > 0 {
344                spans.push(Span::styled(
345                    format!(" ({}/{})", task.steps, task.max_steps),
346                    Style::default().fg(Color::DarkGray),
347                ));
348            }
349
350            ListItem::new(Line::from(spans))
351        })
352        .collect();
353
354    let list = List::new(items)
355        .block(
356            Block::default()
357                .borders(Borders::ALL)
358                .title(" SubTasks "),
359        )
360        .highlight_style(Style::default().add_modifier(Modifier::BOLD));
361
362    f.render_widget(list, area);
363}
364
365fn render_stats(f: &mut Frame, state: &SwarmViewState, area: Rect) {
366    let content = if let Some(ref stats) = state.stats {
367        Line::from(vec![
368            Span::styled("Time: ", Style::default().fg(Color::DarkGray)),
369            Span::styled(
370                format!("{:.1}s", stats.execution_time_ms as f64 / 1000.0),
371                Style::default().fg(Color::White),
372            ),
373            Span::raw("  "),
374            Span::styled("Speedup: ", Style::default().fg(Color::DarkGray)),
375            Span::styled(
376                format!("{:.1}x", stats.speedup_factor),
377                Style::default().fg(Color::Green),
378            ),
379            Span::raw("  "),
380            Span::styled("Tool calls: ", Style::default().fg(Color::DarkGray)),
381            Span::styled(
382                format!("{}", stats.total_tool_calls),
383                Style::default().fg(Color::White),
384            ),
385            Span::raw("  "),
386            Span::styled("Critical path: ", Style::default().fg(Color::DarkGray)),
387            Span::styled(
388                format!("{}", stats.critical_path_length),
389                Style::default().fg(Color::White),
390            ),
391        ])
392    } else if let Some(ref err) = state.error {
393        Line::from(vec![Span::styled(
394            format!("Error: {}", err),
395            Style::default().fg(Color::Red),
396        )])
397    } else {
398        Line::from(vec![Span::styled(
399            "Executing...",
400            Style::default().fg(Color::DarkGray),
401        )])
402    };
403
404    let paragraph = Paragraph::new(content)
405        .block(Block::default().borders(Borders::ALL).title(" Stats "))
406        .wrap(Wrap { trim: true });
407
408    f.render_widget(paragraph, area);
409}