pub const MAX_ATFILE_SUGGESTIONS: usize = 12;
pub const MAX_HSEARCH_RESULTS: usize = 12;
pub fn default_offline_tool_catalog() -> Vec<(String, String)> {
vec![
("read_file".into(), "Read a file from the workspace".into()),
("write_file".into(), "Write a file to the workspace".into()),
("apply_patch".into(), "Apply a V4A patch to a file".into()),
(
"list_dir".into(),
"List a directory under the workspace".into(),
),
(
"run_shell".into(),
"Run a shell command in the workspace".into(),
),
(
"search_files".into(),
"Search files for a regex pattern".into(),
),
]
}
pub fn search_history(history: &[String], query: &str) -> Vec<usize> {
let q = query.to_lowercase();
if q.is_empty() {
let mut all: Vec<usize> = (0..history.len()).rev().collect();
all.truncate(MAX_HSEARCH_RESULTS);
return all;
}
let mut prefix: Vec<usize> = Vec::new();
let mut substr: Vec<usize> = Vec::new();
for (i, entry) in history.iter().enumerate() {
let lower = entry.to_lowercase();
if lower.starts_with(&q) {
prefix.push(i);
} else if lower.contains(&q) {
substr.push(i);
}
}
prefix.reverse();
substr.reverse();
let mut out = prefix;
out.extend(substr);
out.truncate(MAX_HSEARCH_RESULTS);
out
}
pub fn glob_workspace_files(query: &str) -> Vec<String> {
let Ok(cwd) = std::env::current_dir() else {
return Vec::new();
};
let q = query.to_lowercase();
let mut results: Vec<String> = Vec::new();
collect_files(&cwd, &cwd, 0, &q, &mut results);
results.sort();
results.dedup();
results.sort_by_key(|p| {
let name = std::path::Path::new(p)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_lowercase();
if name.starts_with(&q) {
0u8
} else {
1u8
}
});
results.truncate(MAX_ATFILE_SUGGESTIONS);
results
}
pub fn collect_files(
root: &std::path::Path,
dir: &std::path::Path,
depth: usize,
query: &str,
out: &mut Vec<String>,
) {
if depth > 3 {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') || name_str == "target" || name_str == "node_modules" {
continue;
}
if path.is_dir() {
collect_files(root, &path, depth + 1, query, out);
} else if path.is_file() {
let rel = path
.strip_prefix(root)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| name_str.to_string());
if (query.is_empty() || rel.to_lowercase().contains(query))
&& out.len() < MAX_ATFILE_SUGGESTIONS * 4
{
out.push(rel);
}
}
}
}