use super::*;
pub(crate) fn render_text(
nodes: &[SubagentNode],
workflow_runs: &[WorkflowRun],
args: &AgentsArgs,
view: &View,
) {
if nodes.is_empty() {
println!("no subagents found");
return;
}
if view.single_node {
for n in nodes {
print_node_block(n, view, 1);
}
} else {
render_tree_text(nodes, workflow_runs, view);
}
let kinds = if args.kinds.is_empty() {
"all".to_string()
} else {
args.kinds
.iter()
.map(|k| match k {
AgentKindFilter::BuiltinTask => "builtin-task",
AgentKindFilter::Workflow => "workflow",
AgentKindFilter::Teammate => "teammate",
})
.collect::<Vec<_>>()
.join(",")
};
println!();
println!(
"{} subagent(s) · kind={kinds} · window-axis={}",
nodes.len(),
axis_label(args.order_by)
);
if any_teammate(nodes) {
println!();
println!("{TEAMMATE_CONTROL_HINT_L1}");
println!("{TEAMMATE_CONTROL_HINT_L2}");
}
}
pub(crate) fn render_tree_text(nodes: &[SubagentNode], workflow_runs: &[WorkflowRun], view: &View) {
use std::collections::BTreeMap;
let mut by_session: BTreeMap<&str, Vec<&SubagentNode>> = BTreeMap::new();
for n in nodes {
by_session
.entry(n.parent_session_id.as_str())
.or_default()
.push(n);
}
let in_scope_wf: std::collections::BTreeSet<&str> = nodes
.iter()
.filter_map(|n| n.workflow_id.as_deref())
.collect();
let mut first = true;
for (session, snodes) in &by_session {
if !first {
println!();
}
first = false;
println!("SESSION {session}");
for run in workflow_runs {
if !in_scope_wf.contains(run.run_id.as_str()) {
continue;
}
let agents: Vec<&&SubagentNode> = snodes
.iter()
.filter(|n| n.workflow_id.as_deref() == Some(run.run_id.as_str()))
.collect();
if agents.is_empty() {
continue;
}
print_workflow_run(run);
for n in agents {
print_node_block(n, view, 2);
}
}
let builtin: Vec<&SubagentNode> = snodes
.iter()
.filter(|n| n.workflow_id.is_none())
.copied()
.collect();
print_builtin_agents_nested(&builtin, view);
}
}
pub(crate) fn print_builtin_agents_nested(builtin: &[&SubagentNode], view: &View) {
use std::collections::{BTreeMap, HashSet};
let ids: HashSet<&str> = builtin.iter().map(|n| n.agent_id.as_str()).collect();
let mut kids: BTreeMap<&str, Vec<&SubagentNode>> = BTreeMap::new();
let mut roots: Vec<&SubagentNode> = Vec::new();
for &n in builtin {
match n.parent_agent_id.as_deref() {
Some(p) if ids.contains(p) => kids.entry(p).or_default().push(n),
_ => roots.push(n),
}
}
roots.sort_by(|a, b| a.agent_id.cmp(&b.agent_id));
let mut stack: Vec<(&SubagentNode, usize)> =
roots.into_iter().rev().map(|n| (n, 1usize)).collect();
while let Some((n, indent)) = stack.pop() {
print_node_block(n, view, indent);
if let Some(cs) = kids.get(n.agent_id.as_str()) {
let mut cs = cs.clone();
cs.sort_by(|a, b| b.agent_id.cmp(&a.agent_id)); for c in cs {
stack.push((c, indent + 1));
}
}
}
}
pub(crate) fn print_workflow_run(run: &WorkflowRun) {
let mut head = format!(" WORKFLOW {}", run.run_id);
if let Some(name) = &run.workflow_name {
head.push_str(&format!(" [{name}]"));
}
if let Some(s) = &run.status {
head.push_str(&format!(" {s}"));
}
println!("{head}");
if let Some(n) = run.agent_count {
println!(" agents {n}");
}
if let Some(ms) = run.duration_ms {
println!(" duration {}", fmt_ms(ms));
}
if let Some(t) = run.total_tokens {
println!(" tokens {t}");
}
if let Some(m) = &run.default_model {
println!(" model {m}");
}
}
pub(crate) fn print_node_block(n: &SubagentNode, view: &View, depth: usize) {
let ind = " ".repeat(depth);
let ind2 = " ".repeat(depth + 1);
let mut head = format!("{ind}{} {}", n.agent_id, n.kind.label());
if let Some(wf) = &n.workflow_id {
head.push_str(&format!(" ({wf})"));
}
if let Some(t) = &n.agent_type {
head.push_str(&format!(" [{t}]"));
}
head.push_str(&format!(" {}", n.status.label()));
println!("{head}");
if let Some(class) = n.pending_classification {
let tool = n.pending_tool_name.as_deref().unwrap_or("?");
let id = n.pending_tool_use_id.as_deref().unwrap_or("?");
println!(
"{ind2}PENDING {} · {tool} ({id}) · frozen since {}",
class.label(),
format_timestamp(n.pending_since_utc.as_deref())
);
if class == PendingClassification::EscalationBlocked {
println!(
"{ind2} ↑ a dangerous-rm Bash CC HOISTS for human approval even under \
bypass — almost certainly waiting for a Yes (approve/deny in the main UI), NOT dead."
);
}
}
if let Some(tn) = &n.team_name {
match &n.name {
Some(nm) => println!("{ind2}team {tn} (@{nm})"),
None => println!("{ind2}team {tn}"),
}
}
if let Some(d) = &n.description {
println!("{ind2}desc {d}");
}
println!(
"{ind2}triggered {}",
format_timestamp(n.trigger_utc.as_deref())
);
println!(
"{ind2}started {}",
format_timestamp(n.started_utc.as_deref())
);
if n.completed_utc.is_some() {
println!(
"{ind2}completed {}",
format_timestamp(n.completed_utc.as_deref())
);
if let Some(dur) = duration_label(n.trigger_utc.as_deref(), n.completed_utc.as_deref()) {
println!("{ind2}duration {dur}");
}
} else if n.pending_classification.is_none() {
println!(
"{ind2}last-seen {}",
format_timestamp(n.last_activity_utc.as_deref())
);
}
if view.want_returned {
if let (Some(msg), Some(src)) = (&n.returned_message, n.returned_message_source) {
if n.completed_utc.is_none() {
println!(
"{ind2}returned ({} · history — predates the still-open lane, NOT the outcome) {}",
src.label(),
one_line(msg)
);
} else {
println!("{ind2}returned ({}) {}", src.label(), one_line(msg));
}
} else {
println!("{ind2}returned (unresolved)");
}
}
if view.want_files {
if n.files_changed.is_empty() {
println!("{ind2}files (none)");
} else {
println!("{ind2}files {} changed", n.files_changed.len());
for (path, op, is_create) in &n.files_changed {
let tag = if *is_create { "create" } else { op.as_str() };
println!("{ind2} {tag:<12} {path}");
}
}
}
if n.skipped_lines > 0 {
println!(
"{ind2}note {} (among the head/tail lines read — full census: csift stats)",
crate::text::malformed_note(n.skipped_lines)
);
}
}