use super::*;
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 label = normalize_inline_separators(&node.metadata.label);
let task_id = node
.transcript_subagent
.as_ref()
.map(|summary| summary.task_id.clone())
.unwrap_or_default();
let intent = node
.transcript_subagent
.as_ref()
.map(|summary| summary.intent.clone())
.unwrap_or_else(|| label.clone());
let identity = identity_from_fields(&node.metadata.fields);
let agent = agent_from_fields(&node.metadata.fields);
let model_line = model_line_from_fields(&node.metadata.fields);
let status = subagent_card_status_from_activity(node.status);
let body = if status == TranscriptCardStatus::Failed {
first_non_empty_line(&node.preview).unwrap_or_default()
} else {
String::new()
};
let live_tail = task_live_tail(state, child_id);
Some(TranscriptCardChild {
label,
status,
metadata: Vec::new(),
body,
task_id,
intent,
identity,
agent,
model_line,
usage: node.usage,
last_known_cache_percent: node.last_known_cache_percent,
last_known_latest_cache_percent: node.last_known_latest_cache_percent,
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| TranscriptCardChild {
label: child.label.clone(),
status: child
.status
.map(subagent_card_status_from_activity)
.unwrap_or(TranscriptCardStatus::None),
metadata: Vec::new(),
body: String::new(),
task_id: child.task_id.clone(),
intent: child.intent.clone(),
identity: None,
agent: None,
model_line: None,
usage: None,
last_known_cache_percent: None,
last_known_latest_cache_percent: None,
live_tail: Vec::new(),
activity_id: None,
})
.collect()
})
.unwrap_or_default()
}
fn metadata_field_from_fields(fields: &[(String, String)], key: &str) -> Option<String> {
fields.iter().find_map(|(field, value)| {
if field != key {
return None;
}
let value = value.trim();
(!value.is_empty()).then(|| sanitized_single_line(value))
})
}
fn identity_from_fields(fields: &[(String, String)]) -> Option<String> {
metadata_field_from_fields(fields, "identity")
}
fn agent_from_fields(fields: &[(String, String)]) -> Option<String> {
metadata_field_from_fields(fields, "agent")
}
fn model_line_from_fields(fields: &[(String, String)]) -> Option<String> {
let provider = metadata_field_from_fields(fields, "provider")?;
let model = metadata_field_from_fields(fields, "model")?;
let reasoning =
metadata_field_from_fields(fields, "reasoning").unwrap_or_else(|| "default".to_string());
Some(format!("{provider}/{model}({reasoning})"))
}
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> {
if let Some(summary) = &node.transcript_reasoning {
return subagent_card_reasoning_activity_lines(node, summary);
}
match node.kind {
crate::output::ActivityKind::SubagentTask if node.preview.trim().is_empty() => Vec::new(),
crate::output::ActivityKind::Tool => subagent_card_tool_activity_lines(node),
crate::output::ActivityKind::Assistant => {
let source = if node.preview.trim().is_empty() {
&node.metadata.label
} else {
&node.preview
};
subagent_card_activity_source_lines(source, "agent")
}
crate::output::ActivityKind::SubagentTask => {
subagent_card_activity_source_lines(&node.preview, "agent")
}
crate::output::ActivityKind::Diagnostic => subagent_card_activity_source_lines(
if node.preview.trim().is_empty() {
&node.metadata.label
} else {
&node.preview
},
"diagnostic",
),
crate::output::ActivityKind::Hook => {
subagent_card_activity_source_lines(&node.metadata.label, "hook")
}
crate::output::ActivityKind::ProviderContextInjection => {
subagent_card_activity_source_lines(&node.metadata.label, "provider context injection")
}
crate::output::ActivityKind::SubagentBatch => {
subagent_card_activity_source_lines(&node.metadata.label, "subagent batch")
}
crate::output::ActivityKind::Compaction => {
subagent_card_activity_source_lines(&node.metadata.label, "compaction")
}
}
}
fn subagent_card_reasoning_activity_lines(
node: &crate::tui::activity::ActivityNode,
summary: &crate::tui::activity::ActivityReasoningSummary,
) -> Vec<String> {
let label = subagent_card_sanitized_line(&node.metadata.label)
.unwrap_or_else(|| "reasoning".to_string());
let latest = summary
.latest
.as_deref()
.and_then(subagent_card_sanitized_line);
let count = summary.count;
match (count, latest) {
(Some(0 | 1), Some(latest)) => vec![format!("reasoning • {latest}")],
(Some(count), Some(latest)) => {
vec![format!("reasoning summaries ×{count} • {latest}")]
}
(_, Some(latest)) => vec![format!("reasoning • {latest}")],
_ => vec![label],
}
}
fn subagent_card_tool_activity_lines(node: &crate::tui::activity::ActivityNode) -> Vec<String> {
if node.tool_detail.is_none() && !node.preview.trim().is_empty() {
return subagent_card_activity_source_lines(&node.preview, "tool");
}
let status = match node.status {
ActivityStatus::Queued => "○",
ActivityStatus::Running | ActivityStatus::Writing => "●",
ActivityStatus::Success => "✓",
ActivityStatus::Failed => "✗",
ActivityStatus::Canceled => "⊘",
};
let (name, label) = node.tool_detail.as_ref().map_or_else(
|| ("tool".to_string(), node.metadata.label.clone()),
|detail| (detail.tool_name.to_string(), detail.label.to_string()),
);
let Some(name) = subagent_card_sanitized_line(&name) else {
return Vec::new();
};
let label = subagent_card_sanitized_line(&label)
.filter(|label| !label.eq_ignore_ascii_case(&name))
.map(|label| strip_tool_prefix(&label, &name).to_string());
let summary = label.map_or_else(|| name.clone(), |label| format!("{name} • {label}"));
vec![format!("{summary} • {status}")]
}
fn subagent_card_activity_source_lines(source: &str, prefix: &str) -> Vec<String> {
source
.lines()
.rev()
.filter_map(subagent_card_sanitized_line)
.map(|line| {
if line.eq_ignore_ascii_case(prefix) {
prefix.to_string()
} else {
let line = strip_tool_prefix(&line, prefix);
format!("{prefix} • {line}")
}
})
.collect()
}
fn strip_tool_prefix<'a>(label: &'a str, tool: &str) -> &'a str {
let Some(rest) = label.get(tool.len()..) else {
return label;
};
if label[..tool.len()].eq_ignore_ascii_case(tool)
&& rest
.chars()
.next()
.is_some_and(|ch| ch.is_whitespace() || ch == '•' || ch == '·')
{
rest.trim_start_matches([' ', '\t', '•', '·'])
} else {
label
}
}
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) = children.iter().fold(
(0usize, 0usize, 0usize),
|(done, failed, running), child| match child.status {
TranscriptCardStatus::Success => (done + 1, failed, running),
TranscriptCardStatus::Failed => (done, failed + 1, running),
TranscriptCardStatus::Running => (done, failed, running + 1),
TranscriptCardStatus::Canceled => (done + 1, failed, running),
TranscriptCardStatus::None => (done, failed, running),
},
);
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(),
}
}
fn subagent_card_status_from_activity(status: ActivityStatus) -> TranscriptCardStatus {
match status {
ActivityStatus::Queued => TranscriptCardStatus::None,
status => transcript_status_from_activity(status),
}
}
fn subagent_card_sanitized_line(source: &str) -> Option<String> {
let sanitized = normalize_inline_separators(&transcript::sanitize_preview(
&redact_sensitive_text(source.trim()),
));
(!sanitized.is_empty())
.then(|| truncate_preview_line(&sanitized, PREVIEW_LINE_MAX_DISPLAY_WIDTH))
}