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 bash_mode_response = is_bash_mode_response_card(state, id.entry_index, capability);
let mut 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,
preview: node
.map(|node| node.preview.as_str())
.filter(|preview| !preview.trim().is_empty())
.unwrap_or(body),
body,
children: &children,
children_complete: false,
});
if shared.role == ToolCardRole::ViewImage {
let source = shared.blocks.iter().find_map(|block| match block {
ToolBlock::Results { rows } => rows
.iter()
.find(|row| row.label == "Image source")
.and_then(|row| row.value.as_deref()),
_ => None,
});
if let Some(source) = source {
let status = shared.title.strip_prefix("View Image").unwrap_or_default();
shared.title = format!("View Image • {source}{status}");
}
shared.metadata.retain(|metadata| {
metadata.starts_with("Jev:")
|| matches!(
metadata.as_str(),
"response truncated" | "preview truncated"
)
});
}
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: Vec<_> = shared
.blocks
.iter()
.flat_map(|block| render_block(block, shared.role))
.collect();
let body = if matches!(
shared.role,
ToolCardRole::Bash | ToolCardRole::CodeMode | ToolCardRole::Read
) {
body_lines
.iter()
.map(DisplayLine::plain_text)
.collect::<Vec<_>>()
.join("\n")
} else {
shared.body
};
let intent = (!bash_mode_response)
.then(|| {
node?.tool_detail.as_ref()?.params[crate::tools::contract::arg_key::INTENT].as_str()
})
.flatten()
.map(str::trim)
.filter(|intent| !intent.is_empty())
.map(sanitized_single_line);
TranscriptCard {
id,
role,
status: terminal_status(shared.status),
title: shared.title,
metadata: shared.metadata,
intent,
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, role: ToolCardRole) -> Vec<DisplayLine> {
match block {
ToolBlock::Text { lines } | ToolBlock::Diff { lines, .. } => lines
.iter()
.filter(|line| {
!line
.spans
.iter()
.any(|span| span.meaning == TextMeaning::Metadata)
})
.map(render_line)
.collect(),
ToolBlock::Results { .. }
if matches!(
role,
ToolCardRole::Bash
| ToolCardRole::CodeMode
| ToolCardRole::Read
| ToolCardRole::Find
| ToolCardRole::Grep
| ToolCardRole::ListFiles
| ToolCardRole::ViewImage
) =>
{
Vec::new()
}
ToolBlock::Results { rows } => rows
.iter()
.filter(|row| {
role != ToolCardRole::MagiControl
|| !matches!(
row.label.as_str(),
"Control operation"
| "Context used tokens"
| "Context maximum tokens"
| "Context used percent"
)
})
.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 | TextMeaning::Metadata => 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,
}
}