use crate::core::event::EventLogger;
use crate::core::state::StateManager;
use crate::core::task::{TaskManager, TaskStatus};
use colored::Colorize;
use std::path::Path;
pub fn execute(project_root: &Path, event_count: usize) -> anyhow::Result<()> {
let forge_dir = project_root.join(".forge");
if !forge_dir.exists() {
println!(
"{} Forge is not initialized. Run {} first.",
"!".yellow(),
"forge init".cyan()
);
return Ok(());
}
let state_mgr = StateManager::new(&forge_dir);
let task_mgr = TaskManager::new(&forge_dir);
let event_logger = EventLogger::new(&forge_dir);
let state = state_mgr.load()?;
let tasks = task_mgr.list_tasks()?;
let recent_events = event_logger.read_recent(event_count)?;
println!();
println!(
"{}",
"┌──────────────────────────────────────────────┐".cyan()
);
println!(
"{}",
"│ FORGE ORCHESTRATOR STATUS │"
.cyan()
.bold()
);
println!(
"{}",
"└──────────────────────────────────────────────┘".cyan()
);
println!();
println!(" {} {}", "Project:".dimmed(), state.project_name.bold());
println!(
" {} {}",
"Updated:".dimmed(),
state.updated_at.format("%Y-%m-%d %H:%M UTC")
);
println!(
" {} {}",
"Brain: ".dimmed(),
state.brain.provider.yellow()
);
println!();
println!(" {}", "Detected Tools:".bold());
if state.tools.is_empty() {
println!(" {} No AI tools detected", "!".yellow());
} else {
for tool in &state.tools {
let status = if tool.available {
"●".green().to_string()
} else {
"○".red().to_string()
};
println!(" {status} {}", tool.name);
}
}
println!();
let mut in_progress = 0usize;
let mut completed = 0usize;
let mut failed = 0usize;
for task in &tasks {
match task.status {
TaskStatus::Pending | TaskStatus::Blocked => {}
TaskStatus::Assigned | TaskStatus::InProgress => in_progress += 1,
TaskStatus::Completed => completed += 1,
TaskStatus::Failed => failed += 1,
}
}
let total = tasks.len();
let progress = if total > 0 {
(completed as f64 / total as f64 * 100.0) as usize
} else {
0
};
let completed_ids: Vec<String> = tasks
.iter()
.filter(|t| t.status == TaskStatus::Completed)
.map(|t| t.id.clone())
.collect();
println!(" {}", "Task Board:".bold());
if tasks.is_empty() {
println!(
" {} No tasks found. Run {} to create tasks.",
"!".yellow(),
"forge plan --generate".cyan()
);
} else {
println!(
" {:<8} {:<8} {:<12} {:<10} {:<10} {:<32} {}",
"ID".dimmed(),
"Phase".dimmed(),
"Status".dimmed(),
"Agent".dimmed(),
"Type".dimmed(),
"Title".dimmed(),
"Deps".dimmed(),
);
println!(" {}", "─".repeat(96));
for task in &tasks {
let status_str = match task.status {
TaskStatus::Pending => {
if task.is_blocked(&completed_ids) {
"⏳ Blocked".yellow().to_string()
} else {
"○ Ready".white().to_string()
}
}
TaskStatus::Assigned | TaskStatus::InProgress => "⚡ Running".cyan().to_string(),
TaskStatus::Completed => "✓ Done".green().to_string(),
TaskStatus::Failed => "✗ Failed".red().to_string(),
TaskStatus::Blocked => "⏳ Blocked".yellow().to_string(),
};
let agent_str = task
.assigned_to
.as_ref()
.map(|a| a.to_string())
.unwrap_or_else(|| "-".into());
let type_str = task.task_type.as_deref().unwrap_or("-");
let title = truncate_title(&task.title, 30);
let phase_str = task
.phase
.as_ref()
.map(|p| p.to_string())
.unwrap_or_else(|| "build".into());
let deps = if task.depends_on.is_empty() {
"-".to_string()
} else {
task.depends_on
.iter()
.map(|d| {
if completed_ids.contains(d) {
d.green().to_string()
} else {
d.yellow().to_string()
}
})
.collect::<Vec<_>>()
.join(", ")
};
let id_display = if task.parent_task.is_some() {
format!(" {}", task.id).cyan().to_string()
} else {
task.id.cyan().to_string()
};
println!(
" {:<8} {:<8} {:<12} {:<10} {:<10} {:<32} {}",
id_display,
phase_str.dimmed(),
status_str,
agent_str,
type_str.dimmed(),
title,
deps
);
}
}
println!();
let blocked_count = tasks
.iter()
.filter(|t| {
matches!(t.status, TaskStatus::Pending | TaskStatus::Blocked)
&& t.is_blocked(&completed_ids)
})
.count();
let ready_count = tasks
.iter()
.filter(|t| matches!(t.status, TaskStatus::Pending) && !t.is_blocked(&completed_ids))
.count();
println!(
" {} total | {} ready | {} running | {} done | {} failed | {} blocked",
total.to_string().bold(),
ready_count.to_string().white(),
in_progress.to_string().cyan(),
completed.to_string().green(),
failed.to_string().red(),
blocked_count.to_string().yellow(),
);
let bar_width = 30;
let filled = (progress * bar_width) / 100;
let empty = bar_width - filled;
let bar = format!(
" [{}{}] {}%",
"█".repeat(filled).green(),
"░".repeat(empty),
progress
);
println!("{bar}");
println!();
println!(" {}", "Provider Quota (this session):".bold());
println!(
" {} Quota tracking is only active during dashboard sessions",
"\u{26A0}".yellow()
);
println!(" Use `forge dashboard` or `forge start` for live quota monitoring");
println!();
if !state.active_locks.is_empty() {
println!(" {}", "Active Locks:".bold());
for (task_id, lock) in &state.active_locks {
println!(
" {} → {} files locked by {}",
task_id.dimmed(),
lock.files.len(),
lock.agent.to_string().cyan()
);
}
println!();
}
if !recent_events.is_empty() {
println!(" {}", "Recent Events:".bold());
for event in &recent_events {
let time = event.timestamp.format("%H:%M");
let task_str = event.task_id.as_deref().unwrap_or("");
println!(
" {} {} {}",
time.to_string().dimmed(),
task_str.dimmed(),
event.message
);
}
println!();
}
Ok(())
}
fn truncate_title(title: &str, max_len: usize) -> String {
let char_count: usize = title.chars().count();
if char_count > max_len {
let truncated: String = title.chars().take(max_len.saturating_sub(3)).collect();
format!("{truncated}...")
} else {
title.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::task::{AgentType, Task};
use chrono::Utc;
fn make_task(id: &str, status: TaskStatus, deps: Vec<String>) -> Task {
let now = Utc::now();
Task {
id: id.to_string(),
title: format!("Task {id}"),
description: String::new(),
status,
assigned_to: Some(AgentType::Claude),
task_type: Some("implement".to_string()),
depends_on: deps,
locked_files: vec![],
acceptance_criteria: vec![],
created_at: now,
updated_at: now,
completed_at: None,
plan_version: None,
parent_task: None,
phase: None,
retry_count: 0,
verified_by: None,
verified_at: None,
}
}
#[test]
fn test_truncate_title_short() {
assert_eq!(truncate_title("Hello", 32), "Hello");
}
#[test]
fn test_truncate_title_exact() {
let title = "a".repeat(32);
assert_eq!(truncate_title(&title, 32), title);
}
#[test]
fn test_truncate_title_long() {
let title = "a".repeat(50);
let result = truncate_title(&title, 32);
assert!(result.ends_with("..."));
assert_eq!(result.len(), 32);
}
#[test]
fn test_ready_vs_blocked_counting() {
let tasks = vec![
make_task("T-001", TaskStatus::Completed, vec![]),
make_task("T-002", TaskStatus::Pending, vec!["T-001".into()]), make_task("T-003", TaskStatus::Pending, vec!["T-002".into()]), make_task("T-004", TaskStatus::Pending, vec![]), ];
let completed_ids: Vec<String> = tasks
.iter()
.filter(|t| t.status == TaskStatus::Completed)
.map(|t| t.id.clone())
.collect();
let ready_count = tasks
.iter()
.filter(|t| matches!(t.status, TaskStatus::Pending) && !t.is_blocked(&completed_ids))
.count();
let blocked_count = tasks
.iter()
.filter(|t| {
matches!(t.status, TaskStatus::Pending | TaskStatus::Blocked)
&& t.is_blocked(&completed_ids)
})
.count();
assert_eq!(ready_count, 2); assert_eq!(blocked_count, 1); }
#[test]
fn test_status_display_blocked_pending_distinction() {
let completed_ids = vec!["T-001".to_string()];
let blocked_task = make_task("T-003", TaskStatus::Pending, vec!["T-002".into()]);
let ready_task = make_task("T-002", TaskStatus::Pending, vec!["T-001".into()]);
assert!(blocked_task.is_blocked(&completed_ids));
assert!(!ready_task.is_blocked(&completed_ids));
}
#[test]
fn test_empty_task_board_counts() {
let tasks: Vec<Task> = vec![];
let completed_ids: Vec<String> = vec![];
let ready_count = tasks
.iter()
.filter(|t| matches!(t.status, TaskStatus::Pending) && !t.is_blocked(&completed_ids))
.count();
let blocked_count = tasks
.iter()
.filter(|t| {
matches!(t.status, TaskStatus::Pending | TaskStatus::Blocked)
&& t.is_blocked(&completed_ids)
})
.count();
assert_eq!(ready_count, 0);
assert_eq!(blocked_count, 0);
}
}