Skip to content

Commit 07ea67d

Browse files
authored
🤖 fix: spacebar voice input race condition (#866)
The previous implementation assumed space was held when the recording effect ran, but React effects are async. If the user released space during microphone permission request or other delays, `spaceHeldRef` would incorrectly be `true`, blocking the subsequent space press from sending the transcription. **Fix:** Track global key state at module level (outside React lifecycle) and check actual state when effect runs. Also handles window blur to reset state when user switches away. _Generated with `mux`_
1 parent 8478e5e commit 07ea67d

File tree

1 file changed

+39
-7
lines changed

1 file changed

+39
-7
lines changed

src/browser/hooks/useVoiceInput.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,38 @@ const HAS_MEDIA_RECORDER = typeof window !== "undefined" && typeof MediaRecorder
6565
const HAS_GET_USER_MEDIA =
6666
typeof window !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function";
6767

68+
// =============================================================================
69+
// Global Key State Tracking
70+
// =============================================================================
71+
72+
/**
73+
* Track whether space is currently pressed at the module level.
74+
* This runs outside React's render cycle, so it captures key state
75+
* accurately even during async operations like microphone access.
76+
*/
77+
let isSpaceCurrentlyHeld = false;
78+
79+
if (typeof window !== "undefined") {
80+
window.addEventListener(
81+
"keydown",
82+
(e) => {
83+
if (e.key === " ") isSpaceCurrentlyHeld = true;
84+
},
85+
true
86+
);
87+
window.addEventListener(
88+
"keyup",
89+
(e) => {
90+
if (e.key === " ") isSpaceCurrentlyHeld = false;
91+
},
92+
true
93+
);
94+
// Also reset on blur (user switches window while holding space)
95+
window.addEventListener("blur", () => {
96+
isSpaceCurrentlyHeld = false;
97+
});
98+
}
99+
68100
// =============================================================================
69101
// Hook
70102
// =============================================================================
@@ -257,24 +289,24 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
257289
// Recording keybinds (when useRecordingKeybinds is true)
258290
// ---------------------------------------------------------------------------
259291

260-
// Track if space is held to prevent start→send when user holds space
261-
const spaceHeldRef = useRef(false);
292+
// Track if space was held when recording started to prevent immediate send
293+
const spaceHeldAtStartRef = useRef(false);
262294

263295
useEffect(() => {
264296
if (!options.useRecordingKeybinds || state !== "recording") {
265-
spaceHeldRef.current = false;
297+
spaceHeldAtStartRef.current = false;
266298
return;
267299
}
268300

269-
// Assume space is held when recording starts (conservative default)
270-
spaceHeldRef.current = true;
301+
// Use global key state instead of assuming - handles async mic access delay
302+
spaceHeldAtStartRef.current = isSpaceCurrentlyHeld;
271303

272304
const handleKeyUp = (e: KeyboardEvent) => {
273-
if (e.key === " ") spaceHeldRef.current = false;
305+
if (e.key === " ") spaceHeldAtStartRef.current = false;
274306
};
275307

276308
const handleKeyDown = (e: KeyboardEvent) => {
277-
if (e.key === " " && !spaceHeldRef.current) {
309+
if (e.key === " " && !spaceHeldAtStartRef.current) {
278310
e.preventDefault();
279311
stop({ send: true });
280312
} else if (e.key === "Escape") {

0 commit comments

Comments
 (0)