1use 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#[derive(Debug, Clone)]
16pub enum SwarmEvent {
17 Started {
19 task: String,
20 total_subtasks: usize,
21 },
22 Decomposed {
24 subtasks: Vec<SubTaskInfo>,
25 },
26 SubTaskUpdate {
28 id: String,
29 name: String,
30 status: SubTaskStatus,
31 agent_name: Option<String>,
32 },
33 AgentStarted {
35 subtask_id: String,
36 agent_name: String,
37 specialty: String,
38 },
39 AgentToolCall {
41 subtask_id: String,
42 tool_name: String,
43 },
44 AgentComplete {
46 subtask_id: String,
47 success: bool,
48 steps: usize,
49 },
50 StageComplete {
52 stage: usize,
53 completed: usize,
54 failed: usize,
55 },
56 Complete {
58 success: bool,
59 stats: SwarmStats,
60 },
61 Error(String),
63}
64
65#[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#[derive(Debug, Default)]
81pub struct SwarmViewState {
82 pub active: bool,
84 pub task: String,
86 pub subtasks: Vec<SubTaskInfo>,
88 pub current_stage: usize,
90 pub total_stages: usize,
92 pub stats: Option<SwarmStats>,
94 pub error: Option<String>,
96 pub scroll: usize,
98 pub complete: bool,
100}
101
102impl SwarmViewState {
103 pub fn new() -> Self {
104 Self::default()
105 }
106
107 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 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 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 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
200pub 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), Constraint::Length(3), Constraint::Min(1), Constraint::Length(3), ])
210 .split(area);
211
212 render_header(f, state, chunks[0]);
214
215 render_stage_progress(f, state, chunks[1]);
217
218 render_subtask_list(f, state, chunks[2]);
220
221 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 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 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}