* 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>
* increase indicator width to fit German translation
* use text-end for proper alignment of Arabic translation
* use ms-4 instead of ml-4 to fix score bar alignment for Arabic
* use ms-auto to fix download button alignment for Arabic
Fixes#1109
* 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>
* feat(audio): opt-in lazy stream close for bluetooth mic latency
keep the microphone stream open for 30s after recording stops to
reduce latency on back-to-back transcriptions. gated behind an
experimental setting (off by default) since it keeps the mic actively
capturing while idle — degrading bluetooth audio quality on macOS.
adds lazy_stream_close setting, tauri command, toggle in experimental
settings section, and i18n strings.
* fix: gate lazy close on setting in cancel_recording path
cancel_recording was unconditionally calling schedule_lazy_close,
keeping the mic open for 30s even when the setting was disabled.
also bump close_generation on AlwaysOn→OnDemand switch to cancel
any stale lazy close timers.
* feat(audio): opt-in lazy stream close for bluetooth mic latency
keep the microphone stream open for 30s after recording stops to
reduce latency on back-to-back transcriptions. gated behind an
experimental setting (off by default) since it keeps the mic actively
capturing while idle — degrading bluetooth audio quality on macOS.
adds lazy_stream_close setting, tauri command, toggle in experimental
settings section, and i18n strings.
* fix: gate lazy close on setting in cancel_recording path
cancel_recording was unconditionally calling schedule_lazy_close,
keeping the mic open for 30s even when the setting was disabled.
also bump close_generation on AlwaysOn→OnDemand switch to cancel
any stale lazy close timers.
* chore: fix cargo fmt on model.rs line from main merge
* fix race
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* fix: prevent idle watcher from unloading model during recording
the idle watcher treats ModelUnloadTimeout::Immediately as a 0s
timeout, causing idle_ms > 0 to always be true. this unloads the
model on the next 10s tick — even while the user is still recording.
the Immediately variant is already handled correctly by
maybe_unload_immediately() which fires after each transcription
completes. the idle watcher should skip it entirely.
fixes regression introduced in d1da935.
* fix race condition
* cleanup
* format
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* first pass at gpu acceleration
* unload the model when changing
* mock accelerator
* translations and build fix
* format
* clean up helptext
* type cleanup
* format
* lint
* 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>
* fix: show toast notification when model fails to load with ORT error
The model loading failure (e.g. ORT error) was only shown as a small
red dot with truncated text in the footer. Now it also triggers a
toast notification, matching how recording errors are already handled.
https://claude.ai/code/session_015VfQgoDkUDV6wk63RHrwEs
* fix: add missing i18n keys for model load error toast to all locales
https://claude.ai/code/session_015VfQgoDkUDV6wk63RHrwEs
* fix: simplify model load error toast and add proper translations
Remove the unnecessary modelLoadFailed passthrough key that just
forwarded a variable. Use the error string directly with a translated
fallback. Add proper translations for all 16 locales.
https://claude.ai/code/session_015VfQgoDkUDV6wk63RHrwEs
* fix: translate recordingFailed string in all locales
This was previously left untranslated (English) in all non-English
locale files.
https://claude.ai/code/session_015VfQgoDkUDV6wk63RHrwEs
* cleanup
* format
---------
Co-authored-by: Claude <noreply@anthropic.com>
* ui: improve scrollbar UI with custom colors and rounded thumb
* ui: increase scrollbar thickness for better visibility
* ui: refine scrollbar styling from primary pink to text/15 color and add custom buttons for scrollbar
* tweaks
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* Handle microphone start failures without aborting
Return recorder init errors instead of panicking/aborting in the worker thread and reset UI state when recording start fails.
Refs #436
* prop errors to ui
* format
* Add missing recording error translation keys
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* Remove filler words that conflict with European languages
Removed three filler words from the transcription filter that are actual
words in European languages:
- 'um' - Portuguese/Spanish indefinite article meaning 'a/an' (masculine)
Example: Portuguese 'Isto é um teste' (This is a test)
Removing this was breaking Portuguese transcriptions
- 'ha' - Spanish/Italian/Norwegian/Swedish auxiliary verb meaning 'has/have'
Example: Spanish 'Él ha comido' (He has eaten)
This is a very common verb form in Romance and Scandinavian languages
- 'eh' - Italian interjection and Canadian English discourse marker
While less critical, this can appear in legitimate Italian speech
The remaining filler words are primarily English
vocalized hesitations that don't conflict with common words in other
European languages.
Updated tests to use 'uhm' instead of 'um' where needed.
Fixes#941
* customize list
---------
Co-authored-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
* feat: add GigaAM v3 model for Russian speech recognition
Add GigaAM v3 e2e_ctc as a new transcription engine using
transcribe-rs 0.2.7 gigaam feature. Russian speech recognition
with punctuation, Latin characters and digit support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cargo fmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Keep the file name of the model download the same as the file on the
blob website.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* fix: auto-refresh model list when switching post-processing providers
Clear cached model options and auto-fetch available models when the user
switches providers or changes the Custom provider's base URL. Prevents
stale model values from silently causing 404s at runtime.
* fix: address PR review feedback
- Fix race condition in updatePostProcessBaseUrl: persist base_url
before clearing models so a failed write doesn't wipe the model
- Fix indentation in handleProviderSelect callback body
- Add guard before auto-fetching models: skip when provider has no
API key or Custom has no base_url to avoid unnecessary errors
* fix: check result status before clearing models, run Prettier
- Check changePostProcessBaseUrlSetting and changePostProcessModelSetting
result status before proceeding; bail on error to avoid partial state
- Clear cached model options only after both backend writes succeed
- Run Prettier to fix line-length violations (dependency array, destructuring)
* feat: implement structured outputs for Cerebras, OpenRouter, OpenAI, and Apple Intelligence
- Add structured output support with JSON schema in llm_client.rs
- Update actions.rs to use system prompt + user content approach
- Remove legacy ${output} variable substitution for supported providers
- Update Apple Intelligence Swift code to accept system prompts
- All providers now use consistent structured output format
- Remove duplicate check_apple_intelligence_availability function
* wip changes
* fix(structured-outputs): address PR #706 review comments
- Add settings migration to sync supports_structured_output field for existing providers
- Fix fallback behavior: structured output failures now fall through to legacy mode
- Clone api_key to prevent ownership issues in fallback path
- Clean up build_system_prompt(): remove placeholder entirely
(instead of replacing with 'the user's message' which reads awkwardly)
- Add warn import from log crate
* refactor(structured-outputs): apply best practice improvements
- Optimize settings migration: use single match instead of double iteration
- Add TRANSCRIPTION_FIELD constant to replace magic strings
- Keep Apple Intelligence behavior unchanged (no API fallback for privacy)
Addresses code review feedback on PR #706:
1. More efficient provider lookup in ensure_post_process_defaults()
2. Eliminates hardcoded 'transcription' string in JSON parsing
3. Maintains privacy-first approach for Apple Intelligence
* fix groq output
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat(i18n): add Traditional Chinese (zh-TW) locale
* style(i18n): clarify zh as Simplified Chinese and format languages.ts
* tweak
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat(linux): Add option to select typing tool
- Added a Linux-only typing tool setting shown for direct paste, with a new dropdown in advanced settings.
- Wired new typing tool setting through settings store, bindings, and Tauri commands.
- Added typing tool enum/defaults and selection handling in Linux clipboard direct typing.
- Added translations for the new typing tool setting across locales.
* fix compilation on macos
* format
* only list available tools
* format
---------
Co-authored-by: Thinh Vu <phuocthinhvu@gmail.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat: add configurable auto-submit after transcription paste
Add an Advanced output setting that can send Enter, Ctrl+Enter, or Cmd+Enter after text insertion so Handy works smoothly with different editor and agent submit shortcuts. This keeps the behavior opt-in, persists it in settings, and adds focused Rust tests for defaults and paste auto-submit decision logic.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: simplify auto submit configuration UX
Replace the auto-submit toggle and key dropdown with a single dropdown that includes an Off state, so output behavior is clearer and faster to configure. Clarify Meta-key behavior by showing Cmd+Enter on macOS and Super+Enter on Windows/Linux, with matching localized copy.
Co-authored-by: Cursor <cursoragent@cursor.com>
* use bindings generated on macos
* and translations.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* feat: add show tray icon setting
Allow hiding system tray icon via Settings > Advanced.
When hidden, closing window quits app instead of hiding.
* i8n
---------
Co-authored-by: CJ Pais <cj@cjpais.com>