What's Changed in v3.0.4
Bug-fix release tackling eight open issues reported against v3.0.3. Focused on download correctness, webui responsiveness, storage flexibility, and analyzer coverage for new quant formats. Every fix ships with regression tests; go test -race ./... is now fully green across every package.
Bug Fixes
-
Resumable downloads actually resume now (#70) —
downloadSingleand eachdownloadMultipartpart goroutine now open.partfiles withO_RDWR|O_CREATE(no truncate), stat the existing size, and issueRange: bytes=pos-endrequests from that offset. Interrupted downloads resume correctly on rerun, and within-process retries after a flaky-connection cut no longer lose bytes. Previously every Ctrl+C silently re-fetched from zero and single-file downloads literally had no resume code path at all. -
Unsloth Dynamic quant variants correctly labeled (#72) — the GGUF quant regex now supports
Q{2..8}_K_XL/_XXLsuffixes andIQ1_S/IQ1_M. Previously on repos likeunsloth/Qwen3-30B-A3B-GGUFthe regex silently collapsed sixUD-Q*_K_XLfiles intoQ2_K..Q8_Klabels that collided with the real plain-K quants, and missed the two IQ1 files entirely. All 25 GGUF files in that repo now label correctly. Added quality/description map entries forQ2_K_L,Q2..8_K_XL,IQ1_S/M,IQ2_M,IQ3_XXS. -
Multimodal GGUF repos auto-bundle the vision encoder (#76) —
analyzeGGUFnow partitions.gguffiles into LLM quants vsmmprojvision encoders. Avision_encoderSelectableItem is emitted as Recommended by default, so the recommended CLI command for a multimodal repo becomes e.g.-F q4_k_m,mmproj-f16forunsloth/gemma-3-4b-it-GGUF. Downloading a single quant now pulls in the matching projector automatically, matching LM Studio's behavior. -
Progress no longer oscillates on slow/flaky connections (#75) — the multipart progress ticker now stops cleanly before assembly via an explicit done channel + WaitGroup, and an explicit final full-size reading is emitted after all parts complete. Root cause: the ticker kept stating part files while assembly was deleting them, emitting
downloaded=0events that made the UI appear stuck at 2.4% ↔ 2.5% for hours on slow links. Combined with the #70 fix above, flaky downloads of multi-GB files now converge cleanly. -
Pause no longer claims the file is 100% done — fixed a regression in the #75 tail path where
downloadMultiparton cancellation would fall past itserrChdrain (cancelled goroutines return silently viasleepCtxwithout pushing errors) and emit a bogusdownloaded == totalevent followed by assembly over an incomplete part set — corrupting the final file and deleting the very partial bytes the next resume was supposed to continue from. Now bails out withctx.Err()immediately after the ticker shutdown. Regression test inTestDownloadMultipart_CancelMidStreamDoesNotClaim100. -
Web UI no longer floods the browser with updates (#62) — two-layer fix:
- Server-side 250ms per-job WebSocket broadcast coalescer in front of
BroadcastJob. Progress events arriving inside the window collapse to a single flush of the latest state; terminal status changes (completed/failed/cancelled/paused) bypass the gate so pause/cancel transitions still feel instant. - Frontend
renderJobsno longer doescontainer.innerHTML = jobs.map(...).join('')on every tick. Per-job DOM elements are cached and updated in place — progress bar width and stats text change, but the card node, the action buttons, and their event listeners stay stable. Hover states persist and Pause/Cancel buttons are clickable during an active download.
- Server-side 250ms per-job WebSocket broadcast coalescer in front of
-
Dismissed jobs stay dismissed across refresh (#68) — new
POST /api/jobs/{id}/dismissendpoint permanently removes a terminal-state job from the manager. FrontenddismissJobnow calls the server before removing from local state, so page reloads and WebSocket reconnects don't repopulate it. Attempts to dismiss queued or running jobs return 409. The primary per-file-deletion ask in the same issue is tracked separately. -
JobManagerreturns snapshots — data race fixed —CreateJob,GetJob,ListJobs, and the internal WebSocket broadcast path all now return/forward clonedJobsnapshots via a newcloneJobLockedhelper. Previously the HTTP JSON encoder and the WS broadcaster would readJobfields whilerunJobwas mutating them on a separate goroutine.go test -race ./...is now fully green across every package for the first time.
Features
-
--local-dir <path>CLI flag (#71, #73) — new flag mirroringhuggingface-cli download --local-dir. Downloads real files into the chosen directory instead of the HF cache's blobs+symlinks layout. Right choice for feeding weights to llama.cpp / ollama, Windows users without Developer Mode, and NFS/SMB/USB transfers. Equivalent to the existing--legacy -o <path>form — both spellings are permanent and interchangeable, and--legacyis no longer marked for removal. -
Installer defaults to
~/.local/bin— no more sudo prompt (#69) — the one-linerbash <(curl -sSL https://g.bodaay.io/hfd) installnow picks a user-local install path in this order:~/.local/binif already inPATH~/binif already inPATH/usr/local/binif writable- Fallback to
~/.local/binwith a printedexport PATH=line
Explicit targets likeinstall /usr/local/binstill work and still usesudowhere needed. Root users still get/usr/local/binby default.
Documentation
- README now has a prominent Storage Modes section documenting both HF-cache-default and flat-file
--local-dirmodes as first-class, permanent options with when-to-use-which guidance. docs/CLI.mdanddocs/V3_FEATURES.mdupdated to reflect the un-deprecated--legacy/--outputflags and the new--local-dirspelling.
Test Infrastructure
TestAPI_Healthno longer pins to a stale hardcoded version string.TestJobManager_CreateJobno longer races itsTempDircleanup against in-flightrunJobgoroutines — newJobManager.WaitAll(timeout)lets the test block on actual goroutine exit before cleanup runs.
Full Changelog: v3.0.3...v3.0.4