use super::*;
pub(super) fn read_card(input: &ToolCardInput<'_>) -> CardDraft {
let detail = input.detail;
let status = input_status(input);
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").or_else(|| {
results
.filter(|items| items.iter().all(|item| item["success"].as_bool().is_some()))
.map(|items| {
items
.iter()
.filter(|item| item["success"].as_bool() == Some(true))
.count()
})
});
let incomplete = read_usize(detail, "failed").is_some_and(|count| count > 0)
|| read_usize(detail, "omitted").is_some_and(|count| count > 0)
|| succeeded.is_some_and(|count| count < total);
let multi = total > 1;
let title = if multi {
match status {
CardStatus::Success if incomplete && succeeded.is_some() => {
format!("Read • {}/{total} resources", succeeded.unwrap_or_default())
}
_ => format!("Read • {total} resources"),
}
} else {
"Read".to_string()
};
let card_body = if status == CardStatus::Canceled {
targets
.iter()
.cloned()
.chain(std::iter::once("Canceled before completion".to_string()))
.collect::<Vec<_>>()
.join("\n")
} else if status == CardStatus::Running {
targets.join("\n")
} else if multi {
read_multi_body(&targets, results)
} else {
let target = targets.first().cloned().unwrap_or_default();
if status == CardStatus::Failed {
let error = detail
.and_then(read_single_error)
.unwrap_or_else(|| "read failed".to_string());
format!("{target}\n{error}")
} else if status == CardStatus::Success {
format_read_resource(&target, 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);
CardDraft {
role: ToolCardRole::Read,
status,
title,
metadata: Vec::new(),
body: card_body,
body_lines,
activity_rows: Vec::new(),
}
}
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));
match result.and_then(|item| item["success"].as_bool()) {
Some(true) => format!(
"✓ {}",
format_read_resource(target, result.and_then(read_result_line_count))
),
Some(false) => format!(
"✗ {target}\n {}",
result
.and_then(|item| item["error"].as_str())
.and_then(read_first_non_empty_line)
.unwrap_or_else(|| "read failed".to_string())
),
None => format!("○ {target}\n result unavailable"),
}
})
.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>) -> Option<usize> {
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)
}
fn read_body_lines(
body: &str,
targets: &[String],
status: CardStatus,
multi: bool,
) -> Vec<TextLine> {
body.lines()
.enumerate()
.map(|(index, line)| {
let bare_target_row = index == 0
|| (matches!(status, CardStatus::Running | CardStatus::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));
}
TextLine::plain(line)
})
.collect()
}
fn read_resource_display_line(line: &str, target: &str, marker: Option<&str>) -> TextLine {
let mut spans = Vec::new();
let target_start = marker.map_or(0, |marker| {
spans.push(TextSpan::new(
format!("{marker} "),
TextMeaning::OutcomeMark,
));
marker.len() + 1
});
spans.push(TextSpan::new(target, TextMeaning::Resource));
let suffix = &line[target_start + target.len()..];
if let Some((prefix, count_and_unit)) = suffix.split_once('•') {
spans.push(TextSpan::new(prefix, TextMeaning::Plain));
spans.push(TextSpan::new("• ", TextMeaning::Separator));
if let Some((count, unit)) = count_and_unit.trim_start().split_once(' ') {
spans.push(TextSpan::new(count, TextMeaning::Count));
spans.push(TextSpan::new(format!(" {unit}"), TextMeaning::Plain));
}
}
tool_display_line(spans)
}