use std::time::Instant;
pub mod lane;
pub mod progress;
#[derive(Debug, Clone)]
pub enum StreamEvent {
ToolStart { name: String, description: String },
ToolOutput { name: String, output: String },
AgentMessage { content: String },
TaskProgress { current: u64, total: u64, description: String },
TaskComplete { summary: String },
Error { message: String },
}
pub struct StreamSender {
tx: tokio::sync::mpsc::UnboundedSender<StreamEvent>,
}
impl StreamSender {
pub fn send(&self, event: StreamEvent) {
let _ = self.tx.send(event);
}
}
pub struct StreamReceiver {
rx: tokio::sync::mpsc::UnboundedReceiver<StreamEvent>,
display: LiveDisplay,
}
impl StreamReceiver {
pub fn display(&mut self) -> &mut LiveDisplay {
&mut self.display
}
pub async fn process(&mut self) -> Option<StreamEvent> {
match self.rx.recv().await {
Some(event) => {
self.display.update(&event);
Some(event)
}
None => None,
}
}
}
pub fn create_channel() -> (StreamSender, StreamReceiver) {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let sender = StreamSender { tx };
let receiver = StreamReceiver {
rx,
display: LiveDisplay::new(),
};
(sender, receiver)
}
pub struct LiveDisplay {
start_time: Instant,
current_tool: Option<String>,
task_description: String,
steps_done: u64,
steps_total: u64,
}
impl LiveDisplay {
pub fn new() -> Self {
Self {
start_time: Instant::now(),
current_tool: None,
task_description: String::new(),
steps_done: 0,
steps_total: 0,
}
}
pub fn update(&mut self, event: &StreamEvent) {
match event {
StreamEvent::ToolStart { name, description } => {
self.current_tool = Some(name.clone());
let elapsed = self.start_time.elapsed().as_secs();
eprintln!(
"\x1b[36m[{elapsed:>3}s]\x1b[0m \x1b[33m🔧 {name}\x1b[0m — {description}"
);
}
StreamEvent::ToolOutput { name, output } => {
let preview: String = output.lines().take(3).map(|l| format!(" │ {l}")).collect::<Vec<_>>().join("\n");
if !preview.is_empty() {
eprintln!("{preview}");
if output.lines().count() > 3 {
eprintln!(" │ \x1b[2m... ({} more lines)\x1b[0m", output.lines().count() - 3);
}
}
}
StreamEvent::AgentMessage { content } => {
eprintln!("\x1b[35m💬\x1b[0m {content}");
}
StreamEvent::TaskProgress { current, total, description } => {
self.steps_done = *current;
self.steps_total = *total;
self.task_description = description.clone();
let pct = if *total > 0 { (current * 100) / total } else { 0 };
let bar = "â–ˆ".repeat(pct as usize / 5) + &"â–‘".repeat(20 - pct as usize / 5);
let elapsed = self.start_time.elapsed().as_secs();
eprintln!(
"\x1b[36m[{elapsed:>3}s]\x1b[0m [{bar}] {pct}% — {description}"
);
}
StreamEvent::TaskComplete { summary } => {
let elapsed = self.start_time.elapsed().as_secs_f64();
eprintln!("\x1b[32m✓\x1b[0m {summary} \x1b[2m({elapsed:.1}s)\x1b[0m");
}
StreamEvent::Error { message } => {
eprintln!("\x1b[31m✗\x1b[0m {message}");
}
}
}
pub fn finish(&mut self) {
let elapsed = self.start_time.elapsed().as_secs_f64();
eprintln!("\n\x1b[1mDone in {elapsed:.1}s\x1b[0m — {}/{} steps completed", self.steps_done, self.steps_total);
}
pub fn elapsed(&self) -> f64 {
self.start_time.elapsed().as_secs_f64()
}
}