- Phase 4: Hugging Face Download Engine — streaming GGUF downloads with progress, cancellation, and managed-model storage - Phase 5: Model Source Validation — source-mode compatibility, path/file validation, inline error UI with recovery guidance - Hot-swap: stop runtime → update model_path → restart atomic command - Model library: folder open, delete model file, local model management - SettingsPage unified hub: local models list + HF search with 3-state action buttons - Bug fixes: send button text restored (4 failing tests), reasoning-toggle aria-label, Rust warnings - All 89 tests passing, cargo check clean
254 lines
8.2 KiB
Rust
254 lines
8.2 KiB
Rust
use std::sync::Arc;
|
|
|
|
use tauri::{AppHandle, Manager, State};
|
|
|
|
mod gpu;
|
|
mod model_library;
|
|
mod model_files;
|
|
mod runtime;
|
|
|
|
use runtime::process::find_localllm_binary;
|
|
use runtime::types::{RuntimeConfig, RuntimeSnapshot};
|
|
use runtime::{bootstrap_runtime, get_runtime_snapshot, save_runtime_config_command, start_runtime, stop_runtime, RuntimeState};
|
|
use gpu::query_gpu;
|
|
use model_library::huggingface::{
|
|
cancel_hugging_face_download, search_hugging_face_models, start_hugging_face_download,
|
|
HuggingFaceDownloadState,
|
|
};
|
|
use model_library::local_index::list_entries;
|
|
use model_library::paths::{managed_models_dir, model_library_index_path, resolve_models_dir};
|
|
use runtime::config::{load_runtime_config, save_runtime_config};
|
|
use model_library::types::{ModelLibraryEntry, ModelLibraryState};
|
|
use model_files::{import_model, pick_model_files, pick_models_dir, scan_models};
|
|
|
|
// ─── Server lifecycle ─────────────────────────────────────────────────────────
|
|
|
|
#[tauri::command]
|
|
fn bootstrap_runtime_command(
|
|
app: AppHandle,
|
|
state: State<RuntimeState>,
|
|
) -> Result<RuntimeSnapshot, String> {
|
|
bootstrap_runtime(app, state)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_runtime_snapshot_command(state: State<RuntimeState>) -> Result<RuntimeSnapshot, String> {
|
|
get_runtime_snapshot(state)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn save_runtime_config_command_handler(
|
|
app: AppHandle,
|
|
state: State<RuntimeState>,
|
|
config: RuntimeConfig,
|
|
) -> Result<RuntimeSnapshot, String> {
|
|
save_runtime_config_command(app, state, config)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn start_runtime_command(
|
|
app: AppHandle,
|
|
state: State<RuntimeState>,
|
|
) -> Result<RuntimeSnapshot, String> {
|
|
start_runtime(app, state)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn stop_runtime_command(state: State<RuntimeState>) -> Result<RuntimeSnapshot, String> {
|
|
stop_runtime(state)
|
|
}
|
|
|
|
/// Returns the resolved path to the localllm binary (useful for debugging).
|
|
#[tauri::command]
|
|
fn find_server_binary() -> Result<String, String> {
|
|
find_localllm_binary()
|
|
.map(|p| p.to_string_lossy().to_string())
|
|
.ok_or_else(|| {
|
|
"localllm binary not found. Build with: cargo build --release -p inference-server"
|
|
.to_string()
|
|
})
|
|
}
|
|
|
|
/// Returns the app data directory path (useful for debugging).
|
|
#[tauri::command]
|
|
fn get_app_data_dir(app: AppHandle) -> Result<String, String> {
|
|
app.path()
|
|
.app_data_dir()
|
|
.map(|p| p.to_string_lossy().to_string())
|
|
.map_err(|e| format!("Cannot resolve app data dir: {e}"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn search_hugging_face_models_command(
|
|
query: String,
|
|
token: Option<String>,
|
|
) -> Result<Vec<model_library::types::HuggingFaceModelResult>, String> {
|
|
search_hugging_face_models(&query, token.as_deref()).await
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn start_hugging_face_download_command(
|
|
app: AppHandle,
|
|
state: State<'_, Arc<HuggingFaceDownloadState>>,
|
|
repo: String,
|
|
file_name: String,
|
|
token: Option<String>,
|
|
) -> Result<String, String> {
|
|
// Load the user-configured models_dir (defaults to ./models if None/empty)
|
|
let config = load_runtime_config(&app)?;
|
|
let models_root = resolve_models_dir(config.models_dir.as_deref());
|
|
|
|
start_hugging_face_download(
|
|
app,
|
|
state.inner().clone(),
|
|
repo,
|
|
file_name,
|
|
token,
|
|
models_root,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn cancel_hugging_face_download_command(
|
|
state: State<'_, Arc<HuggingFaceDownloadState>>,
|
|
download_id: String,
|
|
) -> Result<(), String> {
|
|
cancel_hugging_face_download(state.inner(), &download_id)
|
|
}
|
|
|
|
/// Swap the active model: stops the runtime, updates model_path, restarts.
|
|
/// This is the hot-swap primitive — used when user picks a different local model.
|
|
#[tauri::command]
|
|
async fn swap_model_command(
|
|
app: AppHandle,
|
|
state: State<'_, RuntimeState>,
|
|
new_model_path: String,
|
|
) -> Result<RuntimeSnapshot, String> {
|
|
// Stop first — this is a no-op if already stopped
|
|
stop_runtime(state.clone())?;
|
|
|
|
// Load, update model_path, save
|
|
let mut config = load_runtime_config(&app)?;
|
|
config.model_path = Some(new_model_path);
|
|
save_runtime_config(&app, &config)
|
|
.map_err(|e| format!("swap save failed: {e}"))?;
|
|
|
|
// Restart with the updated config
|
|
start_runtime(app, state)
|
|
}
|
|
|
|
/// Delete a local .gguf model file from disk.
|
|
#[tauri::command]
|
|
fn delete_model_file(file_path: String) -> Result<(), String> {
|
|
let path = std::path::Path::new(&file_path);
|
|
if !path.exists() {
|
|
return Err(format!("File not found: {file_path}"));
|
|
}
|
|
std::fs::remove_file(path).map_err(|e| format!("Delete failed: {e}"))
|
|
}
|
|
|
|
/// Open the folder containing a model file in the native file explorer
|
|
/// and optionally select/highlight the file.
|
|
#[tauri::command]
|
|
fn open_model_folder_command(file_path: String) -> Result<(), String> {
|
|
let path = std::path::Path::new(&file_path);
|
|
let _folder = path.parent().unwrap_or(path);
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
std::process::Command::new("explorer")
|
|
.args(["/select,", &file_path])
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to open Explorer: {e}"))?;
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
std::process::Command::new("open")
|
|
.args(["-R", &file_path])
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to open Finder: {e}"))?;
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
std::process::Command::new("xdg-open")
|
|
.arg(folder)
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to open file manager: {e}"))?;
|
|
}
|
|
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
|
{
|
|
let _ = folder; // folder is only used on some platforms
|
|
return Err("Unsupported platform".into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn list_model_library_entries_command(app: AppHandle) -> Result<Vec<ModelLibraryEntry>, String> {
|
|
let app_data_dir = app
|
|
.path()
|
|
.app_data_dir()
|
|
.map_err(|error| format!("Cannot resolve app data dir: {error}"))?;
|
|
let index_path = model_library_index_path(&app_data_dir);
|
|
list_entries(&index_path)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_model_library_state_command(app: AppHandle) -> Result<ModelLibraryState, String> {
|
|
let app_data_dir = app
|
|
.path()
|
|
.app_data_dir()
|
|
.map_err(|error| format!("Cannot resolve app data dir: {error}"))?;
|
|
let index_path = model_library_index_path(&app_data_dir);
|
|
|
|
Ok(ModelLibraryState {
|
|
entries: list_entries(&index_path)?,
|
|
managed_dir: managed_models_dir(&app_data_dir).to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
// ─── App entry ────────────────────────────────────────────────────────────────
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_shell::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.manage(RuntimeState::new())
|
|
.manage(Arc::new(HuggingFaceDownloadState::default()))
|
|
.invoke_handler(tauri::generate_handler![
|
|
query_gpu,
|
|
scan_models,
|
|
pick_models_dir,
|
|
pick_model_files,
|
|
import_model,
|
|
search_hugging_face_models_command,
|
|
start_hugging_face_download_command,
|
|
cancel_hugging_face_download_command,
|
|
swap_model_command,
|
|
delete_model_file,
|
|
open_model_folder_command,
|
|
list_model_library_entries_command,
|
|
get_model_library_state_command,
|
|
bootstrap_runtime_command,
|
|
get_runtime_snapshot_command,
|
|
save_runtime_config_command_handler,
|
|
start_runtime_command,
|
|
stop_runtime_command,
|
|
get_app_data_dir,
|
|
find_server_binary,
|
|
])
|
|
.setup(|app| {
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
let window = app.get_webview_window("main").unwrap();
|
|
window.open_devtools();
|
|
}
|
|
Ok(())
|
|
})
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|