feat: Improved UX with colored spinners for parallel graph agents and no clobbering outputs for sub-agents
This commit is contained in:
@@ -39,8 +39,12 @@ impl GraphExecutor {
|
|||||||
ctx: &mut RequestContext,
|
ctx: &mut RequestContext,
|
||||||
abort_signal: AbortSignal,
|
abort_signal: AbortSignal,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let mut logger =
|
let is_nested = ctx.current_depth > 0;
|
||||||
GraphLogger::new(&self.graph.name, self.graph.settings.log_state_snapshots);
|
let mut logger = GraphLogger::with_visibility(
|
||||||
|
&self.graph.name,
|
||||||
|
self.graph.settings.log_state_snapshots,
|
||||||
|
is_nested,
|
||||||
|
);
|
||||||
let result = self.run(&mut logger, ctx, abort_signal).await;
|
let result = self.run(&mut logger, ctx, abort_signal).await;
|
||||||
if let Err(e) = &result {
|
if let Err(e) = &result {
|
||||||
logger.graph_error(e);
|
logger.graph_error(e);
|
||||||
@@ -143,12 +147,14 @@ impl GraphExecutor {
|
|||||||
let semaphore = Arc::new(Semaphore::new(max_concurrency));
|
let semaphore = Arc::new(Semaphore::new(max_concurrency));
|
||||||
|
|
||||||
let frontier_size = frontier.len();
|
let frontier_size = frontier.len();
|
||||||
|
let is_nested = ctx.current_depth > 0;
|
||||||
let has_progress_nodes = frontier.iter().any(|nid| {
|
let has_progress_nodes = frontier.iter().any(|nid| {
|
||||||
graph.get_node(nid).is_some_and(|n| {
|
graph.get_node(nid).is_some_and(|n| {
|
||||||
!matches!(n.node_type, NodeType::Approval(_) | NodeType::Input(_))
|
!matches!(n.node_type, NodeType::Approval(_) | NodeType::Input(_))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let progress_tracker = has_progress_nodes.then(BranchProgressTracker::new);
|
let progress_tracker =
|
||||||
|
(has_progress_nodes && !is_nested).then(BranchProgressTracker::new);
|
||||||
let mut branch_tasks = Vec::with_capacity(frontier_size);
|
let mut branch_tasks = Vec::with_capacity(frontier_size);
|
||||||
for node_id in &frontier {
|
for node_id in &frontier {
|
||||||
let node = graph
|
let node = graph
|
||||||
|
|||||||
+20
-14
@@ -25,14 +25,16 @@ impl NodeTiming {
|
|||||||
pub struct GraphLogger {
|
pub struct GraphLogger {
|
||||||
graph_name: String,
|
graph_name: String,
|
||||||
log_state_snapshots: bool,
|
log_state_snapshots: bool,
|
||||||
|
silent: bool,
|
||||||
timings: IndexMap<String, NodeTiming>,
|
timings: IndexMap<String, NodeTiming>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GraphLogger {
|
impl GraphLogger {
|
||||||
pub fn new(graph_name: &str, log_state_snapshots: bool) -> Self {
|
pub fn with_visibility(graph_name: &str, log_state_snapshots: bool, silent: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
graph_name: graph_name.to_string(),
|
graph_name: graph_name.to_string(),
|
||||||
log_state_snapshots,
|
log_state_snapshots,
|
||||||
|
silent,
|
||||||
timings: IndexMap::new(),
|
timings: IndexMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,13 +44,15 @@ impl GraphLogger {
|
|||||||
"[graph:{}] start at '{}' ({} nodes)",
|
"[graph:{}] start at '{}' ({} nodes)",
|
||||||
self.graph_name, start_node, node_count
|
self.graph_name, start_node, node_count
|
||||||
);
|
);
|
||||||
eprintln!(
|
if !self.silent {
|
||||||
"{}",
|
eprintln!(
|
||||||
dimmed_text(&format!(
|
"{}",
|
||||||
"▸ graph: {} (start: {start_node})",
|
dimmed_text(&format!(
|
||||||
self.graph_name
|
"▸ graph: {} (start: {start_node})",
|
||||||
))
|
self.graph_name
|
||||||
);
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn graph_complete(&self, end_node: &str, elapsed: Duration) {
|
pub fn graph_complete(&self, end_node: &str, elapsed: Duration) {
|
||||||
@@ -56,10 +60,12 @@ impl GraphLogger {
|
|||||||
"[graph:{}] end '{}' (elapsed {:?})",
|
"[graph:{}] end '{}' (elapsed {:?})",
|
||||||
self.graph_name, end_node, elapsed
|
self.graph_name, end_node, elapsed
|
||||||
);
|
);
|
||||||
eprintln!(
|
if !self.silent {
|
||||||
"{}",
|
eprintln!(
|
||||||
dimmed_text(&format!("▸ graph done in {:.2}s", elapsed.as_secs_f64()))
|
"{}",
|
||||||
);
|
dimmed_text(&format!("▸ graph done in {:.2}s", elapsed.as_secs_f64()))
|
||||||
|
);
|
||||||
|
}
|
||||||
self.log_performance_summary();
|
self.log_performance_summary();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +163,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn records_and_aggregates_node_timings() {
|
fn records_and_aggregates_node_timings() {
|
||||||
let mut logger = GraphLogger::new("g", false);
|
let mut logger = GraphLogger::with_visibility("g", false, false);
|
||||||
logger.record_timing("a", Duration::from_millis(100));
|
logger.record_timing("a", Duration::from_millis(100));
|
||||||
logger.record_timing("a", Duration::from_millis(300));
|
logger.record_timing("a", Duration::from_millis(300));
|
||||||
logger.record_timing("b", Duration::from_millis(50));
|
logger.record_timing("b", Duration::from_millis(50));
|
||||||
@@ -187,7 +193,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_logger_has_no_timings() {
|
fn new_logger_has_no_timings() {
|
||||||
let logger = GraphLogger::new("g", true);
|
let logger = GraphLogger::with_visibility("g", true, false);
|
||||||
|
|
||||||
assert!(logger.timings.is_empty());
|
assert!(logger.timings.is_empty());
|
||||||
assert!(logger.log_state_snapshots);
|
assert!(logger.log_state_snapshots);
|
||||||
|
|||||||
+10
-19
@@ -1,10 +1,14 @@
|
|||||||
use crate::utils::IS_STDOUT_TERMINAL;
|
use crate::utils::IS_STDOUT_TERMINAL;
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const GREEN: &str = "\x1b[32m";
|
||||||
|
const RED: &str = "\x1b[31m";
|
||||||
|
const RESET: &str = "\x1b[0m";
|
||||||
|
|
||||||
static SPINNER_STYLE: LazyLock<ProgressStyle> = LazyLock::new(|| {
|
static SPINNER_STYLE: LazyLock<ProgressStyle> = LazyLock::new(|| {
|
||||||
ProgressStyle::with_template("{spinner} [{prefix}] {msg}")
|
ProgressStyle::with_template("{spinner} [{prefix}] {msg} ({elapsed})")
|
||||||
.expect("valid template")
|
.expect("valid template")
|
||||||
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", ""])
|
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", ""])
|
||||||
});
|
});
|
||||||
@@ -33,36 +37,27 @@ impl BranchProgressTracker {
|
|||||||
bar.set_prefix(label.to_string());
|
bar.set_prefix(label.to_string());
|
||||||
bar.set_message("running…");
|
bar.set_message("running…");
|
||||||
bar.enable_steady_tick(Duration::from_millis(80));
|
bar.enable_steady_tick(Duration::from_millis(80));
|
||||||
BranchProgressHandle {
|
BranchProgressHandle { bar: Some(bar) }
|
||||||
bar: Some(bar),
|
|
||||||
started: Instant::now(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct BranchProgressHandle {
|
pub(super) struct BranchProgressHandle {
|
||||||
bar: Option<ProgressBar>,
|
bar: Option<ProgressBar>,
|
||||||
started: Instant,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BranchProgressHandle {
|
impl BranchProgressHandle {
|
||||||
pub fn disabled() -> Self {
|
pub fn disabled() -> Self {
|
||||||
Self {
|
Self { bar: None }
|
||||||
bar: None,
|
|
||||||
started: Instant::now(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete(self) {
|
pub fn complete(self) {
|
||||||
if let Some(bar) = self.bar {
|
if let Some(bar) = self.bar {
|
||||||
let elapsed = self.started.elapsed();
|
bar.finish_with_message(format!("{GREEN}✓ done{RESET}"));
|
||||||
bar.finish_with_message(format!("✓ done ({:.1}s)", elapsed.as_secs_f64()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fail(self, err: &str) {
|
pub fn fail(self, err: &str) {
|
||||||
if let Some(bar) = self.bar {
|
if let Some(bar) = self.bar {
|
||||||
let elapsed = self.started.elapsed();
|
|
||||||
let truncated = if err.len() > 80 {
|
let truncated = if err.len() > 80 {
|
||||||
let mut s = err[..80].to_string();
|
let mut s = err[..80].to_string();
|
||||||
s.push('…');
|
s.push('…');
|
||||||
@@ -70,11 +65,7 @@ impl BranchProgressHandle {
|
|||||||
} else {
|
} else {
|
||||||
err.to_string()
|
err.to_string()
|
||||||
};
|
};
|
||||||
bar.finish_with_message(format!(
|
bar.finish_with_message(format!("{RED}✗ failed {RESET} — {truncated}"));
|
||||||
"✗ failed ({:.1}s) — {}",
|
|
||||||
elapsed.as_secs_f64(),
|
|
||||||
truncated
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user