diff --git a/v2/src-tauri/src/commands/agent.rs b/v2/src-tauri/src/commands/agent.rs new file mode 100644 index 0000000..65ef1b9 --- /dev/null +++ b/v2/src-tauri/src/commands/agent.rs @@ -0,0 +1,29 @@ +use tauri::State; +use crate::AppState; +use crate::sidecar::AgentQueryOptions; + +#[tauri::command] +#[tracing::instrument(skip(state, options), fields(session_id = %options.session_id))] +pub fn agent_query( + state: State<'_, AppState>, + options: AgentQueryOptions, +) -> Result<(), String> { + state.sidecar_manager.query(&options) +} + +#[tauri::command] +#[tracing::instrument(skip(state))] +pub fn agent_stop(state: State<'_, AppState>, session_id: String) -> Result<(), String> { + state.sidecar_manager.stop_session(&session_id) +} + +#[tauri::command] +pub fn agent_ready(state: State<'_, AppState>) -> bool { + state.sidecar_manager.is_ready() +} + +#[tauri::command] +#[tracing::instrument(skip(state))] +pub fn agent_restart(state: State<'_, AppState>) -> Result<(), String> { + state.sidecar_manager.restart() +} diff --git a/v2/src-tauri/src/commands/claude.rs b/v2/src-tauri/src/commands/claude.rs new file mode 100644 index 0000000..f6ece52 --- /dev/null +++ b/v2/src-tauri/src/commands/claude.rs @@ -0,0 +1,158 @@ +// Claude profile and skill discovery commands + +#[derive(serde::Serialize)] +pub struct ClaudeProfile { + pub name: String, + pub email: Option, + pub subscription_type: Option, + pub display_name: Option, + pub config_dir: String, +} + +#[derive(serde::Serialize)] +pub struct ClaudeSkill { + pub name: String, + pub description: String, + pub source_path: String, +} + +#[tauri::command] +pub fn claude_list_profiles() -> Vec { + let mut profiles = Vec::new(); + + let config_dir = dirs::config_dir().unwrap_or_default(); + let profiles_dir = config_dir.join("switcher").join("profiles"); + let alt_dir_root = config_dir.join("switcher-claude"); + + if let Ok(entries) = std::fs::read_dir(&profiles_dir) { + for entry in entries.flatten() { + if !entry.path().is_dir() { continue; } + let name = entry.file_name().to_string_lossy().to_string(); + + let toml_path = entry.path().join("profile.toml"); + let (email, subscription_type, display_name) = if toml_path.exists() { + let content = std::fs::read_to_string(&toml_path).unwrap_or_else(|e| { + log::warn!("Failed to read {}: {e}", toml_path.display()); + String::new() + }); + ( + extract_toml_value(&content, "email"), + extract_toml_value(&content, "subscription_type"), + extract_toml_value(&content, "display_name"), + ) + } else { + (None, None, None) + }; + + let alt_path = alt_dir_root.join(&name); + let config_dir_str = if alt_path.exists() { + alt_path.to_string_lossy().to_string() + } else { + dirs::home_dir() + .unwrap_or_default() + .join(".claude") + .to_string_lossy() + .to_string() + }; + + profiles.push(ClaudeProfile { + name, + email, + subscription_type, + display_name, + config_dir: config_dir_str, + }); + } + } + + if profiles.is_empty() { + let home = dirs::home_dir().unwrap_or_default(); + profiles.push(ClaudeProfile { + name: "default".to_string(), + email: None, + subscription_type: None, + display_name: None, + config_dir: home.join(".claude").to_string_lossy().to_string(), + }); + } + + profiles +} + +fn extract_toml_value(content: &str, key: &str) -> Option { + for line in content.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix(key) { + if let Some(rest) = rest.trim().strip_prefix('=') { + let val = rest.trim().trim_matches('"'); + if !val.is_empty() { + return Some(val.to_string()); + } + } + } + } + None +} + +#[tauri::command] +pub fn claude_list_skills() -> Vec { + let mut skills = Vec::new(); + let home = dirs::home_dir().unwrap_or_default(); + + let skills_dir = home.join(".claude").join("skills"); + if let Ok(entries) = std::fs::read_dir(&skills_dir) { + for entry in entries.flatten() { + let path = entry.path(); + let (name, skill_file) = if path.is_dir() { + let skill_md = path.join("SKILL.md"); + if skill_md.exists() { + (entry.file_name().to_string_lossy().to_string(), skill_md) + } else { + continue; + } + } else if path.extension().map_or(false, |e| e == "md") { + let stem = path.file_stem().unwrap_or_default().to_string_lossy().to_string(); + (stem, path.clone()) + } else { + continue; + }; + + let description = if let Ok(content) = std::fs::read_to_string(&skill_file) { + content.lines() + .filter(|l| !l.trim().is_empty() && !l.starts_with('#')) + .next() + .unwrap_or("") + .trim() + .chars() + .take(120) + .collect() + } else { + String::new() + }; + + skills.push(ClaudeSkill { + name, + description, + source_path: skill_file.to_string_lossy().to_string(), + }); + } + } + + skills +} + +#[tauri::command] +pub fn claude_read_skill(path: String) -> Result { + let skills_dir = dirs::home_dir() + .ok_or("Cannot determine home directory")? + .join(".claude") + .join("skills"); + let canonical_skills = skills_dir.canonicalize() + .map_err(|_| "Skills directory does not exist".to_string())?; + let canonical_path = std::path::Path::new(&path).canonicalize() + .map_err(|e| format!("Invalid skill path: {e}"))?; + if !canonical_path.starts_with(&canonical_skills) { + return Err("Access denied: path is outside skills directory".to_string()); + } + std::fs::read_to_string(&canonical_path).map_err(|e| format!("Failed to read skill: {e}")) +} diff --git a/v2/src-tauri/src/commands/files.rs b/v2/src-tauri/src/commands/files.rs new file mode 100644 index 0000000..157526d --- /dev/null +++ b/v2/src-tauri/src/commands/files.rs @@ -0,0 +1,130 @@ +// File browser commands (Files tab) + +#[derive(serde::Serialize)] +pub struct DirEntry { + pub name: String, + pub path: String, + pub is_dir: bool, + pub size: u64, + pub ext: String, +} + +/// Content types for file viewer routing +#[derive(serde::Serialize)] +#[serde(tag = "type")] +pub enum FileContent { + Text { content: String, lang: String }, + Binary { message: String }, + TooLarge { size: u64 }, +} + +#[tauri::command] +pub 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(); + 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, + }); + } + 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) +} + +#[tauri::command] +pub 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(); + + 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(); + + 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") }); + } + + 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 }) +} + +#[tauri::command] +pub fn write_file_content(path: String, content: String) -> Result<(), String> { + let file_path = std::path::Path::new(&path); + if !file_path.is_file() { + return Err(format!("Not an existing file: {path}")); + } + std::fs::write(&path, content.as_bytes()) + .map_err(|e| format!("Failed to write file: {e}")) +} + +#[tauri::command] +pub async fn pick_directory(window: tauri::Window) -> Result, String> { + let dialog = rfd::AsyncFileDialog::new() + .set_title("Select Directory") + .set_parent(&window); + let folder = dialog.pick_folder().await; + Ok(folder.map(|f| f.path().to_string_lossy().into_owned())) +} diff --git a/v2/src-tauri/src/commands/groups.rs b/v2/src-tauri/src/commands/groups.rs new file mode 100644 index 0000000..b05185a --- /dev/null +++ b/v2/src-tauri/src/commands/groups.rs @@ -0,0 +1,16 @@ +use crate::groups::{GroupsFile, MdFileEntry}; + +#[tauri::command] +pub fn groups_load() -> Result { + crate::groups::load_groups() +} + +#[tauri::command] +pub fn groups_save(config: GroupsFile) -> Result<(), String> { + crate::groups::save_groups(&config) +} + +#[tauri::command] +pub fn discover_markdown_files(cwd: String) -> Result, String> { + crate::groups::discover_markdown_files(&cwd) +} diff --git a/v2/src-tauri/src/commands/knowledge.rs b/v2/src-tauri/src/commands/knowledge.rs new file mode 100644 index 0000000..f0e1296 --- /dev/null +++ b/v2/src-tauri/src/commands/knowledge.rs @@ -0,0 +1,67 @@ +use tauri::State; +use crate::AppState; +use crate::{ctx, memora}; + +// --- ctx commands --- + +#[tauri::command] +pub fn ctx_init_db(state: State<'_, AppState>) -> Result<(), String> { + state.ctx_db.init_db() +} + +#[tauri::command] +pub fn ctx_register_project(state: State<'_, AppState>, name: String, description: String, work_dir: Option) -> Result<(), String> { + state.ctx_db.register_project(&name, &description, work_dir.as_deref()) +} + +#[tauri::command] +pub fn ctx_get_context(state: State<'_, AppState>, project: String) -> Result, String> { + state.ctx_db.get_context(&project) +} + +#[tauri::command] +pub fn ctx_get_shared(state: State<'_, AppState>) -> Result, String> { + state.ctx_db.get_shared() +} + +#[tauri::command] +pub fn ctx_get_summaries(state: State<'_, AppState>, project: String, limit: i64) -> Result, String> { + state.ctx_db.get_summaries(&project, limit) +} + +#[tauri::command] +pub fn ctx_search(state: State<'_, AppState>, query: String) -> Result, String> { + state.ctx_db.search(&query) +} + +// --- Memora commands (read-only) --- + +#[tauri::command] +pub fn memora_available(state: State<'_, AppState>) -> bool { + state.memora_db.is_available() +} + +#[tauri::command] +pub fn memora_list( + state: State<'_, AppState>, + tags: Option>, + limit: Option, + offset: Option, +) -> Result { + state.memora_db.list(tags, limit.unwrap_or(50), offset.unwrap_or(0)) +} + +#[tauri::command] +pub fn memora_search( + state: State<'_, AppState>, + query: String, + tags: Option>, + limit: Option, +) -> Result { + state.memora_db.search(&query, tags, limit.unwrap_or(50)) +} + +#[tauri::command] +pub fn memora_get(state: State<'_, AppState>, id: i64) -> Result, String> { + state.memora_db.get(id) +} diff --git a/v2/src-tauri/src/commands/misc.rs b/v2/src-tauri/src/commands/misc.rs new file mode 100644 index 0000000..a0a32ca --- /dev/null +++ b/v2/src-tauri/src/commands/misc.rs @@ -0,0 +1,41 @@ +// Miscellaneous commands — CLI args, URL opening, frontend telemetry + +#[tauri::command] +pub fn cli_get_group() -> Option { + let args: Vec = std::env::args().collect(); + let mut i = 1; + while i < args.len() { + if args[i] == "--group" { + if i + 1 < args.len() { + return Some(args[i + 1].clone()); + } + } else if let Some(val) = args[i].strip_prefix("--group=") { + return Some(val.to_string()); + } + i += 1; + } + None +} + +#[tauri::command] +pub fn open_url(url: String) -> Result<(), String> { + if !url.starts_with("http://") && !url.starts_with("https://") { + return Err("Only http/https URLs are allowed".into()); + } + std::process::Command::new("xdg-open") + .arg(&url) + .spawn() + .map_err(|e| format!("Failed to open URL: {e}"))?; + Ok(()) +} + +#[tauri::command] +pub fn frontend_log(level: String, message: String, context: Option) { + match level.as_str() { + "error" => tracing::error!(source = "frontend", ?context, "{message}"), + "warn" => tracing::warn!(source = "frontend", ?context, "{message}"), + "info" => tracing::info!(source = "frontend", ?context, "{message}"), + "debug" => tracing::debug!(source = "frontend", ?context, "{message}"), + _ => tracing::trace!(source = "frontend", ?context, "{message}"), + } +} diff --git a/v2/src-tauri/src/commands/mod.rs b/v2/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..1a8db94 --- /dev/null +++ b/v2/src-tauri/src/commands/mod.rs @@ -0,0 +1,11 @@ +pub mod pty; +pub mod agent; +pub mod watcher; +pub mod session; +pub mod persistence; +pub mod knowledge; +pub mod claude; +pub mod groups; +pub mod files; +pub mod remote; +pub mod misc; diff --git a/v2/src-tauri/src/commands/persistence.rs b/v2/src-tauri/src/commands/persistence.rs new file mode 100644 index 0000000..c8c5c39 --- /dev/null +++ b/v2/src-tauri/src/commands/persistence.rs @@ -0,0 +1,109 @@ +use tauri::State; +use crate::AppState; +use crate::session::{AgentMessageRecord, ProjectAgentState, SessionMetric, SessionAnchorRecord}; + +// --- Agent message persistence --- + +#[tauri::command] +pub fn agent_messages_save( + state: State<'_, AppState>, + session_id: String, + project_id: String, + sdk_session_id: Option, + messages: Vec, +) -> Result<(), String> { + state.session_db.save_agent_messages( + &session_id, + &project_id, + sdk_session_id.as_deref(), + &messages, + ) +} + +#[tauri::command] +pub fn agent_messages_load( + state: State<'_, AppState>, + project_id: String, +) -> Result, String> { + state.session_db.load_agent_messages(&project_id) +} + +// --- Project agent state --- + +#[tauri::command] +pub fn project_agent_state_save( + state: State<'_, AppState>, + agent_state: ProjectAgentState, +) -> Result<(), String> { + state.session_db.save_project_agent_state(&agent_state) +} + +#[tauri::command] +pub fn project_agent_state_load( + state: State<'_, AppState>, + project_id: String, +) -> Result, String> { + state.session_db.load_project_agent_state(&project_id) +} + +// --- Session metrics --- + +#[tauri::command] +pub fn session_metric_save( + state: State<'_, AppState>, + metric: SessionMetric, +) -> Result<(), String> { + state.session_db.save_session_metric(&metric) +} + +#[tauri::command] +pub fn session_metrics_load( + state: State<'_, AppState>, + project_id: String, + limit: i64, +) -> Result, String> { + state.session_db.load_session_metrics(&project_id, limit) +} + +// --- Session anchors --- + +#[tauri::command] +pub fn session_anchors_save( + state: State<'_, AppState>, + anchors: Vec, +) -> Result<(), String> { + state.session_db.save_session_anchors(&anchors) +} + +#[tauri::command] +pub fn session_anchors_load( + state: State<'_, AppState>, + project_id: String, +) -> Result, String> { + state.session_db.load_session_anchors(&project_id) +} + +#[tauri::command] +pub fn session_anchor_delete( + state: State<'_, AppState>, + id: String, +) -> Result<(), String> { + state.session_db.delete_session_anchor(&id) +} + +#[tauri::command] +pub fn session_anchors_clear( + state: State<'_, AppState>, + project_id: String, +) -> Result<(), String> { + state.session_db.delete_project_anchors(&project_id) +} + +#[tauri::command] +pub fn session_anchor_update_type( + state: State<'_, AppState>, + id: String, + anchor_type: String, +) -> Result<(), String> { + state.session_db.update_anchor_type(&id, &anchor_type) +} diff --git a/v2/src-tauri/src/commands/pty.rs b/v2/src-tauri/src/commands/pty.rs new file mode 100644 index 0000000..f4aa5cc --- /dev/null +++ b/v2/src-tauri/src/commands/pty.rs @@ -0,0 +1,33 @@ +use tauri::State; +use crate::AppState; +use crate::pty::PtyOptions; + +#[tauri::command] +#[tracing::instrument(skip(state), fields(shell = ?options.shell))] +pub fn pty_spawn( + state: State<'_, AppState>, + options: PtyOptions, +) -> Result { + state.pty_manager.spawn(options) +} + +#[tauri::command] +pub fn pty_write(state: State<'_, AppState>, id: String, data: String) -> Result<(), String> { + state.pty_manager.write(&id, &data) +} + +#[tauri::command] +pub fn pty_resize( + state: State<'_, AppState>, + id: String, + cols: u16, + rows: u16, +) -> Result<(), String> { + state.pty_manager.resize(&id, cols, rows) +} + +#[tauri::command] +#[tracing::instrument(skip(state))] +pub fn pty_kill(state: State<'_, AppState>, id: String) -> Result<(), String> { + state.pty_manager.kill(&id) +} diff --git a/v2/src-tauri/src/commands/remote.rs b/v2/src-tauri/src/commands/remote.rs new file mode 100644 index 0000000..5f7d3a6 --- /dev/null +++ b/v2/src-tauri/src/commands/remote.rs @@ -0,0 +1,65 @@ +use tauri::State; +use crate::AppState; +use crate::remote::{RemoteMachineConfig, RemoteMachineInfo}; +use crate::pty::PtyOptions; +use crate::sidecar::AgentQueryOptions; + +#[tauri::command] +pub async fn remote_list(state: State<'_, AppState>) -> Result, String> { + Ok(state.remote_manager.list_machines().await) +} + +#[tauri::command] +pub async fn remote_add(state: State<'_, AppState>, config: RemoteMachineConfig) -> Result { + Ok(state.remote_manager.add_machine(config).await) +} + +#[tauri::command] +pub async fn remote_remove(state: State<'_, AppState>, machine_id: String) -> Result<(), String> { + state.remote_manager.remove_machine(&machine_id).await +} + +#[tauri::command] +#[tracing::instrument(skip(app, state))] +pub async fn remote_connect(app: tauri::AppHandle, state: State<'_, AppState>, machine_id: String) -> Result<(), String> { + state.remote_manager.connect(&app, &machine_id).await +} + +#[tauri::command] +#[tracing::instrument(skip(state))] +pub async fn remote_disconnect(state: State<'_, AppState>, machine_id: String) -> Result<(), String> { + state.remote_manager.disconnect(&machine_id).await +} + +#[tauri::command] +#[tracing::instrument(skip(state, options), fields(session_id = %options.session_id))] +pub async fn remote_agent_query(state: State<'_, AppState>, machine_id: String, options: AgentQueryOptions) -> Result<(), String> { + state.remote_manager.agent_query(&machine_id, &options).await +} + +#[tauri::command] +#[tracing::instrument(skip(state))] +pub async fn remote_agent_stop(state: State<'_, AppState>, machine_id: String, session_id: String) -> Result<(), String> { + state.remote_manager.agent_stop(&machine_id, &session_id).await +} + +#[tauri::command] +#[tracing::instrument(skip(state), fields(shell = ?options.shell))] +pub async fn remote_pty_spawn(state: State<'_, AppState>, machine_id: String, options: PtyOptions) -> Result { + state.remote_manager.pty_spawn(&machine_id, &options).await +} + +#[tauri::command] +pub async fn remote_pty_write(state: State<'_, AppState>, machine_id: String, id: String, data: String) -> Result<(), String> { + state.remote_manager.pty_write(&machine_id, &id, &data).await +} + +#[tauri::command] +pub async fn remote_pty_resize(state: State<'_, AppState>, machine_id: String, id: String, cols: u16, rows: u16) -> Result<(), String> { + state.remote_manager.pty_resize(&machine_id, &id, cols, rows).await +} + +#[tauri::command] +pub async fn remote_pty_kill(state: State<'_, AppState>, machine_id: String, id: String) -> Result<(), String> { + state.remote_manager.pty_kill(&machine_id, &id).await +} diff --git a/v2/src-tauri/src/commands/session.rs b/v2/src-tauri/src/commands/session.rs new file mode 100644 index 0000000..bbfd6d6 --- /dev/null +++ b/v2/src-tauri/src/commands/session.rs @@ -0,0 +1,81 @@ +use tauri::State; +use crate::AppState; +use crate::session::{Session, LayoutState, SshSession}; + +// --- Session persistence --- + +#[tauri::command] +pub fn session_list(state: State<'_, AppState>) -> Result, String> { + state.session_db.list_sessions() +} + +#[tauri::command] +pub fn session_save(state: State<'_, AppState>, session: Session) -> Result<(), String> { + state.session_db.save_session(&session) +} + +#[tauri::command] +pub fn session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> { + state.session_db.delete_session(&id) +} + +#[tauri::command] +pub fn session_update_title(state: State<'_, AppState>, id: String, title: String) -> Result<(), String> { + state.session_db.update_title(&id, &title) +} + +#[tauri::command] +pub fn session_touch(state: State<'_, AppState>, id: String) -> Result<(), String> { + state.session_db.touch_session(&id) +} + +#[tauri::command] +pub fn session_update_group(state: State<'_, AppState>, id: String, group_name: String) -> Result<(), String> { + state.session_db.update_group(&id, &group_name) +} + +// --- Layout --- + +#[tauri::command] +pub fn layout_save(state: State<'_, AppState>, layout: LayoutState) -> Result<(), String> { + state.session_db.save_layout(&layout) +} + +#[tauri::command] +pub fn layout_load(state: State<'_, AppState>) -> Result { + state.session_db.load_layout() +} + +// --- Settings --- + +#[tauri::command] +pub fn settings_get(state: State<'_, AppState>, key: String) -> Result, String> { + state.session_db.get_setting(&key) +} + +#[tauri::command] +pub fn settings_set(state: State<'_, AppState>, key: String, value: String) -> Result<(), String> { + state.session_db.set_setting(&key, &value) +} + +#[tauri::command] +pub fn settings_list(state: State<'_, AppState>) -> Result, String> { + state.session_db.get_all_settings() +} + +// --- SSH sessions --- + +#[tauri::command] +pub fn ssh_session_list(state: State<'_, AppState>) -> Result, String> { + state.session_db.list_ssh_sessions() +} + +#[tauri::command] +pub fn ssh_session_save(state: State<'_, AppState>, session: SshSession) -> Result<(), String> { + state.session_db.save_ssh_session(&session) +} + +#[tauri::command] +pub fn ssh_session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> { + state.session_db.delete_ssh_session(&id) +} diff --git a/v2/src-tauri/src/commands/watcher.rs b/v2/src-tauri/src/commands/watcher.rs new file mode 100644 index 0000000..ebacbae --- /dev/null +++ b/v2/src-tauri/src/commands/watcher.rs @@ -0,0 +1,43 @@ +use tauri::State; +use crate::AppState; +use crate::fs_watcher::FsWatcherStatus; + +#[tauri::command] +pub fn file_watch( + app: tauri::AppHandle, + state: State<'_, AppState>, + pane_id: String, + path: String, +) -> Result { + state.file_watcher.watch(&app, &pane_id, &path) +} + +#[tauri::command] +pub fn file_unwatch(state: State<'_, AppState>, pane_id: String) { + state.file_watcher.unwatch(&pane_id); +} + +#[tauri::command] +pub fn file_read(state: State<'_, AppState>, path: String) -> Result { + state.file_watcher.read_file(&path) +} + +#[tauri::command] +pub fn fs_watch_project( + app: tauri::AppHandle, + state: State<'_, AppState>, + project_id: String, + cwd: String, +) -> Result<(), String> { + state.fs_watcher.watch_project(&app, &project_id, &cwd) +} + +#[tauri::command] +pub fn fs_unwatch_project(state: State<'_, AppState>, project_id: String) { + state.fs_watcher.unwatch_project(&project_id); +} + +#[tauri::command] +pub fn fs_watcher_status(state: State<'_, AppState>) -> FsWatcherStatus { + state.fs_watcher.status() +} diff --git a/v2/src-tauri/src/lib.rs b/v2/src-tauri/src/lib.rs index 97225b6..e1ef300 100644 --- a/v2/src-tauri/src/lib.rs +++ b/v2/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +mod commands; mod ctx; mod event_sink; mod fs_watcher; @@ -10,813 +11,28 @@ mod session; mod telemetry; mod watcher; -use ctx::CtxDb; use event_sink::TauriEventSink; -use groups::{GroupsFile, MdFileEntry}; -use pty::{PtyManager, PtyOptions}; -use remote::{RemoteManager, RemoteMachineConfig}; -use session::{Session, SessionDb, LayoutState, SshSession, AgentMessageRecord, ProjectAgentState}; -use sidecar::{AgentQueryOptions, SidecarConfig, SidecarManager}; -use fs_watcher::{FsWatcherStatus, ProjectFsWatcher}; +use pty::PtyManager; +use remote::RemoteManager; +use session::SessionDb; +use sidecar::{SidecarConfig, SidecarManager}; +use fs_watcher::ProjectFsWatcher; use watcher::FileWatcherManager; use std::sync::Arc; -use tauri::{Manager, State}; +use tauri::Manager; -struct AppState { - pty_manager: Arc, - sidecar_manager: Arc, - session_db: Arc, - file_watcher: Arc, - fs_watcher: Arc, - ctx_db: Arc, - memora_db: Arc, - remote_manager: Arc, +pub(crate) struct AppState { + pub pty_manager: Arc, + pub sidecar_manager: Arc, + pub session_db: Arc, + pub file_watcher: Arc, + pub fs_watcher: Arc, + pub ctx_db: Arc, + pub memora_db: Arc, + pub remote_manager: Arc, _telemetry: telemetry::TelemetryGuard, } -// --- PTY commands --- - -#[tauri::command] -#[tracing::instrument(skip(state), fields(shell = ?options.shell))] -fn pty_spawn( - state: State<'_, AppState>, - options: PtyOptions, -) -> Result { - state.pty_manager.spawn(options) -} - -#[tauri::command] -fn pty_write(state: State<'_, AppState>, id: String, data: String) -> Result<(), String> { - state.pty_manager.write(&id, &data) -} - -#[tauri::command] -fn pty_resize( - state: State<'_, AppState>, - id: String, - cols: u16, - rows: u16, -) -> Result<(), String> { - state.pty_manager.resize(&id, cols, rows) -} - -#[tauri::command] -#[tracing::instrument(skip(state))] -fn pty_kill(state: State<'_, AppState>, id: String) -> Result<(), String> { - state.pty_manager.kill(&id) -} - -// --- Agent/sidecar commands --- - -#[tauri::command] -#[tracing::instrument(skip(state, options), fields(session_id = %options.session_id))] -fn agent_query( - state: State<'_, AppState>, - options: AgentQueryOptions, -) -> Result<(), String> { - state.sidecar_manager.query(&options) -} - -#[tauri::command] -#[tracing::instrument(skip(state))] -fn agent_stop(state: State<'_, AppState>, session_id: String) -> Result<(), String> { - state.sidecar_manager.stop_session(&session_id) -} - -#[tauri::command] -fn agent_ready(state: State<'_, AppState>) -> bool { - state.sidecar_manager.is_ready() -} - -#[tauri::command] -#[tracing::instrument(skip(state))] -fn agent_restart(state: State<'_, AppState>) -> Result<(), String> { - state.sidecar_manager.restart() -} - -// --- File watcher commands --- - -#[tauri::command] -fn file_watch( - app: tauri::AppHandle, - state: State<'_, AppState>, - pane_id: String, - path: String, -) -> Result { - state.file_watcher.watch(&app, &pane_id, &path) -} - -#[tauri::command] -fn file_unwatch(state: State<'_, AppState>, pane_id: String) { - state.file_watcher.unwatch(&pane_id); -} - -#[tauri::command] -fn file_read(state: State<'_, AppState>, path: String) -> Result { - state.file_watcher.read_file(&path) -} - -// --- Project filesystem watcher commands (S-1 Phase 2) --- - -#[tauri::command] -fn fs_watch_project( - app: tauri::AppHandle, - state: State<'_, AppState>, - project_id: String, - cwd: String, -) -> Result<(), String> { - state.fs_watcher.watch_project(&app, &project_id, &cwd) -} - -#[tauri::command] -fn fs_unwatch_project(state: State<'_, AppState>, project_id: String) { - state.fs_watcher.unwatch_project(&project_id); -} - -#[tauri::command] -fn fs_watcher_status(state: State<'_, AppState>) -> FsWatcherStatus { - state.fs_watcher.status() -} - -// --- Session persistence commands --- - -#[tauri::command] -fn session_list(state: State<'_, AppState>) -> Result, String> { - state.session_db.list_sessions() -} - -#[tauri::command] -fn session_save(state: State<'_, AppState>, session: Session) -> Result<(), String> { - state.session_db.save_session(&session) -} - -#[tauri::command] -fn session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> { - state.session_db.delete_session(&id) -} - -#[tauri::command] -fn session_update_title(state: State<'_, AppState>, id: String, title: String) -> Result<(), String> { - state.session_db.update_title(&id, &title) -} - -#[tauri::command] -fn session_touch(state: State<'_, AppState>, id: String) -> Result<(), String> { - state.session_db.touch_session(&id) -} - -#[tauri::command] -fn session_update_group(state: State<'_, AppState>, id: String, group_name: String) -> Result<(), String> { - state.session_db.update_group(&id, &group_name) -} - -#[tauri::command] -fn layout_save(state: State<'_, AppState>, layout: LayoutState) -> Result<(), String> { - state.session_db.save_layout(&layout) -} - -#[tauri::command] -fn layout_load(state: State<'_, AppState>) -> Result { - state.session_db.load_layout() -} - -// --- Settings commands --- - -#[tauri::command] -fn settings_get(state: State<'_, AppState>, key: String) -> Result, String> { - state.session_db.get_setting(&key) -} - -#[tauri::command] -fn settings_set(state: State<'_, AppState>, key: String, value: String) -> Result<(), String> { - state.session_db.set_setting(&key, &value) -} - -#[tauri::command] -fn settings_list(state: State<'_, AppState>) -> Result, String> { - state.session_db.get_all_settings() -} - -// --- SSH session commands --- - -#[tauri::command] -fn ssh_session_list(state: State<'_, AppState>) -> Result, String> { - state.session_db.list_ssh_sessions() -} - -#[tauri::command] -fn ssh_session_save(state: State<'_, AppState>, session: SshSession) -> Result<(), String> { - state.session_db.save_ssh_session(&session) -} - -#[tauri::command] -fn ssh_session_delete(state: State<'_, AppState>, id: String) -> Result<(), String> { - state.session_db.delete_ssh_session(&id) -} - -// --- ctx commands --- - -#[tauri::command] -fn ctx_init_db(state: State<'_, AppState>) -> Result<(), String> { - state.ctx_db.init_db() -} - -#[tauri::command] -fn ctx_register_project(state: State<'_, AppState>, name: String, description: String, work_dir: Option) -> Result<(), String> { - state.ctx_db.register_project(&name, &description, work_dir.as_deref()) -} - -#[tauri::command] -fn ctx_get_context(state: State<'_, AppState>, project: String) -> Result, String> { - state.ctx_db.get_context(&project) -} - -#[tauri::command] -fn ctx_get_shared(state: State<'_, AppState>) -> Result, String> { - state.ctx_db.get_shared() -} - -#[tauri::command] -fn ctx_get_summaries(state: State<'_, AppState>, project: String, limit: i64) -> Result, String> { - state.ctx_db.get_summaries(&project, limit) -} - -#[tauri::command] -fn ctx_search(state: State<'_, AppState>, query: String) -> Result, String> { - state.ctx_db.search(&query) -} - -// --- Memora commands (read-only) --- - -#[tauri::command] -fn memora_available(state: State<'_, AppState>) -> bool { - state.memora_db.is_available() -} - -#[tauri::command] -fn memora_list( - state: State<'_, AppState>, - tags: Option>, - limit: Option, - offset: Option, -) -> Result { - state.memora_db.list(tags, limit.unwrap_or(50), offset.unwrap_or(0)) -} - -#[tauri::command] -fn memora_search( - state: State<'_, AppState>, - query: String, - tags: Option>, - limit: Option, -) -> Result { - state.memora_db.search(&query, tags, limit.unwrap_or(50)) -} - -#[tauri::command] -fn memora_get(state: State<'_, AppState>, id: i64) -> Result, String> { - state.memora_db.get(id) -} - -// --- Claude profile commands (switcher-claude integration) --- - -#[derive(serde::Serialize)] -struct ClaudeProfile { - name: String, - email: Option, - subscription_type: Option, - display_name: Option, - config_dir: String, -} - -#[tauri::command] -fn claude_list_profiles() -> Vec { - let mut profiles = Vec::new(); - - // Read profiles from ~/.config/switcher/profiles/ - let config_dir = dirs::config_dir().unwrap_or_default(); - let profiles_dir = config_dir.join("switcher").join("profiles"); - let alt_dir_root = config_dir.join("switcher-claude"); - - if let Ok(entries) = std::fs::read_dir(&profiles_dir) { - for entry in entries.flatten() { - if !entry.path().is_dir() { continue; } - let name = entry.file_name().to_string_lossy().to_string(); - - // Read profile.toml for metadata - let toml_path = entry.path().join("profile.toml"); - let (email, subscription_type, display_name) = if toml_path.exists() { - let content = std::fs::read_to_string(&toml_path).unwrap_or_else(|e| { - log::warn!("Failed to read {}: {e}", toml_path.display()); - String::new() - }); - ( - extract_toml_value(&content, "email"), - extract_toml_value(&content, "subscription_type"), - extract_toml_value(&content, "display_name"), - ) - } else { - (None, None, None) - }; - - // Alt dir for CLAUDE_CONFIG_DIR - let alt_path = alt_dir_root.join(&name); - let config_dir_str = if alt_path.exists() { - alt_path.to_string_lossy().to_string() - } else { - // Fallback to default ~/.claude - dirs::home_dir() - .unwrap_or_default() - .join(".claude") - .to_string_lossy() - .to_string() - }; - - profiles.push(ClaudeProfile { - name, - email, - subscription_type, - display_name, - config_dir: config_dir_str, - }); - } - } - - // Always include a "default" profile for ~/.claude - if profiles.is_empty() { - let home = dirs::home_dir().unwrap_or_default(); - profiles.push(ClaudeProfile { - name: "default".to_string(), - email: None, - subscription_type: None, - display_name: None, - config_dir: home.join(".claude").to_string_lossy().to_string(), - }); - } - - profiles -} - -fn extract_toml_value(content: &str, key: &str) -> Option { - for line in content.lines() { - let trimmed = line.trim(); - if let Some(rest) = trimmed.strip_prefix(key) { - if let Some(rest) = rest.trim().strip_prefix('=') { - let val = rest.trim().trim_matches('"'); - if !val.is_empty() { - return Some(val.to_string()); - } - } - } - } - None -} - -// --- Skill discovery commands --- - -#[derive(serde::Serialize)] -struct ClaudeSkill { - name: String, - description: String, - source_path: String, -} - -#[tauri::command] -fn claude_list_skills() -> Vec { - let mut skills = Vec::new(); - let home = dirs::home_dir().unwrap_or_default(); - - // Search for skills in ~/.claude/skills/ (same as Claude Code CLI) - let skills_dir = home.join(".claude").join("skills"); - if let Ok(entries) = std::fs::read_dir(&skills_dir) { - for entry in entries.flatten() { - let path = entry.path(); - // Skills can be directories with SKILL.md or standalone .md files - let (name, skill_file) = if path.is_dir() { - let skill_md = path.join("SKILL.md"); - if skill_md.exists() { - (entry.file_name().to_string_lossy().to_string(), skill_md) - } else { - continue; - } - } else if path.extension().map_or(false, |e| e == "md") { - let stem = path.file_stem().unwrap_or_default().to_string_lossy().to_string(); - (stem, path.clone()) - } else { - continue; - }; - - // Extract description from first non-empty, non-heading line - let description = if let Ok(content) = std::fs::read_to_string(&skill_file) { - content.lines() - .filter(|l| !l.trim().is_empty() && !l.starts_with('#')) - .next() - .unwrap_or("") - .trim() - .chars() - .take(120) - .collect() - } else { - String::new() - }; - - skills.push(ClaudeSkill { - name, - description, - source_path: skill_file.to_string_lossy().to_string(), - }); - } - } - - skills -} - -#[tauri::command] -fn claude_read_skill(path: String) -> Result { - // Validate path is under ~/.claude/skills/ to prevent path traversal - let skills_dir = dirs::home_dir() - .ok_or("Cannot determine home directory")? - .join(".claude") - .join("skills"); - let canonical_skills = skills_dir.canonicalize() - .map_err(|_| "Skills directory does not exist".to_string())?; - let canonical_path = std::path::Path::new(&path).canonicalize() - .map_err(|e| format!("Invalid skill path: {e}"))?; - if !canonical_path.starts_with(&canonical_skills) { - return Err("Access denied: path is outside skills directory".to_string()); - } - std::fs::read_to_string(&canonical_path).map_err(|e| format!("Failed to read skill: {e}")) -} - -// --- Group config commands (v3) --- - -#[tauri::command] -fn groups_load() -> Result { - groups::load_groups() -} - -#[tauri::command] -fn groups_save(config: GroupsFile) -> Result<(), String> { - groups::save_groups(&config) -} - -#[tauri::command] -fn discover_markdown_files(cwd: String) -> Result, String> { - groups::discover_markdown_files(&cwd) -} - -// --- Agent message persistence commands (v3) --- - -#[tauri::command] -fn agent_messages_save( - state: State<'_, AppState>, - session_id: String, - project_id: String, - sdk_session_id: Option, - messages: Vec, -) -> Result<(), String> { - state.session_db.save_agent_messages( - &session_id, - &project_id, - sdk_session_id.as_deref(), - &messages, - ) -} - -#[tauri::command] -fn agent_messages_load( - state: State<'_, AppState>, - project_id: String, -) -> Result, String> { - state.session_db.load_agent_messages(&project_id) -} - -#[tauri::command] -fn project_agent_state_save( - state: State<'_, AppState>, - agent_state: ProjectAgentState, -) -> Result<(), String> { - state.session_db.save_project_agent_state(&agent_state) -} - -#[tauri::command] -fn project_agent_state_load( - state: State<'_, AppState>, - project_id: String, -) -> Result, String> { - state.session_db.load_project_agent_state(&project_id) -} - -// --- Session metrics commands --- - -#[tauri::command] -fn session_metric_save( - state: State<'_, AppState>, - metric: session::SessionMetric, -) -> Result<(), String> { - state.session_db.save_session_metric(&metric) -} - -#[tauri::command] -fn session_metrics_load( - state: State<'_, AppState>, - project_id: String, - limit: i64, -) -> Result, String> { - state.session_db.load_session_metrics(&project_id, limit) -} - -// --- Session anchor commands --- - -#[tauri::command] -fn session_anchors_save( - state: State<'_, AppState>, - anchors: Vec, -) -> Result<(), String> { - state.session_db.save_session_anchors(&anchors) -} - -#[tauri::command] -fn session_anchors_load( - state: State<'_, AppState>, - project_id: String, -) -> Result, String> { - state.session_db.load_session_anchors(&project_id) -} - -#[tauri::command] -fn session_anchor_delete( - state: State<'_, AppState>, - id: String, -) -> Result<(), String> { - state.session_db.delete_session_anchor(&id) -} - -#[tauri::command] -fn session_anchors_clear( - state: State<'_, AppState>, - project_id: String, -) -> Result<(), String> { - state.session_db.delete_project_anchors(&project_id) -} - -#[tauri::command] -fn session_anchor_update_type( - state: State<'_, AppState>, - id: String, - anchor_type: String, -) -> Result<(), String> { - state.session_db.update_anchor_type(&id, &anchor_type) -} - -// --- 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 }) -} - -// --- Write file --- - -#[tauri::command] -fn write_file_content(path: String, content: String) -> Result<(), String> { - let file_path = std::path::Path::new(&path); - // Safety: only write to existing files (no arbitrary path creation) - if !file_path.is_file() { - return Err(format!("Not an existing file: {path}")); - } - std::fs::write(&path, content.as_bytes()) - .map_err(|e| format!("Failed to write file: {e}")) -} - -// Directory picker: custom rfd command with parent window for modal behavior on Linux -#[tauri::command] -async fn pick_directory(window: tauri::Window) -> Result, String> { - let dialog = rfd::AsyncFileDialog::new() - .set_title("Select Directory") - .set_parent(&window); - let folder = dialog.pick_folder().await; - Ok(folder.map(|f| f.path().to_string_lossy().into_owned())) -} - -// --- CLI argument commands --- - -#[tauri::command] -fn cli_get_group() -> Option { - let args: Vec = std::env::args().collect(); - let mut i = 1; - while i < args.len() { - if args[i] == "--group" { - if i + 1 < args.len() { - return Some(args[i + 1].clone()); - } - } else if let Some(val) = args[i].strip_prefix("--group=") { - return Some(val.to_string()); - } - i += 1; - } - None -} - -// --- Frontend telemetry bridge --- - -#[tauri::command] -fn frontend_log(level: String, message: String, context: Option) { - match level.as_str() { - "error" => tracing::error!(source = "frontend", ?context, "{message}"), - "warn" => tracing::warn!(source = "frontend", ?context, "{message}"), - "info" => tracing::info!(source = "frontend", ?context, "{message}"), - "debug" => tracing::debug!(source = "frontend", ?context, "{message}"), - _ => tracing::trace!(source = "frontend", ?context, "{message}"), - } -} - -// --- Remote machine commands --- - -#[tauri::command] -async fn remote_list(state: State<'_, AppState>) -> Result, String> { - Ok(state.remote_manager.list_machines().await) -} - -#[tauri::command] -async fn remote_add(state: State<'_, AppState>, config: RemoteMachineConfig) -> Result { - Ok(state.remote_manager.add_machine(config).await) -} - -#[tauri::command] -async fn remote_remove(state: State<'_, AppState>, machine_id: String) -> Result<(), String> { - state.remote_manager.remove_machine(&machine_id).await -} - -#[tauri::command] -#[tracing::instrument(skip(app, state))] -async fn remote_connect(app: tauri::AppHandle, state: State<'_, AppState>, machine_id: String) -> Result<(), String> { - state.remote_manager.connect(&app, &machine_id).await -} - -#[tauri::command] -#[tracing::instrument(skip(state))] -async fn remote_disconnect(state: State<'_, AppState>, machine_id: String) -> Result<(), String> { - state.remote_manager.disconnect(&machine_id).await -} - -#[tauri::command] -#[tracing::instrument(skip(state, options), fields(session_id = %options.session_id))] -async fn remote_agent_query(state: State<'_, AppState>, machine_id: String, options: AgentQueryOptions) -> Result<(), String> { - state.remote_manager.agent_query(&machine_id, &options).await -} - -#[tauri::command] -#[tracing::instrument(skip(state))] -async fn remote_agent_stop(state: State<'_, AppState>, machine_id: String, session_id: String) -> Result<(), String> { - state.remote_manager.agent_stop(&machine_id, &session_id).await -} - -#[tauri::command] -#[tracing::instrument(skip(state), fields(shell = ?options.shell))] -async fn remote_pty_spawn(state: State<'_, AppState>, machine_id: String, options: PtyOptions) -> Result { - state.remote_manager.pty_spawn(&machine_id, &options).await -} - -#[tauri::command] -async fn remote_pty_write(state: State<'_, AppState>, machine_id: String, id: String, data: String) -> Result<(), String> { - state.remote_manager.pty_write(&machine_id, &id, &data).await -} - -#[tauri::command] -async fn remote_pty_resize(state: State<'_, AppState>, machine_id: String, id: String, cols: u16, rows: u16) -> Result<(), String> { - state.remote_manager.pty_resize(&machine_id, &id, cols, rows).await -} - -#[tauri::command] -async fn remote_pty_kill(state: State<'_, AppState>, machine_id: String, id: String) -> Result<(), String> { - state.remote_manager.pty_kill(&machine_id, &id).await -} - -#[tauri::command] -fn open_url(url: String) -> Result<(), String> { - // Only allow http/https URLs - if !url.starts_with("http://") && !url.starts_with("https://") { - return Err("Only http/https URLs are allowed".into()); - } - std::process::Command::new("xdg-open") - .arg(&url) - .spawn() - .map_err(|e| format!("Failed to open URL: {e}"))?; - Ok(()) -} - #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // Force dark GTK theme for native dialogs (file chooser, etc.) @@ -827,79 +43,90 @@ pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ - pty_spawn, - pty_write, - pty_resize, - pty_kill, - agent_query, - agent_stop, - agent_ready, - agent_restart, - file_watch, - file_unwatch, - file_read, - fs_watch_project, - fs_unwatch_project, - fs_watcher_status, - session_list, - session_save, - session_delete, - session_update_title, - session_touch, - session_update_group, - layout_save, - layout_load, - settings_get, - settings_set, - settings_list, - ssh_session_list, - ssh_session_save, - ssh_session_delete, - ctx_init_db, - ctx_register_project, - ctx_get_context, - ctx_get_shared, - ctx_get_summaries, - ctx_search, - memora_available, - memora_list, - memora_search, - memora_get, - remote_list, - remote_add, - remote_remove, - remote_connect, - remote_disconnect, - remote_agent_query, - remote_agent_stop, - remote_pty_spawn, - remote_pty_write, - remote_pty_resize, - remote_pty_kill, - claude_list_profiles, - claude_list_skills, - claude_read_skill, - groups_load, - groups_save, - discover_markdown_files, - list_directory_children, - read_file_content, - write_file_content, - agent_messages_save, - agent_messages_load, - project_agent_state_save, - project_agent_state_load, - session_metric_save, - session_metrics_load, - session_anchors_save, - session_anchors_load, - session_anchor_delete, - session_anchors_clear, - session_anchor_update_type, - cli_get_group, - pick_directory, - open_url, - frontend_log, + // PTY + commands::pty::pty_spawn, + commands::pty::pty_write, + commands::pty::pty_resize, + commands::pty::pty_kill, + // Agent/sidecar + commands::agent::agent_query, + commands::agent::agent_stop, + commands::agent::agent_ready, + commands::agent::agent_restart, + // File watcher + commands::watcher::file_watch, + commands::watcher::file_unwatch, + commands::watcher::file_read, + commands::watcher::fs_watch_project, + commands::watcher::fs_unwatch_project, + commands::watcher::fs_watcher_status, + // Session/layout/settings/SSH + commands::session::session_list, + commands::session::session_save, + commands::session::session_delete, + commands::session::session_update_title, + commands::session::session_touch, + commands::session::session_update_group, + commands::session::layout_save, + commands::session::layout_load, + commands::session::settings_get, + commands::session::settings_set, + commands::session::settings_list, + commands::session::ssh_session_list, + commands::session::ssh_session_save, + commands::session::ssh_session_delete, + // Agent persistence (messages, state, metrics, anchors) + commands::persistence::agent_messages_save, + commands::persistence::agent_messages_load, + commands::persistence::project_agent_state_save, + commands::persistence::project_agent_state_load, + commands::persistence::session_metric_save, + commands::persistence::session_metrics_load, + commands::persistence::session_anchors_save, + commands::persistence::session_anchors_load, + commands::persistence::session_anchor_delete, + commands::persistence::session_anchors_clear, + commands::persistence::session_anchor_update_type, + // ctx + Memora + commands::knowledge::ctx_init_db, + commands::knowledge::ctx_register_project, + commands::knowledge::ctx_get_context, + commands::knowledge::ctx_get_shared, + commands::knowledge::ctx_get_summaries, + commands::knowledge::ctx_search, + commands::knowledge::memora_available, + commands::knowledge::memora_list, + commands::knowledge::memora_search, + commands::knowledge::memora_get, + // Claude profiles/skills + commands::claude::claude_list_profiles, + commands::claude::claude_list_skills, + commands::claude::claude_read_skill, + // Groups + commands::groups::groups_load, + commands::groups::groups_save, + commands::groups::discover_markdown_files, + // File browser + commands::files::list_directory_children, + commands::files::read_file_content, + commands::files::write_file_content, + commands::files::pick_directory, + // Remote machines + commands::remote::remote_list, + commands::remote::remote_add, + commands::remote::remote_remove, + commands::remote::remote_connect, + commands::remote::remote_disconnect, + commands::remote::remote_agent_query, + commands::remote::remote_agent_stop, + commands::remote::remote_pty_spawn, + commands::remote::remote_pty_write, + commands::remote::remote_pty_resize, + commands::remote::remote_pty_kill, + // Misc + commands::misc::cli_get_group, + commands::misc::open_url, + commands::misc::frontend_log, ]) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_dialog::init()) @@ -946,7 +173,7 @@ pub fn run() { let file_watcher = Arc::new(FileWatcherManager::new()); let fs_watcher = Arc::new(ProjectFsWatcher::new()); - let ctx_db = Arc::new(CtxDb::new()); + let ctx_db = Arc::new(ctx::CtxDb::new()); let memora_db = Arc::new(memora::MemoraDb::new()); let remote_manager = Arc::new(RemoteManager::new());