use super::*;
pub(super) fn read_card(
state: &MissionControlState,
id: TranscriptCardId,
link: Option<TranscriptActivityLink>,
) -> TranscriptCard {
let node = link
.as_ref()
.and_then(|link| state.nodes.get(&link.activity_id));
let detail = node.and_then(|node| node.tool_detail.as_ref());
let status = node
.map(|node| transcript_status_from_activity(node.status))
.or_else(|| detail.map(|detail| transcript_status_from_activity(detail.status)))
.unwrap_or_else(|| transcript_tool_status(state, id.entry_index));
let targets = detail
.and_then(|detail| read_targets(&detail.params))
.unwrap_or_default();
let results = detail
.and_then(|detail| detail.metadata.get("results"))
.and_then(serde_json::Value::as_array);
let total = read_usize(detail, "files")
.unwrap_or_else(|| results.map_or(targets.len(), Vec::len).max(targets.len()));
let succeeded = read_usize(detail, "succeeded").unwrap_or_else(|| {
results.map_or(0, |items| {
items
.iter()
.filter(|item| {
item.get("success").and_then(serde_json::Value::as_bool) == Some(true)
})
.count()
})
});
let failed = read_usize(detail, "failed").unwrap_or_else(|| total.saturating_sub(succeeded));
let omitted = read_usize(detail, "omitted").unwrap_or(0);
let incomplete = failed > 0 || omitted > 0 || succeeded < total;
let multi = total > 1;
let title = if multi {
match status {
TranscriptCardStatus::Success if incomplete => {
format!("Read • {succeeded}/{total} resources")
}
_ => format!("Read • {total} resources"),
}
} else {
"Read".to_string()
};
let card_body = if status == TranscriptCardStatus::Canceled {
targets
.iter()
.cloned()
.chain(std::iter::once("Canceled before completion".to_string()))
.collect::<Vec<_>>()
.join("\n")
} else if status == TranscriptCardStatus::Running {
targets.join("\n")
} else if multi {
read_multi_body(&targets, results)
} else {
let target = targets.first().cloned().unwrap_or_default();
if status == TranscriptCardStatus::Failed {
let error = detail
.and_then(read_single_error)
.unwrap_or_else(|| "read failed".to_string());
format!("{target}\n{error}")
} else if status == TranscriptCardStatus::Success {
format_read_resource(&target, Some(read_line_count(detail)))
} else {
target
}
};
let card_body = sanitize_display_controls(&card_body);
let body_lines = read_body_lines(&card_body, &targets, status, multi);
TranscriptCard {
id,
role: TranscriptCardRole::Read,
status,
title,
metadata: Vec::new(),
body: card_body,
body_lines,
children: Vec::new(),
activity_rows: Vec::new(),
subagent_card: None,
}
}
fn read_targets(params: &serde_json::Value) -> Option<Vec<String>> {
params
.get("paths")
.and_then(serde_json::Value::as_array)
.map(|paths| {
paths
.iter()
.filter_map(serde_json::Value::as_str)
.map(sanitize_read_target)
.collect()
})
.or_else(|| {
params
.get("path")
.and_then(serde_json::Value::as_str)
.map(|path| vec![sanitize_read_target(path)])
})
}
fn sanitize_read_target(text: &str) -> String {
sanitize_display_controls(text)
.chars()
.filter(|character| !character.is_control())
.collect()
}
fn read_multi_body(targets: &[String], results: Option<&Vec<serde_json::Value>>) -> String {
targets
.iter()
.enumerate()
.map(|(index, target)| {
let result = results.and_then(|items| items.get(index));
if result
.and_then(|item| item.get("success"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
{
format!(
"✓ {}",
format_read_resource(target, result.and_then(read_result_line_count))
)
} else {
format!(
"✗ {target}\n {}",
result
.and_then(|item| item.get("error"))
.and_then(serde_json::Value::as_str)
.and_then(read_first_non_empty_line)
.unwrap_or_else(|| "read failed".to_string())
)
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn format_read_resource(target: &str, line_count: Option<usize>) -> String {
line_count.map_or_else(
|| target.to_string(),
|count| {
format!(
"{target} • {count} {}",
if count == 1 { "line" } else { "lines" }
)
},
)
}
fn read_result_line_count(result: &serde_json::Value) -> Option<usize> {
result
.get("hashline_seen_lines")
.and_then(serde_json::Value::as_array)
.map(Vec::len)
}
fn read_usize(detail: Option<&crate::output::ToolActivityDetail>, key: &str) -> Option<usize> {
detail?
.metadata
.get(key)?
.as_u64()
.and_then(|value| usize::try_from(value).ok())
}
fn read_single_error(detail: &crate::output::ToolActivityDetail) -> Option<String> {
detail
.metadata
.get("error")
.and_then(serde_json::Value::as_str)
.and_then(read_first_non_empty_line)
.or_else(|| read_first_non_empty_line(detail.output.as_ref()))
}
fn read_first_non_empty_line(text: &str) -> Option<String> {
sanitize_display_controls(text)
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.map(str::to_string)
}
fn read_line_count(detail: Option<&crate::output::ToolActivityDetail>) -> usize {
let seen_lines = detail
.and_then(|detail| {
detail.metadata.get("hashline_seen_lines").or_else(|| {
detail
.metadata
.get("results")
.and_then(serde_json::Value::as_array)
.and_then(|results| results.first())
.and_then(|result| result.get("hashline_seen_lines"))
})
})
.and_then(serde_json::Value::as_array)
.map(Vec::len);
seen_lines.unwrap_or_else(|| detail.map_or(0, |detail| detail.output.lines().count()))
}
fn read_body_lines(
body: &str,
targets: &[String],
status: TranscriptCardStatus,
multi: bool,
) -> Vec<DisplayLine> {
body.lines()
.enumerate()
.map(|(index, line)| {
let bare_target_row = index == 0
|| (matches!(
status,
TranscriptCardStatus::Running | TranscriptCardStatus::Canceled
) && index < targets.len());
if bare_target_row
&& let Some(target) = targets
.iter()
.filter(|target| {
line == target.as_str() || line.starts_with(&format!("{target} • "))
})
.max_by_key(|target| target.len())
{
return read_resource_display_line(line, target, None);
}
if multi
&& let Some((marker, resource)) = line.split_once(' ')
&& matches!(marker, "✓" | "✗")
&& let Some(target) = targets
.iter()
.filter(|target| {
resource == target.as_str() || resource.starts_with(&format!("{target} • "))
})
.max_by_key(|target| target.len())
{
return read_resource_display_line(line, target, Some(marker));
}
DisplayLine::plain(line)
})
.collect()
}
fn read_resource_display_line(line: &str, target: &str, marker: Option<&str>) -> DisplayLine {
let mut spans = Vec::new();
let target_start = marker.map_or(0, |marker| {
spans.push(DisplaySpan::new(
format!("{marker} "),
DisplayRole::ListMarker,
));
marker.len() + 1
});
spans.push(DisplaySpan::new(target, DisplayRole::InlineCode));
let suffix = &line[target_start + target.len()..];
if let Some((prefix, count_and_unit)) = suffix.split_once('•') {
spans.push(DisplaySpan::new(prefix, DisplayRole::Plain));
spans.push(DisplaySpan::new("• ", DisplayRole::Punctuation));
if let Some((count, unit)) = count_and_unit.trim_start().split_once(' ') {
spans.push(DisplaySpan::new(count, DisplayRole::Number));
spans.push(DisplaySpan::new(format!(" {unit}"), DisplayRole::Plain));
}
}
tool_display_line(spans)
}