* fix: require magic string in portable marker to prevent false portable mode
Scoop's extras bucket extracts the NSIS installer via #/dl.7z, which can
leave a stale empty portable file next to the exe. This caused
portable::init() to falsely trigger portable mode on non-portable scoop
installs, storing data in Data/ next to the exe (wiped on each update).
Changes:
- portable.rs: only enable portable mode when marker contains
"Handy Portable Mode"; extracted is_valid_portable_marker() with tests
- installer.nsi: write magic string when creating marker file
- installer.nsi: validate magic string in update auto-detect block
- installer.nsi: allow desktop shortcut creation from finish page
checkbox in portable mode (was silently doing nothing)
- commands/mod.rs + lib.rs: expose is_portable() as a Tauri command
- UpdateChecker.tsx: show manual-update popup for portable installs
instead of attempting auto-install (which would download MSI)
- en/translation.json: add i18n strings for portable update popup
Fixes#1124
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing translation keys to all locales and fix prettier formatting
Add portableUpdateTitle, portableUpdateMessage, portableUpdateButton to
all 17 non-English locales (English fallback text) so check:translations
passes. Also run prettier on the PR's modified files to fix format:check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: migrate legacy empty portable markers from v0.8.0
v0.8.0 created an empty `portable` marker file. Users who manually
update (drop new exe in place) would silently lose portable mode since
the new magic-string check rejects empty files.
Migration: if the marker exists but has invalid/empty content AND a
`Data/` directory is present, treat it as a real portable install and
rewrite the marker with the magic string. This handles the v0.8.0 →
new upgrade path without regressing the scoop false-positive fix
(scoop does not create a Data/ directory).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* save recordings before transcription
* impart cj preferences
* cleanup
* re-transcribe anything
* format
* fix tests
* format
* ui cleanup
* format
* cleanup
* bunch of fixes, mainly for vm
* put comments back
* update transcription failure text
* alert -> toast
* spawn_blocking
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* fix: add SHA256 verification to prevent corrupt partial download loop
Fixes issue #858 — corrupt .partial files caused an infinite loop where
a failed/cancelled download would resume from EOF, pass extraction, and
loop forever with no way to recover without manual file deletion.
Three-part fix:
- Add sha2 crate and compute_sha256() helper; verify hash post-download
before extraction/rename for all 15 built-in models. Hash mismatch
deletes the .partial so the next attempt starts fresh.
- Delete .partial on extraction failure (independent of checksums) so
corrupt archives can't re-enter the loop via the same path.
- Emit model-download-failed event from the command handler so all
callers (ModelsSettings, Onboarding) get consistent error feedback
via a central store listener rather than per-caller return-value checks.
SHA256 hashes hardcoded alongside each model's URL in ModelInfo.
Custom models (sha256: None) skip verification silently.
* chore: remove doc comment from sha256 field
Doc comments on exported specta types propagate into bindings.ts as
inline JSDoc — not useful in generated TS. Removing at the source.
* test: add SHA256 verification unit tests
Extracts inline verify logic into ModelManager::verify_sha256() and
covers all four branches:
- None expected hash → skipped (custom models)
- Matching hash → ok, file kept
- Mismatched hash → error returned, partial file deleted
- Unreadable file → error returned
Ensures corrupt partial downloads are always cleaned up so the next
attempt starts fresh.
* chore: cargo fmt
* fix: clear cancel flag on sha256 failure, spawn_blocking for hash, fallback cleanup
- remove cancel_flags entry on sha256 failure to prevent a stale flag
from immediately cancelling the next retry attempt (HIGH)
- add fallback state cleanup in modelStore when result.status != ok,
so spinner clears even if model-download-failed event is missed (HIGH)
- run compute_sha256 in spawn_blocking to avoid stalling the async
executor while hashing large model files (up to 1.6 GB) (MEDIUM)
* emit some more events for the ui to pick up
* add translations + format
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* refactor: migrate transcribe-rs from v0.2.8 to v0.3.0
Breaking API migration:
- engines::* → onnx::* module paths, whisper_cpp::* for Whisper
- TranscriptionEngine trait → SpeechModel trait
- Two-step new()+load_model() → one-step Model::load()
- transcribe_samples(Vec<f32>) → transcribe(&[f32])
- Explicit unload_model() → RAII drop
- Canary engine now from transcribe-rs instead of local crate
Removes canary-engine local crate dependency.
Uses path dependency to local transcribe-rs clone (temporary).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: upgrade transcribe-rs to v0.3.1, add Canary models
Update transcribe-rs dependency from v0.2.8 to v0.3.1 with
restructured features (whisper + onnx). Add Canary 180M Flash
(4 languages, 146MB) and Canary 1B v2 (25 EU languages, 692MB)
with translation support. Add i18n translations for all 17 locales.
Languages verified against HuggingFace model cards:
- Canary 180M Flash: en, de, es, fr
- Canary 1B v2: bg, hr, cs, da, nl, en, et, fi, fr, de, el, hu,
it, lv, lt, mt, pl, pt, ro, sk, sl, es, sv, ru, uk
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* make sure when changing models the language changes with it
* format
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* ci: add temporary test workflow for portable ZIP validation
* ci: fix mock setup in test workflow
* ci: remove temporary test workflow
* feat: add portable mode option to NSIS installer
Custom NSIS template based on tauri-v2.9.1 upstream with 6 targeted
modifications for portable mode support:
- New PortableMode variable and /PORTABLE CLI flag for silent installs
- Install type selection page with Normal/Portable radio buttons
- Conditional default directory (Desktop for portable, LocalAppData for normal)
- Skip registry writes, file associations, deep links, and uninstaller for portable
- Skip Start Menu and desktop shortcuts for portable installs
- Create portable marker file and Data/ directory on portable install
The existing portable.rs runtime detection is unchanged — it looks for the
same marker file this installer creates.
Usage:
GUI: select "Portable Installation" on the install type page
Silent: Handy_setup.exe /S /PORTABLE /D=C:\path\to\Handy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: implement true portable mode via source code path override
Detect a `portable` marker file next to the executable at startup.
When present, redirect all data paths (settings, models, recordings,
database, logs) to a local `Data/` directory instead of %APPDATA%.
This approach works because it intercepts path resolution at the Rust
level, bypassing Tauri/Windows Shell API limitations that made the
previous environment-variable and junction approaches unreliable.
Changes:
- Add src-tauri/src/portable.rs with OnceLock-based detection
- Replace all app.path().app_data_dir() calls with portable helper
- Configure tauri-plugin-log to use Folder target when portable
- Configure tauri-plugin-store with absolute path when portable
- Simplify build.yml portable ZIP (exe + resources + marker file)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add missing Manager trait import in portable.rs
The .path() method on AppHandle requires `tauri::Manager` in scope.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: redirect WebView2 cache to portable Data dir
Move main window creation from tauri.conf.json to setup closure so
we can set data_directory when in portable mode. This prevents
WebView2 from creating %LOCALAPPDATA%\com.pais.handy.
Also set data_directory on the recording overlay window.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: auto-detect portable mode during updates
When the Tauri updater runs the installer with /UPDATE, the install type
page is skipped and /PORTABLE is not passed. Detect the existing portable
marker file in .onInit so updates preserve portable mode automatically.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix prettier formatting in portable.rs and lib.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat: add models settings page with filtering and management
- add dedicated models settings page in sidebar
- reuse ModelCard component from onboarding for consistent UI
- add filter buttons for all/multi-language/translation models
- add model deletion with native confirmation dialog (tauri-plugin-dialog)
- consolidate model event listeners into useModels hook (Zustand store)
- remove duplicate event listeners from ModelSelector, LanguageSelector, TranslateToEnglish
- gate AccessibilityPermissions to macOS only
- add is_recommended field to model registry for featured models
- remove unused get_recommended_first_model command
- add translations for all 9 supported languages
* feat: add language filter to models settings page
adds a searchable language dropdown filter to the models page that lets users filter models by language support. when a non-english language is selected, models that don't support multiple languages (like parakeet) are hidden.
- add searchable language dropdown (right-aligned in filter row)
- filter models using supports_language_selection capability
- add allLanguages translation key to all 10 locales
* feat: add translation consistency checker script
- add scripts/check-translations.cjs to validate all language files
- script dynamically discovers languages from directory structure
- checks that all languages have same keys as english reference
- detects missing and extra keys in each language
- add check:translations npm script
- integrate into github actions lint workflow
- validates translations on every pull request
* fix: update imports and add missing translations after rebase
- replace useModels hook with useModelStore in components
- add permission error keys to all languages
- add model settings keys to cs and tr (new languages from main)
* fix: translate english placeholders in cs and tr locale files
* refactor: simplify model dropdown and migrate store to immer
- model dropdown now only shows downloaded models (no download/delete)
- convert store to immer with record types for immutability
- remove unused translation keys (welcome, downloadPrompt, etc.)
- add missing moonshine-base model fields
- sync translations after rebase
* fix: add download cancellation support and ui improvements
- add full download cancellation with Arc<AtomicBool> flags in rust backend
- add progress event throttling (100ms) to prevent ui freeze
- add cancel button to model card in settings page
- add model-deleted event listener to refresh dropdown after deletion
- remove pink background from recommended models in settings (keep badge only)
- add cancel/cancelDownload translation keys to all 14 languages
* fix: remove duplicate language/translate settings from general and advanced
settings are now only in ModelSettingsCard, not duplicated in their old locations
* fix: prevent model dropdown from being clipped by window edge
* add languages explicitly, clean up some ui
* fix: clear bottom progress bar when download is cancelled
The ModelSelector component maintains its own local state for download
progress. When a download was cancelled, the Rust backend would update
its state but never emitted an event to notify the frontend. This caused
the bottom progress bar to remain stuck showing "Downloading X%".
Added model-download-cancelled event emission in Rust and corresponding
listener in ModelSelector to clear progress state on cancellation.
* fix: align model card content to top to prevent floating elements
Changed ModelCard flex alignment from items-center to items-start so
the accuracy/speed bars stay at the top when the card expands (e.g.,
during download with progress bar visible).
* fix: prevent model deletion from interrupting active extractions
Added extracting_models HashSet to track models currently being
extracted. The update_download_status() function now skips cleanup
of .extracting directories for models that are actively extracting,
preventing a race condition where deleting one model would interrupt
another model's extraction process.
* refactor: migrate ModelCard buttons to Button component
Added two new Button variants for common patterns:
- primary-soft: soft/tinted primary buttons (used for download)
- danger-ghost: subtle destructive actions (used for delete/cancel)
Migrated all hardcoded buttons in ModelCard to use the shared Button
component for consistency and maintainability.
* feat: separate downloaded and available models into sections
Split the models list into "Your Models" and "Available to Download"
sections for clearer visual distinction between downloaded and
downloadable models.
Also adds missing translation keys to all locales:
- modelSelector.capabilities.singleLanguage
- modelSelector.capabilities.languageOnly
- settings.models.yourModels
- settings.models.availableModels
* fix: add missing translations after rebase onto main
add post-processing hotkey translations to all 15 locales and
backfill 29 missing keys for korean locale added in main.
* fix: add label to model delete button for clearer destructive state
* check translations as typescript
* format
* better text for dropdown
* wip ui tweaks
* rounded + ui tweaks
* fix download not 0% immediately, ui tweaks
* tweak name
* block for model downloading in onboarding
* small fixes
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* Add permission step to onboarding
* translations
* minor ui
* Update AccessibilityOnboarding.tsx
* claude suggestions
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
defer apple intelligence availability check from app initialization
to when the user actually tries to use the feature. accessing
SystemLanguageModel.default during early app startup causes SIGABRT
on macOS 26.x (tahoe) beta.
- remove availability check from default_post_process_providers()
- always include apple intelligence provider on macos arm64
- add checkAppleIntelligenceAvailable command for lazy checking
- show error message if apple intelligence unavailable when selected
- clear error on any dropdown selection for better UX
* fix: prevent model auto-switch from interrupting active transcription
when a model download completes during an active recording/transcription,
the app now skips auto-switching to prevent data loss.
adds is_recording tauri command to check recording state before
auto-switching models on download or extraction completion.
fixes#418
* Update bindings.ts
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* initial specta commands
* refactor to use the bindings in the ui
* merge fixes
* remove unused commands
* add clamshell to lib
* Remove Invoke from everything.
* fix settings not being loaded properly
* fix settings being overwritten due to deserialization failure + remove
provider kind
* fix model loading text bug
* feat: add more options for deleting recordings automatically and added button to open the folder that olds the recordings
- deleting automatically options: after certain time, never or preserving certain number of recordings
- new button "Open Recordings Folder" located on History page
* refactor: moved "Delete Recordings" selector to Debug settings
* refactor: simplify opening recordings folder logic by using tauri's opener plugin
* clean up ui and code a bit
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat: add fallback microphone for clamshell/desktop mode
* refactor: moved "Closed Laptop Microphone" setting to Debug
* cleanup
* run format
* add check back
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* Add custom sound and control notif volume
* Modularize the picker and slider, consolidate the playback and loading
* Move custom sounds to advanced
* pairs of audio
* custom sound handling
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* Add history limit setting
Dearest Reviewer,
I have added a history limit setting. The limit change is applied instantly.
If the limit is 0 then no recordings are saved.
Becker
* remove extra save
save happens on exit
* move to debug menu
* use the text color
---------
Co-authored-by: CJ Pais <cj@cjpais.com>