1use anyhow::Result;
11use std::collections::{HashMap, HashSet};
12use std::path::PathBuf;
13use std::process::Command;
14use std::time::{Duration, Instant};
15
16use crate::commands::spawn::headless::StreamStore;
17use crate::commands::spawn::monitor::{
18 load_session, save_session, AgentState, AgentStatus, SpawnSession,
19};
20use crate::commands::swarm::session as swarm_session;
21use crate::models::phase::Phase;
22use crate::models::task::{Task, TaskStatus};
23use crate::storage::Storage;
24
25#[derive(Debug, Clone, PartialEq)]
27pub enum ViewMode {
28 Split,
30 Fullscreen,
32 Input,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
38pub enum FocusedPanel {
39 Waves,
40 Agents,
41 Output,
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub enum WaveTaskState {
47 Ready,
49 Running,
51 Done,
53 Blocked,
55 InProgress,
57}
58
59#[derive(Debug, Clone)]
61pub struct WaveTask {
62 pub id: String,
63 pub title: String,
64 pub tag: String,
65 pub state: WaveTaskState,
66 pub complexity: u32,
67 pub dependencies: Vec<String>,
68}
69
70#[derive(Debug, Clone)]
72pub struct Wave {
73 pub number: usize,
74 pub tasks: Vec<WaveTask>,
75}
76
77#[derive(Debug, Clone, Default)]
84pub struct SwarmProgress {
85 pub current_wave: usize,
87 pub total_waves: usize,
89 pub tasks_completed: usize,
91 pub tasks_total: usize,
93 pub tasks_in_progress: usize,
95 pub tasks_failed: usize,
97 pub waves_validated: usize,
99 pub waves_failed_validation: usize,
101 pub total_repairs: usize,
103}
104
105pub struct App {
107 pub project_root: Option<PathBuf>,
109 pub session_name: String,
111 pub session: Option<SpawnSession>,
113 pub selected: usize,
115 pub view_mode: ViewMode,
117 pub show_help: bool,
119 last_refresh: Instant,
121 refresh_interval: Duration,
123 pub error: Option<String>,
125 pub live_output: Vec<String>,
127 last_output_refresh: Instant,
129 output_refresh_interval: Duration,
131 pub input_buffer: String,
133 pub scroll_offset: usize,
135 pub auto_scroll: bool,
137
138 pub focused_panel: FocusedPanel,
141 pub waves: Vec<Wave>,
143 pub selected_tasks: HashSet<String>,
145 pub wave_task_index: usize,
147 pub wave_scroll_offset: usize,
149 pub agents_scroll_offset: usize,
151 pub active_tag: Option<String>,
153 phases: HashMap<String, Phase>,
155
156 pub ralph_mode: bool,
159 pub ralph_max_parallel: usize,
161 last_ralph_check: Instant,
163
164 pub swarm_mode: bool,
167 pub swarm_session_data: Option<swarm_session::SwarmSession>,
169 pub swarm_progress: Option<SwarmProgress>,
171
172 pub stream_store: Option<StreamStore>,
175}
176
177impl App {
178 pub fn new(
186 project_root: Option<PathBuf>,
187 session_name: &str,
188 swarm_mode: bool,
189 stream_store: Option<StreamStore>,
190 ) -> Result<Self> {
191 let storage = Storage::new(project_root.clone());
193 let active_tag = storage.get_active_group().ok().flatten();
194 let phases = storage.load_tasks().unwrap_or_default();
195
196 let mut app = Self {
197 project_root,
198 session_name: session_name.to_string(),
199 session: None,
200 selected: 0,
201 view_mode: ViewMode::Split,
202 show_help: false,
203 last_refresh: Instant::now(),
204 refresh_interval: Duration::from_secs(2),
205 error: None,
206 live_output: Vec::new(),
207 last_output_refresh: Instant::now(),
208 output_refresh_interval: Duration::from_millis(500),
209 input_buffer: String::new(),
210 scroll_offset: 0,
211 auto_scroll: true,
212 focused_panel: FocusedPanel::Waves,
214 waves: Vec::new(),
215 selected_tasks: HashSet::new(),
216 wave_task_index: 0,
217 wave_scroll_offset: 0,
218 agents_scroll_offset: 0,
219 active_tag,
220 phases,
221 ralph_mode: false,
223 ralph_max_parallel: 5,
224 last_ralph_check: Instant::now(),
225 swarm_mode,
227 swarm_session_data: None,
228 swarm_progress: None,
229 stream_store,
231 };
232 app.refresh()?;
233 app.refresh_waves();
234 app.refresh_live_output();
235 Ok(app)
236 }
237
238 pub fn refresh(&mut self) -> Result<()> {
240 if self.swarm_mode {
241 match swarm_session::load_session(self.project_root.as_ref(), &self.session_name) {
243 Ok(session) => {
244 self.swarm_session_data = Some(session);
245 self.error = None;
246 self.swarm_progress = self.compute_swarm_progress();
248 }
249 Err(e) => {
250 self.error = Some(format!("Failed to load swarm session: {}", e));
251 self.swarm_progress = None;
252 }
253 }
254 } else {
255 match load_session(self.project_root.as_ref(), &self.session_name) {
257 Ok(mut session) => {
258 self.refresh_agent_statuses(&mut session);
260
261 let _ = save_session(self.project_root.as_ref(), &session);
263
264 self.session = Some(session);
265 self.error = None;
266 }
267 Err(e) => {
268 self.error = Some(format!("Failed to load session: {}", e));
269 }
270 }
271 }
272 self.last_refresh = Instant::now();
273 Ok(())
274 }
275
276 pub fn refresh_live_output(&mut self) {
280 if let Some(ref store) = self.stream_store {
282 let agents = self.agents();
283 if agents.is_empty() || self.selected >= agents.len() {
284 self.live_output = vec!["No agent selected".to_string()];
285 return;
286 }
287
288 let agent = &agents[self.selected];
289 self.live_output = store.get_output(&agent.task_id, 100);
290
291 if self.live_output.is_empty() {
292 self.live_output = vec!["Waiting for output...".to_string()];
293 }
294
295 self.last_output_refresh = Instant::now();
296 return;
297 }
298
299 let agents = self.agents();
301 if agents.is_empty() || self.selected >= agents.len() {
302 self.live_output = vec!["No agent selected".to_string()];
303 return;
304 }
305
306 let agent = &agents[self.selected];
307 let session = match &self.session {
308 Some(s) => s,
309 None => {
310 self.live_output = vec!["No session loaded".to_string()];
311 return;
312 }
313 };
314
315 let tmux_windows = self.get_tmux_windows(&session.session_name);
317 let matching_window = tmux_windows.iter().find(|(_, name)| {
318 name.starts_with(&agent.window_name) || agent.window_name.starts_with(name)
319 });
320
321 let window_target = match matching_window {
322 Some((index, _)) => format!("{}:{}", session.session_name, index),
323 None => {
324 self.live_output = vec![format!("Window '{}' not found", agent.window_name)];
325 return;
326 }
327 };
328
329 let output = Command::new("tmux")
331 .args([
332 "capture-pane",
333 "-t",
334 &window_target,
335 "-p", "-S",
337 "-100", ])
339 .output();
340
341 match output {
342 Ok(out) if out.status.success() => {
343 let content = String::from_utf8_lossy(&out.stdout);
344 self.live_output = content.lines().map(|s| s.to_string()).collect();
345
346 while self
348 .live_output
349 .last()
350 .map(|s| s.trim().is_empty())
351 .unwrap_or(false)
352 {
353 self.live_output.pop();
354 }
355 }
356 Ok(out) => {
357 self.live_output = vec![format!("Error: {}", String::from_utf8_lossy(&out.stderr))];
358 }
359 Err(e) => {
360 self.live_output = vec![format!("tmux error: {}", e)];
361 }
362 }
363
364 self.last_output_refresh = Instant::now();
365 }
366
367 fn refresh_agent_statuses(&self, session: &mut SpawnSession) {
369 let tmux_windows = self.get_tmux_windows(&session.session_name);
370 let storage = Storage::new(self.project_root.clone());
371 let all_phases = storage.load_tasks().ok();
372
373 for agent in &mut session.agents {
374 let window_exists = tmux_windows.iter().any(|(_, name)| {
375 name.starts_with(&agent.window_name) || agent.window_name.starts_with(name)
376 });
377
378 let task_status = all_phases.as_ref().and_then(|phases| {
379 phases.get(&agent.tag).and_then(|phase| {
380 phase
381 .get_task(&agent.task_id)
382 .map(|task| task.status.clone())
383 })
384 });
385
386 agent.status = match (&task_status, window_exists) {
387 (Some(TaskStatus::Done), _) => AgentStatus::Completed,
388 (Some(TaskStatus::Blocked), _) => AgentStatus::Failed,
389 (Some(TaskStatus::InProgress), true) => AgentStatus::Running,
390 (Some(TaskStatus::InProgress), false) => AgentStatus::Completed,
391 (_, false) => AgentStatus::Completed,
392 (_, true) => AgentStatus::Running,
393 };
394 }
395 }
396
397 fn get_tmux_windows(&self, session_name: &str) -> Vec<(usize, String)> {
399 let output = Command::new("tmux")
400 .args([
401 "list-windows",
402 "-t",
403 session_name,
404 "-F",
405 "#{window_index}:#{window_name}",
406 ])
407 .output();
408
409 match output {
410 Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout)
411 .lines()
412 .filter_map(|line| {
413 let parts: Vec<&str> = line.splitn(2, ':').collect();
414 if parts.len() == 2 {
415 parts[0].parse().ok().map(|idx| (idx, parts[1].to_string()))
416 } else {
417 None
418 }
419 })
420 .collect(),
421 _ => Vec::new(),
422 }
423 }
424
425 pub fn tick(&mut self) -> Result<()> {
427 if self.last_refresh.elapsed() >= self.refresh_interval {
429 self.refresh()?;
430 self.refresh_waves();
431 }
432
433 if self.last_output_refresh.elapsed() >= self.output_refresh_interval {
435 self.refresh_live_output();
436 }
437
438 if self.ralph_mode && self.last_ralph_check.elapsed() >= Duration::from_secs(5) {
440 self.ralph_auto_spawn();
441 self.last_ralph_check = Instant::now();
442 }
443
444 Ok(())
445 }
446
447 pub fn toggle_ralph_mode(&mut self) {
449 self.ralph_mode = !self.ralph_mode;
450 if self.ralph_mode {
451 self.ralph_auto_spawn();
453 }
454 }
455
456 fn ralph_auto_spawn(&mut self) {
458 let running_count = self
460 .agents()
461 .iter()
462 .filter(|a| a.status == AgentStatus::Running || a.status == AgentStatus::Starting)
463 .count();
464
465 if running_count >= self.ralph_max_parallel {
466 return; }
468
469 let slots_available = self.ralph_max_parallel - running_count;
471 let mut tasks_to_spawn: Vec<String> = Vec::new();
472
473 for wave in &self.waves {
474 for task in &wave.tasks {
475 if task.state == WaveTaskState::Ready && !self.selected_tasks.contains(&task.id) {
476 let already_spawned = self.agents().iter().any(|a| a.task_id == task.id);
478 if !already_spawned {
479 tasks_to_spawn.push(task.id.clone());
480 if tasks_to_spawn.len() >= slots_available {
481 break;
482 }
483 }
484 }
485 }
486 if tasks_to_spawn.len() >= slots_available {
487 break;
488 }
489 }
490
491 for task_id in tasks_to_spawn {
493 let _ = self.spawn_task_with_ralph(&task_id);
494 }
495 }
496
497 pub fn agents(&self) -> &[AgentState] {
499 self.session
500 .as_ref()
501 .map(|s| s.agents.as_slice())
502 .unwrap_or(&[])
503 }
504
505 pub fn next_agent(&mut self) {
507 let len = self.agents().len();
508 if len > 0 {
509 self.selected = (self.selected + 1) % len;
510 self.adjust_agents_scroll();
511 self.reset_scroll();
512 self.refresh_live_output();
513 }
514 }
515
516 pub fn previous_agent(&mut self) {
518 let len = self.agents().len();
519 if len > 0 {
520 self.selected = if self.selected > 0 {
521 self.selected - 1
522 } else {
523 len - 1
524 };
525 self.adjust_agents_scroll();
526 self.reset_scroll();
527 self.refresh_live_output();
528 }
529 }
530
531 pub fn adjust_agents_scroll(&mut self) {
534 const VISIBLE_LINES: usize = 8;
535
536 if self.selected < self.agents_scroll_offset {
538 self.agents_scroll_offset = self.selected;
539 }
540 else if self.selected >= self.agents_scroll_offset + VISIBLE_LINES {
542 self.agents_scroll_offset = self.selected.saturating_sub(VISIBLE_LINES - 1);
543 }
544 }
545
546 pub fn toggle_fullscreen(&mut self) {
548 self.view_mode = match self.view_mode {
549 ViewMode::Split => ViewMode::Fullscreen,
550 ViewMode::Fullscreen => ViewMode::Split,
551 ViewMode::Input => ViewMode::Fullscreen,
552 };
553 }
554
555 pub fn exit_fullscreen(&mut self) {
557 self.view_mode = ViewMode::Split;
558 self.input_buffer.clear();
559 }
560
561 pub fn enter_input_mode(&mut self) {
563 self.view_mode = ViewMode::Input;
564 self.input_buffer.clear();
565 }
566
567 pub fn input_char(&mut self, c: char) {
569 self.input_buffer.push(c);
570 }
571
572 pub fn input_backspace(&mut self) {
574 self.input_buffer.pop();
575 }
576
577 pub fn send_input(&mut self) -> Result<()> {
579 if self.input_buffer.is_empty() {
580 return Ok(());
581 }
582
583 let session = match &self.session {
584 Some(s) => s,
585 None => {
586 self.error = Some("No session loaded".to_string());
587 return Ok(());
588 }
589 };
590
591 let agents = self.agents();
592 if agents.is_empty() || self.selected >= agents.len() {
593 self.error = Some("No agent selected".to_string());
594 return Ok(());
595 }
596
597 let agent = &agents[self.selected];
598
599 let tmux_windows = self.get_tmux_windows(&session.session_name);
601 let matching_window = tmux_windows.iter().find(|(_, name)| {
602 name.starts_with(&agent.window_name) || agent.window_name.starts_with(name)
603 });
604
605 let window_target = match matching_window {
606 Some((index, _)) => format!("{}:{}", session.session_name, index),
607 None => {
608 self.error = Some(format!("Window not found for {}", agent.task_id));
609 return Ok(());
610 }
611 };
612
613 let result = Command::new("tmux")
615 .args([
616 "send-keys",
617 "-t",
618 &window_target,
619 &self.input_buffer,
620 "Enter",
621 ])
622 .output();
623
624 match result {
625 Ok(out) if out.status.success() => {
626 self.error = None;
627 self.input_buffer.clear();
628 self.view_mode = ViewMode::Fullscreen; self.refresh_live_output();
630 }
631 Ok(out) => {
632 self.error = Some(format!(
633 "Send failed: {}",
634 String::from_utf8_lossy(&out.stderr)
635 ));
636 }
637 Err(e) => {
638 self.error = Some(format!("tmux error: {}", e));
639 }
640 }
641
642 Ok(())
643 }
644
645 pub fn restart_agent(&mut self) -> Result<()> {
647 let session = match &self.session {
648 Some(s) => s,
649 None => return Ok(()),
650 };
651
652 let agents = self.agents();
653 if agents.is_empty() || self.selected >= agents.len() {
654 return Ok(());
655 }
656
657 let agent = &agents[self.selected];
658
659 let tmux_windows = self.get_tmux_windows(&session.session_name);
661 let matching_window = tmux_windows.iter().find(|(_, name)| {
662 name.starts_with(&agent.window_name) || agent.window_name.starts_with(name)
663 });
664
665 if let Some((index, _)) = matching_window {
666 let target = format!("{}:{}", session.session_name, index);
667
668 let _ = Command::new("tmux")
670 .args(["send-keys", "-t", &target, "C-c"])
671 .output();
672
673 std::thread::sleep(Duration::from_millis(200));
675
676 let _ = Command::new("tmux")
678 .args([
679 "send-keys",
680 "-t",
681 &target,
682 "echo 'Agent restarted by user'",
683 "Enter",
684 ])
685 .output();
686
687 self.error = None;
688 self.refresh_live_output();
689 }
690
691 Ok(())
692 }
693
694 pub fn toggle_help(&mut self) {
696 self.show_help = !self.show_help;
697 }
698
699 pub fn scroll_up(&mut self, lines: usize) {
701 let max_scroll = self.live_output.len().saturating_sub(1);
702 self.scroll_offset = (self.scroll_offset + lines).min(max_scroll);
703 self.auto_scroll = false;
704 }
705
706 pub fn scroll_down(&mut self, lines: usize) {
708 self.scroll_offset = self.scroll_offset.saturating_sub(lines);
709 if self.scroll_offset == 0 {
710 self.auto_scroll = true;
711 }
712 }
713
714 pub fn scroll_to_bottom(&mut self) {
716 self.scroll_offset = 0;
717 self.auto_scroll = true;
718 }
719
720 fn reset_scroll(&mut self) {
722 self.scroll_offset = 0;
723 self.auto_scroll = true;
724 }
725
726 pub fn status_counts(&self) -> (usize, usize, usize, usize) {
728 let agents = self.agents();
729 let starting = agents
730 .iter()
731 .filter(|a| a.status == AgentStatus::Starting)
732 .count();
733 let running = agents
734 .iter()
735 .filter(|a| a.status == AgentStatus::Running)
736 .count();
737 let completed = agents
738 .iter()
739 .filter(|a| a.status == AgentStatus::Completed)
740 .count();
741 let failed = agents
742 .iter()
743 .filter(|a| a.status == AgentStatus::Failed)
744 .count();
745 (starting, running, completed, failed)
746 }
747
748 pub fn selected_agent(&self) -> Option<&AgentState> {
750 let agents = self.agents();
751 if agents.is_empty() || self.selected >= agents.len() {
752 None
753 } else {
754 Some(&agents[self.selected])
755 }
756 }
757
758 pub fn refresh_waves(&mut self) {
762 let storage = Storage::new(self.project_root.clone());
764 self.phases = storage.load_tasks().unwrap_or_default();
765
766 if self.swarm_mode {
768 self.waves = self.compute_swarm_waves();
769 self.swarm_progress = self.compute_swarm_progress();
771 return;
772 }
773
774 let running_task_ids: HashSet<String> = self
776 .agents()
777 .iter()
778 .filter(|a| a.status == AgentStatus::Running || a.status == AgentStatus::Starting)
779 .map(|a| a.task_id.clone())
780 .collect();
781
782 let tag = self.active_tag.clone().or_else(|| {
784 self.session.as_ref().map(|s| s.tag.clone())
786 });
787
788 let Some(tag) = tag else {
789 self.waves = Vec::new();
790 return;
791 };
792
793 let Some(phase) = self.phases.get(&tag) else {
794 self.waves = Vec::new();
795 return;
796 };
797
798 self.waves = self.compute_waves(phase, &running_task_ids);
800 }
801
802 fn compute_swarm_progress(&self) -> Option<SwarmProgress> {
807 let swarm = self.swarm_session_data.as_ref()?;
808 let phase = self.phases.get(&swarm.tag);
809
810 let total_waves = self.waves.len();
811
812 let current_wave = swarm
814 .waves
815 .iter()
816 .find(|w| w.completed_at.is_none())
817 .map(|w| w.wave_number)
818 .unwrap_or_else(|| swarm.waves.last().map(|w| w.wave_number).unwrap_or(0));
819
820 let (tasks_completed, tasks_in_progress, tasks_failed, tasks_total) =
822 if let Some(phase) = phase {
823 let swarm_task_ids: HashSet<String> =
825 swarm.waves.iter().flat_map(|w| w.all_task_ids()).collect();
826
827 let mut completed = 0;
828 let mut in_progress = 0;
829 let mut failed = 0;
830 let total = swarm_task_ids.len();
831
832 for task_id in &swarm_task_ids {
833 if let Some(task) = phase.get_task(task_id) {
834 match task.status {
835 TaskStatus::Done => completed += 1,
836 TaskStatus::InProgress => in_progress += 1,
837 TaskStatus::Blocked => failed += 1,
838 _ => {}
839 }
840 }
841 }
842
843 (completed, in_progress, failed, total)
844 } else {
845 let total = swarm.total_tasks();
847 let failed = swarm.total_failures();
848 (0, 0, failed, total)
849 };
850
851 let (waves_validated, waves_failed_validation) =
853 swarm
854 .waves
855 .iter()
856 .fold((0, 0), |(validated, failed), wave| match &wave.validation {
857 Some(v) if v.all_passed => (validated + 1, failed),
858 Some(_) => (validated, failed + 1),
859 None => (validated, failed),
860 });
861
862 let total_repairs: usize = swarm.waves.iter().map(|w| w.repairs.len()).sum();
864
865 Some(SwarmProgress {
866 current_wave,
867 total_waves,
868 tasks_completed,
869 tasks_total,
870 tasks_in_progress,
871 tasks_failed,
872 waves_validated,
873 waves_failed_validation,
874 total_repairs,
875 })
876 }
877
878 fn compute_swarm_waves(&self) -> Vec<Wave> {
880 let Some(ref swarm) = self.swarm_session_data else {
881 return Vec::new();
882 };
883
884 let tag = &swarm.tag;
885 let phase = self.phases.get(tag);
886
887 swarm
888 .waves
889 .iter()
890 .map(|wave_state| {
891 let task_ids: Vec<String> = wave_state
893 .rounds
894 .iter()
895 .flat_map(|round| round.task_ids.iter().cloned())
896 .collect();
897
898 let tasks: Vec<WaveTask> = task_ids
899 .iter()
900 .map(|task_id| {
901 let (title, complexity, dependencies, task_status) =
903 if let Some(phase) = phase {
904 if let Some(task) = phase.get_task(task_id) {
905 (
906 task.title.clone(),
907 task.complexity,
908 task.dependencies.clone(),
909 Some(task.status.clone()),
910 )
911 } else {
912 (task_id.clone(), 1, vec![], None)
913 }
914 } else {
915 (task_id.clone(), 1, vec![], None)
916 };
917
918 let state = match task_status {
920 Some(TaskStatus::Done) => WaveTaskState::Done,
921 Some(TaskStatus::InProgress) => WaveTaskState::Running,
922 Some(TaskStatus::Blocked) => WaveTaskState::Blocked,
923 Some(TaskStatus::Pending) => {
924 if wave_state.completed_at.is_some() {
925 WaveTaskState::Blocked
927 } else {
928 WaveTaskState::Ready
929 }
930 }
931 _ => WaveTaskState::Ready,
932 };
933
934 WaveTask {
935 id: task_id.clone(),
936 title,
937 tag: tag.clone(),
938 state,
939 complexity,
940 dependencies,
941 }
942 })
943 .collect();
944
945 Wave {
946 number: wave_state.wave_number,
947 tasks,
948 }
949 })
950 .collect()
951 }
952
953 fn compute_waves(&self, phase: &Phase, running_task_ids: &HashSet<String>) -> Vec<Wave> {
955 let mut actionable: Vec<&Task> = Vec::new();
957 for task in &phase.tasks {
958 if task.status == TaskStatus::Done
959 || task.status == TaskStatus::Expanded
960 || task.status == TaskStatus::Cancelled
961 {
962 continue;
963 }
964
965 if !task.subtasks.is_empty() {
967 continue;
968 }
969
970 if let Some(ref parent_id) = task.parent_id {
972 let parent_expanded = phase
973 .get_task(parent_id)
974 .map(|p| p.is_expanded())
975 .unwrap_or(false);
976 if !parent_expanded {
977 continue;
978 }
979 }
980
981 actionable.push(task);
982 }
983
984 if actionable.is_empty() {
985 return Vec::new();
986 }
987
988 let task_ids: HashSet<String> = actionable.iter().map(|t| t.id.clone()).collect();
990 let mut in_degree: HashMap<String, usize> = HashMap::new();
991 let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
992
993 for task in &actionable {
994 in_degree.entry(task.id.clone()).or_insert(0);
995
996 for dep in &task.dependencies {
997 if task_ids.contains(dep) {
998 *in_degree.entry(task.id.clone()).or_insert(0) += 1;
1000 dependents
1001 .entry(dep.clone())
1002 .or_default()
1003 .push(task.id.clone());
1004 } else {
1005 if !self.is_dependency_satisfied(dep, phase) {
1008 *in_degree.entry(task.id.clone()).or_insert(0) += 1000;
1010 }
1011 }
1012 }
1013 }
1014
1015 let mut waves: Vec<Wave> = Vec::new();
1017 let mut remaining = in_degree.clone();
1018 let mut wave_number = 1;
1019
1020 while !remaining.is_empty() {
1021 let mut ready: Vec<String> = remaining
1022 .iter()
1023 .filter(|(_, °)| deg == 0)
1024 .map(|(id, _)| id.clone())
1025 .collect();
1026
1027 if ready.is_empty() {
1028 break; }
1030
1031 ready.sort();
1033
1034 let mut wave_tasks: Vec<WaveTask> = ready
1036 .iter()
1037 .filter_map(|task_id| {
1038 actionable.iter().find(|t| &t.id == task_id).map(|task| {
1039 let state = if task.status == TaskStatus::Done {
1040 WaveTaskState::Done
1041 } else if running_task_ids.contains(&task.id) {
1042 WaveTaskState::Running
1043 } else if task.status == TaskStatus::InProgress {
1044 WaveTaskState::InProgress
1045 } else if task.status == TaskStatus::Blocked {
1046 WaveTaskState::Blocked
1047 } else if self.is_task_ready(task, phase) {
1048 WaveTaskState::Ready
1049 } else {
1050 WaveTaskState::Blocked
1051 };
1052
1053 WaveTask {
1054 id: task.id.clone(),
1055 title: task.title.clone(),
1056 tag: self.active_tag.clone().unwrap_or_default(),
1057 state,
1058 complexity: task.complexity,
1059 dependencies: task.dependencies.clone(),
1060 }
1061 })
1062 })
1063 .collect();
1064
1065 for task_id in &ready {
1067 remaining.remove(task_id);
1068 if let Some(deps) = dependents.get(task_id) {
1069 for dep_id in deps {
1070 if let Some(deg) = remaining.get_mut(dep_id) {
1071 *deg = deg.saturating_sub(1);
1072 }
1073 }
1074 }
1075 }
1076
1077 if !wave_tasks.is_empty() {
1078 wave_tasks.sort_by(|a, b| a.id.cmp(&b.id));
1080 waves.push(Wave {
1081 number: wave_number,
1082 tasks: wave_tasks,
1083 });
1084 }
1085 wave_number += 1;
1086 }
1087
1088 waves
1089 }
1090
1091 fn is_task_ready(&self, task: &Task, phase: &Phase) -> bool {
1093 if task.status != TaskStatus::Pending {
1094 return false;
1095 }
1096
1097 for dep_id in &task.dependencies {
1099 if !self.is_dependency_satisfied(dep_id, phase) {
1100 return false;
1101 }
1102 }
1103
1104 true
1105 }
1106
1107 fn is_dependency_satisfied(&self, dep_id: &str, phase: &Phase) -> bool {
1109 let Some(dep) = phase.get_task(dep_id) else {
1110 return true; };
1112
1113 match dep.status {
1114 TaskStatus::Done => true,
1115 TaskStatus::Expanded => {
1116 if dep.subtasks.is_empty() {
1118 false } else {
1120 dep.subtasks.iter().all(|subtask_id| {
1121 phase
1122 .get_task(subtask_id)
1123 .map(|st| st.status == TaskStatus::Done)
1124 .unwrap_or(false)
1125 })
1126 }
1127 }
1128 _ => false, }
1130 }
1131
1132 pub fn all_wave_tasks(&self) -> Vec<&WaveTask> {
1134 self.waves.iter().flat_map(|w| w.tasks.iter()).collect()
1135 }
1136
1137 pub fn selected_wave_task(&self) -> Option<&WaveTask> {
1139 let all_tasks = self.all_wave_tasks();
1140 all_tasks.get(self.wave_task_index).copied()
1141 }
1142
1143 pub fn next_panel(&mut self) {
1147 self.focused_panel = match self.focused_panel {
1148 FocusedPanel::Waves => FocusedPanel::Agents,
1149 FocusedPanel::Agents => FocusedPanel::Output,
1150 FocusedPanel::Output => FocusedPanel::Waves,
1151 };
1152 }
1153
1154 pub fn previous_panel(&mut self) {
1156 self.focused_panel = match self.focused_panel {
1157 FocusedPanel::Waves => FocusedPanel::Output,
1158 FocusedPanel::Agents => FocusedPanel::Waves,
1159 FocusedPanel::Output => FocusedPanel::Agents,
1160 };
1161 }
1162
1163 pub fn move_up(&mut self) {
1165 match self.focused_panel {
1166 FocusedPanel::Waves => {
1167 if self.wave_task_index > 0 {
1168 self.wave_task_index -= 1;
1169 self.adjust_wave_scroll();
1170 }
1171 }
1172 FocusedPanel::Agents => self.previous_agent(),
1173 FocusedPanel::Output => self.scroll_up(1),
1174 }
1175 }
1176
1177 pub fn move_down(&mut self) {
1179 match self.focused_panel {
1180 FocusedPanel::Waves => {
1181 let max = self.all_wave_tasks().len().saturating_sub(1);
1182 if self.wave_task_index < max {
1183 self.wave_task_index += 1;
1184 self.adjust_wave_scroll();
1185 }
1186 }
1187 FocusedPanel::Agents => self.next_agent(),
1188 FocusedPanel::Output => self.scroll_down(1),
1189 }
1190 }
1191
1192 fn adjust_wave_scroll(&mut self) {
1195 let mut line_idx = 0;
1198 let mut found = false;
1199 let mut task_counter = 0;
1200
1201 for wave in &self.waves {
1202 line_idx += 1; for _ in &wave.tasks {
1204 if task_counter == self.wave_task_index {
1205 found = true;
1206 break;
1207 }
1208 line_idx += 1;
1209 task_counter += 1;
1210 }
1211 if found {
1212 break;
1213 }
1214 }
1215
1216 let visible_height = 4;
1218
1219 if line_idx < self.wave_scroll_offset {
1221 self.wave_scroll_offset = line_idx;
1222 } else if line_idx >= self.wave_scroll_offset + visible_height {
1223 self.wave_scroll_offset = line_idx.saturating_sub(visible_height - 1);
1224 }
1225 }
1226
1227 pub fn toggle_task_selection(&mut self) {
1231 if let Some(task) = self.selected_wave_task() {
1232 let task_id = task.id.clone();
1233 if self.selected_tasks.contains(&task_id) {
1234 self.selected_tasks.remove(&task_id);
1235 } else {
1236 if task.state == WaveTaskState::Ready {
1238 self.selected_tasks.insert(task_id);
1239 }
1240 }
1241 }
1242 }
1243
1244 pub fn select_all_ready(&mut self) {
1246 for wave in &self.waves {
1247 for task in &wave.tasks {
1248 if task.state == WaveTaskState::Ready {
1249 self.selected_tasks.insert(task.id.clone());
1250 }
1251 }
1252 }
1253 }
1254
1255 pub fn clear_selection(&mut self) {
1257 self.selected_tasks.clear();
1258 }
1259
1260 pub fn ready_task_count(&self) -> usize {
1262 self.waves
1263 .iter()
1264 .flat_map(|w| &w.tasks)
1265 .filter(|t| t.state == WaveTaskState::Ready)
1266 .count()
1267 }
1268
1269 pub fn selected_task_count(&self) -> usize {
1271 self.selected_tasks.len()
1272 }
1273
1274 pub fn get_selected_tasks(&self) -> Vec<&WaveTask> {
1276 self.all_wave_tasks()
1277 .into_iter()
1278 .filter(|t| self.selected_tasks.contains(&t.id))
1279 .collect()
1280 }
1281
1282 pub fn spawn_selected_tasks(&mut self) -> Result<usize> {
1285 use crate::commands::spawn::{agent, terminal};
1286
1287 let tasks_to_spawn: Vec<(String, String, String)> = self
1288 .get_selected_tasks()
1289 .iter()
1290 .map(|t| (t.id.clone(), t.title.clone(), t.tag.clone()))
1291 .collect();
1292
1293 if tasks_to_spawn.is_empty() {
1294 return Ok(0);
1295 }
1296
1297 let working_dir = self
1299 .project_root
1300 .clone()
1301 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
1302
1303 let session = match &self.session {
1305 Some(s) => s,
1306 None => {
1307 self.error = Some("No session loaded".to_string());
1308 return Ok(0);
1309 }
1310 };
1311
1312 let session_name = session.session_name.clone();
1313 let mut spawned_count = 0;
1314
1315 let storage = Storage::new(self.project_root.clone());
1317
1318 for (task_id, task_title, tag) in &tasks_to_spawn {
1319 let phase = match self.phases.get(tag) {
1321 Some(p) => p,
1322 None => continue,
1323 };
1324
1325 let task = match phase.get_task(task_id) {
1326 Some(t) => t,
1327 None => continue,
1328 };
1329
1330 let prompt = agent::generate_prompt(task, tag);
1332
1333 match terminal::spawn_terminal(task_id, &prompt, &working_dir, &session_name) {
1335 Ok(_window_index) => {
1336 spawned_count += 1;
1337
1338 if let Some(ref mut session) = self.session {
1340 session.add_agent(task_id, task_title, tag);
1341 }
1342
1343 if let Ok(mut phase) = storage.load_group(tag) {
1345 if let Some(task) = phase.get_task_mut(task_id) {
1346 task.set_status(TaskStatus::InProgress);
1347 let _ = storage.update_group(tag, &phase);
1348 }
1349 }
1350 }
1351 Err(e) => {
1352 self.error = Some(format!("Failed to spawn {}: {}", task_id, e));
1353 }
1354 }
1355
1356 if spawned_count < tasks_to_spawn.len() {
1358 std::thread::sleep(Duration::from_millis(300));
1359 }
1360 }
1361
1362 if spawned_count > 0 {
1364 if let Some(ref session) = self.session {
1365 let _ = crate::commands::spawn::monitor::save_session(
1366 self.project_root.as_ref(),
1367 session,
1368 );
1369 }
1370
1371 self.selected_tasks.clear();
1373 self.refresh()?;
1374 self.refresh_waves();
1375 }
1376
1377 Ok(spawned_count)
1378 }
1379
1380 pub fn prepare_swarm_start(&self) -> Option<(String, String)> {
1382 let tag = self
1384 .session
1385 .as_ref()
1386 .map(|s| s.tag.clone())
1387 .or_else(|| self.active_tag.clone())?;
1388
1389 let session_base = self.session_name.replace("swarm-", "").replace("scud-", "");
1391 let cmd = format!("scud swarm --tag {} --session {}", tag, session_base);
1392
1393 Some((cmd, tag))
1394 }
1395
1396 pub fn set_selected_task_status(&mut self, new_status: TaskStatus) -> Result<()> {
1398 let Some(ref session) = self.session else {
1399 self.error = Some("No session loaded".to_string());
1400 return Ok(());
1401 };
1402
1403 let agents = session.agents.clone();
1404 if agents.is_empty() || self.selected >= agents.len() {
1405 self.error = Some("No agent selected".to_string());
1406 return Ok(());
1407 }
1408
1409 let agent = &agents[self.selected];
1410 let task_id = &agent.task_id;
1411 let tag = &agent.tag;
1412
1413 let storage = Storage::new(self.project_root.clone());
1415 if let Ok(mut phase) = storage.load_group(tag) {
1416 if let Some(task) = phase.get_task_mut(task_id) {
1417 task.set_status(new_status.clone());
1418 if let Err(e) = storage.update_group(tag, &phase) {
1419 self.error = Some(format!("Failed to save: {}", e));
1420 return Ok(());
1421 }
1422 self.error = Some(format!("✓ {} → {}", task_id, new_status.as_str()));
1424 } else {
1425 self.error = Some(format!("Task {} not found", task_id));
1426 }
1427 } else {
1428 self.error = Some(format!("Failed to load phase {}", tag));
1429 }
1430
1431 self.refresh()?;
1433 self.refresh_waves();
1434
1435 Ok(())
1436 }
1437
1438 fn spawn_task_with_ralph(&mut self, task_id: &str) -> Result<()> {
1441 use crate::commands::spawn::{agent, terminal};
1442
1443 let task_info = self
1445 .waves
1446 .iter()
1447 .flat_map(|w| w.tasks.iter())
1448 .find(|t| t.id == task_id)
1449 .map(|t| (t.id.clone(), t.title.clone(), t.tag.clone()));
1450
1451 let (task_id, task_title, tag) = match task_info {
1452 Some(info) => info,
1453 None => return Ok(()),
1454 };
1455
1456 let working_dir = self
1458 .project_root
1459 .clone()
1460 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
1461
1462 let session = match &self.session {
1464 Some(s) => s,
1465 None => {
1466 self.error = Some("No session loaded".to_string());
1467 return Ok(());
1468 }
1469 };
1470
1471 let session_name = session.session_name.clone();
1472
1473 let storage = Storage::new(self.project_root.clone());
1475
1476 let phase = match self.phases.get(&tag) {
1477 Some(p) => p,
1478 None => return Ok(()),
1479 };
1480
1481 let task = match phase.get_task(&task_id) {
1482 Some(t) => t,
1483 None => return Ok(()),
1484 };
1485
1486 let base_prompt = agent::generate_prompt(task, &tag);
1488 let ralph_prompt = format!(
1489 r#"{}
1490
1491═══════════════════════════════════════════════════════════
1492RALPH LOOP MODE - Autonomous Task Completion
1493═══════════════════════════════════════════════════════════
1494
1495CRITICAL: Your task ID is **{task_id}** (NOT any parent task!)
1496
1497You are in a Ralph loop. Keep working until the task is COMPLETE.
1498
1499After EACH attempt:
15001. Run EXACTLY: scud set-status {task_id} done
1501 ⚠️ Use task ID "{task_id}" - do NOT use any other task ID!
15022. Verify the task is truly done (tests pass, code works)
15033. If something failed, fix it and try again
1504
1505The loop will continue until task {task_id} is marked done.
1506Do NOT give up. Keep iterating until success.
1507
1508When you have genuinely completed task {task_id}, output:
1509<promise>TASK {task_id} COMPLETE</promise>
1510
1511DO NOT output this promise unless task {task_id} is TRULY complete!
1512═══════════════════════════════════════════════════════════
1513"#,
1514 base_prompt,
1515 task_id = task_id
1516 );
1517
1518 match terminal::spawn_terminal_ralph(
1520 &task_id,
1521 &ralph_prompt,
1522 &working_dir,
1523 &session_name,
1524 &format!("TASK {} COMPLETE", task_id),
1525 ) {
1526 Ok(()) => {
1527 if let Some(ref mut session) = self.session {
1529 session.add_agent(&task_id, &task_title, &tag);
1530 }
1531
1532 if let Ok(mut phase) = storage.load_group(&tag) {
1534 if let Some(task) = phase.get_task_mut(&task_id) {
1535 task.set_status(TaskStatus::InProgress);
1536 let _ = storage.update_group(&tag, &phase);
1537 }
1538 }
1539
1540 if let Some(ref session) = self.session {
1542 let _ = crate::commands::spawn::monitor::save_session(
1543 self.project_root.as_ref(),
1544 session,
1545 );
1546 }
1547
1548 let _ = self.refresh();
1550 self.refresh_waves();
1551 }
1552 Err(e) => {
1553 self.error = Some(format!(
1554 "Failed to spawn Ralph agent for {}: {}",
1555 task_id, e
1556 ));
1557 }
1558 }
1559
1560 Ok(())
1561 }
1562}