1use crate::state::{MachineState, RunState, RunStatus, SessionStatus};
6use chrono::Utc;
7
8use super::colors::*;
9
10const WARNING_PANEL_WIDTH: usize = 60;
11
12pub fn print_status(state: &RunState) {
14 println!("{BLUE}Run ID:{RESET} {}", state.run_id);
15 println!("{BLUE}Status:{RESET} {:?}", state.status);
16 println!("{BLUE}Spec:{RESET} {}", state.spec_json_path.display());
17 println!("{BLUE}Branch:{RESET} {}", state.branch);
18 if let Some(story) = &state.current_story {
19 println!("{BLUE}Current:{RESET} {}", story);
20 }
21 println!("{BLUE}Task:{RESET} {}", state.iteration);
22 println!(
23 "{BLUE}Started:{RESET} {}",
24 state.started_at.format("%Y-%m-%d %H:%M:%S")
25 );
26 println!("{BLUE}Tasks run:{RESET} {}", state.iterations.len());
27}
28
29pub fn print_global_status(statuses: &[crate::config::ProjectStatus]) {
31 if statuses.is_empty() {
32 println!("{GRAY}No projects found.{RESET}");
33 println!();
34 println!("Run {CYAN}autom8{RESET} in a project directory to create a project.");
35 return;
36 }
37
38 let (needs_attention, idle): (Vec<_>, Vec<_>) =
39 statuses.iter().partition(|s| s.needs_attention());
40
41 if !needs_attention.is_empty() {
42 println!("{BOLD}Projects needing attention:{RESET}");
43 println!();
44
45 for status in &needs_attention {
46 let status_indicator = match status.run_status {
47 Some(RunStatus::Running) => format!("{YELLOW}[running]{RESET}"),
48 Some(RunStatus::Failed) => format!("{RED}[failed]{RESET}"),
49 Some(RunStatus::Interrupted) => format!("{YELLOW}[interrupted]{RESET}"),
50 Some(RunStatus::Completed) => String::new(),
51 None => String::new(),
52 };
53
54 let spec_info = if status.incomplete_spec_count > 0 {
55 format!(
56 " {CYAN}{} incomplete spec{}{RESET}",
57 status.incomplete_spec_count,
58 if status.incomplete_spec_count == 1 {
59 ""
60 } else {
61 "s"
62 }
63 )
64 } else {
65 String::new()
66 };
67
68 if status_indicator.is_empty() {
69 println!(" {BOLD}{}{RESET}{}", status.name, spec_info);
70 } else {
71 println!(
72 " {BOLD}{}{RESET} {}{}",
73 status.name, status_indicator, spec_info
74 );
75 }
76 }
77 println!();
78 }
79
80 if !idle.is_empty() {
81 println!("{GRAY}Idle projects:{RESET}");
82 for status in &idle {
83 println!("{GRAY} {}{RESET}", status.name);
84 }
85 println!();
86 }
87
88 let active_count = statuses
89 .iter()
90 .filter(|s| s.run_status == Some(RunStatus::Running))
91 .count();
92 let failed_count = statuses
93 .iter()
94 .filter(|s| s.run_status == Some(RunStatus::Failed))
95 .count();
96 let incomplete_spec_total: usize = statuses.iter().map(|s| s.incomplete_spec_count).sum();
97
98 println!(
99 "{GRAY}({} project{}, {} active, {} failed, {} incomplete spec{}){RESET}",
100 statuses.len(),
101 if statuses.len() == 1 { "" } else { "s" },
102 active_count,
103 failed_count,
104 incomplete_spec_total,
105 if incomplete_spec_total == 1 { "" } else { "s" }
106 );
107}
108
109pub fn print_project_tree(projects: &[crate::config::ProjectTreeInfo]) {
111 if projects.is_empty() {
112 println!("{GRAY}No projects found in ~/.config/autom8/{RESET}");
113 println!();
114 println!("Run {CYAN}autom8{RESET} in a project directory to create a project.");
115 return;
116 }
117
118 println!("{BOLD}~/.config/autom8/{RESET}");
119
120 let total = projects.len();
121
122 for (idx, project) in projects.iter().enumerate() {
123 let is_last_project = idx == total - 1;
124 let branch_char = if is_last_project { "└" } else { "├" };
125 let cont_char = if is_last_project { " " } else { "│" };
126
127 let (status_indicator, status_color) = match project.run_status {
128 Some(RunStatus::Running) => ("[running]", YELLOW),
129 Some(RunStatus::Failed) => ("[failed]", RED),
130 Some(RunStatus::Interrupted) => ("[interrupted]", YELLOW),
131 Some(RunStatus::Completed) if project.incomplete_spec_count > 0 => {
132 ("[incomplete]", CYAN)
133 }
134 Some(RunStatus::Completed) => ("[complete]", GREEN),
135 None if project.incomplete_spec_count > 0 => ("[incomplete]", CYAN),
136 None if project.has_content() => ("[idle]", GRAY),
137 None => ("", GRAY),
138 };
139
140 if status_indicator.is_empty() {
141 println!("{branch_char}── {BOLD}{}{RESET}", project.name);
142 } else {
143 println!(
144 "{branch_char}── {BOLD}{}{RESET} {status_color}{status_indicator}{RESET}",
145 project.name
146 );
147 }
148
149 let subdirs = [
150 ("spec", project.spec_md_count, "md"),
151 ("spec", project.spec_count, "json"),
152 ("runs", project.runs_count, "archived"),
153 ];
154
155 for (subidx, (name, count, unit)) in subdirs.iter().enumerate() {
156 let is_last_subdir = subidx == subdirs.len() - 1;
157 let sub_branch = if is_last_subdir { "└" } else { "├" };
158
159 let count_str = if *count == 0 {
160 format!("{GRAY}(empty){RESET}")
161 } else if *count == 1 {
162 format!("{GRAY}(1 {unit}){RESET}")
163 } else {
164 format!("{GRAY}({count} {unit}s){RESET}")
165 };
166
167 println!("{cont_char} {sub_branch}── {name}/ {count_str}");
168 }
169
170 if !is_last_project {
171 println!("{cont_char}");
172 }
173 }
174
175 println!();
176 let active_count = projects.iter().filter(|p| p.has_active_run).count();
177 let failed_count = projects
178 .iter()
179 .filter(|p| p.run_status == Some(RunStatus::Failed))
180 .count();
181 let incomplete_total: usize = projects.iter().map(|p| p.incomplete_spec_count).sum();
182
183 println!(
184 "{GRAY}({} project{}, {} active, {} failed, {} incomplete spec{}){RESET}",
185 total,
186 if total == 1 { "" } else { "s" },
187 active_count,
188 failed_count,
189 incomplete_total,
190 if incomplete_total == 1 { "" } else { "s" }
191 );
192}
193
194pub fn print_project_description(desc: &crate::config::ProjectDescription) {
196 println!("{BOLD}Project: {CYAN}{}{RESET}", desc.name);
197 println!("{GRAY}Path: {}{RESET}", desc.path.display());
198 println!();
199
200 let status_indicator = match desc.run_status {
201 Some(RunStatus::Running) => format!("{YELLOW}[running]{RESET}"),
202 Some(RunStatus::Failed) => format!("{RED}[failed]{RESET}"),
203 Some(RunStatus::Interrupted) => format!("{YELLOW}[interrupted]{RESET}"),
204 Some(RunStatus::Completed) => format!("{GREEN}[completed]{RESET}"),
205 None => format!("{GRAY}[idle]{RESET}"),
206 };
207 println!("{BOLD}Status:{RESET} {}", status_indicator);
208
209 if let Some(branch) = &desc.current_branch {
210 println!("{BLUE}Branch:{RESET} {}", branch);
211 }
212
213 if let Some(story) = &desc.current_story {
214 println!("{BLUE}Current Story:{RESET} {}", story);
215 }
216 println!();
217
218 if desc.specs.is_empty() {
219 println!("{GRAY}No specs found.{RESET}");
220 } else {
221 println!("{BOLD}Specs:{RESET} ({} total)", desc.specs.len());
222 println!();
223
224 for spec in &desc.specs {
225 print_spec_summary(spec);
226 }
227 }
228
229 println!("{GRAY}─────────────────────────────────────────────────────────{RESET}");
230 println!(
231 "{GRAY}Files: {} spec md, {} spec json, {} archived runs{RESET}",
232 desc.spec_md_count,
233 desc.specs.len(),
234 desc.runs_count
235 );
236}
237
238fn print_spec_summary(spec: &crate::config::SpecSummary) {
243 let active_label = if spec.is_active {
245 format!(" {YELLOW}(Active){RESET}")
246 } else {
247 String::new()
248 };
249
250 println!(
251 "{CYAN}━━━{RESET} {BOLD}{}{RESET}{}",
252 spec.filename, active_label
253 );
254
255 if !spec.is_active {
258 let desc_preview = if spec.description.len() > 80 {
259 format!("{}...", &spec.description[..80])
260 } else {
261 spec.description.clone()
262 };
263 let first_line = desc_preview.lines().next().unwrap_or(&desc_preview);
264 println!("{GRAY}{}{RESET}", first_line);
265 println!(
266 "{GRAY}({}/{} stories complete){RESET}",
267 spec.completed_count, spec.total_count
268 );
269 println!();
270 return;
271 }
272
273 println!("{BLUE}Project:{RESET} {}", spec.project_name);
275 println!("{BLUE}Branch:{RESET} {}", spec.branch_name);
276
277 let desc_preview = if spec.description.len() > 100 {
278 format!("{}...", &spec.description[..100])
279 } else {
280 spec.description.clone()
281 };
282 let first_line = desc_preview.lines().next().unwrap_or(&desc_preview);
283 println!("{BLUE}Description:{RESET} {}", first_line);
284 println!();
285
286 let progress_bar = make_progress_bar_simple(spec.completed_count, spec.total_count, 12);
287 let progress_color = if spec.completed_count == spec.total_count {
288 GREEN
289 } else if spec.completed_count == 0 {
290 GRAY
291 } else {
292 YELLOW
293 };
294 println!(
295 "{BOLD}Progress:{RESET} [{}] {}{}/{} stories complete{}",
296 progress_bar, progress_color, spec.completed_count, spec.total_count, RESET
297 );
298 println!();
299
300 println!("{BOLD}User Stories:{RESET}");
301 for story in &spec.stories {
302 let status_icon = if story.passes {
303 format!("{GREEN}✓{RESET}")
304 } else {
305 format!("{GRAY}○{RESET}")
306 };
307 let title_color = if story.passes { GREEN } else { RESET };
308 println!(
309 " {} {BOLD}{}{RESET}: {}{}{}",
310 status_icon, story.id, title_color, story.title, RESET
311 );
312 }
313 println!();
314}
315
316fn make_progress_bar_simple(completed: usize, total: usize, width: usize) -> String {
317 if total == 0 {
318 return " ".repeat(width);
319 }
320 let filled = (completed * width) / total;
321 let empty = width - filled;
322 format!(
323 "{GREEN}{}{RESET}{GRAY}{}{RESET}",
324 "█".repeat(filled),
325 "░".repeat(empty)
326 )
327}
328
329pub fn print_history_entry(state: &RunState, index: usize) {
331 let status_color = match state.status {
332 RunStatus::Completed => GREEN,
333 RunStatus::Failed => RED,
334 _ => YELLOW,
335 };
336 println!(
337 "{}. [{}{:?}{}] {} - {} ({} tasks)",
338 index + 1,
339 status_color,
340 state.status,
341 RESET,
342 state.started_at.format("%Y-%m-%d %H:%M"),
343 state.branch,
344 state.iterations.len()
345 );
346}
347
348pub fn print_missing_spec_warning(branch_name: &str, spec_path: &str) {
350 let top_border = format!("╔{}╗", "═".repeat(WARNING_PANEL_WIDTH - 2));
351 let bottom_border = format!("╚{}╝", "═".repeat(WARNING_PANEL_WIDTH - 2));
352 let separator = format!("╟{}╢", "─".repeat(WARNING_PANEL_WIDTH - 2));
353
354 println!();
355 println!("{YELLOW}{BOLD}{}{RESET}", top_border);
356
357 let header = " ⚠ NO SPEC FILE FOUND ";
358 let header_padding = WARNING_PANEL_WIDTH.saturating_sub(header.len() + 2);
359 let left_pad = header_padding / 2;
360 let right_pad = header_padding - left_pad;
361 println!(
362 "{YELLOW}{BOLD}║{}{}{}║{RESET}",
363 " ".repeat(left_pad),
364 header,
365 " ".repeat(right_pad)
366 );
367
368 println!("{YELLOW}{}{RESET}", separator);
369
370 print_warning_panel_line("The PR review will proceed with reduced context.");
371 print_warning_panel_line("");
372 print_warning_panel_line(&format!("Branch: {}", branch_name));
373
374 let max_path_len = WARNING_PANEL_WIDTH - 12;
375 let display_path = if spec_path.len() > max_path_len {
376 format!("...{}", &spec_path[spec_path.len() - max_path_len + 3..])
377 } else {
378 spec_path.to_string()
379 };
380 print_warning_panel_line(&format!("Expected: {}", display_path));
381
382 println!("{YELLOW}{}{RESET}", separator);
383
384 print_warning_panel_line("Create a spec file to provide full context:");
385 print_warning_panel_line(" autom8 --spec <spec.md>");
386
387 println!("{YELLOW}{BOLD}{}{RESET}", bottom_border);
388 println!();
389}
390
391fn print_warning_panel_line(text: &str) {
392 let max_width = WARNING_PANEL_WIDTH - 4;
393 let display_text = if text.len() > max_width {
394 &text[..max_width]
395 } else {
396 text
397 };
398 let padding = max_width.saturating_sub(display_text.len());
399 println!(
400 "{YELLOW}║{RESET} {}{} {YELLOW}║{RESET}",
401 display_text,
402 " ".repeat(padding)
403 );
404}
405
406pub fn print_branch_context_summary(has_spec: bool, commit_count: usize, branch_name: &str) {
408 println!();
409 println!("{CYAN}Branch Context:{RESET} {}", branch_name);
410
411 if has_spec {
412 println!("{GREEN} ✓ Spec file loaded{RESET}");
413 } else {
414 println!("{YELLOW} ⚠ No spec file (reduced context){RESET}");
415 }
416
417 println!(
418 "{BLUE} {} commit{} on branch{RESET}",
419 commit_count,
420 if commit_count == 1 { "" } else { "s" }
421 );
422 println!();
423}
424
425pub fn print_commit_list(commits: &[crate::git::CommitInfo], max_display: usize) {
427 if commits.is_empty() {
428 println!("{GRAY}No commits found on this branch.{RESET}");
429 return;
430 }
431
432 let display_count = commits.len().min(max_display);
433 println!("{BOLD}Recent Commits:{RESET}");
434
435 for commit in commits.iter().take(display_count) {
436 let max_msg_len = 50;
437 let display_msg = if commit.message.len() > max_msg_len {
438 format!("{}...", &commit.message[..max_msg_len - 3])
439 } else {
440 commit.message.clone()
441 };
442
443 println!(" {CYAN}{}{RESET} {}", commit.short_hash, display_msg);
444 }
445
446 if commits.len() > max_display {
447 println!(
448 "{GRAY} ... and {} more commit{}{RESET}",
449 commits.len() - max_display,
450 if commits.len() - max_display == 1 {
451 ""
452 } else {
453 "s"
454 }
455 );
456 }
457 println!();
458}
459
460pub fn print_sessions_status(sessions: &[SessionStatus]) {
468 println!("{BOLD}Sessions for this project:{RESET}");
469 println!();
470
471 for session in sessions {
472 print_session_row(session);
473 }
474
475 let running_count = sessions
477 .iter()
478 .filter(|s| s.metadata.is_running && !s.is_stale)
479 .count();
480 let stale_count = sessions.iter().filter(|s| s.is_stale).count();
481
482 println!();
483 print!(
484 "{GRAY}({} session{}",
485 sessions.len(),
486 if sessions.len() == 1 { "" } else { "s" }
487 );
488 if running_count > 0 {
489 print!(", {} running", running_count);
490 }
491 if stale_count > 0 {
492 print!(", {} stale", stale_count);
493 }
494 println!("){RESET}");
495}
496
497fn print_session_row(session: &SessionStatus) {
499 let metadata = &session.metadata;
500
501 let (indicator, indicator_color) = if session.is_stale {
503 ("✗", GRAY)
504 } else if session.is_current {
505 ("→", GREEN)
506 } else if metadata.is_running {
507 ("●", YELLOW)
508 } else {
509 ("○", GRAY)
510 };
511
512 let current_marker = if session.is_current { " (current)" } else { "" };
514 let stale_marker = if session.is_stale { " [stale]" } else { "" };
515
516 println!(
517 "{indicator_color}{indicator}{RESET} {BOLD}{}{RESET}{GREEN}{}{RESET}{GRAY}{}{RESET}",
518 metadata.session_id, current_marker, stale_marker
519 );
520
521 let path_str = metadata.worktree_path.display().to_string();
523 let display_path = if path_str.len() > 60 {
524 format!("...{}", &path_str[path_str.len() - 57..])
525 } else {
526 path_str
527 };
528 println!(" {GRAY}Path:{RESET} {}", display_path);
529
530 println!(" {BLUE}Branch:{RESET} {}", metadata.branch_name);
532
533 if let Some(state) = &session.machine_state {
535 let state_str = format_machine_state(state);
536 let state_color = machine_state_color(state);
537 println!(" {BLUE}State:{RESET} {state_color}{}{RESET}", state_str);
538 }
539
540 if let Some(story) = &session.current_story {
542 println!(" {BLUE}Story:{RESET} {}", story);
543 }
544
545 let duration = format_duration(metadata.created_at, metadata.last_active_at);
547 println!(
548 " {GRAY}Started:{RESET} {} {}",
549 metadata.created_at.format("%Y-%m-%d %H:%M"),
550 duration
551 );
552
553 println!();
554}
555
556fn format_machine_state(state: &MachineState) -> &'static str {
558 match state {
559 MachineState::Idle => "Idle",
560 MachineState::LoadingSpec => "Loading Spec",
561 MachineState::GeneratingSpec => "Generating Spec",
562 MachineState::Initializing => "Initializing",
563 MachineState::PickingStory => "Picking Story",
564 MachineState::RunningClaude => "Running Claude",
565 MachineState::Reviewing => "Reviewing",
566 MachineState::Correcting => "Correcting",
567 MachineState::Committing => "Committing",
568 MachineState::CreatingPR => "Creating PR",
569 MachineState::Completed => "Completed",
570 MachineState::Failed => "Failed",
571 }
572}
573
574fn machine_state_color(state: &MachineState) -> &'static str {
576 match state {
577 MachineState::Completed => GREEN,
578 MachineState::Failed => RED,
579 MachineState::RunningClaude | MachineState::Reviewing | MachineState::Correcting => YELLOW,
580 _ => CYAN,
581 }
582}
583
584fn format_duration(
586 created_at: chrono::DateTime<chrono::Utc>,
587 last_active_at: chrono::DateTime<chrono::Utc>,
588) -> String {
589 let now = Utc::now();
590 let duration = now.signed_duration_since(created_at);
591
592 let active_duration = last_active_at.signed_duration_since(created_at);
594
595 let days = duration.num_days();
596 let hours = duration.num_hours() % 24;
597 let minutes = duration.num_minutes() % 60;
598
599 let age_str = if days > 0 {
600 format!("{}d {}h ago", days, hours)
601 } else if hours > 0 {
602 format!("{}h {}m ago", hours, minutes)
603 } else if minutes > 0 {
604 format!("{}m ago", minutes)
605 } else {
606 "just now".to_string()
607 };
608
609 let active_hours = active_duration.num_hours();
611 let active_mins = active_duration.num_minutes() % 60;
612 if active_hours > 0 {
613 format!("{} (active {}h {}m)", age_str, active_hours, active_mins)
614 } else if active_mins > 5 {
615 format!("{} (active {}m)", age_str, active_mins)
616 } else {
617 age_str
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624 use crate::state::SessionMetadata;
625 use std::path::PathBuf;
626
627 fn make_session_status(
628 session_id: &str,
629 branch: &str,
630 is_current: bool,
631 is_stale: bool,
632 is_running: bool,
633 machine_state: Option<MachineState>,
634 current_story: Option<&str>,
635 ) -> SessionStatus {
636 SessionStatus {
637 metadata: SessionMetadata {
638 session_id: session_id.to_string(),
639 worktree_path: PathBuf::from(format!("/projects/test-wt-{}", session_id)),
640 branch_name: branch.to_string(),
641 created_at: Utc::now(),
642 last_active_at: Utc::now(),
643 is_running,
644 spec_json_path: None,
645 },
646 machine_state,
647 current_story: current_story.map(|s| s.to_string()),
648 is_current,
649 is_stale,
650 }
651 }
652
653 #[test]
658 fn test_us006_format_machine_state_all_variants() {
659 assert_eq!(format_machine_state(&MachineState::Idle), "Idle");
661 assert_eq!(
662 format_machine_state(&MachineState::LoadingSpec),
663 "Loading Spec"
664 );
665 assert_eq!(
666 format_machine_state(&MachineState::GeneratingSpec),
667 "Generating Spec"
668 );
669 assert_eq!(
670 format_machine_state(&MachineState::Initializing),
671 "Initializing"
672 );
673 assert_eq!(
674 format_machine_state(&MachineState::PickingStory),
675 "Picking Story"
676 );
677 assert_eq!(
678 format_machine_state(&MachineState::RunningClaude),
679 "Running Claude"
680 );
681 assert_eq!(format_machine_state(&MachineState::Reviewing), "Reviewing");
682 assert_eq!(
683 format_machine_state(&MachineState::Correcting),
684 "Correcting"
685 );
686 assert_eq!(
687 format_machine_state(&MachineState::Committing),
688 "Committing"
689 );
690 assert_eq!(
691 format_machine_state(&MachineState::CreatingPR),
692 "Creating PR"
693 );
694 assert_eq!(format_machine_state(&MachineState::Completed), "Completed");
695 assert_eq!(format_machine_state(&MachineState::Failed), "Failed");
696 }
697
698 #[test]
699 fn test_us006_machine_state_colors() {
700 assert_eq!(machine_state_color(&MachineState::Completed), GREEN);
702 assert_eq!(machine_state_color(&MachineState::Failed), RED);
703 assert_eq!(machine_state_color(&MachineState::RunningClaude), YELLOW);
704 assert_eq!(machine_state_color(&MachineState::Reviewing), YELLOW);
705 assert_eq!(machine_state_color(&MachineState::Correcting), YELLOW);
706 assert_eq!(machine_state_color(&MachineState::Idle), CYAN);
708 assert_eq!(machine_state_color(&MachineState::Initializing), CYAN);
709 }
710
711 #[test]
712 fn test_us006_session_row_current_marker() {
713 let session = make_session_status(
715 "main",
716 "feature/test",
717 true, false, true, Some(MachineState::RunningClaude),
721 Some("US-001"),
722 );
723
724 let current_marker = if session.is_current { " (current)" } else { "" };
726 assert_eq!(current_marker, " (current)");
727
728 let (indicator, _) = if session.is_stale {
730 ("✗", GRAY)
731 } else if session.is_current {
732 ("→", GREEN)
733 } else if session.metadata.is_running {
734 ("●", YELLOW)
735 } else {
736 ("○", GRAY)
737 };
738 assert_eq!(indicator, "→");
739 }
740
741 #[test]
742 fn test_us006_session_row_stale_marker() {
743 let session = make_session_status(
745 "abc12345",
746 "feature/old",
747 false, true, true, Some(MachineState::RunningClaude),
751 None,
752 );
753
754 let stale_marker = if session.is_stale { " [stale]" } else { "" };
755 assert_eq!(stale_marker, " [stale]");
756
757 let (indicator, indicator_color) = if session.is_stale {
759 ("✗", GRAY)
760 } else if session.is_current {
761 ("→", GREEN)
762 } else if session.metadata.is_running {
763 ("●", YELLOW)
764 } else {
765 ("○", GRAY)
766 };
767 assert_eq!(indicator, "✗");
768 assert_eq!(indicator_color, GRAY);
769 }
770
771 #[test]
772 fn test_us006_session_row_running_indicator() {
773 let session = make_session_status(
775 "session1",
776 "feature/parallel",
777 false, false, true, Some(MachineState::Reviewing),
781 Some("US-002"),
782 );
783
784 let (indicator, indicator_color) = if session.is_stale {
785 ("✗", GRAY)
786 } else if session.is_current {
787 ("→", GREEN)
788 } else if session.metadata.is_running {
789 ("●", YELLOW)
790 } else {
791 ("○", GRAY)
792 };
793 assert_eq!(indicator, "●");
794 assert_eq!(indicator_color, YELLOW);
795 }
796
797 #[test]
798 fn test_us006_session_row_idle_indicator() {
799 let session = make_session_status(
801 "session2",
802 "feature/done",
803 false, false, false, Some(MachineState::Completed),
807 None,
808 );
809
810 let (indicator, indicator_color) = if session.is_stale {
811 ("✗", GRAY)
812 } else if session.is_current {
813 ("→", GREEN)
814 } else if session.metadata.is_running {
815 ("●", YELLOW)
816 } else {
817 ("○", GRAY)
818 };
819 assert_eq!(indicator, "○");
820 assert_eq!(indicator_color, GRAY);
821 }
822
823 #[test]
824 fn test_us006_summary_counts() {
825 let sessions = vec![
827 make_session_status(
828 "main",
829 "main",
830 true,
831 false,
832 true,
833 Some(MachineState::RunningClaude),
834 Some("US-001"),
835 ),
836 make_session_status(
837 "session1",
838 "feat-1",
839 false,
840 false,
841 true,
842 Some(MachineState::Reviewing),
843 Some("US-002"),
844 ),
845 make_session_status("session2", "feat-2", false, true, false, None, None), make_session_status(
847 "session3",
848 "feat-3",
849 false,
850 false,
851 false,
852 Some(MachineState::Completed),
853 None,
854 ), ];
856
857 let running_count = sessions
859 .iter()
860 .filter(|s| s.metadata.is_running && !s.is_stale)
861 .count();
862 assert_eq!(running_count, 2);
863
864 let stale_count = sessions.iter().filter(|s| s.is_stale).count();
866 assert_eq!(stale_count, 1);
867
868 assert_eq!(sessions.len(), 4);
870 }
871
872 #[test]
873 fn test_us006_worktree_path_truncation() {
874 let long_path =
876 "/very/long/path/that/exceeds/sixty/characters/for/display/purposes/test-worktree";
877 assert!(long_path.len() > 60);
878
879 let display_path = if long_path.len() > 60 {
880 format!("...{}", &long_path[long_path.len() - 57..])
881 } else {
882 long_path.to_string()
883 };
884
885 assert!(display_path.starts_with("..."));
886 assert!(display_path.len() <= 60);
887 }
888
889 #[test]
890 fn test_us006_session_status_displays_all_fields() {
891 let session = make_session_status(
894 "abc12345", "feature/test", true, false, true, Some(MachineState::RunningClaude), Some("US-001"), );
902
903 assert_eq!(session.metadata.session_id, "abc12345");
905
906 assert!(session
908 .metadata
909 .worktree_path
910 .to_string_lossy()
911 .contains("abc12345"));
912
913 assert_eq!(session.metadata.branch_name, "feature/test");
915
916 assert_eq!(session.machine_state, Some(MachineState::RunningClaude));
918
919 assert_eq!(session.current_story, Some("US-001".to_string()));
921
922 assert!(session.is_current);
924 }
925}