use super::*;
use crate::output::tool_card::{
self, CardStatus, ChildToolFacts, TextLine, TextMeaning, ToolBlock, ToolCardInput, ToolCardRole,
};
use crate::rendering::{DisplayRole, DisplaySpan, highlight::highlight_code};
pub(super) fn project_tool_card(
state: &MissionControlState,
id: TranscriptCardId,
link: Option<TranscriptActivityLink>,
body: &str,
) -> TranscriptCard {
let node = link
.as_ref()
.and_then(|link| state.nodes.get(&link.activity_id));
let tool_name = link
.as_ref()
.map(|link| link.tool_name.as_str())
.or_else(|| transcript_tool_name(state, id.entry_index))
.unwrap_or("Tool");
let capability = ToolCapability::from_dispatch_name(tool_name);
let children: Vec<_> = node
.and_then(|node| state.children.get(&node.id))
.into_iter()
.flatten()
.filter_map(|id| state.nodes.get(id))
.filter(|node| node.kind == ActivityKind::Tool)
.map(|child| ChildToolFacts {
id: &child.id,
status: child.status,
tool_name: child.transcript_tool_name.as_deref(),
detail: child.tool_detail.as_ref(),
safe_target: (capability.is_none()
&& ToolCapability::from_dispatch_name(
child.transcript_tool_name.as_deref().unwrap_or("tool"),
)
.is_some())
.then(|| {
project_activity_card(state, child)
.body
.lines()
.next()
.unwrap_or_default()
.to_string()
}),
})
.collect();
let status = node.map(|node| node.status).or_else(|| {
state
.transcript
.get(id.entry_index)
.and_then(TranscriptEntry::tool_data)
.and_then(|data| data.identity.status())
});
let shared = tool_card::project_tool_card(&ToolCardInput {
tool_name,
detail: node.and_then(|node| node.tool_detail.as_ref()),
status,
interrupted: false,
bash_command: node.and_then(|node| node.transcript_bash_command.as_deref()),
bash_mode_response: is_bash_mode_response_card(state, id.entry_index, capability),
preview: node
.map(|node| node.preview.as_str())
.filter(|preview| !preview.trim().is_empty())
.unwrap_or(body),
body,
children: &children,
children_complete: false,
});
let role = match shared.role {
ToolCardRole::Bash => TranscriptCardRole::Bash,
ToolCardRole::CodeMode => TranscriptCardRole::CodeMode,
ToolCardRole::MagiControl => TranscriptCardRole::MagiControl,
ToolCardRole::Read => TranscriptCardRole::Read,
ToolCardRole::Grep => TranscriptCardRole::Grep,
ToolCardRole::AstGrep => TranscriptCardRole::AstGrep,
ToolCardRole::Find => TranscriptCardRole::Find,
ToolCardRole::ListFiles => TranscriptCardRole::ListFiles,
ToolCardRole::Web => TranscriptCardRole::Web,
ToolCardRole::Tool => TranscriptCardRole::Tool,
ToolCardRole::Write => TranscriptCardRole::Write,
ToolCardRole::HashEdit => TranscriptCardRole::HashEdit,
ToolCardRole::SubagentBatch => TranscriptCardRole::SubagentBatch,
ToolCardRole::ViewImage => TranscriptCardRole::ViewImage,
};
let children = if role == TranscriptCardRole::SubagentBatch {
subagent_aggregation::subagent_children(
state,
link.as_ref().map(|link| &link.activity_id),
id.entry_index,
)
} else {
Vec::new()
};
let subagent_card = (role == TranscriptCardRole::SubagentBatch).then(|| {
subagent_aggregation::subagent_card_summary_from_children(
state,
link.as_ref().map(|link| &link.activity_id),
&children,
node,
)
});
let body_lines = shared.blocks.iter().flat_map(render_block).collect();
TranscriptCard {
id,
role,
status: terminal_status(shared.status),
title: shared.title,
metadata: shared.metadata,
body: shared.body,
body_lines,
children,
activity_rows: shared
.activity_rows
.into_iter()
.map(|row| TranscriptCardActivityRow {
label: row.label,
status: terminal_status(row.status),
activity_id: row.activity_id,
})
.collect(),
subagent_card,
}
}
pub(super) fn terminal_status(status: CardStatus) -> TranscriptCardStatus {
match status {
CardStatus::Running => TranscriptCardStatus::Running,
CardStatus::Success => TranscriptCardStatus::Success,
CardStatus::Failed => TranscriptCardStatus::Failed,
CardStatus::Canceled => TranscriptCardStatus::Canceled,
CardStatus::None => TranscriptCardStatus::None,
}
}
fn render_block(block: &ToolBlock) -> Vec<DisplayLine> {
match block {
ToolBlock::Text { lines } | ToolBlock::Diff { lines, .. } => {
lines.iter().map(render_line).collect()
}
ToolBlock::Results { rows } => rows
.iter()
.map(|row| {
DisplayLine::plain(format!(
"{} • {}",
row.label,
row.value.as_deref().unwrap_or("not reported")
))
})
.collect(),
ToolBlock::Resources { rows } => rows.iter().map(|row| render_line(&row.line)).collect(),
ToolBlock::Command {
command: Some(command),
..
} => highlight_code(command, Some("bash")),
ToolBlock::Command { command: None, .. } | ToolBlock::Progress { .. } => Vec::new(),
}
}
fn render_line(line: &TextLine) -> DisplayLine {
DisplayLine {
spans: line
.spans
.iter()
.map(|span| {
DisplaySpan::new(
&span.text,
match span.meaning {
TextMeaning::Plain => DisplayRole::Plain,
TextMeaning::Resource => DisplayRole::InlineCode,
TextMeaning::Pattern => DisplayRole::String,
TextMeaning::Count => DisplayRole::Number,
TextMeaning::Separator => DisplayRole::Punctuation,
TextMeaning::OutcomeMark => DisplayRole::ListMarker,
TextMeaning::Added => DisplayRole::DiffInserted,
TextMeaning::Removed => DisplayRole::DiffRemoved,
},
)
})
.collect(),
table: None,
}
}