Compare commits

...

26 commits

Author SHA1 Message Date
Kit Langton
3e300721b8 docs: point to the Rust rewrite at anomalyco/hex 2026-10-14 14:42:37 -07:00
Kit Langton
f58d52c4cc Release 0.8.5 2026-08-26 23:47:08 -04:00
Kit Langton
df37c99b7c feat: point legacy Hex users to the Rust beta
Add a dismissible Settings banner and a persistent About link to hex.kitlangton.com, without installing or migrating anything. Keep the announcement limited to supported Macs and add isolated native screenshot coverage.

Update the legacy repository README to direct new development to anomalyco/hex while preserving Swift history and downloads. Closes #291.
2026-08-26 23:35:40 -04:00
Kit Langton
881c46f236 Release 0.8.4 2026-07-19 17:00:33 -04:00
Kit Langton
9c597cb77d Prevent first-launch crash during hotkey setup
Hex 0.8.3 added a stale-TCC fallback that synchronously read KeyEventMonitor state while the first handler registration barrier already owned the same dispatch queue. When Input Monitoring was not yet granted, libdispatch trapped on the recursive sync and crashed fresh installs during launch.\n\nMake monitor state reads detect queue re-entry, keep external reads synchronized against barrier writes, and snapshot event handlers only after entering the queue. Inject the system permission probes so the denied-permission registration path has a deterministic regression test.\n\nFixes #254\nFixes #258\nFixes #265
2026-07-19 16:13:13 -04:00
Kit Langton
ca96427990 Harden audio lifecycle changes after review: quit deadlock, lock-state tracking, rebuild gating
Review fixes for the audio engine lifecycle overhaul (8739929), addressing
findings from a three-way simplification/correctness/efficiency review.

Correctness:

- applicationWillTerminate pumps the main run loop while waiting for
  recording.cleanup() instead of blocking on the semaphore outright.
  cleanup() hops to the main actor (media-key resume) and the main queue
  (Core Audio listener removal), so the blocking wait would have
  deadlocked and hit its 3s timeout in exactly the quit-after-recording
  scenario the #245 fix targets, exiting with teardown incomplete.
- Warm-capture suspension is now tracked as a Set<CaptureSuspensionSource>
  (systemSleep / displaySleep / screenLock) instead of one bool. This
  fixes two leaks of the warm mic at a locked screen: wake notifications
  fire while the screen is still locked (the bool version resumed
  immediately; now capture stays suspended until unlock removes the last
  source), and locking during an active recording previously never set
  the flag (now the fact is always recorded and
  finalizeCaptureStateAfterRecording re-suspends once the recording
  stops).
- Mid-recording engine rebuilds are gated on the input device actually
  changing or the engine having died (configuration change). Output-only
  device changes (plugging in headphones) previously triggered
  restartPreservingRecording and caused an avoidable audio gap in active
  dictations.
- startRecording cancels any pending environment-change debounce task;
  a device event within 250ms of the hotkey press would otherwise fire
  into the new recording and force a pointless mid-recording rebuild.

Efficiency:

- Idle rebuilds are skipped when the engine is already running on the
  resolved input device. Bluetooth connects fire 3-5 notifications over
  1-2 seconds (devices-changed, default-input-changed,
  default-output-changed, ...); the first rebuild settles the engine and
  the rest now no-op instead of causing repeated teardown+rebuild cycles.
  This also absorbs the wake+unlock double-resume.

Simplification:

- Shared tearDownWarmCapture() replaces two divergent suspend branches.
- handleCaptureEnvironmentChange is synchronous (no awaits), documenting
  that its recording-state snapshot cannot go stale via actor reentrancy.
- SuperFastCaptureController.stop() does one processingQueue hop via
  detachEngine(clearingRecordingState:) instead of two.
- SoundEffect drops the isEngineRunning flag that shadowed
  engine.isRunning, logs shutdown honestly, and no longer prewarms an
  engine on enable that the idle timer would stop 10 seconds later.
2026-07-09 16:22:50 -04:00
Kit Langton
8739929294 Overhaul audio engine lifecycle: route-change recovery, sleep suspension, teardown fixes
Addresses the audio-engine bug cluster (#209, #218, #226, #245, #246,
#251, #252) while keeping Super Fast Mode's warm-microphone design intact.

Root problems, all variations of 'the engine outlives the world it was
built for':

1. Route/device changes while idle only *deferred* the engine rebuild to
   the next recording, leaving the stale engine running against the old
   route indefinitely. That kept coreaudiod churning while idle (#209)
   and made the first post-change recording unreliable.
2. Route/device changes *during* a recording deferred entirely: the
   recording kept 'running' while the dead engine captured nothing,
   producing empty transcripts after Bluetooth connects, A2DP<->HFP
   switches, and AirPods handoffs (#251, #252, #218, #226).
3. App-quit teardown was a fire-and-forget Task racing process exit,
   with SuperFastCaptureController.deinit as a second competing teardown
   path - the EXC_BAD_ACCESS in AVAudioEngine teardown (#245).
4. The sound-effects output engine (isAutoShutdownEnabled = false) ran
   forever once started, keeping an output IOProc alive even in standard
   mode.

Fixes:

- SuperFastCaptureController gains restartPreservingRecording(reason:),
  which rebuilds engine/tap/converter while keeping the active
  AVAudioFile open, so mid-recording route changes resume capture onto
  the same file (small gap) instead of silently losing the rest of the
  take. startIfNeeded/stop are refactored around shared
  armEngine/detachEngine helpers; detachEngine bumps the capture
  generation so in-flight tap callbacks from the old engine are ignored.
- RecordingClient rebuilds the engine immediately (debounced 250ms) on
  idle environment changes instead of deferring; failures fall back to
  the previous deferred-restart marker.
- The warm engine is suspended on screen lock / display sleep / system
  sleep and rearmed on unlock/wake. Nobody dictates at a locked screen,
  the mic indicator no longer glows while locked, and a whole class of
  stale-engine-across-sleep states disappears. Active recordings are
  never interrupted; an explicit recording request overrides suspension.
  The pre-roll ring buffer is cleared on suspend so no pre-lock audio is
  retained.
- applicationWillTerminate now blocks (3s bound) on recording.cleanup()
  via a detached task + semaphore, making teardown deterministic.
- The sound-effects engine stops 10 seconds after the last play;
  restarting on the next play costs milliseconds.

The persistent mic indicator while unlocked-and-idle (#246) remains
by design when Super Fast Mode is on - that is what keeps the 0.45s
pre-roll available - but it now clears whenever the screen is locked
or asleep.
2026-07-09 14:31:36 -04:00
Kit Langton
2c5d8aea4d Add Whisper Large v3 Turbo to model library, use honest Whisper size names
Adds openai_whisper-large-v3-v20240930_turbo_632MB to the curated model
list (#185): near large-v3 accuracy at several times the decode speed, in
632MB instead of 1.5GB. It slots between Base and Large v3 as the best
accuracy-per-second Whisper option.

Also renames the Whisper entries to their real model sizes: Tiny and Base
were previously displayed as "Whisper Small (Tiny)" and "Whisper Medium
(Base)", which misrepresented which checkpoints they actually are. With
the model library now showing honest install state, the names should be
honest too. Display names are UI-only (selection is stored by
internalName), so existing selections are unaffected. Large v3's accuracy
rating is bumped to 5 stars so the Turbo tradeoff (slightly lower
accuracy, much faster) reads correctly in the list.
2026-07-09 13:18:10 -04:00
Kit Langton
2840b0f511 Release 0.8.3 2026-07-09 12:39:23 -04:00
Kit Langton
224822d51e Fix hotkeys dying from stale permission checks and sleep cycles
Fixes #250, where four reporters on 0.7.6 hit dead hotkeys with symptoms
that fingerprint a stale Input Monitoring TCC grant: modifier-only hotkeys
kept working while keyed hotkeys and the hotkey recorder went dead, often
after sleep or MDM re-login, and toggling the permission in System
Settings did not help.

Root cause was Hex trusting cached permission checks over reality, in two
compounding places in KeyEventMonitorClient:

1. The tap callback dropped events whenever the cached
   IOHIDCheckAccess/AXIsProcessTrusted flags said permissions were
   missing - even for events macOS was demonstrably delivering.
2. desiredMonitoringState() required inputMonitoringTrusted, so the 100ms
   permission watchdog tore down a *working* tap as soon as
   IOHIDCheckAccess returned a stale denial.

macOS withholds keyDown/keyUp from taps lacking Input Monitoring but
still delivers flagsChanged to Accessibility-granted taps, which is why
victims saw modifiers work while keys died.

The fix treats event arrival as the authoritative permission signal:

- A keyDown/keyUp reaching the tap proves Input Monitoring is granted;
  noteEventDelivered() self-heals the cached flag and
  currentInputMonitoringTrust() honors that proof over a stale denial
  (which also keeps the settings permission indicator honest).
- The tap is no longer torn down when the Input Monitoring check reports
  denied; without the permission macOS simply withholds key events, and
  keeping the tap alive is also what triggers the permission prompt.
- The tap is recreated on NSWorkspace.didWakeNotification and
  sessionDidBecomeActiveNotification (matching the reporter whose
  hotkeys died after sleep/MDM logout), and the watchdog re-enables taps
  macOS silently disabled via CGEvent.tapIsEnabled.
2026-07-09 12:31:54 -04:00
Kit Langton
9531c0bbe4 Release 0.8.2 2026-07-09 10:58:06 -04:00
Kit Langton
eae358bbdb Overhaul model library UX and harden model selection state
Model library rows now have explicit Download buttons, a visible menu for
Show in Finder / Remove Download (with confirmation), and clear In Use /
Installed status labels. Row clicks select installed models only.

State hardening behind the UX:
- Availability scans can no longer clear selectedModel to "" on false
  negatives (this hit users after FluidAudio 0.15.5 moved its cache dir:
  the scan came back empty, 0.8.0 wiped the setting, and push-to-talk
  silently did nothing afterward).
- Selection matching is pattern-aware (ModelPatternMatcher.namesMatch)
  instead of exact-ID, so legacy/glob selections resolve correctly.
- downloadModel resolves glob selections to concrete names up front so
  completion updates rows and settings consistently.
- Stopping a recording with no model selected now discards the audio and
  flashes model setup instead of handing an empty model name to the
  transcriber (which produced junk like "[BLANK_AUDIO]").
- The summary card shows an honest orange "Not downloaded" state with an
  inline Download button when the selected model is missing on disk.

Also trims per-tick re-renders of non-downloading rows and redundant
settings writes on repeated scans.
2026-07-09 10:49:57 -04:00
Kit Langton
72e6398b2d Release 0.8.1 2026-07-08 22:25:17 -04:00
Kit Langton
cd971a36ce Reuse Parakeet downloads after FluidAudio upgrade
FluidAudio 0.15.5 changed Parakeet's canonical cache directory by dropping the -coreml suffix. Atomically migrate complete legacy caches into the new location, replacing interrupted partial downloads while preserving both directories if validation fails.
2026-07-08 22:18:07 -04:00
Kit Langton
f5dcba3ea1 Release 0.8.0 2026-07-08 22:03:46 -04:00
Kit Langton
2b5fb1548b Restore downloaded models after restart
Use FluidAudio's version-aware cache APIs instead of guessing its directory layout. Keep a previously completed selection visible while availability loads, and cover the restart state with a regression test.
2026-07-08 14:31:00 -04:00
Kit Langton
10e75b99a6 Simplify output transforms and model suggestions
Restructure the transforms page around a compact preview, word-rule editor, and separate output formatting controls. Suggest English Parakeet for Auto or English output, while retaining multilingual Parakeet for explicitly non-English output.
2026-07-08 14:15:37 -04:00
Kit Langton
8e6e04189f Harden recording and refresh model settings
Make recording stops explicit, reject stale capture callbacks, and clean up captured audio reliably. Replace the old model list with the model library, preserve valid selections across failures, gate download updates by session, and add reducer coverage. Refresh stable dependencies, migrate FluidAudio 0.15.5, restore Xcode test linking, and polish update, changelog, and transform behavior.
2026-07-08 13:30:07 -04:00
Kit Langton
db31b0a31e wip: recording stop-result hardening and model download UI improvements 2026-07-08 12:17:03 -04:00
Ryan Vogel
276da69864
Add lowercase and punctuation paste transforms (#253)
* Add configurable paste formatting transforms

Add independent settings to lowercase pasted transcripts and remove Unicode punctuation. Persist both options, include them in the transforms preview, and apply them after existing word modifications before paste and history storage.

Cover formatting combinations and legacy settings migration with tests.

Opencode-Session: ses_0bd949d00fferT6nCF6ntO80Mp
Opencode-Message: msg_f42760e83001l5zbmkL7mxckWu
Opencode-Agent: build

* Reference PR in paste transforms changeset

Opencode-Session: ses_0bd949d00fferT6nCF6ntO80Mp
Opencode-Message: msg_f4276da02001R7u6klN4JoUXT7
Opencode-Agent: build
2026-07-08 12:07:04 -04:00
Kit Langton
71878b72de
fix(audio): defer capture rebuilds during route changes (#236) 2026-06-04 11:41:15 -04:00
Kit Langton
c00a91d01c
fix: harden microphone and recording reliability (#235)
* fix: harden microphone and recording reliability

* refactor: simplify reliability fixes
2026-06-04 11:35:12 -04:00
Kit Langton
f988cb78c5 Release 0.7.6 2026-05-06 09:03:26 -04:00
Kit Langton
f4cecbbf30 Quiet login launches and reveal Parakeet caches correctly
Two small first-experience fixes that surfaced after 0.7.5 shipped:

- Launching at login no longer pops the Settings window unless the user opens it explicitly. The previous policy only suppressed when the dock icon was hidden, so most 'Open on Login' users got a settings panel in their face every boot. Closes #222.

- 'Show in Finder' on a Parakeet model now opens FluidAudio's on-disk cache at ~/Library/Containers/com.kitlangton.Hex/Data/Library/Application Support/FluidAudio/Models instead of the WhisperKit-only models folder, which made users think their Parakeet download had silently failed. Closes #205.
2026-05-06 08:53:28 -04:00
Kit Langton
f4764d5867 Release 0.7.5 2026-05-03 18:58:19 -04:00
Kit Langton
b5a19b4870 Restore Sonoma support and faster first capture 2026-05-03 18:52:05 -04:00
54 changed files with 3223 additions and 953 deletions