Closes#1845. Closes#1886.
The widget window is created always-on-top, and the setup state had no time
limit at all. On a clean macOS install the pill sat over the first-run setup
window — covering the disk-space line and the Start installation button — and
over every other application, until Accessibility was granted or the user
dismissed it by hand. There was no cap and no safety net: the stranded-pill
reconcile only runs while idle, and this state is not idle.
A permission the user has not granted yet does not outrank what they are
actually doing, and mid-setup they usually cannot grant it yet anyway. The
prompt now gets a bounded claim on the screen and then steps aside.
Polling deliberately continues after the window hides, so granting
Accessibility later still returns the widget to idle on its own — what expires
is the pill's claim on the screen, not the reconciliation. The hide is latched
so it fires once rather than fighting anything that legitimately shows the
window again; both properties have a test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
Closes#1856.
The mandatory-only install path ships no speech-to-text model, and the
dictation step rendered its three script cards regardless. Every card came up
red with "No speech-to-text model is installed", and the step's own copy
invited the user to press the hotkey or hit Replay, neither of which can
transcribe anything. That is the final screen of first-run setup, so the last
thing a new user saw was three failures they were told to cause.
The step now checks readiness the same way the component already checks for
its bundled sample WAVs, and when no model is installed it offers the model
chooser in place of the cards — the same picker the Transcriptions page uses,
so the user installs one and continues rather than reading an error three
times. A model already on disk can be selected without a download.
`checking` deliberately keeps the cards: the probe resolves in well under a
second, and flashing the install panel first would be worse than the wait.
A test pins that.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
Closes#1957.
A download failed with nothing but the OS sentence: "[WinError 448] The path
cannot be traversed because it contains an untrusted mount point". That is a
Windows rule about the VOLUME — Dev Drives, mounted VHD/ReFS volumes and
junctions into another user profile all trigger it — so retrying the same link
can never work, and the message names nothing the user can change.
Classified now, with a remedy that points at Settings → Storage and gives the
fsutil escape hatch for a folder that has to stay put. Matched on the numeric
code first, since Windows translates the sentence, with the English phrase as a
fallback. Allowlisted for context-free surfaces because it arrives through the
global 500 handler, which otherwise attaches no hint at all — and its trigger
is unmistakable, so it cannot land on an unrelated failure.
A test pins that the offending path never comes back in the payload.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
Two CI failures, both mine to fix.
The warning I added to the Colab ASR cell used \n escapes inside the notebook
JSON, and they landed as real newlines, so the cell's Python had an
unterminated string and tests/test_colab_asr_setup.py could not exec it. The
block prints line by line now, with no escapes to get wrong.
test_tauri_log_clear_reports_truncate_failure patched _tauri_log_candidates,
but #1925 moved Clear onto _tauri_plugin_log_candidates, so the patch no longer
reached the code under test and the real resolver was consulted instead. It
passed on a machine with a shell log on disk and failed on a clean runner.
Patches both halves, matching the fixture in test_tauri_log_clear.py.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
#1877 completes the zh-CN translation and drops its ratchet to zero, but the
PR never ran today's gates — it has been conflicting, so CI reported nothing —
and the file it lands does not pass tests/test_locale_parity.py.
Three things fixed here:
- Twelve keys were declared twice inside the same object (timing_concise,
autofit_quality, the plan_* set, the role_* set). Python's parser rejects a
duplicate key outright, so the whole suite errored rather than failing one
assertion. Deduped keeping the first occurrence, which is the block #1877
actually translated.
- The `player` section appeared twice: the complete new one and an older
two-key stub. JSON keeps the LAST, so the stub silently won and six keys
vanished at runtime. The stub is gone.
- `settings.hf_source_*_label` appeared twice with slightly different wording.
The file is rewritten as canonical JSON (indent 2, non-ASCII preserved), which
is byte-identical to how en.json already serialises, so the format matches the
other locales exactly. zh-CN now has zero keys missing and zero beyond en.
Also fixes the review finding on #1959: the capture route picks its engine from
a `mode` form field, not an `accurate` flag, so parametrising on `accurate`
sent a field the route ignores and ran the default fast path twice. Both
engines are exercised now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
tests/test_no_hardcoded_cjk.py fails on any non-English text outside the
translation layer, allowlist aside, and #1892 put the mirror region's Chinese
label into two Rust doc comments. The comments only quote what the UI shows, so
naming the region in English says the same thing and keeps the guard green
without widening the allowlist for a comment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
Bot review raised one blocking and several real findings across these PRs.
Each is fixed here rather than merged and followed up.
#1892 — apply_pypi_index_env() now runs for EVERY uv invocation, and with the
default region "auto" it called the UNCACHED auto_detect_region(), racing two
live network probes with a 4s timeout per uv call. On a blocked or offline
network that is a repeated multi-second stall, and it multiplies the outbound
calls a local-first app makes unasked. The probe is memoised for the life of
the process. It also now clears UV_INDEX_URL before setting it, so a stale
ambient value cannot outrank the region the user picked.
#1925 — backend.rs trimmed OMNIVOICE_LOG_DIR for its emptiness guard but built
the path from the RAW value, while the Python reader strips it. A padded value
therefore had the writer and the reader looking at different directories, which
is the divergence the PR exists to close.
#1920 — the rotation walk caught bare OSError, so a PermissionError or a real
I/O failure was swallowed and the panel silently rendered less. Only the race
the guard exists for (a file that rolled away, and on Windows the handler's own
sharing violation) is skipped now; anything else surfaces.
#1951 — the fix was right but shipped no tests and no changelog entry. Both
added, including a case pinning that the wizard preflight and the diagnostic
route the same host the same way, since they carry separate copies of the
branch.
#1923 — the cell hardcodes the CTranslate2 model, but if the cuDNN 8 step
failed the backend falls back to PyTorch Whisper and downloads a second
multi-gigabyte model. The cell now says so while the download it just spent is
still on screen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
The frame matcher required non-whitespace from line start up to the `@`,
which is right for rejecting a V8 header posing as a frame but wrong for
JSC: it labels top-level frames `global code@url`, `eval code@url` and
`module code@url`. Those are exactly the frames an injected extension
script throws from, so on WKWebView — the macOS desktop shell — and Safari
no frame matched, the origin came back unknown, and the extension's error
still offered "Report this bug". #1901 was fixed on Chromium only.
The three labels are enumerated rather than allowing spaces generally, so
the header false positive the anchoring exists for stays closed; a test
pins that. Each WebKit case uses a distinct message because shouldShow()
throttles by message text and a shared one would pass on the throttle
instead of the frame match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
#1917 adds a Learn more link to the unavailable-engine row, driven by
_ENGINE_DOCS, and a guard that every registered engine has a doc page. It was
written before audiocpp landed on main, so against today's main the guard
failed on audiocpp and the registry shape test failed on the new docs_url key
— PR-green under an older base, main-red on merge.
docs/engines/audio-cpp.md already existed; only the id-to-path mapping was
missing. The shape test now expects docs_url, with a note pointing at the
guard so the next engine added without a doc fails loudly rather than
quietly dropping its link.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
Closes#1943.
A macOS mlx-audio text-to-speech failure returned a 500 advising the user
that "the connection to the video server dropped mid-download". No video was
involved. VIDEO_DOWNLOAD_NETWORK triggers on bare phrases — "timed out",
"connection reset", "broken pipe" — so any unrelated failure carrying one
is handed a confidently wrong next step, which is worse than no hint at all.
failure._CONTEXT_FREE_HINT_CLASSES already existed for exactly this, and its
own comment names VIDEO_DOWNLOAD_NETWORK as the class that must never appear
on a stageless surface. Only append_hint honoured it; public_exception_response
took over the 500 path without carrying the rule across, and the streaming
error frame then inherited the same gap through it.
The filter now lives in public_exception_response, so every context-free
caller gets it. MODEL_CACHE_CORRUPT joins the allowlist — its trigger is a
VoiceStudio-authored sentence, no library can produce it, and the 500 handler
is the surface a corrupt cache actually reaches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
The poll loop treated any status other than "starting" as success, so a
backend that stayed alive but reported a failed startup would pass the
very test meant to catch a broken start (CodeRabbit + Greptile on #1955).
Succeed only when the ML import step is done or status is ready; fail
loudly on any other terminal status or error. Also give the child an
empty HF_HUB_CACHE so it never reads the developer's populated cache.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCDZcBpP6QQa4dzUa8z6rh
Closes#1800.
Every engine failure the taxonomy cannot classify renders one floor message,
"Generation failed. Check the selected engine and try again." The auto bug
reporter puts that message and a stack of minified bundle frames into the
issue, so unrelated faults arrive as byte-identical reports — roughly a dozen
of the open issues are that same report filed again, and none of them can be
told apart, let alone triaged.
The streaming error frame now carries the exception's TYPE NAME, the frontend
keeps it on StreamingPreviewError, and the report prints it as "Backend error
class: …". A MemoryError and a FileNotFoundError stop being the same issue.
Only the class name — no substring of the exception message is copied, so the
response-safety contract still holds and a test pins that a path in the
exception never reaches the payload. This is the same datum the dub routes
already put on the wire as error_class and the analytics allowlist already
treats as content-free.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
frontend/ is a bun workspace, so its package.json is locked by the
repo-root bun.lock. Dependabot bumped only the manifest, so
`bun install --frozen-lockfile` — which CI and deploy/Dockerfile both
run — rejected the tree and the Tests job never got past install.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
tests/test_no_literal_borders.py guards the app-wide border removal: a
`border-[var(--chrome-border…)]` renders a stray hairline the moment that
token stops resolving transparent. The picker's indent rail used one, which
failed the guard. The indent and padding already carry the nesting, so the
rail keeps its width as border-transparent and shows nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
Spawns the real backend the way the desktop shell does (containment marker
plus a piped stdin) and asserts startup gets past the ML import. On the
pre-fix watchdog it times out after 180 s; on the fix it passes in ~4 s.
Windows-only, since the deadlock is a Windows loader-lock interaction and
CI's backend job runs on Linux.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCDZcBpP6QQa4dzUa8z6rh
frontend/package.json moved but the workspace-root bun.lock did not, so
`bun install --frozen-lockfile` — what CI and deploy/Dockerfile both run —
rejected the tree. Plain `bun install` tolerates the drift, so a green local
run said nothing about it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
The Dub landing used to carry a WorkspaceProjects panel, and that panel was
the only caller of the project rename endpoint. Making the landing
history-only removed the panel and left rename unreachable from the entire
UI, while the rename route, the project list and the inline-rename CSS all
stayed. App.jsx's renameProject became an unused variable, which is what
failed CI lint — the lint error was the symptom, the lost capability was the
bug.
Projects now live only in the sidebar rail, so the affordance moves there:
inline rename on each project row, commit on Enter or Save, abandon on
Escape, empty and unchanged names ignored, and the button hidden when no
handler is wired. The orphaned WorkspaceProjects component is deleted.
Also fixes a Windows-only failure in initialLoadRetry.test.js: it took
.pathname off a file:// URL, which on Windows yields "/C:/..." and made
readFileSync resolve "C:\C:\...", so the file ENOENT'd on every Windows
checkout. Uses fileURLToPath instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
(cherry picked from commit 0a164add119335c3a150725cfe4c4ce8959c50a5)
The missing-model empty state offered exactly one action: download the
recommended Whisper Tiny. The six other catalogue models — the more accurate
English Parakeet, the 25–44 MB streaming models that show text while you
speak, the bilingual zh/en ones — were only reachable through Settings, and
a user who already had one on disk was still told to download Whisper Tiny.
The page now lists the whole sherpa-onnx catalogue grouped by the trade-off
the user is actually choosing between (best accuracy vs lowest latency),
with languages and download size on every row. Any model can be installed
in one click, an installed one can be switched to without a download, and
the progress bar names the model that was picked. If the catalogue cannot
be read the single recommended-download button remains as the fallback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
(cherry picked from commit 9c4ba2bab2a2253fbf4f82bf082db530267ddadc)
Adds the sherpa-onnx dictation model picker under the Transcription engine
row so the model the hotkey loads is switchable without opening Settings,
routes the Sherpa transcription path through that same preference, and makes
the Windows desktop dev stack recover instead of demanding Task Manager.
Refreshes the Tauri and npm dependency pins that went with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
Every backend spawned by the Windows desktop shell hung forever in the
startup worker's `import torch`, inside the loader for numpy's OpenBLAS
DLL. The desktop parent-liveness watchdog (0a20aeb0) parks a synchronous
read on the stdin pipe the shell hands the backend, and that pending read
deadlocks the DLL initializer. The identical command from a terminal, with
no stdin pipe and no watchdog, starts in seconds — which is why it only
reproduced under the app.
Bisected outside the app by spawning the backend with the shell's exact
env, pipes, creation flags and job object: a watchdog thread that merely
sleeps is harmless; a pending ReadFile, via the C runtime or straight to
the kernel, hangs it every time. Native stacks (py-spy --native) show the
watchdog in NtReadFile and the importer waiting on a critical section from
inside the OpenBLAS initializer.
Fix: on Windows the watchdog polls PeekNamedPipe and reads only bytes that
are already buffered, so no I/O is ever outstanding on the pipe. It still
exits the instant the desktop closes its end (ERROR_BROKEN_PIPE), and a
non-pipe stdin keeps the shared blocking reader. Verified: the app-style
spawn goes from an indefinite hang to ready in ~3 s, and the desktop-prod
build boots and loads the model.
Not in v0.5.1; the watchdog landed 2026-08-30 on main.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCDZcBpP6QQa4dzUa8z6rh