From 260a21c66aea4de294baffbb232a474b65344426 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Tue, 10 Mar 2026 02:12:05 +0100 Subject: [PATCH] feat(backend): add list_directory_children and read_file_content Rust commands --- v2/src-tauri/src/lib.rs | 120 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/v2/src-tauri/src/lib.rs b/v2/src-tauri/src/lib.rs index 91f08cd..bc52e0a 100644 --- a/v2/src-tauri/src/lib.rs +++ b/v2/src-tauri/src/lib.rs @@ -448,6 +448,124 @@ fn project_agent_state_load( 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, 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 { + 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 #[tauri::command] async fn pick_directory(window: tauri::Window) -> Result, String> { @@ -623,6 +741,8 @@ pub fn run() { groups_load, groups_save, discover_markdown_files, + list_directory_children, + read_file_content, agent_messages_save, agent_messages_load, project_agent_state_save,