- 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
5.5 KiB
Phase 4: Hugging Face Download Engine
Context
After completing the unified runtime workspace (Phase 3), the next priority was implementing the Hugging Face download engine. The HF search UI was already functional, but download buttons used a simulated setInterval progress animation with no actual file transfer.
Architecture decision: Downloads run in Rust (not JavaScript) to avoid IPC bottlenecks for multi-GB GGUF files. Progress is emitted via Tauri v2 events and consumed by a React hook.
Key constraints:
- Tauri v2 + React 18 + TypeScript + Vite
- No
@tauri-apps/plugin-fsconfigured; onlyshellplugin active - Existing HF search already uses
reqwestwith optional bearer token - Managed models directory already resolved via
model_library::paths::managed_models_dir
Implementation Summary
Rust Backend (src-tauri)
model_library/huggingface.rs — Full download engine:
HuggingFaceDownloadStatemanages active downloads withAtomicU64IDs andMutex<HashMap>cancellation flagsstart_hugging_face_download()streams bytes viareqwest+tokio::fs::Fileto a.parttemp file- Emits Tauri events:
started, throttledprogress(~500ms),completed,cancelled,error cancel_hugging_face_download()sets cancellation flag, removes temp file- Path validation rejects empty,
.,.., and backslash-containing segments - Direct resolve URL:
https://huggingface.co/{repo}/resolve/main/{fileName} - Bearer token applied when available
- Temp file renamed to final destination on success
lib.rs — Command registration:
start_hugging_face_download_commandandcancel_hugging_face_download_commandregistered ininvoke_handler- Shared
Arc<HuggingFaceDownloadState>managed via.manage()
model_library/types.rs — Serializable payloads:
HuggingFaceDownloadPhaseenum (Started, Progress, Completed, Cancelled, Error)HuggingFaceDownloadProgressstruct with camelCase JSON fields
Cargo.toml — Added tokio with fs, io-util, sync, time features
Frontend (tauri-app/src)
features/runtime/types.ts — TypeScript types:
HuggingFaceDownloadPhaseunion typeHuggingFaceDownloadProgressinterface matching Rust payload
features/runtime/api.ts — API bridge:
startHuggingFaceDownload(repo, fileName, token)→invoke("start_hugging_face_download_command")cancelHuggingFaceDownload(downloadId)→invoke("cancel_hugging_face_download_command")HUGGING_FACE_DOWNLOAD_EVENTconstant for event listening
features/runtime/useModelDownload.ts — React hook:
useModelDownload(token: string)returnsisDownloading,progress,speed,downloaded,total,activeFile,error,startDownload(),cancelDownload()- Listens to Tauri events, tracks active download ID, ignores stale events
- Explicit
ModelDownloadSnapshotinterface for correct TypeScript typing
features/settings/SettingsPage.tsx — UI integration:
- Removed simulated
setIntervaldownload state - Imported
useModelDownloadhook .sp-hf-fileclass added to each HF file row- Active file row switches to real progress-bar state when downloading
- Shows real
speed,downloaded,total,progressvalues - Cancel button in active progress state
- Download buttons have visible focus rings and disabled state during active downloads
Tests
features/runtime/useModelDownload.test.tsx — Hook unit tests:
- Verifies
startDownload()invokes correct Tauri command with{ repo, fileName, token } - Verifies progress/speed/downloaded/total update from events
- Verifies stale events are ignored
- Verifies
cancelDownload()invokes cancel command with activedownloadId
features/settings/SettingsPage.download.test.tsx — Integration test:
- Mocks HF search results and
useModelDownload - Verifies
.sp-hf-filerow renders and clicking download callsstartDownload("microsoft/phi-4", "quantized/phi-4-q4.gguf")
model_library/huggingface.rs tests — Rust unit tests:
- Direct resolve URL construction
- MiB conversion
- Progress percentage calculation
- Path validation (valid and invalid segments)
Verification
- ✅
npm run build— passes - ✅
npx tsc --noEmit— passes (0 errors) - ✅
npm test -- useModelDownload.test.tsx SettingsPage.download.test.tsx— 3/3 tests pass - ✅
cargo test hugging_face— passes - ⚠️ 6 pre-existing unrelated test failures in
App.test.tsx,ChatView.test.tsx,MessageBubble.test.tsx(not caused by this phase) - ⚠️ 1 pre-existing Rust warning:
save_indexunused inlocal_index.rs(not caused by this phase)
Files Modified
| File | Change |
|---|---|
tauri-app/src-tauri/Cargo.toml |
Added tokio dependency |
tauri-app/src-tauri/src/model_library/types.rs |
Added download phase enum + progress struct |
tauri-app/src-tauri/src/model_library/huggingface.rs |
Full download engine + tests |
tauri-app/src-tauri/src/lib.rs |
Added download commands + managed state |
tauri-app/src/features/runtime/types.ts |
Added download types |
tauri-app/src/features/runtime/api.ts |
Added download API helpers |
tauri-app/src/features/runtime/useModelDownload.ts |
New React hook |
tauri-app/src/features/runtime/useModelDownload.test.tsx |
Hook unit tests |
tauri-app/src/features/settings/SettingsPage.tsx |
Replaced simulation with real download |
tauri-app/src/features/settings/SettingsPage.download.test.tsx |
Integration test |
.planning/ROADMAP.md |
Realigned to HF download milestone |
.planning/STATE.md |
Updated to Phase 4 complete |