* fix: surface paste errors as UI toast notification (#522)
When pasting the transcription fails (e.g. wtype/xdotool/dotool not
available or returns an error on Linux), the error was silently logged
to the backend console only. Users had no idea why their text was not
pasted.
Emit a new 'paste-error' Tauri event from the Rust side and listen for
it in the frontend App component, showing a sonner toast with the error
detail — mirroring the existing 'recording-error' pattern exactly.
Files changed:
- src-tauri/src/actions.rs: add PasteErrorEvent struct, emit event on paste failure
- src/App.tsx: add useEffect listener that shows toast.error on paste-error
- src/lib/types/events.ts: add PasteErrorEvent interface
- src/i18n/locales/en/translation.json: add pasteFailedTitle/pasteFailed keys
* chore: apply cargo fmt and prettier formatting
* add translationss
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
* perf: add reasoning_effort passthrough to avoid thinking-mode latency
Reasoning models (Gemma 4, Qwen 3, etc.) default to thinking mode,
adding 10-40x latency for simple transcript cleanup. Pass through the
OpenAI-compatible reasoning_effort parameter so users can disable
thinking when speed matters more than deep reasoning.
- Add optional reasoning_effort field to ChatCompletionRequest
- Thread through send_chat_completion / send_chat_completion_with_schema
- New set_post_process_reasoning_effort Tauri command for persistence
- Dropdown in post-processing settings, Custom provider only
- i18n for en and ja
* refactor: simplify reasoning_effort to on/off toggle
Addresses review feedback on the reasoning_effort passthrough:
post-processing rarely benefits from reasoning, so a 5-way dropdown
(default/none/low/medium/high) is overkill. Collapse to a single
boolean toggle "Disable reasoning" that defaults to ON for Custom
providers (sends reasoning_effort: "none"). Cloud providers continue
to receive no reasoning_effort parameter.
- Replace post_process_reasoning_effort: Option<String> with
post_process_disable_reasoning: bool (default true) in settings
- Map bool to Option<String> at the request site (true -> "none",
false -> omit) so the API contract is unchanged
- Rename Tauri command to set_post_process_disable_reasoning(disable)
so parameter naming matches the setting and avoids inversion bugs
- Swap the Dropdown for a ToggleSwitch in the Custom provider section
- Collapse five i18n strings to one label + description, and add the
translation to all 18 additional locales so non-en/ja users don't
fall back to English for this control
* refactor: remove disable-reasoning toggle, always disable for custom provider
Per PR #1221 discussion with @cjpais: since this is an alpha feature,
simplify the UI by always sending reasoning_effort: "none" for custom
provider post-processing. If this causes issues with specific provider
setups, we can revisit.
* disable for openrouter too
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
Fixes#1184.
The tray icon was created and refreshed without setting a tooltip, which caused Windows to show an empty hover tooltip.
This change sets the tooltip when building the tray icon and refreshes it during tray menu updates using the existing version label.
* fix: don't log cloud provider keys
* test: make sure keys are redacted
* change to newtype
* change secret struct to public
* test: test secretmap directly
* Update bindings.ts
---------
Co-authored-by: Shaan <shaankhosla@macbook-pro.mynetworksettings.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
* audio_toolkit: remove long repeating words
I've noticed a couple of times that Paraket v3 may produce
repeats of words that are not folded by Handy so I propose
to lift up word length limitation.
* Update text.rs
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
Add a visible feature freeze section near the top of CONTRIBUTING.md
so contributors see it before starting work. Fix "GATEHRED" typo in
the PR template.
* 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>
sign-binaries: false passes empty strings for TAURI_SIGNING_PRIVATE_KEY
and APPLE_CERTIFICATE, but Tauri treats a set-but-empty env var as a
signing attempt, causing all builds to fail. Use sign-binaries: true to
match pr-test-build.yml — secrets are already configured in the repo.
* test build for 0.16.0
* macos 10.15?
* format
* shorter dir for build on windows?
* Update build.yml
* move
* Update build.yml
* long path support
* clang
* try
* try
* move to ort rc12
* use ms prebuilt?
* use lower version for 1.23.1
* download my prebuilt
* Update build.yml
* Update build.yml
* fix?
* fix(nix): use dynamic linking for ort-sys 2.0.0-rc.12
ort-sys rc.12 removed pkg-config support; without ORT_PREFER_DYNAMIC_LINK
it defaults to static linking against ORT_LIB_LOCATION, which fails
because nixpkgs only provides shared libraries (.so).
* fix(nix): override onnxruntime to 1.24.2 for ort-sys rc.12 compatibility
ort 2.0.0-rc.12 enables API v24 by default, but nixpkgs only ships
onnxruntime 1.23.2 (API v23), causing a runtime panic on model load.
Use Microsoft's prebuilt binaries for onnxruntime 1.24.2 via overlay,
patched with autoPatchelfHook for NixOS compatibility.
This overlay should be removed once nixpkgs merges onnxruntime ≥ 1.24:
https://github.com/NixOS/nixpkgs/pull/499389
* updated deps for whisper
* fix(nix): sync devShell with packages for ort-sys rc.12 compatibility
- Extract shared definitions (commonNativeDeps, gstPlugins, commonEnv,
onnxruntimeOverlay) to avoid duplication between packages and devShells
- Add onnxruntime 1.24.2 overlay to devShell (was only in packages)
- Add BINDGEN_EXTRA_CLANG_ARGS to devShell so bindgen can find stdio.h
and generate correct vulkan bindings for whisper-rs-sys
- Add ORT_LIB_LOCATION and ORT_PREFER_DYNAMIC_LINK to devShell
- Replace libappindicator with libayatana-appindicator in devShell
- Add onnxruntime and vulkan-loader to devShell LD_LIBRARY_PATH
* fix missing lib in the handy blob dl
* scope lib verification
* dont run bun2nix on windows
---------
Co-authored-by: Evgeny <evgeny.khudoba@yandex.ru>
* ci: add full build and quality checks on push to main
Adds a new main-build.yml workflow that runs the full 7-platform build
matrix on every push to main, uploading artifacts for 30 days. Also
adds push-to-main triggers to code-quality, test, and nix-check
workflows. The nix full build now always runs on main pushes (not just
when nix packaging files change), catching the expensive breakage that
currently requires manual verification before each release.
* ci: fix permissions and clarify nix full-build intent
- narrow contents: write to contents: read in main-build.yml — no
release assets are uploaded here so write access is unnecessary
- update nix-check comment to reflect that the full build always runs
on push to main (not just when nix files change), matching intent
* 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>
* feat(audio): use device default sample rate and always downsample
instead of forcing the microphone to open at 16kHz (which can cause
issues with bluetooth codecs, some ALSA drivers, and other devices
that advertise 16kHz support but produce suboptimal audio), use the
device's native/default sample rate and let the existing FrameResampler
downsample to 16kHz for the whisper pipeline.
the resampling infrastructure (rubato FftFixedIn) already exists in
run_consumer() and short-circuits when in_hz == out_hz, so devices
that natively default to 16kHz are unaffected.
* fix: graceful fallback when supported_input_configs fails
some ALSA/PipeWire drivers support default_input_config but have
a broken supported_input_configs implementation. fall back to the
default config instead of propagating the error. also add a warn
log when no config matches the device's default rate.