diff --git a/examples/localcowork/src-tauri/src/commands/settings.rs b/examples/localcowork/src-tauri/src/commands/settings.rs index bfe9ff4..5ec9026 100644 --- a/examples/localcowork/src-tauri/src/commands/settings.rs +++ b/examples/localcowork/src-tauri/src/commands/settings.rs @@ -5,9 +5,117 @@ //! MCP server status from the running McpClient. use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; use serde::{Deserialize, Serialize}; +static SETTINGS_CHANGED: AtomicBool = AtomicBool::new(false); + +pub fn settings_changed() { + SETTINGS_CHANGED.store(true, Ordering::SeqCst); +} + +pub fn has_settings_changed() -> bool { + SETTINGS_CHANGED.swap(false, Ordering::SeqCst) +} + +/// Unified app settings that persist across restarts. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSettings { + /// Currently active model key from _models/config.yaml + pub active_model_key: Option, + /// Allowed filesystem paths for sandboxed operations + pub allowed_paths: Vec, + /// UI theme preference + pub theme: String, + /// Whether to show tool traces + pub show_tool_traces: bool, + /// Sampling config (integrated from existing system) + pub sampling: SamplingConfig, +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + active_model_key: None, + allowed_paths: Vec::new(), + theme: "system".to_string(), + show_tool_traces: true, + sampling: SamplingConfig::default(), + } + // Default allowed paths + } +} + +impl AppSettings { + const FILE_NAME: &'static str = "settings.json"; + + fn persist_path() -> PathBuf { + crate::data_dir().join(Self::FILE_NAME) + } + + pub fn load_or_default() -> Self { + let path = Self::persist_path(); + if !path.exists() { + return Self::default(); + } + match std::fs::read_to_string(&path) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(settings) => { + tracing::info!(path = %path.display(), "loaded app settings"); + settings + } + Err(e) => { + tracing::warn!(error = %e, "failed to parse settings, using defaults"); + Self::default() + } + }, + Err(e) => { + tracing::warn!(error = %e, "failed to read settings, using defaults"); + Self::default() + } + } + } + + pub fn save(&self) { + let path = Self::persist_path(); + let content = match serde_json::to_string_pretty(self) { + Ok(c) => c, + Err(e) => { + tracing::error!(error = %e, "failed to serialize settings"); + return; + } + }; + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + let tmp_path = path.with_extension("json.tmp"); + if let Err(e) = std::fs::write(&tmp_path, &content) { + tracing::error!(error = %e, "failed to write settings temp file"); + return; + } + if let Err(e) = std::fs::rename(&tmp_path, &path) { + tracing::error!(error = %e, "failed to rename settings file"); + return; + } + settings_changed(); + tracing::debug!("saved app settings"); + } + + pub fn export_to_json(&self) -> Result { + serde_json::to_string_pretty(self).map_err(|e| format!("export failed: {}", e)) + } + + pub fn import_from_json(json: &str) -> Result { + let settings: Self = + serde_json::from_str(json).map_err(|e| format!("invalid settings JSON: {}", e))?; + settings.sampling.save(); + settings.save(); + Ok(settings) + } +} + /// Model configuration exposed to the frontend. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] @@ -289,3 +397,66 @@ pub async fn reset_sampling_config( tracing::info!("sampling config reset to defaults"); Ok(cfg.clone()) } + +// ─── Unified App Settings ──────────────────────────────────────────────────── + +/// Get the current app settings. +#[tauri::command] +pub fn get_app_settings() -> AppSettings { + AppSettings::load_or_default() +} + +/// Update app settings and persist to disk. +#[tauri::command] +pub fn update_app_settings(settings: AppSettings) -> AppSettings { + settings.save(); + tracing::info!( + active_model = ?settings.active_model_key, + theme = %settings.theme, + allowed_paths = settings.allowed_paths.len(), + "app settings updated" + ); + settings +} + +/// Add an allowed path to settings. +#[tauri::command] +pub fn add_allowed_path(path: String) -> AppSettings { + let mut settings = AppSettings::load_or_default(); + if !settings.allowed_paths.contains(&path) { + settings.allowed_paths.push(path.clone()); + settings.save(); + tracing::info!(path = %path, "allowed path added"); + } + settings +} + +/// Remove an allowed path from settings. +#[tauri::command] +pub fn remove_allowed_path(path: String) -> AppSettings { + let mut settings = AppSettings::load_or_default(); + let path_clone = path.clone(); + settings.allowed_paths.retain(|p| p != &path); + settings.save(); + tracing::info!(path = %path_clone, "allowed path removed"); + settings +} + +/// Export settings to JSON string. +#[tauri::command] +pub fn export_settings() -> Result { + let settings = AppSettings::load_or_default(); + settings.export_to_json() +} + +/// Import settings from JSON string. +#[tauri::command] +pub fn import_settings(json: String) -> Result { + AppSettings::import_from_json(&json) +} + +/// Check if settings have changed since last check (for file watching). +#[tauri::command] +pub fn poll_settings_changed() -> bool { + has_settings_changed() +} diff --git a/examples/localcowork/src-tauri/src/lib.rs b/examples/localcowork/src-tauri/src/lib.rs index b8c1de0..c3d303c 100644 --- a/examples/localcowork/src-tauri/src/lib.rs +++ b/examples/localcowork/src-tauri/src/lib.rs @@ -34,6 +34,11 @@ pub(crate) fn data_dir() -> std::path::PathBuf { .join(".localcowork") } +/// Returns the cache directory for the app (embedding indexes, etc.). +pub(crate) fn cache_dir() -> std::path::PathBuf { + data_dir().join("cache") +} + /// Initialize the tracing subscriber — writes structured logs to the app data directory. /// /// On each app startup: @@ -608,6 +613,13 @@ pub fn run() { commands::settings::get_sampling_config, commands::settings::update_sampling_config, commands::settings::reset_sampling_config, + commands::settings::get_app_settings, + commands::settings::update_app_settings, + commands::settings::add_allowed_path, + commands::settings::remove_allowed_path, + commands::settings::export_settings, + commands::settings::import_settings, + commands::settings::poll_settings_changed, commands::hardware::detect_hardware, commands::model_download::download_model, commands::model_download::verify_model,