use super::*;
use serde_json::Value;
use std::collections::BTreeMap;
pub(super) fn project_code_mode_card(input: &ToolCardInput<'_>) -> CardDraft {
let detail = input.detail;
let status = input_status(input);
let running = status == CardStatus::Running;
let interrupted = input.interrupted
|| input.status.or_else(|| detail.map(|detail| detail.status))
== Some(ActivityStatus::Interrupted);
let execution = detail
.and_then(|detail| detail.metadata.get("code_mode_execution"))
.filter(|value| value.is_object());
let children = input.children;
let count = |key: &str, fallback: usize| {
execution
.and_then(|value| value.get(key))
.and_then(Value::as_u64)
.unwrap_or(fallback as u64)
};
let calls = count("inner_calls", children.len());
let succeeded = count(
"successful",
children
.iter()
.filter(|node| node.status == ActivityStatus::Success)
.count(),
);
let failed = count(
"recoverable_failures",
children
.iter()
.filter(|node| node.status == ActivityStatus::Failed)
.count(),
);
let unknown = count(
"unknown_outcomes",
children
.iter()
.filter(|node| {
!matches!(
node.status,
ActivityStatus::Success | ActivityStatus::Failed
)
})
.count(),
);
let warnings: Vec<_> = execution
.and_then(|value| value.get("warnings"))
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect();
let calls_known = input.children_complete
|| execution
.and_then(|value| value["inner_calls"].as_u64())
.is_some();
let counts_known = input.children_complete
|| execution.is_some_and(|value| {
["successful", "recoverable_failures", "unknown_outcomes"]
.iter()
.all(|key| value[*key].as_u64().is_some())
});
let mut title = if calls_known {
format!("Code Mode • {}", counted(calls, "call", "calls"))
} else {
"Code Mode".into()
};
if let Some(elapsed) = execution.and_then(|value| value["elapsed_ms"].as_u64()) {
title.push_str(&if elapsed < 1000 {
format!(" • {elapsed} ms")
} else {
format!(" • {:.1}s", elapsed as f64 / 1000.0)
});
}
let mut rows = Vec::new();
if !running {
if let Some(tokens) = detail
.and_then(|detail| detail.metadata.get("code_mode_response_token_estimate"))
.and_then(Value::as_u64)
{
rows.push(format!("Model response: ≈{tokens} tokens"));
} else {
rows.push("Model response: token estimate unavailable".into());
}
let mut tools = BTreeMap::<&str, usize>::new();
if let Some(actions) = execution.and_then(|value| value["actions"].as_array()) {
for name in actions.iter().filter_map(|action| action["tool"].as_str()) {
*tools.entry(name).or_default() += 1;
}
} else {
for child in children {
*tools.entry(child.tool_name.unwrap_or("tool")).or_default() += 1;
}
}
if !tools.is_empty() {
rows.push(compact_line(
&tools
.into_iter()
.map(|(name, count)| format!("{name} ×{count}"))
.collect::<Vec<_>>()
.join(" · "),
));
}
let omitted = count("actions_omitted", 0);
if omitted > 0 {
rows.push(format!(
"… {} not listed",
counted(omitted, "call", "calls")
));
}
}
let metadata_row_count = rows.len();
let has_warning = failed > 0 || (!running && unknown > 0) || !warnings.is_empty();
let mut summary = if running {
format!("{} completed", counted(succeeded + failed, "call", "calls"))
} else {
match status {
CardStatus::Success if has_warning => {
format!("⚠ Script completed · {succeeded} succeeded")
}
CardStatus::Success => format!("✓ {succeeded} succeeded"),
CardStatus::Canceled => format!("⊘ Script canceled · {succeeded} succeeded"),
CardStatus::None if interrupted => "Script interrupted".into(),
CardStatus::None => "Script outcome unavailable".into(),
_ => format!("✗ Script failed · {succeeded} succeeded"),
}
};
if failed > 0 {
summary.push_str(&format!(" · {failed} failed"));
if running {
summary.insert_str(0, "⚠ ");
}
}
if !running && unknown > 0 {
summary.push_str(&format!(
" · {}",
counted(unknown, "unknown outcome", "unknown outcomes")
));
}
rows.push(if counts_known {
summary
} else {
match status {
CardStatus::Running => "Call counts unavailable",
CardStatus::Success => "Script completed • call counts unavailable",
CardStatus::Canceled => "Script canceled • call counts unavailable",
CardStatus::None if interrupted => "Script interrupted • call counts unavailable",
CardStatus::None => "Script outcome and call counts unavailable",
_ => "Script failed • call counts unavailable",
}
.into()
});
if matches!(status, CardStatus::Failed | CardStatus::Canceled) {
rows.push("⚠ Completed actions remain applied; no rollback".into());
}
for child in children
.iter()
.filter(|node| node.status == ActivityStatus::Failed)
.take(2)
{
let name = child.tool_name.unwrap_or("tool");
let child_card = super::adapters::project_tool_card(&ToolCardInput {
tool_name: name,
detail: child.detail,
status: Some(child.status),
interrupted: false,
bash_command: None,
bash_mode_response: false,
preview: "",
body: "",
children: &[],
children_complete: false,
});
rows.push(format!(
"✗ {}: {}",
compact_line(name),
super::adapters::common::bounded_error_line(&child_card.body)
));
}
for warning in warnings.iter().take(2) {
rows.push(format!("⚠ {}", compact_line(warning)));
}
if warnings.len() > 2 {
rows.push(format!(
"⚠ {} more warnings; inspect activity details",
warnings.len() - 2
));
}
let activity_rows = children
.iter()
.rev()
.take(TOOL_PREVIEW_LINES)
.rev()
.map(|child| {
let name = child.tool_name.unwrap_or("tool");
let target = child
.safe_target
.as_deref()
.filter(|_| ToolCapability::from_dispatch_name(name).is_some())
.map(compact_line)
.unwrap_or_default();
ToolActivityRow {
label: format!("{} {target}", compact_line(name))
.trim_end()
.to_string(),
status: card_status_from_activity(child.status),
activity_id: child.id.clone(),
}
})
.collect();
if let Some(execution) = execution {
let changed = execution["changed_path_count"].as_u64();
let incomplete = execution["changed_paths_incomplete"].as_bool() == Some(true);
if changed.is_some_and(|changed| changed > 0) || incomplete {
let qualifier = if incomplete { "at least " } else { "" };
rows.push(changed.map_or_else(
|| "Tracked change count unavailable".into(),
|changed| {
format!(
"{qualifier}{} changed",
counted(changed, "tracked file", "tracked files")
)
},
));
let paths = execution["changed_paths"].as_array();
for path in paths
.into_iter()
.flatten()
.take(3)
.filter_map(Value::as_str)
{
rows.push(format!(" {}", compact_line(path)));
}
let shown = paths.map_or(0, |paths| paths.len().min(3)) as u64;
if let Some(changed) = changed.filter(|changed| *changed > shown) {
rows.push(format!("… {} more tracked files", changed - shown));
}
}
}
CardDraft {
role: ToolCardRole::CodeMode,
status,
title,
metadata: Vec::new(),
body: rows.join("\n"),
body_lines: rows
.into_iter()
.enumerate()
.map(|(index, row)| {
TextLine::from_span(
row,
if index < metadata_row_count {
TextMeaning::Metadata
} else {
TextMeaning::Plain
},
)
})
.collect(),
activity_rows,
}
}
fn compact_line(text: &str) -> String {
truncate_preview_line(&sanitized_single_line(text), PREVIEW_LINE_MAX_CHARS)
}
fn counted(count: u64, singular: &str, plural: &str) -> String {
format!("{count} {}", if count == 1 { singular } else { plural })
}