//! Tauri command surface — every JS-callable function lives here. use std::path::PathBuf; use crate::paths::{list_wsl_distros, resolve_roots, ResolvedRoots}; use crate::settings::{save as save_settings, Caps, Settings}; use crate::state::SharedState; use crate::tier::{detect_in_with_diagnostics, PlanTier}; use crate::usage::{build_snapshot, UsageSnapshot}; use crate::watch::refresh_and_emit; #[tauri::command] pub async fn get_snapshot(state: tauri::State<'_, SharedState>) -> Result { let mut events = state.collect_events(); events.sort_by_key(|e| e.ts); let caps = state.settings.read().caps.clone(); Ok(build_snapshot(&events, &caps, chrono::Utc::now())) } #[tauri::command] pub async fn get_settings(state: tauri::State<'_, SharedState>) -> Result { Ok(state.settings.read().clone()) } #[tauri::command] pub async fn set_settings( state: tauri::State<'_, SharedState>, app: tauri::AppHandle, new: Settings, ) -> Result<(), String> { { *state.settings.write() = new.clone(); } save_settings(&new).map_err(|e| e.to_string())?; // If roots-related settings changed, force a re-resolve + rescan. { let mut roots = state.roots.write(); roots.clear(); } refresh_and_emit(&app, state.inner(), None) .await .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub async fn list_distros() -> Result, String> { list_wsl_distros().map_err(|e| e.to_string()) } #[tauri::command] pub async fn get_roots(state: tauri::State<'_, SharedState>) -> Result { let s = state.settings.read().clone(); Ok(resolve_roots(s.wsl_distro_override.as_deref(), s.include_native)) } #[tauri::command] pub async fn force_rescan( state: tauri::State<'_, SharedState>, app: tauri::AppHandle, ) -> Result<(), String> { // Wipe caches so every file is reparsed from offset 0. state.files.write().clear(); state.seen_ids.write().clear(); refresh_and_emit(&app, state.inner(), None) .await .map_err(|e| e.to_string()) } #[tauri::command] pub async fn quit_app(app: tauri::AppHandle) -> Result<(), String> { app.exit(0); Ok(()) } #[derive(serde::Serialize)] pub struct TierInfo { /// The classified tier — `Pro`, `Max5x`, `Max20x`, etc. pub tier: PlanTier, /// Human-readable label for the UI ("Max 5×" / "Pro" / "Unknown"). pub label: String, /// The caps that this tier maps to. The user's current Settings may /// differ if they tuned manually. pub recommended_caps: Caps, /// Diagnostic: the candidate `.claude/` directories that were probed. /// Useful for users on novel setups (multi-distro, custom $HOME, etc.). pub searched: Vec, } #[tauri::command] pub async fn detect_plan_tier( state: tauri::State<'_, SharedState>, ) -> Result { // Resolve roots fresh — `state.roots` may be empty if this command is // invoked before the cold-start refresh finishes (which IS what happens // when the user opens Settings shortly after launch). let s = state.settings.read().clone(); let resolved = resolve_roots(s.wsl_distro_override.as_deref(), s.include_native); let mut candidates: Vec = resolved .roots .iter() .filter_map(|r| r.parent().map(PathBuf::from)) .collect(); if let Some(home) = dirs::home_dir() { candidates.push(home.join(".claude")); } // Dedupe (canonicalize is unreliable on UNC paths, so compare raw). candidates.sort(); candidates.dedup(); let (tier, diagnostics) = detect_in_with_diagnostics(&candidates); let label = tier.label(); let recommended_caps = tier.caps(); Ok(TierInfo { tier, label, recommended_caps, searched: diagnostics }) }