use super::tool_group::{GroupEntry, entry_icon};
pub(crate) const PAGE_SIZE: usize = 5;
pub(crate) const COLOR_RUNNING: u32 = 0xF0B232;
pub(crate) const COLOR_CLEAN: u32 = 0x57F287;
pub(crate) const COLOR_FAILED: u32 = 0xED4245;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct CardRow {
pub name: String,
pub context: String,
pub count: usize,
pub status: Option<bool>,
}
pub(crate) fn entry_matches_completion(entry: &GroupEntry, name: &str, call_id: &str) -> bool {
if !call_id.is_empty() && !entry.call_id.is_empty() {
entry.call_id == call_id
} else {
entry.name == name
}
}
fn merge_status(a: Option<bool>, b: Option<bool>) -> Option<bool> {
match (a, b) {
(None, _) | (_, None) => None,
(Some(false), _) | (_, Some(false)) => Some(false),
_ => Some(true),
}
}
pub(crate) fn collapse_repeats(entries: &[GroupEntry]) -> Vec<CardRow> {
let mut rows: Vec<CardRow> = Vec::new();
for entry in entries {
match rows.last_mut() {
Some(row) if row.name == entry.name && row.context == entry.context => {
row.count += 1;
row.status = merge_status(row.status, entry.status);
}
_ => rows.push(CardRow {
name: entry.name.clone(),
context: entry.context.clone(),
count: 1,
status: entry.status,
}),
}
}
rows
}
pub(crate) fn page_count(rows: usize) -> usize {
rows.div_ceil(PAGE_SIZE).max(1)
}
pub(crate) fn clamp_page(page: usize, rows: usize) -> usize {
page.min(page_count(rows) - 1)
}
pub(crate) fn page_slice(rows: &[CardRow], page: usize) -> &[CardRow] {
let page = clamp_page(page, rows.len());
let start = (page * PAGE_SIZE).min(rows.len());
let end = (start + PAGE_SIZE).min(rows.len());
&rows[start..end]
}
pub(crate) const ROW_MAX_CHARS: usize = 160;
pub(crate) const TITLE_MAX_CHARS: usize = 200;
pub(crate) fn clip(text: &str, max: usize) -> String {
if text.chars().count() <= max {
return text.to_string();
}
let cut: String = text.chars().take(max.saturating_sub(1)).collect();
format!("{cut}…")
}
pub(crate) fn row_line(row: &CardRow) -> String {
let icon = entry_icon(row.status);
let repeats = if row.count > 1 {
format!(" ×{}", row.count)
} else {
String::new()
};
clip(
&format!("{icon} **{}**{}{repeats}", row.name, row.context),
ROW_MAX_CHARS,
)
}
pub(crate) fn card_color(rows: &[CardRow]) -> u32 {
if rows.iter().any(|r| r.status.is_none()) {
COLOR_RUNNING
} else if rows.iter().any(|r| r.status == Some(false)) {
COLOR_FAILED
} else {
COLOR_CLEAN
}
}
pub(crate) fn card_title(rows: &[CardRow]) -> String {
clip(&card_title_text(rows), TITLE_MAX_CHARS)
}
fn card_title_text(rows: &[CardRow]) -> String {
match rows.iter().rev().find(|r| r.status.is_none()) {
Some(row) => {
let ctx = row.context.trim();
if ctx.is_empty() {
format!("Running: {}", row.name)
} else {
format!("Running: {} {ctx}", row.name)
}
}
None => {
let failed = rows.iter().filter(|r| r.status == Some(false)).count();
if failed > 0 {
format!("Done · {} rows · {failed} failed", rows.len())
} else {
format!("Done · {} rows", rows.len())
}
}
}
}
pub(crate) fn card_footer(rows: &[CardRow], page: usize, total_calls: usize) -> String {
let page = clamp_page(page, rows.len());
let shown = page_slice(rows, page).len();
let start = page * PAGE_SIZE + 1;
let end = page * PAGE_SIZE + shown;
format!(
"Page {}/{} · rows {start}-{end} of {} ({total_calls} calls, repeats collapsed)",
page + 1,
page_count(rows.len()),
rows.len(),
)
}
pub(crate) fn log_markdown(entries: &[GroupEntry]) -> String {
let failed = entries.iter().filter(|e| e.status == Some(false)).count();
let mut out = format!(
"# Tool calls\n\n{} calls · {failed} failed\n\n",
entries.len()
);
for (i, entry) in entries.iter().enumerate() {
out.push_str(&format!(
"{}. {} **{}**{}\n",
i + 1,
entry_icon(entry.status),
entry.name,
entry.context
));
}
out
}