Adds oxfmt (Rust formatter, Prettier-conformant) — the repo had no formatter, so
this is a one-time normalization of the JS/TS/JSX code (257 files; purely
cosmetic — full suite stays 638/638).
Scope is deliberately narrowed in .oxfmtrc.json to JS/TS/JSX only:
- singleQuote:true + jsxSingleQuote:false — preserve the project's existing
style (single-quoted JS, double-quoted JSX attrs), not oxfmt's double-quote
default. (Flipping quotes globally also broke a source-string-parsing test;
preserving them keeps featureCoverage green.)
- Excludes **/*.css (the CSS→Tailwind migration will rewrite those — formatting
them now is wasted churn), **/*.json (avoids reformatting 20 i18n locale
files + config), **/*.toml, and src-tauri/** (Rust/Tauri config — out of scope
for a frontend JS formatter; oxfmt was reformatting Cargo.toml/tauri.conf.json).
Tooling:
- `bun run format` (write) / `bun run format:check` (verify).
- ci.yml: new "Frontend format check (oxfmt)" gate after the oxlint gate.
Verified: format:check clean; oxlint 0 errors; vite build; full suite 638/638;
bun install --frozen-lockfile in sync.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The in-app goal bar and its bundled snapshot showed $137.50 / $200 (23
sponsors) — fabricated numbers. Updated both the runtime JSON and the
TypeScript fallback to reflect the real amount: $10 / $200, 1 sponsor.
Also updates the README badge color from red to yellow (in-progress).
Problem
OmniVoice's donate surface was a static link page. There was no sense of
shared progress toward a concrete funding goal, and no gentle, success-only
ask after a user got value — only an always-on footer heart.
Design
Phase 1 — Goal bar + data (Option B):
- frontend/public/donation_progress.json (committed snapshot) + a bundled
offline fallback in api/donation.ts. loadDonationProgress() best-effort
fetches a fresher copy and gracefully falls back to the bundle on any
failure (offline / non-2xx / bad JSON). Never throws.
- <GoalBar> (page + `mini` variant), --goal-pct-driven fill, Pip mascot
perched on the fill, ONE shimmer pass, reduced-motion guard on every
animation. Added to SupportPage above the payment cards with
"Join {n} supporters" social proof + suggested amounts ($3/$5/$10/Custom,
middle flagged "most common", NONE pre-selected).
Phase 2 — Pip + postcard + state machine:
- Pip.jsx (currentColor->accent, pipBob/pipWave idle, reduced-motion off).
- donationSlice.ts composed into the store: added to partialize (all EXCEPT
shownThisSession), version 5->6 with a pass-through migrate branch.
shouldShow rules: first-3 grace, <=1/session, escalating 7d/14d/30d/75d
cooldowns, optedOut terminal, success-only.
- Postcard.jsx rendered via react-hot-toast as a NON-BLOCKING custom toast
(no backdrop, no focus steal, ~12s auto-dismiss, pause on hover) with the
perforation / dot-grain / stampThunk / postcardIn / .is-leaving art,
a mini GoalBar, and Chip in / Maybe later / quiet Don't ask again /
free Star on GitHub actions.
- One shared evaluateDonationPrompt() called right after each SUCCESS
(dub-complete, clone-save resolve, longform export) — never on the
error / in-progress / setup / first-run paths.
Phase 3 — Milestones + pill:
- Milestone eval (1st clone / 10th dub / 30-day sustained, each once-ever,
same cooldowns + opt-out) inside the shared evaluator.
- Quiet nav-rail .donate-pill (🩷 Support) that warms to the accent on
hover and opens setMode('donate').
Tests (vitest, all green: 60 files / 533 tests)
- donationSlice.test.ts: shouldShow truth table with injected `now` — grace,
each cooldown rung, session cap, opted-out terminal, success-only.
- GoalBar.test.jsx: renders from injected JSON, offline fallback to bundle,
goal-met state, mini variant; plus the data module's clamp/normalize/fetch.
- evaluateDonationPrompt.test.jsx: gating + that the postcard never fires on
the error path (success-only contract).
bun run typecheck:ci clean; vite build green; root bun.lock untouched
(frozen install verified); i18n: all user-facing strings via t('donate.…')
with English defaultValue fallbacks — no hardcoded CJK.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes Action 8: dictate-over-playback echo cancellation now works
end-to-end, gated behind a new off-by-default 'aecEnabled' pref so the
standard dictation + playback paths are untouched when off.
- utils/aec/{pcm,farEndBus,micCapture,playbackTap}.js + public/aec-worklet.js:
AudioWorklet captures the mic as raw int16 PCM; a player tap routes playback
output through Web Audio to a singleton far-end bus. Pure framing/encode
helpers are unit-tested.
- CaptureWidget: when aecEnabled, opens /ws/transcribe?aec=1, streams tagged
PCM (0x00 mic / 0x01 far-end) instead of MediaRecorder/WebM. Default path
unchanged; no POST fallback in AEC mode (the WS is the sole channel).
- WaveformPlayer: while actually playing AND aecEnabled, taps its decoded
output as the echo reference. Gated on isPlaying so only the one active
player holds an AudioContext (well under the browser cap); audio stays
audible (source always reconnected to destination).
- Settings → Capture: AecPanel toggle. prefsSlice: aecEnabled (persisted).
Runtime-unverifiable here (jsdom has no Web Audio); needs in-app testing in
the Tauri shell. 7 new pure-helper tests; full vitest (319) + vite build green.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>