use crate::config::{MigrationConfig, NotificationLevel, RobinConfig, SourceOs}; use std::sync::Mutex; use tauri::{Emitter, State}; pub struct AppState { pub config: Mutex, } #[tauri::command] pub fn get_config(state: State<'_, AppState>) -> Result { state .config .lock() .map(|c| c.clone()) .map_err(|e| e.to_string()) } #[tauri::command] pub fn needs_onboarding(state: State<'_, AppState>) -> bool { state .config .lock() .map(|c| c.needs_onboarding()) .unwrap_or(true) } #[tauri::command] pub fn complete_onboarding( source_os: String, distro: String, source_distro: Option, dual_boot_with: Option, state: State<'_, AppState>, ) -> Result<(), String> { let source = match source_os.to_lowercase().as_str() { "macos" | "mac" => SourceOs::Macos, "windows" => SourceOs::Windows, "linux" => SourceOs::Linux, "android" => SourceOs::Android, "ipad" | "ios" | "ipados" => SourceOs::IpadOs, _ => SourceOs::Unknown, }; let detected = if distro == "unknown" || distro.is_empty() { crate::distro::detect() } else { distro }; let source_distro_family = source_distro.as_deref().and_then(|sd| { let family = crate::distro::distro_family(sd); if family == "unknown" { None } else { Some(family.to_string()) } }); // Normalise dual_boot_with to a canonical name; reject unrecognised values. let dual_boot_with = dual_boot_with.and_then(|s| match s.to_lowercase().as_str() { "windows" => Some("windows".to_string()), "macos" | "mac" => Some("macos".to_string()), _ => None, }); let mut config = state.config.lock().map_err(|e| e.to_string())?; config.migration = Some(MigrationConfig { source_os: source, distro: detected, source_distro_family, dual_boot_with, fluency_level: 0, }); config.save().map_err(|e| e.to_string()) } #[tauri::command] pub fn update_notification_level(level: String, state: State<'_, AppState>) -> Result<(), String> { let parsed = match level.as_str() { "off" => NotificationLevel::Off, "badge_only" => NotificationLevel::BadgeOnly, "badge_and_toast" => NotificationLevel::BadgeAndToast, _ => return Err(format!("unknown notification level: {level}")), }; let mut config = state.config.lock().map_err(|e| e.to_string())?; config.display.notification_level = parsed; config.save().map_err(|e| e.to_string()) } #[tauri::command] pub fn get_pending_events() -> Vec { crate::notify::take_pending() } #[tauri::command] pub fn panel_opened() { crate::notify::set_panel_open(true); } #[tauri::command] pub fn panel_closed() { crate::notify::set_panel_open(false); } #[tauri::command] pub async fn chat( message: String, state: State<'_, AppState>, app_handle: tauri::AppHandle, ) -> Result<(), String> { let (base_url, model, source_os, distro) = { let cfg = state.config.lock().map_err(|e| e.to_string())?; let (source_os, distro) = if let Some(ref m) = cfg.migration { let os = match m.source_os { crate::config::SourceOs::Macos => "macOS", crate::config::SourceOs::Windows => "Windows", crate::config::SourceOs::Linux => "Linux", crate::config::SourceOs::Android => "Android", crate::config::SourceOs::IpadOs => "iPad/iOS", crate::config::SourceOs::Unknown => "Unknown", }; (os.to_string(), m.distro.clone()) } else { ("Unknown".to_string(), "unknown".to_string()) }; ( cfg.ollama.base_url.clone(), cfg.ollama.model.clone(), source_os, distro, ) }; let system_prompt = crate::llm::build_system_prompt(&source_os, &distro); let messages = vec![ crate::llm::ChatMessage { role: "system".into(), content: system_prompt }, crate::llm::ChatMessage { role: "user".into(), content: message }, ]; tauri::async_runtime::spawn(async move { if let Err(e) = crate::llm::chat_stream(&base_url, &model, messages, &app_handle).await { log::error!("chat stream error: {e}"); if let Err(emit_err) = app_handle.emit("robin:chat-error", e.to_string()) { log::warn!("failed to emit robin:chat-error: {emit_err}"); } } }); Ok(()) }