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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
* 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
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.
Multichannel input devices and video-call apps can expose the microphone as a multi-channel stream. Explicitly route channel 0 into Hex's mono converter and restart the capture engine when AVAudioEngine reports a configuration change, avoiding all-zero capture buffers and hallucinated transcripts like "Thank you."
Fixes#204. Related to #206.
Hex currently preloads and starts its sound-effects AVAudioEngine on launch, even when sound effects are disabled. That keeps an unnecessary Core Audio output assertion alive for the app lifetime.
This change keeps the low-latency path for users who have sound effects enabled, but avoids starting the engine when the feature is disabled and shuts it back down if the setting is toggled off.
Validation:
- cd HexCore && swift test
- xcodebuild -scheme Hex -configuration Release CODE_SIGNING_ALLOWED=NO build
- xcodebuild -scheme Hex -configuration Debug CODE_SIGNING_ALLOWED=NO build
- runtime check: patched debug app launched with sound effects disabled did not create a new audio-out sleep assertion
Note: xcodebuild test still fails in this repo because HexTests TEST_HOST resolves to Hex.app while Debug builds produce Hex Debug.app.
The Xcode 26.4 toolchain no longer accepts mutating shared settings directly through view bindings in these settings and remapping screens. Route those controls through explicit reducer actions so the release archive can compile cleanly with the newer TCA/runtime combination.
Verified by producing a fresh Release archive at build/Hex.xcarchive.
Release archives were failing in swift-composable-architecture because the current pinned revision still used non-sendable writable key paths under this Xcode toolchain. Pin to upstream commit 055ca45, which contains the Xcode 26.4 compatibility fix, alongside the existing Pow pin so release builds can complete again.
Verified by resolving packages and producing a fresh Release archive at build/Hex.xcarchive.
Release archives started failing inside Pow 1.0.5 with an ambiguous use of .pi in Anvil.swift on the current Xcode toolchain. Pin the package to upstream commit 1b4b1dd, which contains the minimal fix, so Hex can archive again without changing app behavior.
Verified by resolving packages and producing a fresh Release archive at build/Hex.xcarchive.
Double-tap lock could leave the UI recording after a short-recording cleanup from the first tap arrived after the second tap had already started a new session. Cancel stale cleanup work, ignore delayed capture-engine stops once a newer session exists, and add regression coverage around the reducer/client boundary. Fixes#193.
HexCore swift tests pass. App-level xcodebuild remains blocked locally by the IDESimulatorFoundation plugin failure in this Xcode install.
Base capture-engine stop finalization on the observed callback cadence so Hex can keep the last in-flight audio without always paying the full worst-case delay. Also move the stop chime until after capture finishes so the grace window does not risk recording Hex's own feedback.
Use the capture engine for standard recordings so both recording modes share the same restart and telemetry path while keeping AVAudioRecorder only as a fallback.
Also keep the microphone picker visible, add a stable unavailable-device fallback row, and refresh the device list on wake, app activation, and Core Audio device changes so dock and interface swaps stop leaving the menu blank or stale.
Detect when Hex starts as a login item and skip opening windows or activating the app when the Dock icon is hidden.
This also limits app-mode updates to the Dock Icon setting itself so ordinary settings edits stop bouncing the settings window out of the active state.
Restart the warmed recording path when the Mac wakes or the active audio route changes, and defer that rebuild until stop if a take is already in progress.
This also adds startup and first-buffer telemetry so we can see idle time, device selection, restart reasons, and capture latency in the unified logs while tuning the fast-start path.
Unify settings hotkey capture, remove duplicate model refresh work, and centralize Hex storage paths in HexCore so settings, history, and model management reuse the same helpers.
This trims repeated file-management code, makes Parakeet checks typed instead of string-based, and keeps cleanup behavior consistent without touching the in-flight recording changes.
Tighten the warm capture window and clear the in-memory ring buffer when a recording finishes so the previous tail does not leak into the next take.
This keeps the fast-start path while reducing repeated last-word artifacts during local testing.
Add an opt-in Super Fast mode that keeps live capture warm with a short in-memory preroll and falls back to the existing recorder path when needed.
This targets the first-word clipping that shows up after Hex has been idle for a while, and exposes the tradeoff clearly in Settings so users can choose instant capture when they want it.
Adds a settings switch to disable double-tap lock behavior while keeping press-and-hold recording available. Also normalizes persisted settings so double-tap-only cannot remain enabled when lock is off, with regression tests for migration and decode paths.
Users can now use \n, \t, \r, and \\ in replacement strings to insert
newlines, tabs, carriage returns, and literal backslashes. This enables
voice commands like "new line" → \n without requiring UI changes.
Adds 10 tests covering escape sequence processing and edge cases.
`Task.sleep(nanoseconds: .max)` returns immediately instead of sleeping
indefinitely. This broke the cancellation handler mechanism, causing hotkeys to hang.
Demonstration of sleep(.max) that returns immediately.
```bash
% swift -e '
import Foundation
func timestamp() -> String {
let formatter = DateFormatter()
formatter.timeStyle = .full
return formatter.string(from: Date())
}
Task {
// Test 1: Normal sleep
print("=== Task.sleep(for: .seconds(5)) ===")
print("Before: \(timestamp())")
do {
try await Task.sleep(for: .seconds(5))
print("After: \(timestamp())")
} catch {
print("Error: \(error)")
}
// Test 2: Max sleep
print("\n=== Task.sleep(nanoseconds: .max) ===")
print("Before: \(timestamp())")
do {
try await Task.sleep(nanoseconds: .max)
print("After: \(timestamp())")
} catch {
print("Error: \(error)")
}
}
RunLoop.main.run()
' 2>&1
=== Task.sleep(for: .seconds(5)) ===
Before: 9:55:53 PM Korean Standard Time
After: 9:55:59 PM Korean Standard Time
=== Task.sleep(nanoseconds: .max) ===
Before: 9:55:59 PM Korean Standard Time
After: 9:55:59 PM Korean Standard Time
```
The solution: Replace the problematic sleep pattern with a proper cancellation loop that
polls every 60 seconds, ensuring the cancellation handler is actually invoked when the
task is cancelled.
commented out 'defer { token.cancel() }' is now revived.
The icon from PR #148 wasn't showing because:
1. ASSETCATALOG_COMPILER_ICON_NAME should be ASSETCATALOG_COMPILER_APPICON_NAME
(the APPICON variant tells Xcode to pass --app-icon to actool)
2. The .icon file was in Copy Bundle Resources but should only be compiled by actool
3. Info.plist needed CFBundleIconFile and CFBundleIconName entries
Fixes the Icon Composer integration so the app icon renders correctly on macOS Tahoe.
Reworks the remappings screen into a scrollable, tabbed layout with clearer rows, applies word removals in transcription, and removes the broken log export settings. Includes new defaults, tests, and changeset.
- Add mach-lookup entitlements for Sparkle XPC services
- Add SUEnableInstallerLauncherService to Info.plist
- Add disable-library-validation entitlement
The 0.6.3 release broke Sparkle updates by enabling sandboxing without
the required XPC entitlements. Users on 0.6.3 will need to manually
download this update, but future updates will work automatically.
Discovered that macOS can have microphones muted at the Core Audio device
level (kAudioDevicePropertyMute) without any UI indication. This caused
recordings to be completely silent, and only a system restart would fix it.
Added detection and auto-fix:
- isInputDeviceMuted() checks device-level mute state
- unmuteInputDevice() fixes it via Core Audio API
- ensureInputDeviceUnmuted() called before each recording
Also fixed Swift 6 Sendable warnings in KeyEventMonitorClient by marking
handler closures as @Sendable.
- Delete LLMExecutionClient, ToolServer, and all provider code
- Remove TextTransformationFeature and related UI
- Add simple WordRemapping model and settings UI
- Clean up 3800+ lines of unused experimental code
- Display actual device name in "System Default (MacBook Pro Microphone)" picker
- Fix Parakeet polling cleanup with defer
- Refactor paste strategies using CaseIterable enum
- Encapsulate pasteboard state in PasteboardSnapshot struct
Root cause: setInputDevice() was called on every recording start,
even when the device was already set correctly. This caused:
1. coreaudiod instability over time (mic freezing system-wide)
2. Primed recorder becoming stale (speech cutoff at start)
Changes:
- Add getDefaultInputDevice() to query current input device
- Track lastPrimedDeviceID to know what device recorder was primed for
- Only call setInputDevice() when target differs from current default
- Invalidate primed state when device changes, forcing re-prime
- Add cleanup() method and call it on app termination
Now custom mic users get fast recording on 2nd+ recordings (device
already set), and system stays stable over extended use.
Sequoia hotkey fix:
- Remove Input Monitoring guard blocking CGEventTap creation
- Tap creation triggers permission prompt naturally on macOS 15
- Re-add "force quit Hex now" voice escape hatch (#122#124)
Model missing UX:
- App activates and switches to settings when no model available
- Blue "Download a model" banner flashes to draw attention
- Added logging for debugging model missing flow
Settings improvements:
- Hide Permissions section when all permissions granted
- Add "BEST FOR ENGLISH/MULTILINGUAL" badges to Parakeet models
- Update Parakeet TDT v2 display name (remove parenthetical)
- Fix unused provider variable warning
LLM provider simplification:
- Remove "Preferred Provider" picker from Settings UI
- Remove preferred provider fallback logic in backend
- Transformations now strictly use configured providerID
- Throw error if provider not found instead of silent fallback
- Simplify LLMExecutionClient by removing ProviderResolution
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The voice prefix matcher now properly handles common punctuation marks
(.,;:!?) that may appear after spoken prefixes. Previously, only commas
and whitespace were recognized, causing transcriptions like "Shakespeare,
I have to take out the trash" to fail matching when "Shakespeare" was
configured as a voice prefix.
Updated the regex pattern in modeByVoicePrefix to accept any combination
of common punctuation and whitespace after the prefix word. Added
comprehensive test coverage for various punctuation scenarios.
The fix ensures natural speech patterns with pauses (transcribed as
punctuation) don't break voice prefix detection, improving the user
experience for mode activation.
Adds optional `tooling` field to `LLMTransformationConfig` so each LLM transformation can specify its own MCP tool groups. LLMExecutionClient prefers transformation-level tooling, falls back to provider-level. Migrated Hex Command mode to use transformation-level tooling (app-control, app-discovery, context), removed global provider tooling.
- Stop monitoring when Input Monitoring missing instead of continuing with Accessibility-only
- Track permission state via shared HotkeyPermissionState model
- Add UI banner in Settings when Input Monitoring denied with button to open System Settings
- Check both accessibility AND input monitoring before starting event tap
- Replace AVAudioPlayer cache with single AVAudioEngine using pre-loaded buffers
- Eliminates delayed playback after idle periods
- Re-encode startRecording.mp3 for new playback graph
- Maintain user volume scaling
Use AXIsProcessTrustedWithOptions throughout event-tap lifecycle to properly
handle macOS 15's split Accessibility/Input Monitoring permissions. Add
NSInputMonitoringUsageDescription so system shows dedicated Input Monitoring
dialog. Trust watchdog now polls same API Settings UI uses, ensuring runtime
checks stay aligned with Sequoia 15.7.1.
Add soundEffectsVolume setting with slider UI in SoundSectionView:
- Add baseSoundEffectsVolume constant (0.2) as 100% reference
- Slider shows 0-100% relative to baseline volume
- Update SoundEffect client to use configurable volume
- Add migration test coverage for new setting
- Include changeset documenting the feature (#000)
- Remove variant-specific recommendation badges (simpler UI)
- Reorder models.json to show English v2 before multilingual v3
- Keep preferredParakeetIdentifier logic for language-based defaults
Document implementation strategy for handling clips shorter than
Parakeet's minimum chunk duration, including research tasks,
touchpoints, and validation approach.
CuratedRow:
- Display custom recommendation text for Parakeet variants
- Show primary badge (filled) for preferred variant based on language
- Show secondary badge (outlined) for non-preferred variant
- Fall back to generic "Recommended" for other models
Localizable.xcstrings:
- Add new localization strings for updated UI
- Remove unused strings ("English only", "Multilingual")
ModelDownloadFeature:
- Add recommendation badge system for Parakeet variants
- Prefer English v2 when output language is English
- Default to multilingual v3 for other languages
- Ensure all Parakeet variants appear in available models list
- Add preferredParakeetIdentifier computed property
SettingsFeature:
- Clear currentPasteLastModifiers when starting hotkey capture
SettingsView:
- Minor whitespace cleanup
Introduce ParakeetModel enum to manage v2 (English) and v3 (Multilingual):
- Add ParakeetModel.swift with variant metadata and capabilities
- Update ParakeetClient to accept model name and load correct variant
- Map ParakeetModel to AsrVersion (v2/v3) for FluidAudio
- Refactor isModelAvailable/ensureLoaded/deleteCaches to be variant-aware
- Extract candidateRoots helper for cache detection
- Update TranscriptionClient to iterate all Parakeet variants
- Add Parakeet TDT v2 (English) to curated models list
- Use ParakeetModel.multilingualV3 constant in HexSettings default
Enables users to choose between English-only (v2) and multilingual (v3)
Parakeet models based on their transcription needs.
HistorySectionView:
- Redesign layout to vertical stack for better readability
- Use HotKeyView component instead of custom text rendering
- Add instructional text when setting hotkey
- Improve clear button with label and borderless style
- Show "Not set" placeholder when no hotkey configured
HotKey.swift:
- Remove L/R suffix from modifier stringValue (cleaner display)
- Simplify modifier display logic
Enhance RecordingClient with direct MediaRemote framework integration:
- Add MRMediaRemoteSendCommand function binding
- Implement play/pause/toggle commands via MediaRemote API
- Prefer MediaRemote over simulated key presses when available
- Track pause method separately (MediaRemote vs key press)
- Fall back to media key if MediaRemote fails during resume
Improves media pausing reliability, especially with system media players
and reduces dependency on simulated keyboard events.
Major refactor to handle accessibility permission changes and tap failures:
- Add accessibility trust monitoring task polling every 100ms
- Detect and recover from tap disabled events (timeout/user input)
- Implement token-based handler lifecycle for proper cleanup
- Track monitoring intent separately from actual tap state
- Gracefully suspend/resume tap when permissions change
- Remove modifier diagnostics logging (no longer needed)
Fixes long-standing issues with hotkeys stopping after permission revocation
or system tap disablement (#89, #81, #87).
- Revert to declarative MenuBarExtra (removed broken programmatic approach)
- Fix paste-last-transcript to use .first (history is reversed)
- Start app lifecycle task to enable hotkeys and permission monitoring
- Remove Help menu
- Move HexSettings from Hex to HexCore
- Make HotKey Sendable
- Add custom Codable for backward compatibility
- Add fixtures & tests for v1 migration
- Update Package.swift to copy test fixtures
https://github.com/claude.com/claude-code
- Play start sound immediately before recording setup
- Make media pause/resume non-blocking (run in background)
- Remove cancel sound from transcription errors (UI error msg sufficient)
Recording now starts ~200-700ms faster, sound feedback is instant.
- Modifier model tracks kind + side (either/left/right) instead of simple enum
- HotKeyProcessor matches events by side preference
- KeyEventMonitorClient captures left/right state via NX_DEVICE masks, tracks Fn separately to eliminate phantom fn on arrow keys
- Settings UI: segmented controls for modifier side, arrow glyphs (←→↑↓)
- Menu bar respects new modifier model
- Diagnostic logging gated behind HEX_DIAG_MODIFIERS=1
Extract IOKit sleep prevention into SleepManagementClient in HexCore:
- Create SleepManagementClient with named arguments for clarity
- Actor-based live implementation manages assertion lifecycle internally
- Update TranscriptionFeature to use client instead of direct IOKit calls
- Remove IOKit imports and assertion state from TranscriptionFeature
- Remove 26 lines of private sleep prevention functions
Benefits: testable, no state leaks, cleaner feature code, encapsulated IOKit complexity
Extract permission handling into clean PermissionClient in HexCore:
- Create PermissionClient with async API for mic/accessibility permissions
- Implement live actor-based implementation with app activation monitoring
- Integrate into AppFeature with reactive permission updates
- Refactor SettingsFeature to use PermissionClient directly (no delegation)
- Update views to receive permission state as props
Benefits: testable, decoupled from platform APIs, eliminates feature coupling