feat(backend): add list_directory_children and read_file_content Rust commands
This commit is contained in:
parent
6744e1beaf
commit
260a21c66a
1 changed files with 120 additions and 0 deletions
|
|
@ -448,6 +448,124 @@ fn project_agent_state_load(
|
||||||
state.session_db.load_project_agent_state(&project_id)
|
state.session_db.load_project_agent_state(&project_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- File browser commands (Files tab) ---
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct DirEntry {
|
||||||
|
name: String,
|
||||||
|
path: String,
|
||||||
|
is_dir: bool,
|
||||||
|
size: u64,
|
||||||
|
/// File extension (lowercase, without dot), empty for dirs
|
||||||
|
ext: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn list_directory_children(path: String) -> Result<Vec<DirEntry>, String> {
|
||||||
|
let dir = std::path::Path::new(&path);
|
||||||
|
if !dir.is_dir() {
|
||||||
|
return Err(format!("Not a directory: {path}"));
|
||||||
|
}
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let read_dir = std::fs::read_dir(dir).map_err(|e| format!("Failed to read directory: {e}"))?;
|
||||||
|
for entry in read_dir {
|
||||||
|
let entry = entry.map_err(|e| format!("Failed to read entry: {e}"))?;
|
||||||
|
let metadata = entry.metadata().map_err(|e| format!("Failed to read metadata: {e}"))?;
|
||||||
|
let name = entry.file_name().to_string_lossy().into_owned();
|
||||||
|
// Skip hidden files/dirs
|
||||||
|
if name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let is_dir = metadata.is_dir();
|
||||||
|
let ext = if is_dir {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
std::path::Path::new(&name)
|
||||||
|
.extension()
|
||||||
|
.map(|e| e.to_string_lossy().to_lowercase())
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
entries.push(DirEntry {
|
||||||
|
name,
|
||||||
|
path: entry.path().to_string_lossy().into_owned(),
|
||||||
|
is_dir,
|
||||||
|
size: metadata.len(),
|
||||||
|
ext,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Sort: dirs first, then files, alphabetical within each group
|
||||||
|
entries.sort_by(|a, b| {
|
||||||
|
b.is_dir.cmp(&a.is_dir).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
|
||||||
|
});
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content types for file viewer routing
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
enum FileContent {
|
||||||
|
Text { content: String, lang: String },
|
||||||
|
Binary { message: String },
|
||||||
|
TooLarge { size: u64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn read_file_content(path: String) -> Result<FileContent, String> {
|
||||||
|
let file_path = std::path::Path::new(&path);
|
||||||
|
if !file_path.is_file() {
|
||||||
|
return Err(format!("Not a file: {path}"));
|
||||||
|
}
|
||||||
|
let metadata = std::fs::metadata(&path).map_err(|e| format!("Failed to read metadata: {e}"))?;
|
||||||
|
let size = metadata.len();
|
||||||
|
|
||||||
|
// Gate: files over 10MB
|
||||||
|
if size > 10 * 1024 * 1024 {
|
||||||
|
return Ok(FileContent::TooLarge { size });
|
||||||
|
}
|
||||||
|
|
||||||
|
let ext = file_path
|
||||||
|
.extension()
|
||||||
|
.map(|e| e.to_string_lossy().to_lowercase())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Binary file types — return message, frontend handles display
|
||||||
|
let binary_exts = ["png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp",
|
||||||
|
"pdf", "zip", "tar", "gz", "7z", "rar",
|
||||||
|
"mp3", "mp4", "wav", "ogg", "webm", "avi",
|
||||||
|
"woff", "woff2", "ttf", "otf", "eot",
|
||||||
|
"exe", "dll", "so", "dylib", "wasm"];
|
||||||
|
if binary_exts.contains(&ext.as_str()) {
|
||||||
|
return Ok(FileContent::Binary { message: format!("Binary file ({ext}), {size} bytes") });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text files — read content with language hint
|
||||||
|
let content = std::fs::read_to_string(&path)
|
||||||
|
.map_err(|_| format!("Binary or non-UTF-8 file"))?;
|
||||||
|
|
||||||
|
let lang = match ext.as_str() {
|
||||||
|
"rs" => "rust",
|
||||||
|
"ts" | "tsx" => "typescript",
|
||||||
|
"js" | "jsx" | "mjs" | "cjs" => "javascript",
|
||||||
|
"py" => "python",
|
||||||
|
"svelte" => "svelte",
|
||||||
|
"html" | "htm" => "html",
|
||||||
|
"css" | "scss" | "less" => "css",
|
||||||
|
"json" => "json",
|
||||||
|
"toml" => "toml",
|
||||||
|
"yaml" | "yml" => "yaml",
|
||||||
|
"md" | "markdown" => "markdown",
|
||||||
|
"sh" | "bash" | "zsh" => "bash",
|
||||||
|
"sql" => "sql",
|
||||||
|
"xml" => "xml",
|
||||||
|
"csv" => "csv",
|
||||||
|
"dockerfile" => "dockerfile",
|
||||||
|
"lock" => "text",
|
||||||
|
_ => "text",
|
||||||
|
}.to_string();
|
||||||
|
|
||||||
|
Ok(FileContent::Text { content, lang })
|
||||||
|
}
|
||||||
|
|
||||||
// Directory picker: custom rfd command with parent window for modal behavior on Linux
|
// Directory picker: custom rfd command with parent window for modal behavior on Linux
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn pick_directory(window: tauri::Window) -> Result<Option<String>, String> {
|
async fn pick_directory(window: tauri::Window) -> Result<Option<String>, String> {
|
||||||
|
|
@ -623,6 +741,8 @@ pub fn run() {
|
||||||
groups_load,
|
groups_load,
|
||||||
groups_save,
|
groups_save,
|
||||||
discover_markdown_files,
|
discover_markdown_files,
|
||||||
|
list_directory_children,
|
||||||
|
read_file_content,
|
||||||
agent_messages_save,
|
agent_messages_save,
|
||||||
agent_messages_load,
|
agent_messages_load,
|
||||||
project_agent_state_save,
|
project_agent_state_save,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue