use super::operations::AgentOperation;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
pub struct ProgressTracker {
#[allow(dead_code)]
multi_progress: MultiProgress,
overall_bar: ProgressBar,
agent_bars: Vec<ProgressBar>,
tick_handle: Option<JoinHandle<()>>,
is_finished: Arc<AtomicBool>,
#[allow(dead_code)]
start_time: Instant,
agent_operations: Arc<RwLock<Vec<AgentOperation>>>,
}
impl ProgressTracker {
pub fn new(total_items: usize, max_parallel: usize) -> Self {
let multi_progress = MultiProgress::new();
let overall_bar = multi_progress.add(ProgressBar::new(total_items as u64));
overall_bar.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}",
)
.unwrap()
.progress_chars("##-"),
);
overall_bar.set_message("Processing items...");
overall_bar.enable_steady_tick(Duration::from_millis(100));
let mut agent_bars = Vec::new();
let mut agent_operations = Vec::new();
for i in 0..max_parallel.min(total_items) {
let bar = multi_progress.add(ProgressBar::new(100));
bar.set_style(
ProgressStyle::default_bar()
.template(&format!(" Agent {:2}: {{msg}}", i + 1))
.unwrap(),
);
bar.set_message("Idle");
agent_bars.push(bar);
agent_operations.push(AgentOperation::Idle);
}
Self {
multi_progress,
overall_bar,
agent_bars,
tick_handle: None,
is_finished: Arc::new(AtomicBool::new(false)),
start_time: Instant::now(),
agent_operations: Arc::new(RwLock::new(agent_operations)),
}
}
pub fn update_agent(&self, agent_index: usize, message: &str) {
if agent_index < self.agent_bars.len() {
self.agent_bars[agent_index].set_message(message.to_string());
}
}
pub async fn update_agent_operation(&self, agent_index: usize, operation: AgentOperation) {
let mut ops = self.agent_operations.write().await;
if agent_index < ops.len() {
ops[agent_index] = operation.clone();
let message = format_operation(&operation);
self.update_agent(agent_index, &message);
}
}
pub fn complete_item(&self) {
self.overall_bar.inc(1);
}
pub fn finish(&self, message: &str) {
self.is_finished.store(true, Ordering::Relaxed);
self.overall_bar.finish_with_message(message.to_string());
for bar in &self.agent_bars {
bar.finish_and_clear();
}
}
pub fn start_timer(&mut self) {
let is_finished = self.is_finished.clone();
let overall_bar = self.overall_bar.clone();
let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_millis(100));
loop {
interval.tick().await;
if is_finished.load(Ordering::Relaxed) {
break;
}
overall_bar.tick();
}
});
self.tick_handle = Some(handle);
}
pub async fn get_agent_operations(&self) -> Vec<AgentOperation> {
self.agent_operations.read().await.clone()
}
pub fn agent_count(&self) -> usize {
self.agent_bars.len()
}
pub fn is_finished(&self) -> bool {
self.is_finished.load(Ordering::Relaxed)
}
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
pub fn set_message(&self, message: &str) {
self.overall_bar.set_message(message.to_string());
}
pub fn position(&self) -> u64 {
self.overall_bar.position()
}
pub fn length(&self) -> u64 {
self.overall_bar.length().unwrap_or(0)
}
pub fn inc(&self, delta: u64) {
self.overall_bar.inc(delta);
}
pub fn set_position(&self, pos: u64) {
self.overall_bar.set_position(pos);
}
pub fn clear(&self) {
self.overall_bar.finish_and_clear();
for bar in &self.agent_bars {
bar.finish_and_clear();
}
}
}
fn format_operation(operation: &AgentOperation) -> String {
match operation {
AgentOperation::Idle => "Idle".to_string(),
AgentOperation::Setup(cmd) => {
format!("[setup] {}", truncate_command(cmd, 40))
}
AgentOperation::Claude(cmd) => {
format!("[claude] {}", truncate_command(cmd, 40))
}
AgentOperation::Shell(cmd) => {
format!("[shell] {}", truncate_command(cmd, 40))
}
AgentOperation::Test(cmd) => {
format!("[test] {}", truncate_command(cmd, 40))
}
AgentOperation::Handler(name) => format!("[handler] {}", name),
AgentOperation::Retrying(item, attempt) => {
format!("Retrying {} (attempt {})", item, attempt)
}
AgentOperation::Complete => "Complete".to_string(),
}
}
fn truncate_command(cmd: &str, max_len: usize) -> String {
if cmd.len() <= max_len {
cmd.to_string()
} else {
format!("{}...", &cmd[..max_len - 3])
}
}
impl Drop for ProgressTracker {
fn drop(&mut self) {
if let Some(handle) = self.tick_handle.take() {
handle.abort();
}
self.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_progress_tracker_creation() {
let tracker = ProgressTracker::new(100, 10);
assert_eq!(tracker.agent_count(), 10);
assert!(!tracker.is_finished());
assert_eq!(tracker.length(), 100);
}
#[tokio::test]
async fn test_progress_operations() {
let mut tracker = ProgressTracker::new(10, 5);
tracker.start_timer();
tracker
.update_agent_operation(0, AgentOperation::Setup("test setup".to_string()))
.await;
tracker
.update_agent_operation(1, AgentOperation::Complete)
.await;
let ops = tracker.get_agent_operations().await;
assert!(matches!(ops[0], AgentOperation::Setup(_)));
assert!(matches!(ops[1], AgentOperation::Complete));
tracker.complete_item();
assert_eq!(tracker.position(), 1);
tracker.finish("Done");
assert!(tracker.is_finished());
}
#[test]
fn test_truncate_command() {
assert_eq!(truncate_command("short", 10), "short");
assert_eq!(
truncate_command("this is a very long command", 10),
"this is..."
);
}
#[test]
fn test_format_operation() {
let op = AgentOperation::Claude("claude command".to_string());
assert!(format_operation(&op).contains("[claude]"));
let op = AgentOperation::Retrying("item1".to_string(), 2);
let formatted = format_operation(&op);
assert!(formatted.contains("Retrying item1"));
assert!(formatted.contains("attempt 2"));
}
}