use super::*;
pub(super) fn find_card(
state: &MissionControlState,
id: TranscriptCardId,
link: Option<TranscriptActivityLink>,
) -> TranscriptCard {
const DEFAULT_LIMIT: usize = 50;
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 query = find_metadata_str(detail, "query")
.or_else(|| find_param(detail, "query"))
.unwrap_or("");
let path = find_param(detail, "path").unwrap_or(".");
let kind = find_param(detail, "kind").unwrap_or("files");
let offset = find_param_usize(detail, "offset").unwrap_or(0);
let limit = find_param_usize(detail, "limit").unwrap_or(DEFAULT_LIMIT);
let returned = find_usize(detail, "matches_returned")
.or_else(|| detail.map(|detail| find_output_paths(detail.output.as_ref()).len()))
.unwrap_or(0);
let total = find_usize(detail, "total_matched");
let incomplete = find_bool(detail, "truncated")
|| offset != 0
|| total.is_some_and(|total| returned != total);
let title = match status {
TranscriptCardStatus::Success => format!(
"Find • {returned} {}{}",
plural(returned, "path", "paths"),
if incomplete { " returned" } else { "" }
),
TranscriptCardStatus::Canceled => "Find • canceled".to_string(),
_ => "Find".to_string(),
};
let mut lines = vec![
find_quoted(query),
format!("{} in {}", find_kind(kind), sanitized_single_line(path)),
];
if offset != 0 || limit != DEFAULT_LIMIT {
lines.push(format!("offset {offset} · limit {limit}"));
}
match status {
TranscriptCardStatus::Running => {}
TranscriptCardStatus::Success => {
let paths = detail
.map(|detail| find_output_paths(detail.output.as_ref()))
.unwrap_or_default();
lines.extend(paths.iter().take(TOOL_PREVIEW_LINES).cloned());
if paths.len() > TOOL_PREVIEW_LINES {
lines.push(format!("… preview limited to {TOOL_PREVIEW_LINES} paths"));
}
if incomplete {
if let Some(total) = total {
if returned == 0 || offset >= total {
lines.push(format!("… showing 0 of {total} paths"));
} else {
let start = offset.saturating_add(1);
let end = offset.saturating_add(returned).min(total);
lines.push(format!("… showing {start}–{end} of {total} paths"));
}
} else {
lines.push("… more paths may exist".to_string());
}
}
if detail
.and_then(|detail| detail.metadata.get("index_ready"))
.and_then(serde_json::Value::as_bool)
== Some(false)
{
lines.push("… index not ready; results may be incomplete".to_string());
}
}
TranscriptCardStatus::Failed => lines.push(
detail
.and_then(|detail| find_first_non_empty_line(detail.output.as_ref()))
.or_else(|| node.and_then(|node| find_first_non_empty_line(&node.preview)))
.unwrap_or_else(|| "find failed".to_string()),
),
TranscriptCardStatus::Canceled => lines.push("canceled".to_string()),
TranscriptCardStatus::None => {}
}
TranscriptCard {
id,
role: TranscriptCardRole::Find,
status,
title,
metadata: Vec::new(),
body: lines.join("\n"),
body_lines: Vec::new(),
children: Vec::new(),
activity_rows: Vec::new(),
subagent_card: None,
}
}
fn find_param<'a>(
detail: Option<&'a crate::output::ToolActivityDetail>,
key: &str,
) -> Option<&'a str> {
detail?
.params
.get(key)?
.as_str()
.filter(|value| !value.is_empty())
}
fn find_metadata_str<'a>(
detail: Option<&'a crate::output::ToolActivityDetail>,
key: &str,
) -> Option<&'a str> {
detail?
.metadata
.get(key)?
.as_str()
.filter(|value| !value.is_empty())
}
fn find_param_usize(
detail: Option<&crate::output::ToolActivityDetail>,
key: &str,
) -> Option<usize> {
detail
.and_then(|detail| detail.params.get(key))
.and_then(serde_json::Value::as_u64)
.and_then(|value| usize::try_from(value).ok())
.or_else(|| find_usize(detail, key))
}
fn find_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 find_bool(detail: Option<&crate::output::ToolActivityDetail>, key: &str) -> bool {
detail
.and_then(|detail| detail.metadata.get(key))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
}
fn find_kind(kind: &str) -> &'static str {
match kind {
"directories" | "dirs" => "directories",
"mixed" | "both" | "files_and_directories" => "files and directories",
_ => "files",
}
}
fn find_quoted(query: &str) -> String {
serde_json::to_string(&transcript::sanitize_preview(query))
.unwrap_or_else(|_| "\"find query\"".to_string())
}
fn find_output_paths(output: &str) -> Vec<String> {
output
.lines()
.filter(|line| !line.is_empty())
.map(|line| {
let safe = sanitized_single_line(line);
truncate_preview_line(&safe, PREVIEW_LINE_MAX_DISPLAY_WIDTH)
})
.collect()
}
fn find_first_non_empty_line(text: &str) -> Option<String> {
transcript::sanitize_preview(text)
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.map(normalize_inline_separators)
}