use super::*;
use crate::output::tool_card::{self, TaskActivityFacts, TaskFacts};
fn subagent_card_total_from_fields(node: &crate::tui::activity::ActivityNode) -> Option<usize> {
node.metadata.fields.iter().find_map(|(key, value)| {
(key == "tasks")
.then(|| value.trim().parse::<usize>().ok())
.flatten()
})
}
pub(super) fn subagent_children(
state: &MissionControlState,
activity_id: Option<&ActivityId>,
entry_index: usize,
) -> Vec<TranscriptCardChild> {
let from_activity = activity_id
.and_then(|id| state.children.get(id))
.map(|children| {
children
.iter()
.filter_map(|child_id| {
let node = state.nodes.get(child_id)?;
let task = node.transcript_subagent.as_ref();
let live_tail = task_live_tail(state, child_id);
let shared = tool_card::project_task(&TaskFacts {
label: &node.metadata.label,
intent: task.map(|task| task.intent.as_str()),
status: Some(node.status),
fields: &node.metadata.fields,
failure: (node.status == ActivityStatus::Failed)
.then_some(node.preview.as_str()),
usage: node.usage,
live_tail: &live_tail,
});
Some(TranscriptCardChild {
label: shared.label,
status: tool_adapters::terminal_status(shared.status),
metadata: Vec::new(),
body: shared.body,
task_id: task.map(|task| task.task_id.clone()).unwrap_or_default(),
intent: shared.intent,
identity: shared.identity,
agent: shared.agent,
model_line: shared.model_line,
subagent_display: node.subagent_display.clone(),
usage: shared.usage,
last_known_cache_percent: node.last_known_cache_percent,
last_known_latest_cache_percent: node.last_known_latest_cache_percent,
live_tail: shared.live_tail,
activity_id: Some(child_id.clone()),
})
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if !from_activity.is_empty() {
return from_activity;
}
state
.transcript
.get(entry_index)
.and_then(TranscriptEntry::tool_data)
.map(|data| {
data.children
.iter()
.map(|child| {
let shared = tool_card::project_task(&TaskFacts {
label: &child.label,
intent: Some(&child.intent),
status: child.status,
fields: &[],
failure: None,
usage: None,
live_tail: &[],
});
TranscriptCardChild {
label: shared.label,
status: tool_adapters::terminal_status(shared.status),
metadata: Vec::new(),
body: shared.body,
task_id: child.task_id.clone(),
intent: shared.intent,
identity: shared.identity,
agent: shared.agent,
model_line: shared.model_line,
subagent_display: Default::default(),
usage: shared.usage,
last_known_cache_percent: None,
last_known_latest_cache_percent: None,
live_tail: shared.live_tail,
activity_id: None,
}
})
.collect()
})
.unwrap_or_default()
}
fn task_live_tail(state: &MissionControlState, task_id: &ActivityId) -> Vec<String> {
let mut events = state
.nodes
.values()
.filter(|node| node.id == *task_id || activity_is_descendant_of(state, &node.id, task_id))
.collect::<Vec<_>>();
events.sort_by(|left, right| {
right
.own_update_sequence
.cmp(&left.own_update_sequence)
.then_with(|| right.id.as_str().cmp(left.id.as_str()))
});
let mut lines = Vec::new();
for node in events {
for line in subagent_card_activity_lines(node) {
lines.push(line);
if lines.len() == state.subagent_card_rows() {
lines.reverse();
return lines;
}
}
}
lines.reverse();
lines
}
fn activity_is_descendant_of(
state: &MissionControlState,
id: &ActivityId,
ancestor: &ActivityId,
) -> bool {
let mut parent = state.nodes.get(id).and_then(|node| node.parent_id.as_ref());
let mut remaining = state.nodes.len();
while let Some(parent_id) = parent {
if parent_id == ancestor {
return true;
}
if remaining == 0 {
break;
}
remaining -= 1;
parent = state
.nodes
.get(parent_id)
.and_then(|node| node.parent_id.as_ref());
}
false
}
fn subagent_card_activity_lines(node: &crate::tui::activity::ActivityNode) -> Vec<String> {
tool_card::project_task_activity(&TaskActivityFacts {
kind: &node.kind,
status: node.status,
label: &node.metadata.label,
permitted_preview: &node.preview,
tool_detail: node.tool_detail.as_ref(),
reasoning_summary: node
.transcript_reasoning
.as_ref()
.map(|summary| (summary.count, summary.latest.as_deref())),
})
}
pub(super) fn subagent_card_summary_from_children(
state: &MissionControlState,
batch_id: Option<&ActivityId>,
children: &[TranscriptCardChild],
node: Option<&crate::tui::activity::ActivityNode>,
) -> SubagentCardSummary {
let (done, failed, running) =
tool_card::task_counts(children.iter().map(|child| match child.status {
TranscriptCardStatus::Success => tool_card::CardStatus::Success,
TranscriptCardStatus::Canceled => tool_card::CardStatus::Canceled,
TranscriptCardStatus::Failed => tool_card::CardStatus::Failed,
TranscriptCardStatus::Running => tool_card::CardStatus::Running,
TranscriptCardStatus::None => tool_card::CardStatus::None,
}));
let total = children
.len()
.max(node.and_then(subagent_card_total_from_fields).unwrap_or(0));
let selection = batch_id.and_then(|id| state.subagent_card_selection.get(id));
let selected = batch_id.and_then(|batch_id| {
let pinned = selection
.and_then(|selection| selection.task_id.as_ref())
.and_then(|task_id| {
children
.iter()
.find(|child| child.activity_id.as_ref() == Some(task_id))
});
pinned.or_else(|| children.first()).and_then(|child| {
child
.activity_id
.clone()
.or_else(|| (!child.task_id.is_empty()).then(|| batch_id.child(&child.task_id)))
})
});
SubagentCardSummary {
total: total.max(done + failed + running),
done,
failed,
running,
selected_task_id: selected
.as_ref()
.map(|id| id.as_str().rsplit('/').next().unwrap_or("").to_string()),
batch_id: batch_id.cloned(),
}
}