use super::*;
pub(super) fn list_files_card(input: &ToolCardInput<'_>) -> CardDraft {
use crate::tool_contract::{arg_key, metadata_key};
let detail = input.detail;
let body = input.body;
let mut status = input_status(input);
if status == CardStatus::Failed && tool_was_canceled(detail) {
status = CardStatus::Canceled;
}
let metadata = detail.map(|detail| &detail.metadata);
let params = detail.map(|detail| &detail.params);
let path = if status == CardStatus::Success {
metadata
.and_then(|value| value.get(metadata_key::PATH))
.and_then(serde_json::Value::as_str)
.or_else(|| {
params
.and_then(|value| value.get(arg_key::PATH))
.and_then(serde_json::Value::as_str)
})
} else {
params
.and_then(|value| value.get(arg_key::PATH))
.and_then(serde_json::Value::as_str)
.or_else(|| {
metadata
.and_then(|value| value.get(metadata_key::PATH))
.and_then(serde_json::Value::as_str)
})
}
.unwrap_or(".");
let path = truncate_preview_line(&sanitized_single_line(path), PREVIEW_LINE_MAX_CHARS);
let files = list_files_count(metadata, metadata_key::FILES);
let directories = list_files_count(metadata, metadata_key::DIRECTORIES);
let include_directories = params
.and_then(|value| value.get(arg_key::INCLUDE_DIRECTORIES))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let includes_directories = include_directories || directories.is_some_and(|count| count > 0);
let truncated = metadata
.and_then(|value| value.get(metadata_key::TRUNCATED))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let returned = files
.zip(directories)
.map(|(files, directories)| files.saturating_add(directories));
let title = match status {
CardStatus::Success if truncated => returned.map_or_else(
|| "List Files • entries returned".to_string(),
|returned| format!("List Files • {returned} entries returned"),
),
CardStatus::Success if includes_directories => returned.map_or_else(
|| "List Files".to_string(),
|returned| format!("List Files • {returned} entries"),
),
CardStatus::Success => files.map_or_else(
|| "List Files".to_string(),
|files| format!("List Files • {files} {}", plural(files, "file", "files")),
),
CardStatus::Canceled => "List Files • canceled".to_string(),
_ => "List Files".to_string(),
};
let mut lines = vec![path];
match status {
CardStatus::Running => {
if include_directories {
lines.push("include directories".to_string());
}
}
CardStatus::Success => {
let output_lines = detail
.map(|detail| {
detail
.output
.lines()
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
})
.unwrap_or_default();
lines.extend(output_lines.iter().take(TOOL_PREVIEW_LINES).map(|line| {
truncate_preview_line(&sanitized_single_line(line), PREVIEW_LINE_MAX_CHARS)
}));
if output_lines.len() > TOOL_PREVIEW_LINES {
lines.push(format!("… preview limited to {TOOL_PREVIEW_LINES} lines"));
}
if truncated {
lines.push("… output capped at 1000 entries".to_string());
}
if output_lines.is_empty() && returned == Some(0) {
lines.push("0 entries".to_string());
}
}
CardStatus::Failed => lines.push(bounded_error_line(
detail.map_or(body, |detail| detail.output.as_ref()),
)),
CardStatus::Canceled => lines.push("Canceled before completion".to_string()),
CardStatus::None => {}
}
CardDraft {
role: ToolCardRole::ListFiles,
status,
title,
metadata: Vec::new(),
body: lines.join("\n"),
body_lines: Vec::new(),
activity_rows: Vec::new(),
}
}
fn list_files_count(metadata: Option<&serde_json::Value>, key: &str) -> Option<usize> {
metadata
.and_then(|value| value.get(key))
.and_then(serde_json::Value::as_u64)
.and_then(|value| usize::try_from(value).ok())
}