Opt-in watch folder on the batch queue: pick a directory once and new videos are auto-enqueued with the last Add-to-queue settings, with pause/stop controls and copy-in-progress protection. Files stream to the loopback backend as bytes; paths never leave the app. Also gives the batch queue a reachable UI entry point and streams multipart uploads to disk. Maintainer fix: the watched directory handle is opened with full share mode on Windows so users can rename or delete the folder while it is watched, matching macOS/Linux behaviour, with a cross-platform regression test. Thanks @mvanhorn!
Synchronize VoiceStudio release metadata, lockfiles, installers, container references, documentation, and the dated v0.5.2 changelog after all planned fixes landed.
Prepare the tested main branch for the v0.5.1 patch release with synchronized version sources, mirrors, lockfiles, release notes, and install guidance.
Closes#1687.
* chore: bump to 0.5.0
Owner-requested minor bump. package.json is the source of truth; the
three mirrors (Cargo.toml, pyproject.toml, _FALLBACK_VERSION), the three
lockfiles and the branding pin move in lockstep, and the accumulated
Unreleased section becomes the curated 0.5.0 release notes — quiet
Highlights first, one-liner subsections after, duplicates folded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: fresh README for the VoiceStudio era; 0.5.0 notes wear the release
README: 551 lines from 656 — a What's-new-in-0.5.0 section with real
captures, the engine tables corrected to the actual 16 TTS registrations,
a stale Settings path and a broken Colab link fixed, roadmap/FAQ/credits
trimmed to what earns its place.
Release notes: the quick-switch GIF and catalogue/gallery screenshots,
captured from the running app during the pre-bump test pass, embedded
after the Highlights; #1542's gallery work and the ffmpeg CI fallback
recorded in their subsections.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: the feature inventory's Remote Model Downloads mention survives the README trim
check-docs-drift requires every docs/features.yaml name verbatim in the
README; the overhaul folded the phrase away. It now lives in the remote
workers feature line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: one blockquote, and an architecture claim that survives the opt-ins
CodeRabbit on #1540: MD028 blank line inside adjacent blockquotes, and
'every layer is on your machine' contradicted the opt-in remote paths
documented two sections away — it now states local-by-default with the
opt-ins named.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Close unauthenticated remote mutation paths and keep filesystem destinations behind one-shot Tauri capabilities. CodeRabbit and Greptile findings were fixed on-branch; all review threads are resolved. Full CI, Security, Rust, and cross-platform smoke checks are green.
Version files (package.json + the three mirrors) and both lockfiles bumped
0.4.1 → 0.4.2; [Unreleased] renamed to [0.4.2] — 2026-07-28.
Ships five user-facing fixes: the update toast, the restart guard that used
to discard in-flight work, the shutdown-is-not-a-crash 503, the self-healing
half-downloaded model, and the dub-history purge ordering — plus the
"Dismiss" mistranslation in five locales.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* release: v0.4.1
Seven user-reported issues fixed since v0.4.0 (#1221–#1229). Version bumped
across the single source of truth (frontend/package.json) and its three
toolchain mirrors; [Unreleased] renamed to the release section that
release.yml extracts verbatim as the GitHub Release body.
Docker tag examples in docs/install/docker.md and deploy/dockerhub-overview.md
updated to 0.4.1 (docs-sync rule).
* release: #1239 review — sync Cargo.lock to 0.4.1
Greptile: the manifest said 0.4.1 while Cargo.lock still recorded 0.4.0, so a
`cargo build --locked` (and the Tauri bundler's own locked build) would fail
on the mismatch. Regenerating locally updated it but it was never staged.
* release: re-sync [0.4.1] after the fix merges, date it 2026-07-27
Picks up everything merged since the section was first written: the first-run
wizard chrome (#1241), the MCP host allowlist (#1249), the macOS 12 startup
crash (#1245), and the six error-message fixes (#1247, #1251, #1254, #1256,
#1257, #1262).
Deliberately NOT included:
- the dub delete-resurrection fix (#1252, #1253) — split to #1270 after it
needed six rounds of correction, the last two finding that the fix did not
close the reported case and that its own bound reintroduced it;
- the Linux AppImage WebKit fix (#1258, #1244) — held on #1265 pending
confirmation on a Mesa 26.1 host, which nobody has run.
Bump the single source of truth (frontend/package.json) and the three
toolchain mirrors (Cargo.toml, pyproject.toml, backend/core/version.py) plus
the lockfiles (Cargo.lock, uv.lock) from 0.3.22 → 0.4.0, and rename the
CHANGELOG's [Unreleased] section to [0.4.0] with milestone highlights.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dubbing release. package.json (source of truth) + the three mirrors
(Cargo.toml, pyproject.toml, version.py fallback) to 0.3.22; uv.lock +
Cargo.lock refreshed; CHANGELOG's Unreleased section (29 entries) becomes
## [0.3.22] — 2026-07-14 with the headline, split Added blocks merged.
Gates on the frozen content, all green before any version mutation:
backend 3033 + 204, frontend 1253, format, lockstep 6/6.
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
package.json (single source of truth) + the three toolchain mirrors to 0.3.21;
Cargo.lock + uv.lock regenerated (version lines only). CHANGELOG [Unreleased]
renamed to [0.3.21] — 2026-07-12, "the memory release", sections merged into
house style (one Added, one Fixed).
Ships the 16 GB OOM class fixes end to end: idle-release the dictation ASR
(#1104) + one TTS engine resident at a time (#1105), plus the scoped
Settings → Storage reset/uninstall pair (#1089/#1099/#1100) and the release-
asset-split workflow fix (#1106) so this tag uploads to a single release.
test_app_version.py lockstep: 6 passed. bun install --frozen-lockfile: clean.
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(settings): factory reset gets scopes — preferences, settings, assets, everything
Factory reset did exactly one thing: clear localStorage. The only other option
was "Remove all data", which deletes the Python env and quits. Between "forget
my theme" and "wipe the machine" sat every reset a user actually needs — drop a
corrupt model download, remove a wedged sidecar engine, put the settings back
without losing a single voice — and none of them existed.
Settings → Storage → "Reset & remove" now offers four tiers (UI preferences /
all settings / downloaded assets & models / everything OmniVoice did) plus a
per-scope checklist. Every scope shows its real on-disk size, and the number on
the confirm button is exactly what gets freed.
Why the shell and not the backend: a loaded model memory-maps its weights out of
the HF cache (locked on Windows while mapped), and ensure_dirs() runs at import,
so a backend cannot delete voices/ or outputs/ and still write to them. reset.rs
stops the backend, deletes, and starts it again — and that restart is also the
repair: the fresh process re-runs ensure_dirs() and alembic, so a removed
database comes back empty rather than missing. retry_bootstrap's respawn path is
extracted to bootstrap::respawn_backend so both callers share one implementation.
Deliberate scope choices:
- "Everything" stops short of the managed Python env, so a reset hands back a
working app on the first-run screen. The env is the uninstaller's business.
- A settings reset keeps the storage locations (config.json, the user env file).
Clearing the model-cache pointer would strand gigabytes at a path the app no
longer looks in — install shape is not a preference.
- content deletes the DB with the media: rows without files is how you get a
library full of broken entries.
- The shared HF cache is flagged as shared only when it IS — computed, so Windows
and portable installs (app-private cache) get no caveat they don't need.
Safety: nothing is removed unless it sits inside a validated root — one carrying
an OmniVoice-owned path component OR holding an OmniVoice signature file, which
is what lets a custom data dir on an external volume be cleared while a mis-set
data_dir: "/" is refused. Voices/projects/audio need the word typed.
9 Rust tests (guard, scope composition, shared-cache computation) + 14 frontend
(planning purity, typed confirm, disk-vs-frontend split, shared warning).
Border utilities follow the design guard (tests/test_no_literal_borders.py).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(settings): give the Storage panels a design — proportional sizes, live totals, real tokens
"Remove all data" listed four folders as a flat run of text: a 7.5 GB model cache
and a 391-byte config file rendered at identical visual weight, so the one thing
worth seeing — where the space actually went — was the one thing you couldn't.
And the 391 B folder said "0 KB", which reads as "nothing here".
- New shared StorageTargetRow, used by BOTH destructive panels so they read as
one system: icon, label, dimmed path (truncated, full text on hover), size, and
a proportional bar showing that row's share of what will be freed. Unticked
rows claim none of the bar — the bars must sum to what the button promises.
- The shared HF cache moves OUT of the confirm dialog into its own "Optional"
row with the checkbox and the caveat in the list. Ticking it now moves the
running total in front of the user, instead of springing a different number on
them at the point of no return. The dialog lists exactly what is going.
- One byte formatter for both panels (settings/bytes.js). models/format.fmtBytes
floors at kilobytes, hence "0 KB"; it stays where it is for the model store.
Real fix underneath: three of the tokens these panels styled with DO NOT EXIST
(--chrome-fg-subtle, --chrome-bg-raised, --color-warning). An undefined var()
makes the declaration invalid, the browser drops it, and the element silently
inherits — which is why the paths that were meant to recede rendered at full body
weight. That is a whole class of bug that fails invisibly, so it gets a guard:
src/test/cssTokens.test.js fails on any var(--token) in JSX not defined in a
stylesheet, with runtime-injected tokens (Radix, inline-style hues) allowlisted
by reason. Six pre-existing offenders elsewhere in the app are recorded as
known-broken and ratcheted so the list can only shrink — they are real bugs, but
each is a visual change that wants its own review.
Frontend suite 1196 → 1205 (6 UninstallPanel component tests incl. the live
total and the bar proportions; 3 token-guard tests, verified fail-before).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: green CI + finish the token sweep + snapshot the panels
Three things on top of the redesign:
1. CI was red on tests/probe/test_probe_i18n.py — removing the eight dead
`factory_reset_*` keys from en.json orphaned them in all 20 other locales
(the probe forbids a non-en key absent from en). Removed them everywhere.
This guard scans locales at pytest time; a frontend-only run never sees it.
2. Finished the undefined-token sweep instead of grandfathering it. Six bare
`var(--token)` references resolved to nothing; the only genuinely undefined,
fallback-less one in shipping panels was `--chrome-input-bg` (input fields
AND progress-bar tracks AND skeletons across StoragePanel, StorageUsagePanel,
HistoryRetentionPanel, ModelStoreTab — tracks were rendering with no
background at all). Repointed to --chrome-hover-bg. The rest
(--chrome-menu-bg, --chrome-bg-inset, --border, --input-bg, --muted) already
carry `var(--x, fallback)`, which is valid CSS. So cssTokens.test.js now
checks only the BARE form and ships with zero exceptions — no known-broken
ratchet, because there is nothing left broken.
3. Registered both Storage panels in the visual-regression harness (a Tauri
`invoke` stub added to providers.jsx alongside the existing fetch stub) and
committed baselines across all three themes. This is how I actually looked at
the redesign: the bars render proportional (the 720 KB voices row fills, the
391 B row is a sliver), the shared-cache row sits in its own Optional group,
and every token now resolves in default/midnight/catppuccin. `_forceAdvanced`
on ResetPanel opens the checklist for the snapshot; no effect on the toggle.
Full backend suite 2897 passed (incl. the i18n probe). Frontend 1205.
* style: oxfmt the new panels and specs
Format-check is a CI gate (oxfmt --check); the new files weren't run through
oxfmt --write. No behavior change.
* chore: stop tracking the node_modules symlink
A worktree-local symlink slipped past .gitignore (which lists node_modules/ —
the directory form — so it never matched the symlink file). Removed from the
index; the symlink stays on disk for local test runs.
---------
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
package.json (single source of truth) + the three toolchain mirrors to 0.3.20;
Cargo.lock + uv.lock regenerated (version lines only). CHANGELOG [Unreleased]
renamed to [0.3.20] — 2026-07-12 with the release headline, sections merged into
house style (one Added, one Fixed).
Ships the #1101 stale-"ready" race fix (0.3.19 users are hitting it today), the
in-app uninstaller (#1099), and the Linux/Windows backend-log-dir fix.
test_app_version.py lockstep: 6 passed. bun install --frozen-lockfile: clean.
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
package.json (single source of truth) + the three toolchain mirrors
(Cargo.toml, pyproject.toml, core/version.py) to 0.3.19; Cargo.lock + uv.lock
regenerated (version lines only). CHANGELOG [Unreleased] renamed to
[0.3.19] — 2026-07-12 with the release headline; all six entries carry their
(#PR) refs and extract cleanly as the GitHub Release body.
Rebuilt on current main so the freeze covers everything that landed after the
first cut: #1088 (streaming preview), #1097 (uninstaller), #1098 (stream-drop
honesty), plus the backfilled #1088 changelog entry.
test_app_version.py lockstep: 6 passed. bun install --frozen-lockfile: clean.
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
19-issue GitHub sweep: 16 PRs merged since v0.3.11, most fixing reports
filed same-day or in the days prior. Highlights: MLX-Audio's 6 other
curated models are finally selectable (was silently stuck on Kokoro
regardless of what was downloaded), first-run no longer dead-ends behind
restricted networks or corporate TLS-inspecting proxies, dubbing/batch
TTS honor your active engine selection, and a run of sharp community
diagnoses (ROCm wheel index, Windows dictation focus-steal, a genuine
frontend crash regression) got fixed largely because reporters did the
hard diagnostic work themselves.
Full backend suite: 2390 passed, 0 failed. Full frontend suite: 918
passed, 0 failed. Version lockstep (tests/test_app_version.py): 6/6
passed. Docker frozen-lockfile parity (bun install --frozen-lockfile):
clean, no drift.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Root-caused and verified against the actual code (community diagnosis in
#982 was accurate): showing the dictation pill — an always-on-top top-level
WebviewWindow — grants it Win32 foreground activation by default on
Windows, which macOS doesn't do for a shown window. Nothing marked the pill
non-activating, so on Windows the synthesized Ctrl+V from `simulate_paste`
landed back in the pill instead of the app being dictated into. Code review
also found a second, more direct culprit at the same call sites: the
global-shortcut Pressed handler's `win.set_focus()` was only skipped on
macOS (`#[cfg(not(target_os = "macos"))]`), so Windows explicitly focused
the pill on top of the implicit Win32 activation; the tray "dictate" menu
handler called `win.show(); win.set_focus();` unconditionally on every
platform, including Windows.
This is the Windows counterpart of #287 (macOS auto-paste — don't steal
focus): same class of bug, same "pill must stay unfocused so the paste
lands in the target app" intent (already stated in the `grant_webview_
media_permissions` module comment), different OS mechanism.
Fix, mirroring #287's platform-cfg pattern:
1. WS_EX_NOACTIVATE applied to the pill's HWND once, right after creation
(`mark_pill_noactivate`), via the `windows` crate pinned to the exact
0.61.x tauri itself already resolves to — Cargo.lock confirms this
unifies to the same `windows 0.61.3` already in the graph, so
`WebviewWindow::hwnd()`'s HWND and our Win32 calls share one type and
no new crate version was added.
2. `ShowWindow(SW_SHOWNOACTIVATE)` (`show_pill_noactivate`) in place of
`.show()` at the two dictation-trigger call sites (global shortcut +
tray "Start Dictation"), since `.show()` alone still raced the style
bit on some paths.
3. The explicit `set_focus()` calls at those same two sites are now
skipped on Windows too, the same way they already were on macOS.
macOS and Linux are untouched: the macOS cfg branch is unchanged, and the
Linux branch of the `set_focus()` guard still runs exactly as before.
The pill's auto-dismiss (`scheduleDismiss`/`dismiss` in CaptureWidget.jsx)
was checked and is a plain unconditional setTimeout chain — it is not
gated on any native focus-loss/paste-completion signal, so there's no
independent bug to fix there. The "never dismisses" symptom is a
consequence of the focus-steal, not a separate stall: once the pill wrongly
held foreground for the whole session, hiding it later left Windows'
foreground state inconsistent. With the pill never taking focus, the
target app stays foreground throughout and there's nothing to reconcile.
Win32 window-activation syscalls (`#[cfg(target_os = "windows")]`) can't
run under `cargo test`/`cargo build` on this non-Windows CI runner, so the
new `pill_noactivate_tests` module tests the pure flag math
(`with_noactivate_style`) instead — platform-agnostic, runs everywhere,
verified passing here. The actual HWND-touching code is logic-reviewed but
UNVERIFIED on real Windows; the reporter offered to test a patched build,
which is the recommended next step before this ships in a release.
`cargo build` and `cargo test --lib` both pass (60/60 tests, including the
3 new ones); the pre-existing `setup.rs` unreachable_code warning (#286) is
unrelated.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
package.json + three mirrors -> 0.3.11 in lockstep; Cargo.lock/uv.lock/
bun.lock regenerated; CHANGELOG [Unreleased] -> [0.3.11] — 2026-07-05
with the multi-language-release headline; nine entries since v0.3.10.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
package.json (source of truth) + the three mirrors -> 0.3.10, in lockstep;
Cargo.lock/uv.lock/bun.lock regenerated (one line each; bun --frozen-lockfile
verified). CHANGELOG [Unreleased] -> [0.3.10] — 2026-07-05 with the release
headline; nine fixes since v0.3.9, mostly same-day field-report turnarounds.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The floating dictation pill (the "widget" window) was positioned bottom-center
by three near-duplicate blocks in lib.rs that each read primary_monitor(),
divided size by scale_factor, and called set_position(LogicalPosition...), with
a win.center() fallback. Replace all three with the official
tauri-plugin-positioner: window.move_window(Position::BottomCenter), preserving
the center() fallback on error.
- Add tauri-plugin-positioner = { version = "2", features = ["tray-icon"] }
(tray-icon enabled because the app ships a system tray).
- Register .plugin(tauri_plugin_positioner::init()) after single-instance.
- Collapse the global-shortcut, tray "dictate", and pill-mode pre-position
blocks to the plugin API. Behavior-preserving: same window, same trigger
points, still bottom-center.
Verified with cargo check (passes; the one warning is pre-existing in setup.rs).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Can't reach the local OmniVoice backend" cluster was a long-standing
supervision gap (dates to v0.3.0/#38), not a v0.3.7 regression: the backend
was spawned once and never watched again — `spawn_backend_and_wait` returned
the instant it was healthy. When the uvicorn process then died mid-session (a
CUDA OOM/context fault under a burst of generations — #571's log shows the
startup banner replaying 6× during a 20-generate burst — an antivirus kill, any
crash), nothing restarted it, so every later request threw connection-refused
and the user was stuck on the toast until a full app restart.
Two layers, both default-mode and platform-neutral:
1. Backend auto-restart supervisor (bootstrap.rs). After Ready, the bootstrap
thread (which used to just return) keeps watching the child and respawns it
on a *confirmed process exit* (try_wait — never a slow health probe, so a
busy-but-alive backend is never killed). Bounded to 5 restarts/60s (then
Failed) so a deterministic startup crash can't fork-bomb; the #314
broken-venv self-heal stays the venv-failure path. Strictly gated on
AppFlags.quitting so it never resurrects the backend during shutdown. A
single-supervisor guard (compare_exchange) prevents duplicate loops when
Retry re-enters concurrently. Emits backend-restarting/backend-restored
events (the splash poll stops post-Ready, so the stage alone can't show it).
2. Client transport-retry (client.ts). A *thrown* fetch (the backend briefly
down while it respawns) is retried a bounded few times with backoff
(~2.9s total) before surfacing the actionable ApiError, making the restart
window invisible. HTTP errors and deliberate aborts are never retried.
Resolves the whole cluster regardless of the crash trigger. Tests: Rust
backoff-policy unit test (cap + window-pruning); 4 client-retry vitest cases
(retry-then-succeed, no-retry-on-HTTP-error, no-retry-on-abort, bounded
give-up). Also corrects a stale Cargo.lock omnivoice-studio version (0.3.6→0.3.8).
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Versioning hard rule (owner-set 2026-06-11), codified in CLAUDE.md:
- main's three version sources (tauri.conf.json, Cargo.toml,
pyproject.toml) always carry last release + 1 patch; bumped 0.3.5 ->
0.3.6 now.
- Preview builds stamp BASE-N which now sorts ABOVE the last stable
(0.3.6-N > 0.3.5) — the updater ordering becomes natural and the
Windows MSI ProductVersion wrinkle disappears.
- Docker: :latest = rolling main preview; :stable + :X.Y.Z + :X.Y =
tagged releases. workflow_dispatch still only emits throwaway :sha-.
- release.yml gains a version-bump job: on every stable v* tag it
bumps main to the next patch automatically.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
On Windows 11 the dictation pill (Ctrl+Shift+Space) always reported
"Microphone access denied" even though OS-level mic permission was
granted (Voice Clone worked, backend transcribed fine). Root cause:
no WebView2 PermissionRequested handler was registered, so WebView2
fell back to its own permission UI — which the 300x64 transparent,
undecorated, deliberately-unfocused pill window can never host — and
getUserMedia() rejected with NotAllowedError.
Per-platform fixes:
- Windows (WebView2): register a PermissionRequested handler on both
the main and widget webviews that allows microphone/camera requests
in code, for the app's own origin only (tauri.localhost + dev
loopback). The Windows privacy toggle still applies on top.
- Linux (WebKitGTK): the media-stream enable + permission auto-grant
previously covered only the "main" window — the dictation widget is
a separate WebView and was silently denied. Now applied to both.
- macOS: already correct — NSMicrophoneUsageDescription ships in
src-tauri/Info.plist and wry grants media capture to the app origin;
documented in the shared helper.
Frontend: getUserMedia failures are now mapped by error name
(utils/micError.js) instead of one blanket "access denied" toast —
permission denials get a per-OS "where to re-enable it" hint
(Windows hint now mentions the desktop-apps mic toggle), missing
devices and busy devices get their own messages, and the previously
hardcoded English toast in useRecording goes through i18n. New keys
added to all 21 locales.
Tests: vitest unit tests for the error mapping (19 cases) and a Rust
unit test for the WebView2 origin allow-list; Windows handler code
cross-checked against webview2-com 0.38.2 / windows-core 0.61.2.
Fixes#323
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Root cause, two layers:
1. tauri-plugin-updater's default comparator is plain semver
(remote > current). Preview builds are published as X.Y.Z-N
(e.g. 0.3.5-41 = main, 41 builds after the 0.3.5 tag), which semver
treats as a *pre-release* of X.Y.Z — so it sorts BELOW stable X.Y.Z.
Once stable 0.3.5 shipped, preview users were told "you already have
the latest version" forever.
2. The endpoint list [preview, stable] is not a "best of both" — the
plugin stops at the first manifest that parses and uses later
endpoints only as network fallbacks, so a reachable preview manifest
hid a newer stable release entirely.
Fix: for the preview channel, check BOTH manifests with a custom
version_comparator implementing cross-channel ordering (higher base
version wins; on equal base a suffixed preview build outranks the bare
stable it was built on; preview-vs-preview uses numeric-aware semver
pre-release comparison), then offer the newest candidate. A manifest
error is non-fatal while the other manifest answers. The stable channel
keeps the single endpoint and the plugin's default comparison —
default behavior unchanged on all platforms.
Adds 7 unit tests covering preview ahead of stable (the bug case),
stable passing preview, equal-base both directions, equal versions
(no ping-pong), numeric build-counter ordering, and base dominance.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Dictation via the global shortcut transcribed fine but the text never reached
the target app on macOS, due to two stacked bugs (diagnosed, patched, and
verified by @geektf in #287):
1. The ShortcutState::Pressed handler called win.set_focus(), making the
widget frontmost — the simulated ⌘V from simulate_paste() landed in the
widget instead of the app being dictated into. Skip set_focus() on macOS
(same #[cfg(not(target_os = "macos"))] guard the other widget call sites
already use).
2. With the widget unfocused, the WebView clipboard APIs
(navigator.clipboard.writeText / execCommand('copy')) fail silently in
WKWebView, so ⌘V pasted whatever was previously on the clipboard.
simulate_paste now takes Option<String> and writes the transcript to the
clipboard natively (arboard) before sending the keystroke — no window
focus required. CaptureWidget passes the transcript; copyText() stays as
best-effort for browser (non-Tauri) mode, and the optional param keeps
any text-less call sites working.
cargo check clean (the unreachable_code warning in setup.rs is pre-existing
from #286); frontend node:test suite passes. End-to-end behavior verified by
the reporter on macOS 26 / M4 Pro with both patches applied.
Fixes#287
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch release. Version bumped across all sources + lock files; [0.3.5] CHANGELOG.
Ships:
- #270 — speaker diarization fixed on PyTorch >=2.6 (weights_only=True rejected
the pyannote checkpoint's TorchVersion global); the loader now registers the
shared safe-globals allowlist before loading.
Tagging v0.3.5 triggers release.yml (desktop) + docker.yml.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch release. Version bumped across all sources + lock files; [0.3.4] CHANGELOG.
Ships:
- #255 — PyTorch-Whisper backend works as a standalone fallback (no cuDNN 8,
no OMNIVOICE_PRELOAD_TTS_ASR=1), unblocking Windows+NVIDIA users hitting the
cudnn_ops_infer64_8.dll error.
Tagging v0.3.4 triggers release.yml (desktop) + docker.yml.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch release. Bumps version across all sources + lock files; adds [0.3.3]
CHANGELOG.
Ships:
- #262 — Settings → About now shows the server's CPU architecture (was the
client browser's platform, e.g. "Win32", in Docker).
- Validates the bash-3.2 checksum CI fix on a real release (the macOS
SHA256SUMS should now upload automatically).
Tagging v0.3.3 triggers release.yml (desktop) + docker.yml (GHCR
:0.3.3/:0.3/:latest).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch release. Bumps the version across all sources + lock files and adds the
[0.3.2] CHANGELOG section.
Ships:
- #261 — "Loopback origin required" 403s in the Docker admin UI (and blank
version): the image now runs in OMNIVOICE_SERVER_MODE so the loopback gate
is relaxed for the headless deployment; desktop loopback boundary unchanged.
Tagging v0.3.2 triggers release.yml (desktop) and docker.yml (GHCR
:0.3.2/:0.3/:latest).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First tagged build of the 0.3 line off main. Bumps the version across all
sources (pyproject + frontend package.json + Tauri conf + Cargo + both lock
files + the source-checkout fallback in core/version.py) and adds the [0.3.1]
CHANGELOG section.
Ships:
- #256 — browser/Docker file-export crash (invoke undefined)
- #249 — version surfaced in web/Docker UI + desktop-only updater hidden
- #255 — transcribe stream now surfaces the real ASR/model-load failure
Tagging v0.3.1 triggers release.yml (desktop binaries) and docker.yml
(GHCR :0.3.1 / :0.3 / :latest).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(spec): updates-in-status-bar design (move pill to LogsFooter + Updates panel)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(plan): updates-in-status-bar implementation plan (11 tasks, TDD)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(plan): pin i18n task to scripts/translate_all.py backfill
* feat(update): pure chip + release presentation helpers
* feat(update): listReleases + fetchAppVersion wrappers
* feat(update): transient releasesSlice composed into store
* feat(update): app version + channel in updaterSlice
* feat(update): list_releases Tauri command (GitHub releases)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(update): UpdateStatusChip bar indicator
* feat(update): UpdatesPanel (live row + channel + releases list)
* feat(update): mount chip+panel in LogsFooter, retire floating UpdateBadge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(update): Settings channel switcher shares store value (auto-sync)
* i18n(update): add updates.* keys across 21 locales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(deps): lock reqwest for list_releases command
* polish(update): a11y radiogroup on channel switch, safer release key, drop dead test seam
Addresses final-review nits (non-blocking): role=radiogroup/radio + aria-checked
on the channel Segmented; key={r.name||r.version} to avoid collisions; remove the
unused vi import + __loader seam in releasesSlice.test.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(update): i18n the channel-set error + correct flagged updates.* translations (PR #240 review)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(update): add 10s timeout to list_releases HTTP client (PR #240 review)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(update): guard chip Restart against in-flight dub job (greptile P1, PR #240)
The always-visible status chip's one-click Restart (ready state) called
installUpdate→relaunch without the dub-busy guard the panel uses, so a user
with a dub/transcription job running could lose in-flight work. Mirror the
panel's gate: toast update.busy and bail when dubStep === 'generating'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(update): surface channel-switch failures in the Updates panel (greptile, PR #240)
Mirror Settings' error handling: the panel's stable/preview switch now catches a
failed set_update_channel and toasts settings.channel_set_failed instead of an
unhandled rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: drop stray v0.4 references — everything ships on the v0.3.0 line
Per the project's versioning rule (no v0.4, no unprompted version chatter):
- backend/main.py + marketplace.py: the app reported version "0.4.0" (ahead of
even pyproject's 0.2.7 and referencing a forbidden version). Aligned to
"0.2.7" to match pyproject.toml / tauri.conf.json — a consistency fix, not a
bump.
- errorDocsMap.ts / indextts/bootstrap.py / _secret_key.py: reworded "v0.4"
deferral comments to version-agnostic "deferred / later hardening pass".
- docs/install/troubleshooting.md: the "tracked for v0.4" notarization line now
matches macos.md (signing is wired; activates on the Apple cert secrets).
Note: historical planning records under .planning/ still contain "defer to v0.4"
notes; left as-is (a record of superseded decisions) — CLAUDE.md + the
constitution are the live source of truth.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: set version to 0.3.0 across all sources (current dev line)
The current/upcoming version is v0.3.0 (0.2.7 is the prior stable). Bump every
version source so the codebase consistently reports 0.3.0 — the in-code dev
version; the git *tag* still happens later per the release cadence.
- pyproject.toml, frontend/src-tauri/Cargo.toml, tauri.conf.json,
frontend/package.json: 0.2.7 → 0.3.0
- backend/main.py (FastAPI) + marketplace.py export metadata → 0.3.0
(these had drifted to a phantom "0.4.0")
- CHANGELOG.md: "[0.2.7] — Unreleased" → "[0.3.0] — Unreleased"
- uv.lock + Cargo.lock reconciled (1-line each) so `--frozen` installs hold.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(version): read app version from package metadata (no more drift)
Greptile (#145): the FastAPI version + marketplace bundle metadata were bare
string literals — they'd go stale-wrong again at the next bump (the exact class
of bug this PR fixes; that's how "0.4.0" happened). Read once from
importlib.metadata.version("omnivoice") via core.version.APP_VERSION, with a
"0.3.0" fallback only for a non-installed source checkout. pyproject.toml is now
the single source of truth for the runtime version.
Tests: tests/test_app_version.py (semver + equals installed metadata). 2 pass.
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: implement frameless OS-level floating dictation widget
- Refactor CaptureButton into standalone CaptureWidget
- Add secondary transparent Tauri window configuration
- Map global hotkey to show/hide widget instead of focusing main app
- Implement auto-hide post-paste
- Add social preview image
* docs: up the game with enhanced README
- Use the high-quality social preview image as the hero image
- Bump download release links to v0.2.7
- Highlight the new Frameless Dictation Widget feature
* docs: complete README overhaul for maximum virality
- Add Highlights section with 2-column feature grid
- Move Quickstart to top with one-command install
- Collapse technical details into expandable sections
- Add 'Up Next' roadmap with concrete upcoming features
- Add star call-to-action banner
- Tighten navigation links and section hierarchy
* docs: add beta warning banner
* docs: add star request to beta banner
* docs: rewrite README with cognitive hooks, remove redundant CTAs
- Remove 2 premature star asks (beta banner + highlights)
- Rewrite highlights with loss-aversion framing
- Keep single earned CTA at the very bottom
- Use action-oriented headings that describe outcomes
* docs: rename section to 'Why OmniVoice Studio?'
* docs: rename 'What you get' to 'Features'
* docs: concise scannable features, remove duplicate section
- Each feature is one punchy emoji-led line
- No verbose paragraphs, no redundant collapsibles
- Removed duplicate Features section from merge
* docs: 3-column feature card grid for visual impact
Replaces flat bullet list with 4x3 HTML table grid.
Each feature gets its own visual cell with emoji header,
bold keywords, and 2-line description. Pops on dark mode.
* docs: fix feature grid vertical alignment
* chore: bump version to 0.2.7, add changelog entry
* fix: apply CodeRabbit auto-fixes
Fixed 1 file(s) based on 1 unresolved review comment.
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>