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::monitor::{
17 load_session, save_session, AgentState, AgentStatus, SpawnSession,
18};
19use crate::commands::swarm::session as swarm_session;
20use crate::models::phase::Phase;
21use crate::models::task::{Task, TaskStatus};
22use crate::storage::Storage;
23
24#[derive(Debug, Clone, PartialEq)]
26pub enum ViewMode {
27 Split,
29 Fullscreen,
31 Input,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq)]
37pub enum FocusedPanel {
38 Waves,
39 Agents,
40 Output,
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub enum WaveTaskState {
46 Ready,
48 Running,
50 Done,
52 Blocked,
54 InProgress,
56}
57
58#[derive(Debug, Clone)]
60pub struct WaveTask {
61 pub id: String,
62 pub title: String,
63 pub tag: String,
64 pub state: WaveTaskState,
65 pub complexity: u32,
66 pub dependencies: Vec<String>,
67}
68
69#[derive(Debug, Clone)]
71pub struct Wave {
72 pub number: usize,
73 pub tasks: Vec<WaveTask>,
74}
75
76pub struct App {
78 pub project_root: Option<PathBuf>,
80 pub session_name: String,
82 pub session: Option<SpawnSession>,
84 pub selected: usize,
86 pub view_mode: ViewMode,
88 pub show_help: bool,
90 last_refresh: Instant,
92 refresh_interval: Duration,
94 pub error: Option<String>,
96 pub live_output: Vec<String>,
98 last_output_refresh: Instant,
100 output_refresh_interval: Duration,
102 pub input_buffer: String,
104 pub scroll_offset: usize,
106 pub auto_scroll: bool,
108
109 pub focused_panel: FocusedPanel,
112 pub waves: Vec<Wave>,
114 pub selected_tasks: HashSet<String>,
116 pub wave_task_index: usize,
118 pub wave_scroll_offset: usize,
120 pub agents_scroll_offset: usize,
122 pub active_tag: Option<String>,
124 phases: HashMap<String, Phase>,
126
127 pub ralph_mode: bool,
130 pub ralph_max_parallel: usize,
132 last_ralph_check: Instant,
134
135 pub swarm_mode: bool,
138 pub swarm_session_data: Option<swarm_session::SwarmSession>,
140}
141
142impl App {
143 pub fn new(project_root: Option<PathBuf>, session_name: &str, swarm_mode: bool) -> Result<Self> {
145 let storage = Storage::new(project_root.clone());
147 let active_tag = storage.get_active_group().ok().flatten();
148 let phases = storage.load_tasks().unwrap_or_default();
149
150 let mut app = Self {
151 project_root,
152 session_name: session_name.to_string(),
153 session: None,
154 selected: 0,
155 view_mode: ViewMode::Split,
156 show_help: false,
157 last_refresh: Instant::now(),
158 refresh_interval: Duration::from_secs(2),
159 error: None,
160 live_output: Vec::new(),
161 last_output_refresh: Instant::now(),
162 output_refresh_interval: Duration::from_millis(500),
163 input_buffer: String::new(),
164 scroll_offset: 0,
165 auto_scroll: true,
166 focused_panel: FocusedPanel::Waves,
168 waves: Vec::new(),
169 selected_tasks: HashSet::new(),
170 wave_task_index: 0,
171 wave_scroll_offset: 0,
172 agents_scroll_offset: 0,
173 active_tag,
174 phases,
175 ralph_mode: false,
177 ralph_max_parallel: 5,
178 last_ralph_check: Instant::now(),
179 swarm_mode,
181 swarm_session_data: None,
182 };
183 app.refresh()?;
184 app.refresh_waves();
185 app.refresh_live_output();
186 Ok(app)
187 }
188
189 pub fn refresh(&mut self) -> Result<()> {
191 if self.swarm_mode {
192 match swarm_session::load_session(self.project_root.as_ref(), &self.session_name) {
194 Ok(session) => {
195 self.swarm_session_data = Some(session);
196 self.error = None;
197 }
198 Err(e) => {
199 self.error = Some(format!("Failed to load swarm session: {}", e));
200 }
201 }
202 } else {
203 match load_session(self.project_root.as_ref(), &self.session_name) {
205 Ok(mut session) => {
206 self.refresh_agent_statuses(&mut session);
208
209 let _ = save_session(self.project_root.as_ref(), &session);
211
212 self.session = Some(session);
213 self.error = None;
214 }
215 Err(e) => {
216 self.error = Some(format!("Failed to load session: {}", e));
217 }
218 }
219 }
220 self.last_refresh = Instant::now();
221 Ok(())
222 }
223
224 pub fn refresh_live_output(&mut self) {
226 let agents = self.agents();
227 if agents.is_empty() || self.selected >= agents.len() {
228 self.live_output = vec!["No agent selected".to_string()];
229 return;
230 }
231
232 let agent = &agents[self.selected];
233 let session = match &self.session {
234 Some(s) => s,
235 None => {
236 self.live_output = vec!["No session loaded".to_string()];
237 return;
238 }
239 };
240
241 let tmux_windows = self.get_tmux_windows(&session.session_name);
243 let matching_window = tmux_windows.iter().find(|(_, name)| {
244 name.starts_with(&agent.window_name) || agent.window_name.starts_with(name)
245 });
246
247 let window_target = match matching_window {
248 Some((index, _)) => format!("{}:{}", session.session_name, index),
249 None => {
250 self.live_output = vec![format!("Window '{}' not found", agent.window_name)];
251 return;
252 }
253 };
254
255 let output = Command::new("tmux")
257 .args([
258 "capture-pane",
259 "-t",
260 &window_target,
261 "-p", "-S",
263 "-100", ])
265 .output();
266
267 match output {
268 Ok(out) if out.status.success() => {
269 let content = String::from_utf8_lossy(&out.stdout);
270 self.live_output = content.lines().map(|s| s.to_string()).collect();
271
272 while self
274 .live_output
275 .last()
276 .map(|s| s.trim().is_empty())
277 .unwrap_or(false)
278 {
279 self.live_output.pop();
280 }
281 }
282 Ok(out) => {
283 self.live_output = vec![format!("Error: {}", String::from_utf8_lossy(&out.stderr))];
284 }
285 Err(e) => {
286 self.live_output = vec![format!("tmux error: {}", e)];
287 }
288 }
289
290 self.last_output_refresh = Instant::now();
291 }
292
293 fn refresh_agent_statuses(&self, session: &mut SpawnSession) {
295 let tmux_windows = self.get_tmux_windows(&session.session_name);
296 let storage = Storage::new(self.project_root.clone());
297 let all_phases = storage.load_tasks().ok();
298
299 for agent in &mut session.agents {
300 let window_exists = tmux_windows.iter().any(|(_, name)| {
301 name.starts_with(&agent.window_name) || agent.window_name.starts_with(name)
302 });
303
304 let task_status = all_phases.as_ref().and_then(|phases| {
305 phases.get(&agent.tag).and_then(|phase| {
306 phase
307 .get_task(&agent.task_id)
308 .map(|task| task.status.clone())
309 })
310 });
311
312 agent.status = match (&task_status, window_exists) {
313 (Some(TaskStatus::Done), _) => AgentStatus::Completed,
314 (Some(TaskStatus::Blocked), _) => AgentStatus::Failed,
315 (Some(TaskStatus::InProgress), true) => AgentStatus::Running,
316 (Some(TaskStatus::InProgress), false) => AgentStatus::Completed,
317 (_, false) => AgentStatus::Completed,
318 (_, true) => AgentStatus::Running,
319 };
320 }
321 }
322
323 fn get_tmux_windows(&self, session_name: &str) -> Vec<(usize, String)> {
325 let output = Command::new("tmux")
326 .args([
327 "list-windows",
328 "-t",
329 session_name,
330 "-F",
331 "#{window_index}:#{window_name}",
332 ])
333 .output();
334
335 match output {
336 Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout)
337 .lines()
338 .filter_map(|line| {
339 let parts: Vec<&str> = line.splitn(2, ':').collect();
340 if parts.len() == 2 {
341 parts[0].parse().ok().map(|idx| (idx, parts[1].to_string()))
342 } else {
343 None
344 }
345 })
346 .collect(),
347 _ => Vec::new(),
348 }
349 }
350
351 pub fn tick(&mut self) -> Result<()> {
353 if self.last_refresh.elapsed() >= self.refresh_interval {
355 self.refresh()?;
356 self.refresh_waves();
357 }
358
359 if self.last_output_refresh.elapsed() >= self.output_refresh_interval {
361 self.refresh_live_output();
362 }
363
364 if self.ralph_mode && self.last_ralph_check.elapsed() >= Duration::from_secs(5) {
366 self.ralph_auto_spawn();
367 self.last_ralph_check = Instant::now();
368 }
369
370 Ok(())
371 }
372
373 pub fn toggle_ralph_mode(&mut self) {
375 self.ralph_mode = !self.ralph_mode;
376 if self.ralph_mode {
377 self.ralph_auto_spawn();
379 }
380 }
381
382 fn ralph_auto_spawn(&mut self) {
384 let running_count = self
386 .agents()
387 .iter()
388 .filter(|a| a.status == AgentStatus::Running || a.status == AgentStatus::Starting)
389 .count();
390
391 if running_count >= self.ralph_max_parallel {
392 return; }
394
395 let slots_available = self.ralph_max_parallel - running_count;
397 let mut tasks_to_spawn: Vec<String> = Vec::new();
398
399 for wave in &self.waves {
400 for task in &wave.tasks {
401 if task.state == WaveTaskState::Ready && !self.selected_tasks.contains(&task.id) {
402 let already_spawned = self.agents().iter().any(|a| a.task_id == task.id);
404 if !already_spawned {
405 tasks_to_spawn.push(task.id.clone());
406 if tasks_to_spawn.len() >= slots_available {
407 break;
408 }
409 }
410 }
411 }
412 if tasks_to_spawn.len() >= slots_available {
413 break;
414 }
415 }
416
417 for task_id in tasks_to_spawn {
419 let _ = self.spawn_task_with_ralph(&task_id);
420 }
421 }
422
423 pub fn agents(&self) -> &[AgentState] {
425 self.session
426 .as_ref()
427 .map(|s| s.agents.as_slice())
428 .unwrap_or(&[])
429 }
430
431 pub fn next_agent(&mut self) {
433 let len = self.agents().len();
434 if len > 0 {
435 self.selected = (self.selected + 1) % len;
436 self.adjust_agents_scroll();
437 self.reset_scroll();
438 self.refresh_live_output();
439 }
440 }
441
442 pub fn previous_agent(&mut self) {
444 let len = self.agents().len();
445 if len > 0 {
446 self.selected = if self.selected > 0 {
447 self.selected - 1
448 } else {
449 len - 1
450 };
451 self.adjust_agents_scroll();
452 self.reset_scroll();
453 self.refresh_live_output();
454 }
455 }
456
457 pub fn adjust_agents_scroll(&mut self) {
460 const VISIBLE_LINES: usize = 8;
461
462 if self.selected < self.agents_scroll_offset {
464 self.agents_scroll_offset = self.selected;
465 }
466 else if self.selected >= self.agents_scroll_offset + VISIBLE_LINES {
468 self.agents_scroll_offset = self.selected.saturating_sub(VISIBLE_LINES - 1);
469 }
470 }
471
472 pub fn toggle_fullscreen(&mut self) {
474 self.view_mode = match self.view_mode {
475 ViewMode::Split => ViewMode::Fullscreen,
476 ViewMode::Fullscreen => ViewMode::Split,
477 ViewMode::Input => ViewMode::Fullscreen,
478 };
479 }
480
481 pub fn exit_fullscreen(&mut self) {
483 self.view_mode = ViewMode::Split;
484 self.input_buffer.clear();
485 }
486
487 pub fn enter_input_mode(&mut self) {
489 self.view_mode = ViewMode::Input;
490 self.input_buffer.clear();
491 }
492
493 pub fn input_char(&mut self, c: char) {
495 self.input_buffer.push(c);
496 }
497
498 pub fn input_backspace(&mut self) {
500 self.input_buffer.pop();
501 }
502
503 pub fn send_input(&mut self) -> Result<()> {
505 if self.input_buffer.is_empty() {
506 return Ok(());
507 }
508
509 let session = match &self.session {
510 Some(s) => s,
511 None => {
512 self.error = Some("No session loaded".to_string());
513 return Ok(());
514 }
515 };
516
517 let agents = self.agents();
518 if agents.is_empty() || self.selected >= agents.len() {
519 self.error = Some("No agent selected".to_string());
520 return Ok(());
521 }
522
523 let agent = &agents[self.selected];
524
525 let tmux_windows = self.get_tmux_windows(&session.session_name);
527 let matching_window = tmux_windows.iter().find(|(_, name)| {
528 name.starts_with(&agent.window_name) || agent.window_name.starts_with(name)
529 });
530
531 let window_target = match matching_window {
532 Some((index, _)) => format!("{}:{}", session.session_name, index),
533 None => {
534 self.error = Some(format!("Window not found for {}", agent.task_id));
535 return Ok(());
536 }
537 };
538
539 let result = Command::new("tmux")
541 .args([
542 "send-keys",
543 "-t",
544 &window_target,
545 &self.input_buffer,
546 "Enter",
547 ])
548 .output();
549
550 match result {
551 Ok(out) if out.status.success() => {
552 self.error = None;
553 self.input_buffer.clear();
554 self.view_mode = ViewMode::Fullscreen; self.refresh_live_output();
556 }
557 Ok(out) => {
558 self.error = Some(format!(
559 "Send failed: {}",
560 String::from_utf8_lossy(&out.stderr)
561 ));
562 }
563 Err(e) => {
564 self.error = Some(format!("tmux error: {}", e));
565 }
566 }
567
568 Ok(())
569 }
570
571 pub fn restart_agent(&mut self) -> Result<()> {
573 let session = match &self.session {
574 Some(s) => s,
575 None => return Ok(()),
576 };
577
578 let agents = self.agents();
579 if agents.is_empty() || self.selected >= agents.len() {
580 return Ok(());
581 }
582
583 let agent = &agents[self.selected];
584
585 let tmux_windows = self.get_tmux_windows(&session.session_name);
587 let matching_window = tmux_windows.iter().find(|(_, name)| {
588 name.starts_with(&agent.window_name) || agent.window_name.starts_with(name)
589 });
590
591 if let Some((index, _)) = matching_window {
592 let target = format!("{}:{}", session.session_name, index);
593
594 let _ = Command::new("tmux")
596 .args(["send-keys", "-t", &target, "C-c"])
597 .output();
598
599 std::thread::sleep(Duration::from_millis(200));
601
602 let _ = Command::new("tmux")
604 .args([
605 "send-keys",
606 "-t",
607 &target,
608 "echo 'Agent restarted by user'",
609 "Enter",
610 ])
611 .output();
612
613 self.error = None;
614 self.refresh_live_output();
615 }
616
617 Ok(())
618 }
619
620 pub fn toggle_help(&mut self) {
622 self.show_help = !self.show_help;
623 }
624
625 pub fn scroll_up(&mut self, lines: usize) {
627 let max_scroll = self.live_output.len().saturating_sub(1);
628 self.scroll_offset = (self.scroll_offset + lines).min(max_scroll);
629 self.auto_scroll = false;
630 }
631
632 pub fn scroll_down(&mut self, lines: usize) {
634 self.scroll_offset = self.scroll_offset.saturating_sub(lines);
635 if self.scroll_offset == 0 {
636 self.auto_scroll = true;
637 }
638 }
639
640 pub fn scroll_to_bottom(&mut self) {
642 self.scroll_offset = 0;
643 self.auto_scroll = true;
644 }
645
646 fn reset_scroll(&mut self) {
648 self.scroll_offset = 0;
649 self.auto_scroll = true;
650 }
651
652 pub fn status_counts(&self) -> (usize, usize, usize, usize) {
654 let agents = self.agents();
655 let starting = agents
656 .iter()
657 .filter(|a| a.status == AgentStatus::Starting)
658 .count();
659 let running = agents
660 .iter()
661 .filter(|a| a.status == AgentStatus::Running)
662 .count();
663 let completed = agents
664 .iter()
665 .filter(|a| a.status == AgentStatus::Completed)
666 .count();
667 let failed = agents
668 .iter()
669 .filter(|a| a.status == AgentStatus::Failed)
670 .count();
671 (starting, running, completed, failed)
672 }
673
674 pub fn selected_agent(&self) -> Option<&AgentState> {
676 let agents = self.agents();
677 if agents.is_empty() || self.selected >= agents.len() {
678 None
679 } else {
680 Some(&agents[self.selected])
681 }
682 }
683
684 pub fn refresh_waves(&mut self) {
688 let storage = Storage::new(self.project_root.clone());
690 self.phases = storage.load_tasks().unwrap_or_default();
691
692 if self.swarm_mode {
694 self.waves = self.compute_swarm_waves();
695 return;
696 }
697
698 let running_task_ids: HashSet<String> = self
700 .agents()
701 .iter()
702 .filter(|a| a.status == AgentStatus::Running || a.status == AgentStatus::Starting)
703 .map(|a| a.task_id.clone())
704 .collect();
705
706 let tag = self.active_tag.clone().or_else(|| {
708 self.session.as_ref().map(|s| s.tag.clone())
710 });
711
712 let Some(tag) = tag else {
713 self.waves = Vec::new();
714 return;
715 };
716
717 let Some(phase) = self.phases.get(&tag) else {
718 self.waves = Vec::new();
719 return;
720 };
721
722 self.waves = self.compute_waves(phase, &running_task_ids);
724 }
725
726 fn compute_swarm_waves(&self) -> Vec<Wave> {
728 let Some(ref swarm) = self.swarm_session_data else {
729 return Vec::new();
730 };
731
732 let tag = &swarm.tag;
733 let phase = self.phases.get(tag);
734
735 swarm
736 .waves
737 .iter()
738 .map(|wave_state| {
739 let task_ids: Vec<String> = wave_state
741 .rounds
742 .iter()
743 .flat_map(|round| round.task_ids.iter().cloned())
744 .collect();
745
746 let tasks: Vec<WaveTask> = task_ids
747 .iter()
748 .map(|task_id| {
749 let (title, complexity, dependencies, task_status) =
751 if let Some(phase) = phase {
752 if let Some(task) = phase.get_task(task_id) {
753 (
754 task.title.clone(),
755 task.complexity,
756 task.dependencies.clone(),
757 Some(task.status.clone()),
758 )
759 } else {
760 (task_id.clone(), 1, vec![], None)
761 }
762 } else {
763 (task_id.clone(), 1, vec![], None)
764 };
765
766 let state = match task_status {
768 Some(TaskStatus::Done) => WaveTaskState::Done,
769 Some(TaskStatus::InProgress) => WaveTaskState::Running,
770 Some(TaskStatus::Blocked) => WaveTaskState::Blocked,
771 Some(TaskStatus::Pending) => {
772 if wave_state.completed_at.is_some() {
773 WaveTaskState::Blocked
775 } else {
776 WaveTaskState::Ready
777 }
778 }
779 _ => WaveTaskState::Ready,
780 };
781
782 WaveTask {
783 id: task_id.clone(),
784 title,
785 tag: tag.clone(),
786 state,
787 complexity,
788 dependencies,
789 }
790 })
791 .collect();
792
793 Wave {
794 number: wave_state.wave_number,
795 tasks,
796 }
797 })
798 .collect()
799 }
800
801 fn compute_waves(&self, phase: &Phase, running_task_ids: &HashSet<String>) -> Vec<Wave> {
803 let mut actionable: Vec<&Task> = Vec::new();
805 for task in &phase.tasks {
806 if task.status == TaskStatus::Done
807 || task.status == TaskStatus::Expanded
808 || task.status == TaskStatus::Cancelled
809 {
810 continue;
811 }
812
813 if !task.subtasks.is_empty() {
815 continue;
816 }
817
818 if let Some(ref parent_id) = task.parent_id {
820 let parent_expanded = phase
821 .get_task(parent_id)
822 .map(|p| p.is_expanded())
823 .unwrap_or(false);
824 if !parent_expanded {
825 continue;
826 }
827 }
828
829 actionable.push(task);
830 }
831
832 if actionable.is_empty() {
833 return Vec::new();
834 }
835
836 let task_ids: HashSet<String> = actionable.iter().map(|t| t.id.clone()).collect();
838 let mut in_degree: HashMap<String, usize> = HashMap::new();
839 let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
840
841 for task in &actionable {
842 in_degree.entry(task.id.clone()).or_insert(0);
843
844 for dep in &task.dependencies {
845 if task_ids.contains(dep) {
846 *in_degree.entry(task.id.clone()).or_insert(0) += 1;
848 dependents
849 .entry(dep.clone())
850 .or_default()
851 .push(task.id.clone());
852 } else {
853 if !self.is_dependency_satisfied(dep, phase) {
856 *in_degree.entry(task.id.clone()).or_insert(0) += 1000;
858 }
859 }
860 }
861 }
862
863 let mut waves: Vec<Wave> = Vec::new();
865 let mut remaining = in_degree.clone();
866 let mut wave_number = 1;
867
868 while !remaining.is_empty() {
869 let mut ready: Vec<String> = remaining
870 .iter()
871 .filter(|(_, °)| deg == 0)
872 .map(|(id, _)| id.clone())
873 .collect();
874
875 if ready.is_empty() {
876 break; }
878
879 ready.sort();
881
882 let mut wave_tasks: Vec<WaveTask> = ready
884 .iter()
885 .filter_map(|task_id| {
886 actionable.iter().find(|t| &t.id == task_id).map(|task| {
887 let state = if task.status == TaskStatus::Done {
888 WaveTaskState::Done
889 } else if running_task_ids.contains(&task.id) {
890 WaveTaskState::Running
891 } else if task.status == TaskStatus::InProgress {
892 WaveTaskState::InProgress
893 } else if task.status == TaskStatus::Blocked {
894 WaveTaskState::Blocked
895 } else if self.is_task_ready(task, phase) {
896 WaveTaskState::Ready
897 } else {
898 WaveTaskState::Blocked
899 };
900
901 WaveTask {
902 id: task.id.clone(),
903 title: task.title.clone(),
904 tag: self.active_tag.clone().unwrap_or_default(),
905 state,
906 complexity: task.complexity,
907 dependencies: task.dependencies.clone(),
908 }
909 })
910 })
911 .collect();
912
913 for task_id in &ready {
915 remaining.remove(task_id);
916 if let Some(deps) = dependents.get(task_id) {
917 for dep_id in deps {
918 if let Some(deg) = remaining.get_mut(dep_id) {
919 *deg = deg.saturating_sub(1);
920 }
921 }
922 }
923 }
924
925 if !wave_tasks.is_empty() {
926 wave_tasks.sort_by(|a, b| a.id.cmp(&b.id));
928 waves.push(Wave {
929 number: wave_number,
930 tasks: wave_tasks,
931 });
932 }
933 wave_number += 1;
934 }
935
936 waves
937 }
938
939 fn is_task_ready(&self, task: &Task, phase: &Phase) -> bool {
941 if task.status != TaskStatus::Pending {
942 return false;
943 }
944
945 for dep_id in &task.dependencies {
947 if !self.is_dependency_satisfied(dep_id, phase) {
948 return false;
949 }
950 }
951
952 true
953 }
954
955 fn is_dependency_satisfied(&self, dep_id: &str, phase: &Phase) -> bool {
957 let Some(dep) = phase.get_task(dep_id) else {
958 return true; };
960
961 match dep.status {
962 TaskStatus::Done => true,
963 TaskStatus::Expanded => {
964 if dep.subtasks.is_empty() {
966 false } else {
968 dep.subtasks.iter().all(|subtask_id| {
969 phase
970 .get_task(subtask_id)
971 .map(|st| st.status == TaskStatus::Done)
972 .unwrap_or(false)
973 })
974 }
975 }
976 _ => false, }
978 }
979
980 pub fn all_wave_tasks(&self) -> Vec<&WaveTask> {
982 self.waves.iter().flat_map(|w| w.tasks.iter()).collect()
983 }
984
985 pub fn selected_wave_task(&self) -> Option<&WaveTask> {
987 let all_tasks = self.all_wave_tasks();
988 all_tasks.get(self.wave_task_index).copied()
989 }
990
991 pub fn next_panel(&mut self) {
995 self.focused_panel = match self.focused_panel {
996 FocusedPanel::Waves => FocusedPanel::Agents,
997 FocusedPanel::Agents => FocusedPanel::Output,
998 FocusedPanel::Output => FocusedPanel::Waves,
999 };
1000 }
1001
1002 pub fn previous_panel(&mut self) {
1004 self.focused_panel = match self.focused_panel {
1005 FocusedPanel::Waves => FocusedPanel::Output,
1006 FocusedPanel::Agents => FocusedPanel::Waves,
1007 FocusedPanel::Output => FocusedPanel::Agents,
1008 };
1009 }
1010
1011 pub fn move_up(&mut self) {
1013 match self.focused_panel {
1014 FocusedPanel::Waves => {
1015 if self.wave_task_index > 0 {
1016 self.wave_task_index -= 1;
1017 self.adjust_wave_scroll();
1018 }
1019 }
1020 FocusedPanel::Agents => self.previous_agent(),
1021 FocusedPanel::Output => self.scroll_up(1),
1022 }
1023 }
1024
1025 pub fn move_down(&mut self) {
1027 match self.focused_panel {
1028 FocusedPanel::Waves => {
1029 let max = self.all_wave_tasks().len().saturating_sub(1);
1030 if self.wave_task_index < max {
1031 self.wave_task_index += 1;
1032 self.adjust_wave_scroll();
1033 }
1034 }
1035 FocusedPanel::Agents => self.next_agent(),
1036 FocusedPanel::Output => self.scroll_down(1),
1037 }
1038 }
1039
1040 fn adjust_wave_scroll(&mut self) {
1043 let mut line_idx = 0;
1046 let mut found = false;
1047 let mut task_counter = 0;
1048
1049 for wave in &self.waves {
1050 line_idx += 1; for _ in &wave.tasks {
1052 if task_counter == self.wave_task_index {
1053 found = true;
1054 break;
1055 }
1056 line_idx += 1;
1057 task_counter += 1;
1058 }
1059 if found {
1060 break;
1061 }
1062 }
1063
1064 let visible_height = 4;
1066
1067 if line_idx < self.wave_scroll_offset {
1069 self.wave_scroll_offset = line_idx;
1070 } else if line_idx >= self.wave_scroll_offset + visible_height {
1071 self.wave_scroll_offset = line_idx.saturating_sub(visible_height - 1);
1072 }
1073 }
1074
1075 pub fn toggle_task_selection(&mut self) {
1079 if let Some(task) = self.selected_wave_task() {
1080 let task_id = task.id.clone();
1081 if self.selected_tasks.contains(&task_id) {
1082 self.selected_tasks.remove(&task_id);
1083 } else {
1084 if task.state == WaveTaskState::Ready {
1086 self.selected_tasks.insert(task_id);
1087 }
1088 }
1089 }
1090 }
1091
1092 pub fn select_all_ready(&mut self) {
1094 for wave in &self.waves {
1095 for task in &wave.tasks {
1096 if task.state == WaveTaskState::Ready {
1097 self.selected_tasks.insert(task.id.clone());
1098 }
1099 }
1100 }
1101 }
1102
1103 pub fn clear_selection(&mut self) {
1105 self.selected_tasks.clear();
1106 }
1107
1108 pub fn ready_task_count(&self) -> usize {
1110 self.waves
1111 .iter()
1112 .flat_map(|w| &w.tasks)
1113 .filter(|t| t.state == WaveTaskState::Ready)
1114 .count()
1115 }
1116
1117 pub fn selected_task_count(&self) -> usize {
1119 self.selected_tasks.len()
1120 }
1121
1122 pub fn get_selected_tasks(&self) -> Vec<&WaveTask> {
1124 self.all_wave_tasks()
1125 .into_iter()
1126 .filter(|t| self.selected_tasks.contains(&t.id))
1127 .collect()
1128 }
1129
1130 pub fn spawn_selected_tasks(&mut self) -> Result<usize> {
1133 use crate::commands::spawn::{agent, terminal};
1134
1135 let tasks_to_spawn: Vec<(String, String, String)> = self
1136 .get_selected_tasks()
1137 .iter()
1138 .map(|t| (t.id.clone(), t.title.clone(), t.tag.clone()))
1139 .collect();
1140
1141 if tasks_to_spawn.is_empty() {
1142 return Ok(0);
1143 }
1144
1145 let working_dir = self
1147 .project_root
1148 .clone()
1149 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
1150
1151 let session = match &self.session {
1153 Some(s) => s,
1154 None => {
1155 self.error = Some("No session loaded".to_string());
1156 return Ok(0);
1157 }
1158 };
1159
1160 let session_name = session.session_name.clone();
1161 let mut spawned_count = 0;
1162
1163 let storage = Storage::new(self.project_root.clone());
1165
1166 for (task_id, task_title, tag) in &tasks_to_spawn {
1167 let phase = match self.phases.get(tag) {
1169 Some(p) => p,
1170 None => continue,
1171 };
1172
1173 let task = match phase.get_task(task_id) {
1174 Some(t) => t,
1175 None => continue,
1176 };
1177
1178 let prompt = agent::generate_prompt(task, tag);
1180
1181 match terminal::spawn_terminal(task_id, &prompt, &working_dir, &session_name) {
1183 Ok(_window_index) => {
1184 spawned_count += 1;
1185
1186 if let Some(ref mut session) = self.session {
1188 session.add_agent(task_id, task_title, tag);
1189 }
1190
1191 if let Ok(mut phase) = storage.load_group(tag) {
1193 if let Some(task) = phase.get_task_mut(task_id) {
1194 task.set_status(TaskStatus::InProgress);
1195 let _ = storage.update_group(tag, &phase);
1196 }
1197 }
1198 }
1199 Err(e) => {
1200 self.error = Some(format!("Failed to spawn {}: {}", task_id, e));
1201 }
1202 }
1203
1204 if spawned_count < tasks_to_spawn.len() {
1206 std::thread::sleep(Duration::from_millis(300));
1207 }
1208 }
1209
1210 if spawned_count > 0 {
1212 if let Some(ref session) = self.session {
1213 let _ = crate::commands::spawn::monitor::save_session(
1214 self.project_root.as_ref(),
1215 session,
1216 );
1217 }
1218
1219 self.selected_tasks.clear();
1221 self.refresh()?;
1222 self.refresh_waves();
1223 }
1224
1225 Ok(spawned_count)
1226 }
1227
1228 pub fn prepare_swarm_start(&self) -> Option<(String, String)> {
1230 let tag = self
1232 .session
1233 .as_ref()
1234 .map(|s| s.tag.clone())
1235 .or_else(|| self.active_tag.clone())?;
1236
1237 let session_base = self
1239 .session_name
1240 .replace("swarm-", "")
1241 .replace("scud-", "");
1242 let cmd = format!("scud swarm --tag {} --session {}", tag, session_base);
1243
1244 Some((cmd, tag))
1245 }
1246
1247 pub fn set_selected_task_status(&mut self, new_status: TaskStatus) -> Result<()> {
1249 let Some(ref session) = self.session else {
1250 self.error = Some("No session loaded".to_string());
1251 return Ok(());
1252 };
1253
1254 let agents = session.agents.clone();
1255 if agents.is_empty() || self.selected >= agents.len() {
1256 self.error = Some("No agent selected".to_string());
1257 return Ok(());
1258 }
1259
1260 let agent = &agents[self.selected];
1261 let task_id = &agent.task_id;
1262 let tag = &agent.tag;
1263
1264 let storage = Storage::new(self.project_root.clone());
1266 if let Ok(mut phase) = storage.load_group(tag) {
1267 if let Some(task) = phase.get_task_mut(task_id) {
1268 task.set_status(new_status.clone());
1269 if let Err(e) = storage.update_group(tag, &phase) {
1270 self.error = Some(format!("Failed to save: {}", e));
1271 return Ok(());
1272 }
1273 self.error = Some(format!(
1275 "✓ {} → {}",
1276 task_id,
1277 new_status.as_str()
1278 ));
1279 } else {
1280 self.error = Some(format!("Task {} not found", task_id));
1281 }
1282 } else {
1283 self.error = Some(format!("Failed to load phase {}", tag));
1284 }
1285
1286 self.refresh()?;
1288 self.refresh_waves();
1289
1290 Ok(())
1291 }
1292
1293 fn spawn_task_with_ralph(&mut self, task_id: &str) -> Result<()> {
1296 use crate::commands::spawn::{agent, terminal};
1297
1298 let task_info = self
1300 .waves
1301 .iter()
1302 .flat_map(|w| w.tasks.iter())
1303 .find(|t| t.id == task_id)
1304 .map(|t| (t.id.clone(), t.title.clone(), t.tag.clone()));
1305
1306 let (task_id, task_title, tag) = match task_info {
1307 Some(info) => info,
1308 None => return Ok(()),
1309 };
1310
1311 let working_dir = self
1313 .project_root
1314 .clone()
1315 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
1316
1317 let session = match &self.session {
1319 Some(s) => s,
1320 None => {
1321 self.error = Some("No session loaded".to_string());
1322 return Ok(());
1323 }
1324 };
1325
1326 let session_name = session.session_name.clone();
1327
1328 let storage = Storage::new(self.project_root.clone());
1330
1331 let phase = match self.phases.get(&tag) {
1332 Some(p) => p,
1333 None => return Ok(()),
1334 };
1335
1336 let task = match phase.get_task(&task_id) {
1337 Some(t) => t,
1338 None => return Ok(()),
1339 };
1340
1341 let base_prompt = agent::generate_prompt(task, &tag);
1343 let ralph_prompt = format!(
1344 r#"{}
1345
1346═══════════════════════════════════════════════════════════
1347RALPH LOOP MODE - Autonomous Task Completion
1348═══════════════════════════════════════════════════════════
1349
1350CRITICAL: Your task ID is **{task_id}** (NOT any parent task!)
1351
1352You are in a Ralph loop. Keep working until the task is COMPLETE.
1353
1354After EACH attempt:
13551. Run EXACTLY: scud set-status {task_id} done
1356 ⚠️ Use task ID "{task_id}" - do NOT use any other task ID!
13572. Verify the task is truly done (tests pass, code works)
13583. If something failed, fix it and try again
1359
1360The loop will continue until task {task_id} is marked done.
1361Do NOT give up. Keep iterating until success.
1362
1363When you have genuinely completed task {task_id}, output:
1364<promise>TASK {task_id} COMPLETE</promise>
1365
1366DO NOT output this promise unless task {task_id} is TRULY complete!
1367═══════════════════════════════════════════════════════════
1368"#,
1369 base_prompt,
1370 task_id = task_id
1371 );
1372
1373 match terminal::spawn_terminal_ralph(
1375 &task_id,
1376 &ralph_prompt,
1377 &working_dir,
1378 &session_name,
1379 &format!("TASK {} COMPLETE", task_id),
1380 ) {
1381 Ok(()) => {
1382 if let Some(ref mut session) = self.session {
1384 session.add_agent(&task_id, &task_title, &tag);
1385 }
1386
1387 if let Ok(mut phase) = storage.load_group(&tag) {
1389 if let Some(task) = phase.get_task_mut(&task_id) {
1390 task.set_status(TaskStatus::InProgress);
1391 let _ = storage.update_group(&tag, &phase);
1392 }
1393 }
1394
1395 if let Some(ref session) = self.session {
1397 let _ = crate::commands::spawn::monitor::save_session(
1398 self.project_root.as_ref(),
1399 session,
1400 );
1401 }
1402
1403 let _ = self.refresh();
1405 self.refresh_waves();
1406 }
1407 Err(e) => {
1408 self.error = Some(format!(
1409 "Failed to spawn Ralph agent for {}: {}",
1410 task_id, e
1411 ));
1412 }
1413 }
1414
1415 Ok(())
1416 }
1417}