use sgr_agent_core::agent_tool::ToolError;
pub fn backend_err(e: anyhow::Error) -> ToolError {
ToolError::Execution(e.to_string())
}
pub fn def_root() -> String {
"/".into()
}
pub fn def_level() -> i32 {
2
}
pub fn has_matches(output: &str) -> bool {
output.lines().any(|l| !l.starts_with('$') && !l.is_empty())
}
pub fn unique_files_from_search(output: &str, max: usize) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut files = Vec::new();
for line in output.lines() {
if line.starts_with('$') || line.is_empty() {
continue;
}
if let Some(path) = line.split(':').next() {
let path = path.trim();
if !path.is_empty() && seen.insert(path.to_string()) {
files.push(path.to_string());
if files.len() > max {
return files;
}
}
}
}
files
}
const MAX_OUTPUT_CHARS: usize = 30_000;
const PREFIX_LINES: usize = 200;
const SUFFIX_LINES: usize = 100;
pub fn truncate_output(output: &str) -> String {
if output.chars().count() <= MAX_OUTPUT_CHARS {
return output.to_string();
}
let lines: Vec<&str> = output.lines().collect();
let total = lines.len();
if total <= PREFIX_LINES + SUFFIX_LINES {
let prefix: String = output.chars().take(MAX_OUTPUT_CHARS / 2).collect();
let suffix: String = output
.chars()
.rev()
.take(MAX_OUTPUT_CHARS / 4)
.collect::<String>()
.chars()
.rev()
.collect();
return format!(
"{}\n\n...[{} chars omitted]...\n\n{}",
prefix,
output.chars().count() - MAX_OUTPUT_CHARS / 2 - MAX_OUTPUT_CHARS / 4,
suffix
);
}
let prefix = &lines[..PREFIX_LINES];
let suffix = &lines[total - SUFFIX_LINES..];
let omitted = total - PREFIX_LINES - SUFFIX_LINES;
format!(
"Total output: {} lines\n\n{}\n\n...[{} lines omitted]...\n\n{}",
total,
prefix.join("\n"),
omitted,
suffix.join("\n"),
)
}
#[cfg(test)]
mod tests {
use super::truncate_output;
use super::*;
#[test]
fn has_matches_empty() {
assert!(!has_matches(""));
assert!(!has_matches("$ rg pattern"));
assert!(!has_matches("$ rg pattern\n"));
}
#[test]
fn has_matches_with_results() {
assert!(has_matches("$ rg pattern\nfile.txt:1:match"));
assert!(has_matches("file.txt:1:match"));
}
#[test]
fn unique_files_basic() {
let output = "$ rg test\nfoo.txt:1:line1\nfoo.txt:2:line2\nbar.txt:1:line1";
let files = unique_files_from_search(output, 10);
assert_eq!(files, vec!["foo.txt", "bar.txt"]);
}
#[test]
fn unique_files_respects_max() {
let output = "a.txt:1:x\nb.txt:1:x\nc.txt:1:x";
let files = unique_files_from_search(output, 2);
assert_eq!(files.len(), 3); }
#[test]
fn unique_files_skips_header() {
let output = "$ rg test\n\nfile.txt:1:match";
let files = unique_files_from_search(output, 10);
assert_eq!(files, vec!["file.txt"]);
}
#[test]
fn truncate_short_unchanged() {
assert_eq!(truncate_output("hello\nworld"), "hello\nworld");
}
#[test]
fn truncate_long_output() {
let lines: Vec<String> = (0..500)
.map(|i| format!("line {:04}: {}", i, "x".repeat(90)))
.collect();
let input = lines.join("\n");
let result = truncate_output(&input);
assert!(result.len() < input.len());
assert!(result.contains("lines omitted"));
assert!(result.contains("line 0000"));
assert!(result.contains("line 0499"));
}
}