1use ratatui::{
6 layout::Rect,
7 style::{Modifier, Style, Stylize},
8 text::{Line, Span},
9 widgets::{Block, BorderType, Borders, List, ListItem, Padding, Paragraph},
10 Frame,
11};
12
13use super::app::{App, FocusedPanel, WaveTaskState};
14use super::theme::*;
15
16pub fn render_waves_panel(frame: &mut Frame, area: Rect, app: &App) {
18 let is_focused = app.focused_panel == FocusedPanel::Waves;
19 let border_color = if is_focused {
20 BORDER_ACTIVE
21 } else {
22 BORDER_DEFAULT
23 };
24 let title_color = if is_focused { ACCENT } else { TEXT_MUTED };
25
26 let ready_count = app.ready_task_count();
27 let selected_count = app.selected_task_count();
28 let title = if app.swarm_mode {
29 let wave_count = app.waves.len();
31 format!(" SWARM Waves ({} waves) ", wave_count)
32 } else if selected_count > 0 {
33 format!(
34 " Waves & Tasks ({} selected / {} ready) ",
35 selected_count, ready_count
36 )
37 } else {
38 format!(" Waves & Tasks ({} ready) ", ready_count)
39 };
40
41 let block = Block::default()
42 .borders(Borders::ALL)
43 .border_type(BorderType::Rounded)
44 .border_style(Style::default().fg(border_color))
45 .title(Line::from(title).fg(title_color))
46 .style(Style::default().bg(BG_SECONDARY))
47 .padding(Padding::new(1, 1, 0, 0));
48
49 if app.waves.is_empty() {
50 let empty_msg = Paragraph::new("No actionable tasks")
51 .style(Style::default().fg(TEXT_MUTED))
52 .block(block);
53 frame.render_widget(empty_msg, area);
54 return;
55 }
56
57 let mut all_items: Vec<ListItem> = Vec::new();
59 let mut task_index = 0;
60
61 for wave in &app.waves {
62 let wave_header = if app.swarm_mode {
64 build_swarm_wave_header(app, wave)
65 } else {
66 build_spawn_wave_header(wave)
67 };
68 all_items.push(ListItem::new(wave_header));
69
70 for task in &wave.tasks {
72 let is_selected_in_list = task_index == app.wave_task_index && is_focused;
73 let is_selected_for_spawn = app.selected_tasks.contains(&task.id);
74
75 let line = build_task_line(task, is_selected_in_list, is_selected_for_spawn);
76 all_items.push(ListItem::new(line));
77 task_index += 1;
78 }
79 }
80
81 let visible_items: Vec<ListItem> = all_items.into_iter().skip(app.wave_scroll_offset).collect();
83
84 let list = List::new(visible_items).block(block);
85 frame.render_widget(list, area);
86}
87
88fn build_swarm_wave_header(app: &App, wave: &super::app::Wave) -> Line<'static> {
90 let validation_info = if let Some(ref swarm) = app.swarm_session_data {
92 if let Some(wave_state) = swarm.waves.iter().find(|w| w.wave_number == wave.number) {
93 match &wave_state.validation {
94 Some(v) if v.all_passed => ("✓", STATUS_COMPLETED, "VALIDATED"),
95 Some(_) => ("✗", FAILED_VALIDATION_RED, "FAILED"),
96 None if wave_state.completed_at.is_some() => ("◌", TEXT_MUTED, "NO VALIDATION"),
97 None => ("●", STATUS_RUNNING, "IN PROGRESS"),
98 }
99 } else {
100 ("◌", TEXT_MUTED, "PENDING")
101 }
102 } else {
103 ("◌", TEXT_MUTED, "PENDING")
104 };
105
106 let round_count = if let Some(ref swarm) = app.swarm_session_data {
107 swarm
108 .waves
109 .iter()
110 .find(|w| w.wave_number == wave.number)
111 .map(|w| w.rounds.len())
112 .unwrap_or(0)
113 } else {
114 0
115 };
116
117 let repair_count = if let Some(ref swarm) = app.swarm_session_data {
118 swarm
119 .waves
120 .iter()
121 .find(|w| w.wave_number == wave.number)
122 .map(|w| w.repairs.len())
123 .unwrap_or(0)
124 } else {
125 0
126 };
127
128 let repair_info = if repair_count > 0 {
129 format!(" [{} repairs]", repair_count)
130 } else {
131 String::new()
132 };
133
134 Line::from(vec![
135 Span::styled(
136 format!("Wave {} ", wave.number),
137 Style::default().fg(ACCENT).add_modifier(Modifier::BOLD),
138 ),
139 Span::styled(
140 format!("{} ", validation_info.0),
141 Style::default().fg(validation_info.1),
142 ),
143 Span::styled(
144 format!("{} ", validation_info.2),
145 Style::default().fg(validation_info.1),
146 ),
147 Span::styled(
148 format!(
149 "({} tasks, {} rounds{})",
150 wave.tasks.len(),
151 round_count,
152 repair_info
153 ),
154 Style::default().fg(TEXT_MUTED),
155 ),
156 ])
157}
158
159fn build_spawn_wave_header(wave: &super::app::Wave) -> Line<'static> {
161 let ready_in_wave = wave
162 .tasks
163 .iter()
164 .filter(|t| t.state == WaveTaskState::Ready)
165 .count();
166
167 Line::from(vec![
168 Span::styled(
169 format!("Wave {} ", wave.number),
170 Style::default().fg(ACCENT).add_modifier(Modifier::BOLD),
171 ),
172 Span::styled(
173 format!("({} tasks, {} ready)", wave.tasks.len(), ready_in_wave),
174 Style::default().fg(TEXT_MUTED),
175 ),
176 ])
177}
178
179fn build_task_line(
181 task: &super::app::WaveTask,
182 is_selected_in_list: bool,
183 is_selected_for_spawn: bool,
184) -> Line<'static> {
185 let state_icon = match task.state {
186 WaveTaskState::Ready => ("○", STATUS_COMPLETED), WaveTaskState::Running => ("●", STATUS_RUNNING), WaveTaskState::Done => ("✓", STATUS_COMPLETED), WaveTaskState::Blocked => ("◌", TEXT_MUTED), WaveTaskState::InProgress => ("◐", STATUS_RUNNING), };
192
193 let checkbox = if is_selected_for_spawn {
194 "[x]"
195 } else if task.state == WaveTaskState::Ready {
196 "[ ]"
197 } else {
198 " "
199 };
200
201 let max_len = 40;
203 let title_display = if task.title.len() > max_len {
204 format!("{}…", &task.title[..max_len - 1])
205 } else {
206 task.title.clone()
207 };
208
209 let complexity = if task.complexity > 0 {
210 format!(" [{}]", task.complexity)
211 } else {
212 String::new()
213 };
214
215 Line::from(vec![
216 Span::styled(
217 if is_selected_in_list { "▸ " } else { " " },
218 Style::default().fg(ACCENT),
219 ),
220 Span::styled(
221 format!("{} ", checkbox),
222 Style::default().fg(if is_selected_for_spawn {
223 ACCENT
224 } else {
225 TEXT_MUTED
226 }),
227 ),
228 Span::styled(
229 format!("{} ", state_icon.0),
230 Style::default().fg(state_icon.1),
231 ),
232 Span::styled(format!("{} ", task.id), Style::default().fg(TEXT_MUTED)),
233 Span::styled(
234 title_display,
235 Style::default()
236 .fg(if is_selected_in_list {
237 ACCENT
238 } else {
239 TEXT_PRIMARY
240 })
241 .add_modifier(if is_selected_in_list {
242 Modifier::BOLD
243 } else {
244 Modifier::empty()
245 }),
246 ),
247 Span::styled(complexity, Style::default().fg(TEXT_MUTED)),
248 ])
249}