Improve microphone routing and app guidance - #1
Draft
antonynjoro wants to merge 7 commits into
Draft
antonynjoro wants to merge 7 commits into
antonynjoro wants to merge 7 commits into
Conversation
Two field bugs, one root: missed/misread trigger-key release. - Fn/Globe: macOS swallows the key-up (globe action), the listen-only tap never sees the release, and the app stays stuck in .recording — the next tap then pastes the previous recording's audio. - Right Shift: CGEventSource.keyState() misreports held modifiers (reads "up" mid-hold), so the earlier watchdog draft force-stopped recording after its first poll. The watchdog now polls CGEventSource.flagsState() — the session's live modifier flags, the same domain as the flagsChanged events the tap consumes. Each TriggerKey carries its family mask (maskShift, maskSecondaryFn, ...) for that check. Held key keeps its flag set (no false stop); a swallowed release still clears the flag, so the watchdog ends the recording within ~0.12s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stuck-recording report turned out flaky and the release build is unobservable: every pipeline breadcrumb was Log.debug, compiled out of release, so 'log show' had nothing. Three changes: - Log.info: always-on NSLog breadcrumbs (unified log) for press/release/ watchdog/timeout, audio + inference durations, and paste outcome. Transcript content stays debug-only; release logs lengths, not text. - ParakeetTranscriptionEngine.transcribe now runs under a 30s hard deadline. A hung ANE call surfaced as indefinite .processing — the UI sat on 'transcribing' forever and silently dropped the next press. Now it errors and the state machine recovers. - A trigger press while still .processing plays the Basso error sound. Previously the press was swallowed silently, the user spoke into a dead mic, and the late-arriving previous transcription pasted 'on its own' — the exact reported symptom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd on all not-ready presses
Field capture 2026-07-03 15:44 confirmed two gaps:
- NSLog dynamic content is redacted to <private> in the unified log even
with %{public}@ — that specifier is honored by os_log only. Log.info/
Log.error now use os.Logger with privacy: .public interpolation, so
'log show --predicate' finally yields readable breadcrumbs from a
release build.
- The captured incident was a press swallowed ~40s after relaunch while
the model was still loading — a state the Basso feedback didn't cover
(it was gated to .processing). The busy sound now plays for every
not-ready press, so a dead-feeling hotkey is always audible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field-captured root cause of the flaky stuck-recording / stale-paste
bug (breadcrumbs, 2026-07-03 15:53): resetToIdle schedules an
unconditional success → idle transition ~2s after a paste. Pressing the
trigger again inside that window went success → recording, then the
stale timer fired and forced recording → idle. On release,
beginProcessing's 'are we recording?' state check failed, so the mic
was never stopped — the session kept capturing and merged into the next
tap's audio, which then pasted both utterances late ('pastes what I
said earlier'). Reproduced only when the next press came within ~2s of
the previous paste, hence the flakiness.
- resetToIdle now captures the state it was scheduled from and no-ops
if the state has moved on by the time the timer fires.
- beginProcessing gates on the recorder's own isRecording instead of
the display state, so a missed stop can't recur even if some future
transition knocks the state machine out of .recording mid-capture.
- Regression test: resetToIdle must not stomp a recording that started
inside the delay window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Settings Clicking the Accessibility step's button fired the AX prompt and opened the Settings pane in the same breath. The pane landed on top (hiding the OS 'ZeroG would like to…' dialog) and could render before the prompt's pre-listing registered — users arrived at an Accessibility list with no ZeroG row to toggle. First click now fires only the native prompt; its own Open System Settings button lands on a pane where ZeroG is already listed. macOS shows that prompt at most once per app record, so subsequent clicks fall through to the existing request-then-deep-link path, with the button retitled accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why
A macOS call could leave the long-lived audio engine with an invalid input route, producing a "No microphone available" error. ZeroG also followed the system default input, so connecting a Bluetooth headset could unexpectedly select its microphone and degrade playback quality.
Impact
ZeroG now defaults to the built-in Mac microphone, remembers explicit external choices, falls back safely when a selected device disappears, and retries a stale route with a fresh AVAudioEngine capture session.
Validation