Voice light follows the microphone, not a 180-second guess - #7
Merged
Conversation
The ring lit on the keypress and stayed lit on a timer, because dictation reports nothing back — not Claude Code, not macOS. It still reports nothing, but the microphone does: CoreAudio's "device is running somewhere" property (the truth behind the orange menu-bar dot) says whether anything is actually recording, event-driven, no polling, no mic permission. So the belief is now corroborated. A tap that never started dictation — text in the input box turns the same tap into a typed space — goes dark when a 3-second grace window expires unconfirmed, instead of lying for three minutes. A real dictation goes dark the moment the mic stops, however it was stopped: second tap, Escape, submit. The mic is system-wide, so the conjunction is deliberate — a call alone never lights it, and a call held open past dictation's end degrades to the old bounds, never worse than the belief-only version. VoiceSignal is the pure state machine, clock-injected and tested; MicActivity is the CoreAudio listener pair (running state on the default input device, plus the default-device move that would otherwise strand the first listener on unplugged hardware); BoardController owns the wiring and the repaints. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hardware test surfaced the honest version. Lighting on the tap and giving the mic three seconds to veto still lights the ring for taps that only typed a space — the exact lie, just shorter. So the gate flips: the belief opens a grace window, and the mic starting is what paints. A failed tap never lights at all; a real dictation lights a beat after the tap, when recording actually begins. The grace task becomes bookkeeping — it sweeps an unconfirmed belief and logs that the tap typed a space, repainting nothing because nothing was lit. The space itself is unavoidable from here: the key synthesizes the space that IS the dictation toggle, and whether it toggles or types depends on input state the board cannot see. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Space's dual role is the root of the remaining complaint: with text in the chat input the voice tap types a space and starts nothing. Claude Code's voice:pushToTalk action is rebindable, and a modifier chord invokes it while typing nothing, whatever the input holds. New voiceChord preference (default off — it only works once ~/.claude/keybindings.json binds ⌃Y) switches the voiceTap key from tapping space to tapping the chord. Belief and mic corroboration are unchanged; the log now says which variant fired. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chord preference shipped needing a hand-edit to keybindings.json, which means it shipped for one machine. KeybindingInstall mirrors HookInstall's discipline for the same file-that-is-not-ours reasons: audit is read-only and runs at launch (only when the preference is on — a missing binding then is a dead key), install writes only from the settings toggle, backs up first, and preserves every binding that is not ours. A ⌃Y the user already bound to something else is named and refused, never overwritten. The Device pane grows a "voice key" section: one toggle, the binding written when it flips on, and the same new-sessions-only caveat the hook repair carries — keybindings load when a session starts. 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.
The problem
The voice rainbow lit when the voice key was pressed and turned off on a 180-second timer, not when dictation actually ended. And a tap with text already in the chat input types a space — dictation never starts — but the ring lit anyway.
Dictation is undetectable directly: no Claude Code hook, no state file, and dictated prompts record as
typedin the transcript. The keypress belief is the only way to know recording was asked for.The fix
Corroborate the belief with the one real signal available: CoreAudio's
kAudioDevicePropertyDeviceIsRunningSomewhere— the truth behind the orange menu-bar dot. Event-driven, no polling, no mic permission needed. The ring requires both, and the mic is the half that paints:The space itself is out of reach: the key synthesizes the space that is Claude Code's dictation toggle, and whether it toggles or types depends on chat-input state the board cannot see.
Shape
OpenBoardKit/VoiceSignal.swift— the pure state machine, clock-injected.OpenBoardKit/MicActivity.swift— the CoreAudio listener pair: running-state on the current default input device, plus default-device moves (AirPods connecting would otherwise strand the listener on hardware nobody records from).BoardController— owns the wiring: mic transitions paint, a sweep task retires beliefs whose mic never started, and a mic already running when the belief begins counts as confirmation (dictation joining a call can't flip a flag that's already up).Tests
9 cases in
VoiceSignalTests.swiftcovering every transition above. 453/453 passing. Hardware-tested on the pad: the tap-with-text case no longer lights the ring.🤖 Generated with Claude Code