* feat(setup): first-run install gate — nothing installs until the user confirms a plan
New `setup` module parks first runs in BootstrapStage::AwaitingSetup instead
of auto-installing. complete_setup validates the user's InstallPlan and only
then starts the existing bootstrap:
- install modes: installed (platform dirs) / portable (one folder next to
the exe / AppImage, config.json travels with it)
- user-chosen storage: env dir, data dir (OMNIVOICE_DATA_DIR), model cache
(OMNIVOICE_CACHE_DIR) — None = legacy default, byte-identical behavior
- minimum-space gate: per-volume free-space check (fs4 statvfs), grouped by
filesystem so dirs sharing a disk sum their requirements; install refused
when short (9 GiB env + 7 GiB models + 1 GiB data, measured + headroom)
- custom mirrors (PyPI index, HF endpoint, python-build-standalone) take
precedence over region presets in the venv/sync/backend env wiring
- ROCm torch variant selectable via config (env var still wins)
- existing installs migrate silently: venv present → setup_complete=true,
no questions re-asked; dev trees skip the gate entirely
19 unit tests (disk probing, space grouping, mirror validation, legacy
config compat).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): first-run setup screen — mode, storage with space gate, mirrors, compute
FirstRunSetup renders when the Rust side reports awaiting_setup (lazy-loaded;
regular launches pay nothing). One screen, defaults all work:
- language picker first (rest re-renders translated), 21 locales shipped
- Installed / Portable mode cards (portable disabled with reason when the
exe-adjacent folder isn't writable)
- storage rows with live per-path free-space probes (debounced
check_install_target), 'needs ~X / Y free' readouts, folder pickers
- client mirrors the Rust per-volume space gate: Start installation is
disabled with an explicit reason until every volume fits
- compute (CUDA-auto / ROCm), update channel, region + custom mirror URLs
- complete_setup errors surface inline; on success the normal bootstrap
progress UI takes over on the next status poll
Verified on a wiped machine: gate parks (no spawn, no downloads), screen
renders, 450 GB ≥ 17 GB requirement → Start enabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): studio-console redesign of the first-run screen
The setup screen now reads as powering on studio hardware rather than a web
form — true to a voice studio, and self-sufficient offline (every font and
asset is bundled; a first run may be on a restricted network):
- breathing waveform masthead (CSS-only, deterministic speech-cadence
silhouette, staggered per-bar delays)
- Source Serif 4 display headline + engraved IBM Plex Mono panel labels +
Inter body — the three faces the app already ships
- rack-unit panels with corner screws, engraved title rules, serial plate
(OVS · vX.Y.Z)
- disk space as segmented LED capacity meters: lit = what the install
consumes, alarm-blink red on insufficient volumes
- mode cards with indicator LEDs; 'armed' Start button — LED lights and a
halo pulses only once every volume passes the space gate
- atmosphere: corner accent glows + SVG film grain; staggered rise-in
choreography on load
- all motion transform/opacity only; prefers-reduced-motion holds every
frame still; theme-token derived colors; focus-visible rings throughout
No logic changes: same IPC calls, same i18n keys, same space-gate math.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): wide desktop deck, hardware-aware Compute + Update channel cards
Three pieces of feedback addressed:
- width: the console is now a 1240px two-column deck (storage rail left,
decision rail right) that uses desktop real estate; collapses to one
column under 980px and stacks fully under 620px
- no outer chassis box: panels float directly on the atmospheric backdrop,
each carrying its own rack-unit treatment
- Compute and Update channel split into separate cards with real
information: get_setup_state now detects hardware (nvidia-smi → CUDA
name, /sys/class/drm vendor 0x1002 → AMD/ROCm, Apple Silicon → MPS,
CPU cores + RAM via sysinfo; best-effort, never blocks) — the Compute
card shows a live 'Detected: …' readout, badges the option that matches
the machine, and pre-selects ROCm on AMD boxes; both cards use LED
radio options with full descriptions (6 new i18n keys × 21 locales)
Also pins playwright-core as an explicit devDep — bun did not materialize
it through @playwright/test, breaking programmatic browser use.
20/20 Rust tests · vite build · CJK guard green. Verified live (gate
engaged, responsive single-column) and at 1600×1000 via mocked-IPC
browser shot (two-column deck).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): move network (region + mirrors) into the masthead with language
Language and download region are the two 'where am I' choices — they now
sit together top-right of the masthead, with the custom-mirrors disclosure
tucked beneath the subtitle. The Network panel is gone, leaving a balanced
deck: Install mode + Storage left, Compute + Update channel right.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): strip the boxes — fills and rules carry the structure
One design rule now: borders only where state demands them. Panels lose
their boxes entirely (engraved mono title + rule separates sections);
option cards, storage rows, selects/inputs, the hw readout, the version
plate and the ghost buttons are all flat fills; active options glow with
an accent tint + LED; blocked rows and errors use a red tint + 2px inset
edge bar instead of a border. The badge chip is fill-only too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): quiet pass — every element earns its visual weight
- waveform becomes a whisper: 22px trace, 2px bars, ~half opacity — an
ambient signature instead of a billboard
- storage readouts collapse to one mono line ('needs ~9 GB · 449 GB free');
the LED meter now appears only when it carries information (install
would consume >35% of free space, or the volume is blocked) — at 449 GB
free a bar was a meaningless sliver
- Change… buttons go text-quiet (transparent until hover)
- custom-mirrors disclosure right-aligns under the region select it
extends, instead of floating under the subtitle
- version plate moves to the footer next to the disk total — the masthead
keeps only title, subtitle, and the two locale/region selects
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): platform-matrix awareness — distro+arch detection, ROCm gated to Linux, no Windows console flash
The install matrix is OS family × distro × arch × GPU vendor, and the
setup screen now both shows it and only offers choices valid for it:
- HardwareInfo gains os_name (distro PRETTY_NAME from /etc/os-release on
Linux, macOS/Windows elsewhere) and arch (x86_64/aarch64) — the detected
line reads 'CachyOS x86_64 · NVIDIA RTX 4070 · 32×CPU · 31 GB RAM',
exactly what bug reports cite
- SetupState gains os; the ROCm option renders on Linux only (wheels
don't exist elsewhere) and complete_setup clamps rocm→auto on
non-Linux as the server-side backstop
- nvidia-smi probe gets CREATE_NO_WINDOW on Windows — no cmd flash on
the first screen a user ever sees
- Apple Silicon → MPS, Intel mac → CPU, ARM Linux → CPU: all matrix
cells resolve through the same base constructor
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): unify the whole first-run journey under the studio-console system
Setup → Installing → Model wizard now read as one continuous experience:
the same atmosphere, whisper waveform masthead, serif/mono type, LED
language and quiet fills across all three acts.
- Installing (BootstrapSplash): rebuilt in frs-* — segmented LED journey
meter (completed steps + live byte progress), LED step rail (done=green,
active=pulsing accent, pending=dim), engraved ACTIVITY panel with the
quiet mono log (collapse/copy as text-quiet actions), failure act with
red-tint error + hints + armed Retry. All logic untouched: stage poll,
event subscription + backfill, dedupe, hints, region/language selects.
- Model wizard (SetupWizard): same masthead with the step rail as engraved
mono LED steps top-right, welcome cards as option-card surfaces,
preflight as LED check rows (pass/warn/fail), frs nav buttons with armed
primaries, embedded Model Store / Engines / Dictation panels scroll
inside the act. Old 556-line stylesheet replaced by ~60 lines of glue;
BootstrapSplash.css reduced to a resolving stub.
- FirstRunSetup.css is now the journey's shared design system (step rails,
log panel, banners, hints, wizard chrome, check rows appended).
- 2 new strings (Installing / Activity) translated across all 21 locales.
Validated end-to-end on this machine: setup screen → Start installation →
real venv bootstrap (~10 min) → backend healthy on 3900 → model wizard.
20/20 Rust tests · vite build · CJK guard green · installing act verified
via mocked-IPC screenshot at stage=installing_deps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): --setup re-entry flag + make the install-plan screen un-stealable
The setup stage is first-run-only by design (completed installs skip it),
but it must be reachable on demand and must actually win the mount when
engaged. Three fixes:
- 'omnivoice-studio --setup' parks the bootstrap in AwaitingSetup on any
launch — checked before the attach-to-healthy-backend shortcut, so a
running backend can't skip past it
- App routing: awaiting_setup now outranks everything (a live backend
answering /setup/status used to route straight to the model wizard);
the wizard additionally requires stage === 'ready' so it can't mount
during the initial stage race
- useBootstrapStage: a transient IPC miss no longer permanently declares
'ready' (which killed the poll loop and silently skipped the setup /
progress screens) — it retries up to 5 ticks before conceding
Plus journey-wide titlebar clearance (content never sits under the GTK
headerbar / macOS traffic lights / Windows controls) and drag-region
mastheads on all three acts.
Verified: mocked-IPC harness with stage=awaiting_setup + a LIVE backend
answering /setup/status renders the setup screen, not the wizard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(setup): remove backdrop decoration — flat surface, state-only emphasis
The corner accent glows and SVG film grain rendered as visible banding /
noise artifacts on many panels — both gone; the journey now sits on a
clean flat chrome background. Also swept the remaining decorative bloom:
the active option card drops its glow shadow (flat accent tint + LED carry
the state), and the armed Start button loses its pulsing halo (the lit LED
already signals actionable). Remaining shadows are functional micro-detail
only: 6px LED glows, meter track inset, red edge bars.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): journey rail + verbosity diet — clean, smooth, elegant
The setup page is now visibly stage 1 of the install flow: a quiet
breadcrumb rail (SETUP → INSTALLING → MODELS & ENGINES) sits between the
waveform and the headline on both the setup and installing acts, LEDs
marking done/active/pending — one continuous story across the journey.
Verbosity halved without hiding information:
- option descriptions unfold (260ms ease) only on the selected card; the
page shows exactly one explanation per group, collapsed cards keep the
text as a tooltip
- storage rows drop their always-on caption (label + path + readout +
Change… on one line; caption lives in the row tooltip)
The whole page now fits a laptop window without scrolling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): merge Models + Engines into one wizard act
Two tabs weren't necessary: models are the required gate, engines the
optional extras — now two stacked panels in a single 'Models & engines'
step (label reuses the journey-rail key, translated in 21 locales).
Wizard shrinks to 4 steps: Welcome → System check → Models & engines →
Dictation. Continue still gates on models_ready only; engines stay
optional. Welcome cards updated to the 3 remaining acts; static cards
keep their descriptions visible (the active-only fold is for radios).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(setup): wizard was skipped after first-run install — probe /setup/status on bootstrap ready
The models-needed probe started at mount with a ~30s retry ceiling. On a
first run, mount happens at the setup page — by the time the user reads
it and the multi-minute install finishes, the attempts were long burned,
so setupChecked landed as 'no wizard needed' and the studio rendered with
zero models on disk. The probe is now keyed on bootstrapStage and runs
when it hits 'ready' — the first moment a backend exists to answer.
Normal launches (backend up quickly) behave exactly as before.
Caught by running the full journey three times end-to-end: rounds 2–3
skipped Models & engines after install; with the fix the wizard mounts
with models_ready=false (Whisper large-v3 listed missing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): drop the Welcome step — wizard opens on System check
The welcome act had nothing left to say: the journey rail names the
stages, the setup page already oriented the user, and the cards repeated
both. The wizard is now three steps — System check (auto-runs on mount) →
Models & engines → Try dictation — landing the user directly on live
preflight results instead of a page about the pages to come.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): true unified library — models + engines as ONE list
'Merge them' meant one list, not two panels stacked — fair criticism.
The wizard's Models & engines act is now a purpose-built WizardLibrary:
every installable is a row of the same grammar (LED · name · chip ·
size · action):
- required models lead (REQUIRED chip, Download action, live SSE
progress bar + percent, green LED when installed) — they gate continue
- TTS engines follow (ENGINE chip): active engine glows accent,
available ones offer one-click Use (selectEngine), heavy installs
defer honestly to Settings ('install later in Settings' + reason
tooltip)
- the optional-model tail folds behind 'Show N optional models'
The full management surface (search, HF token, deletes, sorting) stays
in Settings — a first run needs a checklist, not a store. 9 new strings
× 21 locales. Verified against the live backend via the browser harness:
required/installed/engine/active/Use/defer states all render in one list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(diagnostics): local-first self-check, error journal, and bug-report pipeline (#296)
* feat(diagnostics): local self-check + scrubbed bug-report pipeline
Closes the gap between 'something broke' and 'a useful GitHub issue
exists' — entirely within the local-first constraint: the only outbound
path remains the user's own browser opening a prefilled issues/new URL.
Backend:
- core/scrub.py: privacy scrubber for anything leaving the machine —
env-var secret values (*TOKEN*|*KEY*|*SECRET*|*PASSWORD*), credential
shapes (hf_/ghp_/github_pat_/sk-), home dirs on all three OSes
- core/diagnose.py: 9-check self-check (device+GPU, ffmpeg, HF token,
disk, data-dir writability, RAM, engine registry, hub reachability),
pre-scrubbed, ASCII-safe output
- GET /system/diagnose + 'python main.py --diagnose' (exit 0/1)
- /system/info: hardware inventory (os_version, cpu_model, cpu_count,
ram_total_gb, gpu_name, vram_total_gb, disk_free_gb), cached statics
Frontend:
- utils/bugReport.js: single source for the prefilled-URL builder —
scrubText twin, hardware context capture, scrubbed error+stack embed,
URL-length cap; ReportBugButton refactored onto it
- ErrorBoundary 'Report this bug' action with the error attached
- utils/errorToast.jsx toastErrorWithReport(); wired into export toasts
- Settings > About 'Run self-check' with per-check status badges
Tests: 27 pytest (scrub, diagnose) + 15 vitest (bugReport); existing
suites green; verified live (--diagnose, TestClient, vite build).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(diagnostics): error journal, diagnostic bundle, crash notice, global handlers
Second slice of the bug-tracking work — still zero outbound paths beyond
the user's own browser/file manager.
- core/error_journal.py: deduped ring of recent unhandled backend errors
(fingerprint counts, error_class triage: GPU_OOM, HF_AUTH_FAILED,
PYANNOTE_LICENSE_REQUIRED, DISK_FULL, FFMPEG_MISSING, NETWORK_ERROR),
scrubbed, JSONL-persisted so the error that killed the last run survives
restart. Wired into the global exception handler; 500 bodies now carry
error_class; GET /system/errors/recent.
- core/diagnostic_bundle.py + POST /system/diagnostic-bundle + Settings >
About 'Save diagnostic bundle': zip of self-check report, error journal,
scrubbed log tails — drag onto a GitHub issue; bypasses the ~8k
prefill-URL ceiling.
- crash-on-next-launch: /system/notifications flags a crash logged before
this session started (size vs acked-size in prefs, mtime vs process
start); POST /system/crash/ack; LogsFooter acks on action click.
- utils/globalErrorHandlers.js: uncaught errors + unhandled rejections get
a throttled, noise-filtered 'Report this bug' toast.
- sidecar log parity fix: _tauri_log_candidates() now lists the Rust
sidecar's backend.log/backend_err.log on Linux (XDG state dir) and
Windows (LOCALAPPDATA) — sidecar crashes were only visible on macOS.
Tests: +19 pytest (journal, bundle); suite at 102 passed. Vitest 124
passed; vite build green. Live-verified: journal recorded and classified
a real HF 401 from the test run (HF_AUTH_FAILED, paths scrubbed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(diagnostics): breadcrumbs, deep self-check, report sweep, issue search
Final slice of the bug-tracking work.
- toastErrorWithReport adopted at the high-traffic failure sites: TTS
generation, dub upload/ingest/transcribe, engine install, engines-matrix
load, voice profile save/delete/test, batch enqueue/cancel/delete.
Validation toasts and cancellations stay plain on purpose.
- utils/breadcrumbs.js: local-only ring of the last 20 action names
(closed-set names only — never content or paths), embedded as a
'Recent actions' section in the prefilled report. Instrumented: view
changes, generate, dub pipeline, export, engine switch.
- deep self-check: /system/diagnose?deep=true and --diagnose --deep load
the active engine and synthesize a short utterance (num_step=4) —
catches 'installed but broken'. 180s time-box, skips during model load,
scrubbed failure detail. Verified live: cold-loaded omnivoice and
produced 2.2s of audio in 43.9s on CUDA.
- 'Search similar issues' action on the ErrorBoundary: scrubbed,
noise-stripped GitHub issue search URL — dedupe before filing.
- bug_report.md template now points at the diagnostic bundle and the
--diagnose CLI so manual reports arrive with the same evidence.
Tests: pytest 107 passed (4 new deep-check tests, CJK gate green);
vitest 218 passed (breadcrumbs + issue-search suites); vite build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(diagnostics): self-diagnosis section in troubleshooting + README pointer
Settings > About self-check / --diagnose / --deep / diagnostic bundle are
now the documented first step before the per-error entries — and the
support team's first ask on every issue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(setup): flush sticky action bar, global dbl-click maximize, open maximized
First-run polish on the studio-console journey:
- FirstRunSetup: fixed-footer / scrollable-middle layout — mast + decision grid
live in a dedicated .frs__scroll region; the install action bar is the last
flex item, so it sits flush at the window's bottom edge and nothing (e.g. an
expanded compute-option description) can render beneath it on small windows.
- Double-click-to-maximize on the custom borderless titlebar now works on EVERY
drag region (splash, first-run, wizard, main header) via one delegated
listener in main.jsx, on all platforms; removed App.jsx's redundant inline
handler so it doesn't double-toggle. Skips interactive controls in the bar.
- Window opens maximized to the available desktop size (tauri.conf.json).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(diagnostics): quiet Bandit on the journal hash and hub probe
The journal fingerprint is a dedup key, not a security boundary —
usedforsecurity=False. The hub reachability probe gets an explicit
https scheme guard on its constant URL so the urlopen sink is audited.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(setup): address PR #295 review findings — security, lifecycle, privacy, i18n
Security:
- setup.rs valid_mirror: reject plaintext http:// mirror URLs (MITM
supply-chain path into UV_PYTHON_INSTALL_MIRROR / UV_INDEX_URL /
HF_ENDPOINT); explicit http://localhost / 127.0.0.1 / [::1] exceptions
only. Tests extended incl. loopback-lookalike hosts.
- setup.rs detect_hardware: AMD vendor ID alone no longer maps to
kind="rocm" — a cheap ROCm userspace probe (/opt/rocm or rocminfo on
PATH) gates it; bare AMD GPUs report kind="amd" so the UI offers ROCm
without pre-selecting it ("matches this machine" only when verified).
Functional:
- lib.rs/setup.rs --setup re-entry: complete_setup now kills any backend
still serving on the port before retry_bootstrap, so changed
env/mirror/layout settings actually apply instead of re-attaching.
- setup.rs: nvidia-smi probe runs behind a 3 s timeout thread — a wedged
driver degrades to CPU instead of hanging the first-run IPC.
- setup.rs: is_first_run is now a pure read; the existing-install
migration write moved to migrate_existing_install_if_needed, invoked
only from the bootstrap thread (get_setup_state no longer writes).
- setup.rs complete_setup: config save errors now abort setup and surface
in the UI instead of bootstrapping into a stale on-disk layout.
- setup.rs complete_setup: logs default-vs-custom flags instead of the
user's absolute env/data/models paths (privacy rule).
- scrub.py + bugReport.js: also redact forward-slash Windows homes
(C:/Users/<name>, file:///C:/Users/...), ordered before the macOS
pattern so "C:~" residue can't form. Tests added on both sides.
- bugReport.js: context fetches bounded by a 2.5 s AbortController
timeout so report assembly degrades to partial context instead of
hanging on a stalled backend.
- system.py: crash ack is now {size, mtime} (legacy size-only ack still
honored) and /system/logs/clear drops the ack — truncation can no
longer permanently suppress 'crash-last-session'.
- system.py: Linux Tauri-log probe honors XDG_DATA_HOME.
- setup.ts/WizardLibrary.jsx: SetupProgressEvent type now documents the
full phase taxonomy actually emitted (per-file start/progress/done +
install_*/delete_* lifecycle); reducer verified correct against the
backend stream and annotated — a file-level 'done' must not clear the
repo row.
- SetupWizard.jsx: step rail clamps to the highest unlocked step
(preflight/models gates) — no more jumping straight to "Enter studio".
Polish:
- BootstrapSplash.jsx: Waveform heights wrapped in useMemo([bars]) like
its siblings.
- BootstrapSplash.jsx: detectHints returns i18n keys (bootstrap.hint_*)
rendered through t(); translated in all 21 locales.
- SetupWizard.jsx: step rail aria-label localized (setup.step_aria /
setup.step_completed) in all 21 locales.
- FirstRunSetup.css: deprecated word-break: break-word → overflow-wrap:
anywhere; reduced-motion override also stops the frs-hw-pulse LEDs
(.frs-step.is-active LED + .swiz-lib__led--busy).
Deferred (design-level, follow-up PR): --setup re-entry round-tripping of
custom dirs/mirrors into the form (setup.rs), and worker-thread leak on
timed-out deep checks (diagnose.py).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(i18n): translate back-filled keys in all 20 locales, drop inline fallbacks
The reconciliation merge back-filled 16 new keys (about.self_check*,
about.*bundle*, dub.num_speakers_*, errors.*) with English text in
every non-English locale — CodeRabbit flagged 9 locales; fixed all 20.
Interpolation tokens preserved and asserted during the rewrite. Also
removed the two inline English fallback strings in App.jsx
(firstrun.first_sound_*) so copy lives only in locales/*.json.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: mergetest <test@local>