Updates

Updates are free. Forever.

Buy aiTTS once and every update is yours at no extra cost: new voices, new features, fixes, all of it. There is no subscription and no upgrade fee. New builds arrive automatically through the in-app updater, so you are always on the latest version without lifting a finger.

Download the latest build or read what has shipped below.


Changelog

1.11.0

aiTTS 1.11.0

Dictation, fixed in the spots it actually bit.

Dictation

  • Narration resumes after you dictate. Push-to-talk dictation used to leave narration paused once you let go. It now picks back up where it left off.
  • The speech model downloads itself. Starting dictation with no whisper model present now kicks off an automatic download instead of quietly doing nothing. The only remaining setup is one button.
  • No more emoji popup on the fn key. Holding fn for push-to-talk no longer triggers the system emoji and character picker over your text.
  • The first word is no longer dropped. The opening word of a dictation is captured instead of getting clipped off the front.

1.10.0

aiTTS 1.10.0

Teach it to say your words right, and a caption window that stays out of the way.

Pronunciation, your way

  • New Pronunciation section in Settings. Map a word to how it should be spoken ("nginx" to "engine x", "k8s" to "kubernetes") and every narration uses it, live sessions and one-off Speak alike. Captions and transcripts keep the original spelling; only the audio changes.
  • Same overrides from the CLI: tts lexicon list|add|remove.

Personas are gone

  • The four persona presets bundled a voice and a speed together, and that coupling caused more confusion than it saved clicks. Voice and speed are now separate, first-class settings (Speed has its own slider in Settings), and onboarding picks a real voice instead of a persona. tts persona now points you at tts voice and tts speed.

Caption window

  • The audio visualizer is now a subtle backdrop behind the captions: thin bars, faded, sitting under the text instead of competing with it. Long captions no longer get cut off.

Session menus

  • Attach and Now Narrating rows now show the git branch and the opening prompt of each session, so parallel sessions in the same repo are finally tellable apart. Short session ids appear only when two rows would otherwise look identical, and each narrating session gets its own detach row.

Test totals

Full Mac build green: Swift selftests (including the new lexicon parity suite and a 64-check menu suite), SearchKit / SpeechKit / LicenseKit XCTests, UI showcase. JS suites green (628 tests passing across parallel and serial, 0 failures), with new lexicon unit and cross-language parity suites.

1.9.2

aiTTS 1.9.2

Everything you need now ships inside the app.

The headline

  • iPhone pairing works straight from the DMG. The daemon that hosts the iPhone bridge now ships inside the app bundle, so Pair iPhone no longer needs a repo clone or any Terminal setup. The one remaining requirement is Node.js, and the pairing sheet now says exactly that if it's missing.
  • Other agents, same story. The Agents pane's copy button now hands you a command that actually runs on a fresh install, so enabling narration for Codex, Gemini CLI, OpenCode, Aider, or Cline doesn't assume a tts command you never installed.

Claude Code plugin

  • The /tts plugin is now a slim remote control: it asks the app to speak and the app does everything (synthesis, PDF and image extraction, playback). Same two install commands as before. If you installed the plugin in the last day, update it; earlier shim builds couldn't reach the app.

Fixes

  • Commands copied from the app now work even if the app lives in a folder with spaces in its name.
  • Friendlier pairing guidance when Node.js is missing.

Test totals

Full Mac build green: deployment-target gate, CLI-bundle assertions, Swift selftests (ctl-invocation 11, resolver 8), SearchKit / SpeechKit / LicenseKit XCTests, UI showcase. JS suites green (607 tests, including 11 plugin-shim contract cases and 14 CLI-bundle cases). Web green (368 tests).

1.9.1

aiTTS 1.9.1

The launch-readiness release: the app now runs on the Macs we say it runs on, the install instructions work for everyone, and a round of correctness fixes that came out of a full-surface review.

Compatibility, the headline

  • The app now supports macOS 14 Sonoma and later on Apple Silicon, and says so honestly. Previous builds silently inherited the build machine's OS as their minimum (1.9.0 required macOS 26), so the app could refuse to launch on Macs it was sold for. The deployment target is now pinned at macOS 14.0, enforced by a build gate, and stamped into the update feed so Sparkle never offers an update your Mac can't run. The bundled whisper dictation engine was rebuilt to match.

Install path

  • Plugin install works for everyone now. claude plugin marketplace add wickdninja/aitts-plugin is the new public home of the /tts skill and the narration CLI; every install instruction (site, README, in-app connect sheet, one-click installer) points at it.

Mac

  • Trial expiry no longer goes silent. If the 14-day trial ends while the app is running, the menu updates, dictation gates, and the license prompt appears, instead of narration just stopping with a stale "1 day left".
  • The caption EQ bars are honest now. Fixed permanently-dead low bands, a sync offset that made the bars lead the audio by up to 43ms, and over-smoothing that blurred inter-word gaps into a wash. The bars track speech cadence, sibilance, and silence like they should.
  • Friendlier model-download errors and truthful first-run copy (the voice engine download takes more than "a few seconds").

iPhone

  • Switch Mac pairing sends your device's real name instead of a generic "iPhone", so multi-device households can tell their phones apart.

Web and licensing

  • Async payments mint licenses only after funds settle. Bank-debit style checkouts no longer get a license before the payment clears.
  • The download page's auto-start actually starts (a security policy was blocking it), expired sign-in links explain themselves, and the privacy policy now fully discloses what an opt-in diagnostics bundle contains and how to inspect one before sending.

Test totals

Full Mac build green: deployment-target gate, Swift selftests (including new spectrum and trial-expiry pins), SearchKit / SpeechKit / LicenseKit XCTests, and the UI showcase. iOS suite green (318 tests, including a new pairing-label audit). Web green (368 tests, including new webhook replay, CSP, and privacy pins). JS suites green.

1.9.0

aiTTS 1.9.0

The iPhone companion grows up: local neural voice on the phone, sturdier dictation and pairing, and a Now Playing card that looks the part. Plus a round of Mac caption and Settings polish.

iPhone

  • Kokoro runs on the phone now. The local neural voice that powers the Mac app now runs on iOS too, behind the Speaker seam, with your voice choice persisted across launches. No API key, no cloud round-trip.
  • Dictation you can lean on. Push-to-talk handles long takes (with a cap), survives interruptions, keeps chunks in order, can resend on a dropped connection, and gives you haptic confirmation.
  • Pairing that tells the truth. Unique device ids, real scan feedback, a device label you can read, and an honest timeout instead of a spinner that never resolves.
  • Brand Now Playing artwork. The lock-screen card shows proper artwork, and lock-screen controls are gated on actual playback state so the buttons mean what they say.
  • Steadier streaming. The SSE stream re-opens on foreground when the socket dropped, stale cross-session events are dropped via epoch tagging, and reconnect gap events are decoded and handled.
  • No double-play on handoff. Phone audio is gated on takeover so Hand Back doesn't play the same thing twice.
  • Home and first-run polish, in-app Siri tips with a Shortcuts link, and a pass on caption performance and accessibility.

Mac

  • Caption controls. Font size, opacity, and a reset-position control for the floating caption window.
  • Settings window fixes. HIG-correct window behavior (dropped the stray Done button, frame validation, Spaces and title handling).
  • Copy and jargon sweep across Settings and the menus so the language reads like plain English.

Test totals

Full Mac build green: Swift selftests, SearchKit / SpeechKit XCTests, and the UI showcase. iOS suites green (Kokoro inference, dictation, pairing, stream handling). JS suites green (pairing, harness parity, one-off isolation).

1.8.0

aiTTS 1.8.0

Live narration feedback, a faster first word, and a round of Settings and menu polish.

Mac

  • Narration tells you what it's doing. The caption window now streams status instead of sitting on a blank "Waiting for narration…": connecting to the session, warming up the voice, synthesizing, then playing. Pausing shows "Paused · N queued" so a held queue is never invisible. This also fixes a case where a leftover pause silently stopped narration with no feedback at all.
  • Attach starts talking right away. Connecting to a running session now narrates the previous message for context instead of sitting silent until the next one arrives, so you hear something immediately.
  • Faster to first word. The local voice model warms at launch, so the first narration after you attach no longer waits on a cold start.
  • The license window can't trap you. The license sheet now dismisses correctly when shown modally (it could previously get stuck right after you entered a key).
  • Honest menu-bar transport. Play / Pause / Skip / Stop enable only when there is something to act on, with HIG-correct toggles.
  • Settings cleanup. The auto-update toggle now reflects and drives Sparkle's real setting; start-at-login reconciles to the actual launch-agent state and the dictation hotkey label refreshes live; the dead Player-engine picker is gone; the Agents pane states its CLI prerequisite honestly.
  • Devices pane. See your paired iPhones and unpair them right from Settings.

Test totals

Full Mac build green: Swift selftests (including new narration-status, attach-narrate-previous, prewarm-launch, license-sheet-dismiss, and menu-bar cases), SearchKit / SpeechKit XCTests (107 + 4 + 62), and the UI showcase. JS suites green (pairing, harness parity, one-off isolation).

1.7.4

aiTTS 1.7.4

A clean DMG install now narrates with zero command line. The app does the whole job in-process: it tails your agent sessions, synthesizes, and plays, with no JS daemon, socket, or background helper in the narration path.

Mac

  • Narration runs entirely inside the app. The Mac app now tails sessions, synthesizes, and plays in-process (SessionTailer to a shared parser to NarrationPump to the audio queue). A fresh DMG narrates immediately with nothing installed at the terminal. The old daemon/service/socket path is gone for narration, which removes the "Waiting for narration..." hang, socket churn, and the per-chunk process spawn storms it caused.
  • Faster, steadier voice. Local synthesis now runs through a resident Kokoro worker that loads the model once and keeps it warm, about 9x faster than the previous per-chunk reload. Less lag between your agent finishing and the audio starting.
  • All seven agents narrate natively. Claude Code, Codex, Aider, Gemini, Cline, OpenCode, and pi all narrate through the same in-process reader. Live narration and history scrubbing now share one parser per agent, so what you hear live and what you replay from history always match.
  • The terminal skill and iPhone bridge are unchanged. The standalone /tts Claude Code skill and the iPhone companion still work exactly as before; they keep their own daemon, which the app no longer needs for itself.

Test totals

Full Mac build green: Swift selftests (including new native-tailer, chunker, and harness-parity pins), the SearchKit/SpeechKit/LicenseKit XCTest suites, and the UI showcase; JS suite green, including new source-parity tests for the live-only agents and the gemini and empty-text branches.

1.7.3

aiTTS 1.7.3

A focused pass on the first-run and licensing experience: activation that works on a messy paste and tells you what went wrong, a trial that ends with an offer instead of silence, onboarding that actually explains the product, and windows and Quit that behave.

Mac

  • License activation is forgiving and honest. Pasting a key with stray spaces, line breaks, smart quotes, or a leading "key:" label now activates instead of silently failing. When activation does fail, the sheet shows a clear, specific message (bad key, device limit, refunded, revoked, no network, local save error) with the right next step, rather than a tiny line of red text that was easy to miss.
  • One-click activation from the web portal. The license page now has an "Activate on this Mac" button that hands your key straight to the app, so you no longer have to copy and paste a long key. The app opens with the key filled in and you confirm with one click (it never activates without that click).
  • A trial that ends no longer just goes quiet. Expiry used to silently mute narration with no explanation and no way forward. Now the menu bar counts down in the last stretch and an expiring or expired trial surfaces a dismissible prompt to buy a license or enter a key, re-surfaced at the moment you next try to narrate.
  • Onboarding explains what aiTTS does. The Welcome window now teaches both halves of the product: narration of your agent's replies (Listen) and push-to-talk dictation (Talk), with the real hotkeys and the one-time dictation model download.
  • The standalone terminal skill is discoverable. A new Settings card and menu item show what the /tts Claude Code skill does and the exact install and usage commands, with a copy button, so the daemon path is no longer hidden.
  • Less ceremony to start narrating. When exactly one Claude Code session is active and nothing is attached, aiTTS attaches it automatically. Two or more concurrent sessions still require an explicit choice, so sessions never cross-narrate.
  • Quit always quits. Quitting no longer blocks the main thread on daemon teardown; an off-main watchdog guarantees the app exits promptly even if shutdown stalls.
  • Welcome, About, and Settings open and close reliably. Fixed the window-retain and activation-policy issues that could leave a window opening behind everything or not at all, and the Done buttons that sometimes did nothing.

Test totals

Full Mac build green: Swift selftests (including new pins for license-key normalization, the trial-expiry copy and menu, terminal-skill card, quit teardown, single-session auto-attach, the activation deep link, and Welcome window lifecycle), and three XCTest suites, SearchKit (105), SpeechKit (4), and the newly-extracted LicenseKit package (62 license crypto, decode, trial, and lock tests that now run in CI), plus the UI showcase.

1.7.2

aiTTS 1.7.2

Small reliability and efficiency fixes.

Mac

  • No wasted synthesis for locked or expired licenses. Scrubbing back through history no longer runs the local voice model to produce audio that playback would only drop. The check now happens before synthesis, not after, so a locked or trial-expired app does not spend CPU on audio it will not play.

iPhone companion

  • Dictation queue is race-safe. Rapid back-to-back dictations that arrive at the same moment the queue file hits its size cap can no longer drop a line. The queue writes are now serialized so a trim and an append can't interleave.

Test totals

JS suite green (parallel + serial lanes, 548 tests); full Mac build green: Swift selftests (including the new resynth license-gate cases), SearchKit/SpeechKit XCTests (104 + 4), and the UI showcase.

1.7.1

aiTTS 1.7.1

A focused cleanup of the "attach a session" picker.

Mac

  • One recency-sorted session list. The attach picker used to split sessions into a "LIVE" and a "RECENT" group, which went stale and rarely matched what you were actually working in. It is now a single list ordered by most recent activity, with a folder path and a short snippet of the last reply so you can tell sessions apart at a glance.
  • Accurate "active" indicator. The green active dot is now computed from real activity time at the moment the menu opens, so a session can no longer show as active next to a timestamp from 40 minutes ago.

Test totals

JS suite green (parallel + serial lanes); full Mac build green: Swift selftests, SearchKit/SpeechKit XCTests (104 + 4), and the UI showcase. The new picker recency, dedupe, and cap behavior is pinned by tests/attach_picker_recency.test.js and the extended [menubar] selftest.

1.7.0

aiTTS 1.7.0

Faster narration, a cleaner settings experience, and broader agent support. The local voice now starts instantly instead of reloading its model for every reply, the menu and settings were reorganized so everything has an obvious home, and every coding agent is a first-class citizen instead of a label you could not act on.

Mac

  • Instant local narration. The local Kokoro voice now runs as a persistent engine that loads its model once, instead of spawning a fresh process per message. The seconds-long pause before each reply is gone. Falls back to the old path automatically if the engine is unavailable.
  • Settings, reorganized. The menu is now purely live controls. Configuration moved into a clearer Settings layout:
    • Voices is the one home for text-to-speech: voice, speed, the Speak hotkey, and the Gemini cloud-TTS key (which used to live in the menu).
    • Dictation is the dedicated home for speech-to-text: model, microphone, and the dictation hotkey.
    • Agents lists every coding agent as a first-class row with its status and a one-tap way to connect or enable it (no more six labels where only one worked).
    • Devices is where you pair your iPhone.
  • Dictation tells you what is wrong. Pressing the hotkey before the speech model is downloaded now shows the reason and opens Settings to fix it, instead of silently doing nothing.
  • Quieter and lighter. Playback writes the now-playing card and lock-screen info far less often, and the background daemon backs off when nothing is attached, so it stops fighting App Nap and the battery.
  • Cancel actually cancels. Cancelling the speech-model download now stops the transfer instead of finishing it in the background.

iPhone companion

  • Timeline stays in sync. A Mac restart no longer freezes the phone's conversation timeline, and the model download is faster.

Under the hood

  • Broader, better-tested agent support: a backward-history reader for Aider, a fix so Gemini whole-document sessions show scrollback, and parity tests across the harness readers.
  • Hardening: the daemon now exits cleanly (releasing its lock) on an unexpected error rather than limping on; cloud API keys are redacted everywhere; the Mac<->iPhone bridge bounds per-device connections.
  • Licensing verification is re-checked at the point of playback, not just at launch.

Test totals

JS suite green (parallel + serial lanes); full Mac build green: Swift selftests (including the persistent-engine, licensing, and harness-registry checks), SearchKit/SpeechKit XCTests (104 + 4), and the UI showcase (now covering the new Voices / Agents / Devices panes). Cross-language redaction and harness-registry parity are pinned by tests.

1.6.1

aiTTS 1.6.1

A reliability and polish pass from a full publish-readiness review: the first-run experience no longer fails silently, the post-purchase screens look the part, and the iPhone link recovers on its own when your network blips.

Mac

  • Dictation tells you what's wrong. Pressing the dictation hotkey before the speech model is downloaded (or without microphone access) used to flash the pill and do nothing. It now shows the actual reason and a tap opens Settings to the Dictation pane so you can fix it in one step.
  • Polished post-purchase screen. The license entry sheet now matches the rest of the app instead of a plain text box.
  • Smoother first run. The "Hear how it sounds" button warms the local voice in the background so the first preview doesn't hang or surface a developer-style error on a clean install.
  • Clearer dictation setup. Settings now shows at a glance whether the model is downloaded and microphone access is granted.
  • Copy and iconography cleanups across the onboarding and caption surfaces.

iPhone companion

  • Stays connected on flaky Wi-Fi. A heartbeat plus an idle watchdog mean a dropped or roamed connection now reconnects on its own instead of showing "connected" while silently receiving nothing.
  • App Store readiness: opaque app icon, iPhone-tuned build, and a working in-app link to the website, privacy policy, and support.

Under the hood

  • Licensing verification hardened and re-checked at the point of playback, not just at launch.
  • Cross-device control (takeover/release) now broadcasts only after the change is durably recorded, so two devices can't briefly disagree.
  • The narration daemon no longer crawls your home folders to populate menus you haven't opened, and parallel sessions of the same agent no longer suppress each other's identical lines.

Test totals

JS suite green (524 passing); full Mac build green: Swift selftests including the licensing boot-verify and feature-output gate (license-boot 8), SearchKit/SpeechKit XCTests (101 + 4), and the 20-shot UI showcase. Fork-bomb attach regression net passes with zero leaked descendants.

1.6.0

aiTTS 1.6.0

Talk to your agent, from your desk or your phone. This release closes the loop: dictated text can now be injected straight into a focused terminal agent (with an opt-in, gated auto-submit), and the playback timeline projects across devices over a single command bus so the iPhone companion can show where you are and scrub back.

Mac

  • Inject dictation into your agent. Transcribed speech now routes through an input-sink layer instead of always landing on the clipboard. A terminal-agent sink recognizes Terminal, iTerm, Ghostty, and VS Code / Cursor and can press Return for you. Auto-submit is double-gated: it fires only when the caller explicitly asks AND you have turned on the setting, which defaults OFF. Paste-only is the universal fallback and never submits on its own. Focus is snapshotted at recording start and the inject bails if you switched windows.
  • Cross-surface command bus. A single command verb set (skip, pause, scrub, jumpToLive, inject, takeover, and the rest) is now declared once and shared by every transport: the unix socket, the bridge control endpoint, and the state file. Adding a control verb is one manifest edit, not per-transport glue.
  • Live timeline cursor on disk. The now-playing cursor (which message, how many, how many new arrived, synthesizing or idle) is written behind the cross-process lock and revisioned, so a remote device can read the live position and drop stale frames.
  • Per-harness adapter. Reading a harness's transcript and injecting back into it now live behind one adapter, the seam future harnesses extend.

iPhone companion

  • The conversation timeline reaches your phone. The companion app projects the live <- M / N indicator and a +N new badge from the Mac, and a back tap scrubs through history over the bus. The Mac stays the single source of truth: the phone moves only when the new cursor fans back, never optimistically.
  • Speech input adopts a recognizer protocol, and "skip" / "pause" spoken on the phone intercept as control commands rather than dictated text.

Under the hood

  • The unix-socket protocol gained an additive command envelope. The pinned flat play shape is byte-identical, so already-installed builds keep narrating; the wire proto integer is deliberately unchanged and command envelopes are tagged by kind, pinned by a selftest so a future change can't silently break daemon-originated scrub and inject.
  • The dictation queue the iPhone writes finally has a consumer: the daemon forwards it to the Mac as an inject command.
  • Closed out the 94 player-history series and reconciled the task board.

Test totals

JS suite 509 passing (2 release-only tests skipped), full Mac build green: Swift selftests (including the cross-surface cursor, input-sink, harness-adapter, remote-inject, and IPC envelope cases), SearchKit/SpeechKit XCTests (101 + 4), and the 20-shot UI showcase all green. The fork-bomb attach regression net passes with zero leaked descendants.

1.5.0

aiTTS 1.5.0

Scrub back through your whole session. The player is no longer a one-way live stream: the prev/next controls now navigate the message timeline, and messages older than this launch are re-synthesized on demand so you can replay anything you missed.

Mac

  • Smart prev/next navigation. Lock-screen, caption-window, and menu prev/next now move through the message timeline instead of only restarting or skipping the current chunk. |< mid-message restarts; at a chunk boundary it steps back a chunk, then to the previous message. >| steps forward then on to the next message or back to live. The caption pill shows where you are (<- M / N) and is tappable to jump straight back to live.
  • Scrub into older messages (on-demand resynth). Scrubbing past the in-memory ring now resolves the older message's text from the session transcript and re-synthesizes its audio with local Kokoro, streamed and cached like live narration. Text appears immediately; audio follows. No persistent disk cache.
  • Unified speech-synthesis engine. Kokoro and Gemini synthesis paths (live narration, Speak, voice preview, resynth) now run through one engine abstraction, removing duplicate spawn logic and making the voice path consistent.
  • Cross-surface playback cursor. The now-playing snapshot carries the timeline cursor (which message, how many, how many new arrived), laying the groundwork for the player timeline to surface on the iPhone.

Under the hood

  • Live (daemon) and history (Mac) transcript parsing are now locked in lockstep by a shared golden-fixture parity suite, fixing a Gemini message-id mismatch that could double-count messages on scrub-back.
  • The player timeline was refactored onto a single message index plus a pluggable chunk provider (ring cache then on-demand synth), so future replayable surfaces are one small addition rather than a new code path.
  • Voice input gained a recognizer protocol, a single-registration voice-command grammar, and an input-sink seam (paste-only for now).

Test totals

Swift selftests + SearchKit/SpeechKit XCTests + JS suite all green; the player timeline, resynth, parity, and engine paths are covered by dedicated selftests.

1.4.8

1.4.8

Scrub backwards through a narrated session. The new player history lets you replay every assistant message from the current launch, with a LIVE / position pill in the caption window and a "+N new" badge when fresh messages land while you're parked in the past.

Mac

  • Player history: scrub-back replay. Step backward through every assistant message in an attached session and hear it again. Audio replays from a bounded in-memory ring of WAVs narrated this launch (doubly capped: 20 messages or 200 MB, LRU eviction), so there's no disk cache and no resynth latency for recent messages. |< / >| skip by message; the cursor lives in (message, chunk) space. Backed by MessageHistoryQueue plus a reverse-paginated SessionHistory reader that never loads the whole transcript and is UTF-8 safe across chunk joins.
  • Caption pill: LIVE / position / +N new. The caption window shows nothing when idle, LIVE on the live edge, ← M / N when you scrub back, and a +N new badge counting live messages that arrived while you were in history. Returning to live resets the counter.
  • Captions auto-open on session attach. Attaching a session turns captions on and shows the window. Explicit dismissals and reattaches are respected via a session-scoped latch that resets on detach.
  • Search palette (⌘⇧F) accepts keystrokes. The palette opened but took no input: a borderless NSPanel returns canBecomeKey == false by default. New KeyableSearchPanel subclass overrides it, so the palette is actually focusable.
  • Build fork-bomb fixed. A selftest that set HOME to a temp dir and called attachSessionLocal() was booting the real daemon/service/standby stack against that temp HOME; deleting it on exit orphaned a self-replicating loop. runCtl/runCtlSync now no-op under any selftest, ensureDaemonRunning takes an O_EXCL start lock so concurrent callers collapse to one daemon, and ensureService rate-limits to one spawn per 3s. Root cause writeup in docs/incidents/2026-05-24-build-forkbomb.md.

Test totals

SessionHistoryTests 11 XCTest cases. TTS_HISTORY_QUEUE_SELFTEST 26, TTS_HISTORY_RING_FILE_SELFTEST 11, TTS_RING_REPLAY_E2E_SELFTEST 4, TTS_AUDIOQUEUE_EPHEMERAL_SELFTEST 2, TTS_CAPTION_HISTORY_BADGE_SELFTEST 18, TTS_SEARCH_PANEL_KEY_SELFTEST 4. New tests/daemon_start_lock.test.js, tests/forkbomb_attach_selftest.test.js, tests/message_id_propagation.test.js; tests/ipc_contract.test.js extended to pin populated session + message ids. Full npm suite 472 pass.

1.4.5

1.4.5

iOS audio reliability, one small Mac UX win, and a stack of web fixes around Stripe + sessions + email.

Mac

  • Settings window now appears in Cmd+Tab and the Dock while it's open. App stays .accessory (menu-bar-only) by default; promotes to .regular only for the duration of the Settings session, then demotes back. Ref-counted so over-release is a no-op. Pinned by TTS_SETTINGS_POLICY_SELFTEST.

iOS

  • AirPods disconnect no longer silently routes audio to the iPhone earpiece. AVSpeechEngine + KokoroEngine pause on route-change oldDeviceUnavailable instead of re-routing to the receiver.
  • AVAudioEngine survives Core Audio media-services reset. AudioSessionCoordinator observes mediaServicesWereReset/Lost; engines reset() (not pause()) so the next start runs against a fresh audio unit.
  • KokoroEngine drops the Core ML model on memory warnings when not mid-utterance, reclaiming ANE memory between long agent runs.
  • Dropped .duckOthers from .playAndRecord. Dictation is record-only; ducking other apps there is a no-op cost and a UX surprise.
  • SSEParser bounds-check moved pre-allocation. Oversized chunks with no \n\n boundary are now dropped without allocating up to maxFrameBytes first.
  • Explicit per-request timeouts (10-15s) on ControlClient / SessionsClient / DictationClient / PairingClient. LAN calls no longer ride the 60s URLSession default.

Web

  • Stripe webhook: charge.refunded arriving before checkout.session.completed no longer mints a permanent free license. The handler now throws RetryableError when the license row doesn't exist yet, which rolls back the stripe_events row and forces Stripe's retry. Refund applies on the next attempt, after the checkout event lands.
  • Stripe webhook: any handler throw now rolls back the stripe_events row, not just RetryableError. Previously a non-Retryable throw poisoned future retries: row stayed, retry short-circuited as "already processed", customer paid with no license and no email. Permanent loss path, closed.
  • Security headers landed in vercel.json. HSTS (2y + preload), X-Frame-Options: DENY, CSP with frame-ancestors 'none' and conservative default-src 'self', Referrer-Policy: strict-origin-when-cross-origin (magic-link tokens no longer leak via Referer), X-Content-Type-Options: nosniff, Permissions-Policy denying camera/mic/geo. Pinned by 10 tests in security-headers.test.ts so a future "simplification" trips the build.
  • clearSession() cookie deletion now uses object form with Path=/ + Secure. Previous bare-string call left the __Host- cookie in the browser. Latent session-revival vector if we ever move to stateless/signed-cookie sessions.
  • Magic-link /send email validation tightened. Previous [^@]+@[^@]+\.[^@]+ regex accepted CRLF, NUL, whitespace, HTML, quoted locals, and address-list separators. New regex is narrower than RFC 5322 on purpose; widen later if a real customer asks.
  • /download/dmg/[version] diagnostics-upload contract pinned. Revoked licenses now provably get 403, missing licenses 404. Two paths the route already gated but nothing tested.

Test totals

No new gating regressions. Existing CI suites still green.

1.4.4

1.4.4

Two real bugs the audit caught in v1.4.3 plus a much wider regression net.

Bug fixes

  • Speak menu image-describe (Cmd+Opt+S on a screenshot) now works for users without /usr/bin/node. The fallback path used /usr/bin/env node, which inherits the GUI's PATH, NOT the user's shell PATH. Anyone whose node lives in /opt/homebrew/bin/, ~/.nvm/..., or any other shell-managed location got a silent no-op. Resolved through the same ScriptResolver story as v1.4.2.
  • Voice preview failures now surface the actual error. Previously every Kokoro spawn failure rendered as "Synth exited with code N." with the real stderr (uv: command not found, model download failed, out of disk) discarded. The tooltip now shows what actually broke.

Why the audit caught these and prior releases didn't

The voice-preview selftest injected a stub synth closure so the state machine was perfectly tested while the real spawn path was never exercised. v1.4.0 - v1.4.3 each had a real bug in that unprotected path. The audit added tests that run against the BUILT .app and exercise the actual spawn (no stubs), plus a static check that no Mac source walks parent dirs to find spawn-target scripts.

Regression nets added (~1300 lines of test, 0 mocks)

  • tests/mac_first_run_smoke.test.js (8 cases): bundled uv + tts_kokoro.py on a brew-free PATH; built .app resolves spawn targets without dev-tree fallbacks; first-run on a clean machine.
  • tests/no_console_log.test.js (3 cases): no console.log slips into lib/, bridge/, or sources/.
  • tests/redact_parity_python.test.js (3 cases): tts_kokoro.py:_REDACT_KEYS and lib/log.js:REDACT_LEAF_KEYS agree.
  • tests/source_truncation.test.js (8 cases): file-truncation contract pinned across claude-code/codex/gemini/aider readers.
  • tests/ipc_sender_robustness.test.js (4 cases): connect-failure, server-immediate-close, oversized payload paths.
  • tests/bridge_sessions_endpoint.test.js (4 cases): /sessions envelope shape, provider-throws, null returns.
  • iphone/aiTTSTests/AuditFindingsTests.swift (20 cases): force-quit pairing survival, real Bonjour discovery, PrivacyInfo.xcprivacy parity, deployment target, accessibility labels, view crash-rendering smoke.
  • web/tests/download-dmg-version.test.ts (10 cases): path-traversal, CRLF, oversized DoS, SQLi shapes on /download/dmg/[version].
  • web/tests/webhook-replay.test.ts (3 cases): Stripe replay-attack, missing signature, DB-outage idempotency.
  • web/tests/portal-me.test.ts (5 cases): cross-tenant isolation.
  • web/tests/magic-send-rate-limit.test.ts (8 cases): both rate-limit buckets, case-folding.
  • web/tests/checkout-rate-limit.test.ts (4 cases).
  • web/tests/appcast-escaping.test.ts (7 cases): <, &, emoji, multi-]]>.

Plus iOS bug fixes (TestFlight / App Store): Switch Mac dropped per-pairing nonce → fixed. MacStore.remove leaked LegacyPairingNotice flag → fixed.

Plus web fix: /download/dmg/[version] now SemVer-whitelists before any GH API call.

Test totals

  • Mac DMG ./scripts/ci.sh: 450 tests / 448 pass / 0 fail / 2 skip.
  • iOS XCTest: 167 / 167 / 0 / 1 skip.
  • Web Vitest: 281 / 281 / 0 / 1 skip.

Notes

Three deferred items documented in /tmp/mac-audit-findings.md (ctl.js dev-tree resolver, voice preview tooltip-only error, AppCoordinator runCtl boot errors). Each needs design work beyond a single audit pass. Tracking, not blocking.

1.4.3

1.4.3

Voice preview + Kokoro narration work on a fresh DMG with no setup. Period.

What's new

  • Bundled uv (universal arm64+x86_64, ~100 MB) inside the .app at Contents/Resources/bin/uv. Kokoro spawn now invokes <bundled-uv> run --script tts_kokoro.py directly instead of relying on the script's #!/usr/bin/env -S uv run --script shebang resolving uv from the user's PATH.
  • DMG is now ~107 MB (was ~7.7 MB on 1.4.2). The trade: zero install steps for default TTS. No more "preview failed" tooltip on every voice for users without brew install uv.

How this closes the dev-vs-installed gap

1.4.2 fixed the path resolution bug (3 call sites resolving spawn targets via parent-of-bundle). 1.4.3 fixes the implicit dependency on uv being on the user's PATH. Together they make voice preview + Speak Services + image-describe work end-to-end on a clean Mac with nothing installed.

Tests

  • Extended TTS_SCRIPT_RESOLVER_SELFTEST (build-gating in build.sh): now also asserts Bundle.main.resourceURL/bin/uv exists, is > 5 MB, and --version exits 0 with the expected output.
  • Extended tests/script_bundle.test.js: built .app ships universal uv at Resources/bin/uv, executable, lipo-info confirms arm64 + x86_64, smoke --version succeeds.

Notes

The Kokoro model itself (~300 MB) still downloads on first synth via huggingface_hub (uv handles this transparently). Bundling the model is a separate ticket. With this release, that download is the only first-use friction point — and uv reports progress in the spawned process's stderr instead of failing silently.

1.4.2

1.4.2

Voice preview + Speak Services actually work for installed users now. (1.4.1 silently failed for everyone who didn't install via git clone.)

Bug fixes

  • Settings → Voice preview button no longer shows the orange ⓘ on every voice for installed users. VoicePreviewController was resolving tts_kokoro.py by walking four directory levels above aiTTS.app, which only worked when the bundle lived at <repo>/nowplaying_player/aiTTS.app/. Any user who dragged the DMG to /Applications got a path that resolved to /tts_kokoro.py and failed to spawn. Same bug class also broke the Services menu Speak action and the image-describe fallback. Three call sites total.

  • Same bug in Speak.swift's kokoroScript and ttsJsPath. Now centralized through a new ScriptResolver that searches inside the .app first, then ~/.claude/tts/, then the dev tree. Bundled scripts ship inside aiTTS.app/Contents/Resources/scripts/.

Tests

  • New TTS_SCRIPT_RESOLVER_SELFTEST (in build.sh's SELFTESTS) asserts the resolver returns a path inside the bundled .app, not the dev fallback. Build fails if the bundling step regresses.
  • New tests/script_bundle.test.js asserts on every CI run that the .app ships tts_kokoro.py + tts.js in Contents/Resources/scripts/, with the bundled file SHA matching the repo source.
  • Plus a static check that no Mac source file walks parent dirs to find these scripts anymore.

Notes

The voice preview UX still needs uv on the user's PATH for actual Kokoro synthesis (the bundled script's #!/usr/bin/env -S uv run --script shebang). Bundling uv itself is a separate ticket. With this release the path resolves cleanly; if uv is missing the error is now visible in the row's tooltip ("Preview failed: …") instead of being a totally silent dev-machine-only success path.

1.4.1

1.4.1

Settings sheet on Mac. The menu bar dropdown is now half its previous size.

What's new

  • Settings… sheet absorbs the wide menu items (Pair iPhone, Gemini API Key, Connect to Claude Code, Speak hotkey, Start at login, log file access, diagnostics export). Menu bar is now playback + sliders + Voice/Player/Persona + Captions + Search + Dictation + Settings + Check for Updates + Quit. Cut from 17 menu items to 8.
  • iOS 17 onChange API migration under the iPhone companion (Xcode 26 toolchain). No user-visible change; resolves a deprecation warning that would have broken on a future SDK bump.
  • xcuserdata gitignore so per-user Xcode UI state stops landing in diffs.

Hardening

Same release window also pulled in:

  • iPhone Settings screen + multi-Mac storage with v1 migration
  • iPhone QR pairing (camera-based)
  • iPhone Live Captions screen (scrolling silent transcript)
  • Per-session subscription via /sessions + ?sessionId
  • iPhone host-pinned bearer token (per-pairing nonce)
  • Multi-session picker

Notes

If you hit "menu bar feels cluttered" with 1.4.0, this is the fix. Sparkle picks up the update within ~60 s now (down from 5 min after the appcast TTL fix).

1.4.0

1.4.0

Self-contained dictation, in-app cloud TTS setup, an iPhone pairing UI that doesn't make you drop into a terminal, and a wide hardening pass.

Highlights

  • Dictation works on a fresh DMG with zero install. whisper.cpp + the ggml runtime now ship inside the .app as a universal binary (arm64 + x86_64). No brew install whisper-cpp step. Drop the model via the existing "Download model" button and you're done. vendor/whisper-cli/LICENSES.md ships the upstream MIT attribution.
  • Pair iPhone, in the menu bar. New "Pair iPhone…" item opens a brand-styled sheet showing a 6-digit code with a live countdown and an auto-dismissing "Paired" confirmation. Replaces the old tts pair-iphone CLI.
  • Cloud TTS without GOOGLE_API_KEY in your shell. New "Gemini API Key…" sheet stores your own key in macOS Keychain (per-device, only-when-unlocked). aiTTS doesn't ship with one. tts.js falls back to the Keychain entry when the env var isn't set.
  • Kokoro Core ML engine ported to Mac. Engine + manifest + store land in nowplaying_player/, gated on a vendor/kokoro-coreml/ artifact that the converter still has to produce. Default TTS path is unchanged; this is plumbing for offline Kokoro after the converter unblocks.

Hardening

  • IPC server: byte cap, peer auth, schema v1.
  • Now Playing: drain stops the lock-screen card cleanly; remote commands wired.
  • SSE replay buffer for iPhone reconnects.
  • Bridge bearer-token lifecycle.
  • Web auth + license rate-limit + audit hardening.
  • 5 iPhone correctness fixes.
  • MenuBarController.exportDiagnostics: pipe-deadlock fix + NDJSON re-redact on copy.
  • daemon: bounded narratedIds Set (50K LRU).
  • Supabase RLS enabled on the right tables.
  • Magic-verify CSRF + idempotent welcome email + welcome-page email-leak fix.
  • O_EXCL daemon lock + mkdtemp temp paths + redact-list parity across JS surfaces.
  • Licensing heartbeat respects revoked + refunded; lastEpoch only updates on success.
  • state.json cross-process lock (JS + Swift).

Build / CI

  • New scripts/ci.sh is the single source of truth for the full local + CI gate. .github/workflows/ci.yml is workflow_dispatch only while we pay down macOS-minute spend; same for release.yml. Run ./scripts/ci.sh locally.
  • Sparkle cache version pin + EdDSA / CFBundleIdentifier contract pins.

Notes

  • Self-contained DMG: still no Kokoro model bundle. Default Kokoro TTS path uses tts_kokoro.py via uv until the Core ML converter unblocks.
  • Auto-update via Sparkle: no contract changes; existing 1.x installs pick up 1.4.0 within ~5 minutes of publish.

1.3.4

1.3.4

UX fix: the menu bar actually works on first launch.

Bug fixes

  • First-run "Connect to Claude Code" no longer auto-opens. It used to pop up uninvited every launch (until detection happened to succeed), which felt invasive on a fresh install. The window is now user-invoked only via the menu bar's "Connect to Claude Code…" item.
  • Quit Service / every other menu action fires reliably. The connect sheet was presented via NSApp.runModal(for:), which blocks the entire app event loop. While that window was up, no menu bar action could fire — Quit included. The sheet now uses makeKeyAndOrderFront so the menu bar keeps working alongside it.
  • Connect sheet's "Skip" / "Done" buttons close the window. They didn't on the new non-modal path until this release.

Notes

If you hit "the menu bar items don't do anything" with 1.3.3, this fixes it. Sparkle picks up the update within ~5 minutes.

1.3.3

1.3.3

Fixes the "I downloaded the app and it won't open" experience.

Bug fixes

  • Double-clicking aiTTS now actually starts it. Previously a no-args launch (Finder, Dock, Applications double-click) tried to send a "search palette" command to a running service and exited 1 if no service was running. So a fresh install looked dead. The no-args path now starts the service when none is reachable, and only sends the palette command when a service already exists.
  • Quit Service / SIGTERM clean-exit reliably. The signal-handler ordering was inverted: dispatch sources for SIGTERM and SIGINT were armed before signal(_, SIG_IGN), leaving a window where the kernel's default terminate action could fire before the handler. Quit Service now uses NSApp.terminate(nil) for proper Cocoa shutdown with exit(0) as belt-and-suspenders.
  • Offline first-launch works. The release pipeline now stapled both the inner .app bundle AND the outer DMG. Previously only the DMG was stapled, so an offline user who dragged the .app to Applications hit Gatekeeper's online check.

Notes

If you hit the "won't open" issue with 1.3.2, this update fixes it. Existing installs auto-update via Sparkle within ~5 minutes; otherwise download fresh from https://aitts.dev/download.

1.3.2

1.3.2

Menu-bar polish + a long-standing duplicate-instance bug.

What's new

  • License submenu actions actually fire. "Buy a license", "Enter License", "Open portal", and "Deactivate this device" all work from the menu bar now. Previously they looked enabled but did nothing.
  • Trial label deduped. The submenu used to repeat "Trial: X days remaining" right under the parent "Trial: X days left". Cleaner now.
  • "Connect to Claude Code" copy clarified. The loading state now reads "Checking Claude Code plugin..." instead of the ambiguous "Looking for Claude Code...".

Bug fixes

  • No more duplicate menu-bar icons. A flock-based single-instance guard refuses to start a second --service process against the same user. If the daemon or a login item double-spawns, the duplicate exits cleanly with one stderr line.
  • License entry sheet always lands on top. LSUIElement apps need explicit activation before a modal sheet; previously the sheet could land off-screen on first invocation.

Notes

If you previously installed v1.3.1 or earlier, Sparkle will pick up this update automatically. Otherwise grab the installer at https://aitts.dev/download.

1.3.1

1.3.1

Hardening pass on top of 1.3.0.

  • Slim down the hardened-runtime entitlements set. Drop com.apple.security.cs.allow-jit, com.apple.security.cs.allow-unsigned-executable-memory, and com.apple.security.automation.apple-events — none of them apply to aiTTS. Keep com.apple.security.cs.disable-library-validation (Sparkle) and com.apple.security.device.audio-input (dictation). Tighter contract with macOS for the same behavior.
  • Release pipeline: idempotent version bump (skip the no-op commit when the branch already has the right VERSION) and reorder the appcast insertion to land after the GitHub Release is published, so a remote rebase mid-release can't strand the appcast in a dirty working tree.
  • CI now runs the web component test suite alongside the existing root tests.

1.3.0

1.3.0

Notarized installer, redesigned account portal, new app icon.

  • Installer is now signed with a Developer ID and notarized by Apple. macOS opens it on a normal double-click; the "Apple could not verify" dialog is gone.
  • New /download landing page walks through Open / Drag / Launch and includes a fallback unblock path for older builds.
  • Account portal (welcome, sign-in confirm, account, devices) rebuilt to match the in-app design: amber-on-slate, glass cards, copyable license keys, relative-time device list with deactivate.
  • New app icon: amber waveform on slate, matching the new player UI.

1.2.0

Speak selection / clipboard / right-click

New entry points for TTS, no Claude session required:

  • Right-click → Services → Speak with TTS in any app with a text selection (Cursor, Mail, Safari, Notes, Slack, Terminal, etc.).
  • ⌥⌘S (configurable) speaks the selection, or falls back to the clipboard if nothing is selected.
  • node ctl.js speak subcommand for scripts: speak <text>, speak --file <path>, speak --clipboard.

Clipboard handling supports text, file paths from Finder (md/txt/pdf), and images. Images run through Vision OCR locally first; if there's no readable text, the image is sent to Gemini 2.5 Flash (GOOGLE_API_KEY) or Claude Haiku (ANTHROPIC_API_KEY) for a 2-3 sentence description.

The hotkey picker (menu bar → Speak hotkey…) shares the recording UI with the Dictation hotkey via a Dictation/Speak segment.

1.0.12

Remove Browse Sessions window; search palette is now the only session UI (attach-only). Dictation via fn key is live.