vv-agent 0.6.2

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::collections::BTreeMap;

use serde_json::Value;

pub(super) const MAX_STRUCTURED_ITEMS: usize = 200;
pub(super) const MAX_STRUCTURED_CHARS: usize = 20_000;

const MAX_RESULT_LINES: usize = 500;
const MAX_RESULT_CHARS: usize = 30_000;

pub(super) fn render_grep_content(
    output_mode: &str,
    pattern: &str,
    payload: &Value,
    show_line_numbers: bool,
    head_limited: bool,
) -> String {
    let summary = &payload["summary"];
    let total_matches = summary["total_matches"].as_u64().unwrap_or_default();
    let files_with_matches = summary["files_with_matches"].as_u64().unwrap_or_default();
    match output_mode {
        "files_with_matches" => {
            let files = payload["files"].as_array().cloned().unwrap_or_default();
            let mut lines = vec![format!(
                "Found {files_with_matches} files matching pattern {pattern:?}"
            )];
            if files.is_empty() {
                lines.push("No matches found.".to_string());
            } else {
                if head_limited {
                    lines.push(format!("Showing first {} files.", files.len()));
                }
                lines.extend(
                    files
                        .into_iter()
                        .filter_map(|file| file.as_str().map(str::to_string)),
                );
            }
            lines.join("\n")
        }
        "count" => {
            let mut lines = vec![format!("Match counts for pattern {pattern:?}")];
            if head_limited {
                lines.push(format!(
                    "Showing first {} files.",
                    payload["file_counts"]
                        .as_object()
                        .map_or(0, |items| items.len())
                ));
            }
            if let Some(counts) = payload["file_counts"].as_object() {
                for (file, count) in counts {
                    lines.push(format!("{}: {}", file, count.as_u64().unwrap_or_default()));
                }
            }
            lines.push(format!(
                "Total: {total_matches} matches in {files_with_matches} files"
            ));
            lines.join("\n")
        }
        _ => {
            let mut lines = vec![format!(
                "Found {total_matches} matches in {files_with_matches} files for pattern {pattern:?}"
            )];
            let rows = payload["matches"].as_array().cloned().unwrap_or_default();
            if rows.is_empty() {
                lines.push("No matches found.".to_string());
                return lines.join("\n");
            }
            if head_limited {
                lines.push(format!("Showing first {} rows.", rows.len()));
            }
            let mut current_file = String::new();
            for row in rows {
                let row_path = row["path"].as_str().unwrap_or_default();
                if current_file != row_path {
                    lines.push(format!("File: {row_path}"));
                    current_file = row_path.to_string();
                }
                let marker = if row["is_match"].as_bool().unwrap_or(false) {
                    ""
                } else {
                    "-"
                };
                let text = row["text"].as_str().unwrap_or_default();
                if show_line_numbers {
                    let line = row["line"].as_u64().unwrap_or_default();
                    lines.push(format!("  {marker}{line}: {text}"));
                } else {
                    lines.push(format!("  {marker}{text}"));
                }
            }
            lines.join("\n")
        }
    }
}

pub(super) fn truncate_result_text(
    result_text: String,
    total_matches: usize,
    files_with_matches: usize,
) -> (String, bool) {
    let line_count = result_text.lines().count();
    if line_count <= MAX_RESULT_LINES && result_text.len() <= MAX_RESULT_CHARS {
        return (result_text, false);
    }

    let truncated = if result_text.len() > MAX_RESULT_CHARS {
        let mut end = MAX_RESULT_CHARS.min(result_text.len());
        while !result_text.is_char_boundary(end) {
            end -= 1;
        }
        let candidate = &result_text[..end];
        match candidate.rfind('\n') {
            Some(last_newline) if last_newline > MAX_RESULT_CHARS * 4 / 5 => {
                candidate[..last_newline].to_string()
            }
            _ => candidate.to_string(),
        }
    } else {
        result_text
            .lines()
            .take(MAX_RESULT_LINES)
            .collect::<Vec<_>>()
            .join("\n")
    };

    let shown_lines = truncated.lines().count();
    let truncated_info = format!(
        "\n\n--- TRUNCATED ---\n\
         Shown: {shown_lines} lines, {} characters\n\
         Total matches: {total_matches} in {files_with_matches} files\n\
         Use a narrower pattern/path/glob/type/head_limit for more focused output.",
        truncated.len()
    );
    (format!("{truncated}{truncated_info}"), true)
}

pub(super) fn cap_file_paths(items: Vec<String>) -> (Vec<String>, bool) {
    cap_structured_items(items, |path| path.len() + 4)
}

pub(super) fn cap_file_counts(
    file_counts: BTreeMap<String, usize>,
) -> (BTreeMap<String, usize>, bool) {
    let count_items = file_counts.into_iter().collect::<Vec<_>>();
    let (capped_items, capped) = cap_structured_items(count_items, |(path, count)| {
        path.len() + count.to_string().len() + 8
    });
    (capped_items.into_iter().collect(), capped)
}

pub(super) fn cap_match_rows(rows: Vec<Value>) -> (Vec<Value>, bool) {
    cap_structured_items(rows, |row| {
        row["path"].as_str().map_or(0, str::len)
            + row["line"]
                .as_u64()
                .map_or(0, |line| line.to_string().len())
            + row["text"].as_str().map_or(0, str::len)
            + 32
    })
}

fn cap_structured_items<T>(items: Vec<T>, estimator: impl Fn(&T) -> usize) -> (Vec<T>, bool) {
    let mut capped = Vec::new();
    let mut used_chars = 0usize;

    for item in items {
        let item_size = estimator(&item).max(1);
        if !capped.is_empty()
            && (capped.len() >= MAX_STRUCTURED_ITEMS
                || used_chars.saturating_add(item_size) > MAX_STRUCTURED_CHARS)
        {
            return (capped, true);
        }
        capped.push(item);
        used_chars = used_chars.saturating_add(item_size);
    }

    (capped, false)
}