Skip to content

Commit 3b86e94

Browse files
committed
🤖 fix: spacebar voice input race condition
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.
1 parent d249193 commit 3b86e94

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
@@ -62,6 +62,38 @@ const HAS_MEDIA_RECORDER = typeof window !== "undefined" && typeof MediaRecorder
6262
const HAS_GET_USER_MEDIA =
6363
typeof window !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function";
6464

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

251-
// Track if space is held to prevent start→send when user holds space
252-
const spaceHeldRef = useRef(false);
283+
// Track if space was held when recording started to prevent immediate send
284+
const spaceHeldAtStartRef = useRef(false);
253285

254286
useEffect(() => {
255287
if (!options.useRecordingKeybinds || state !== "recording") {
256-
spaceHeldRef.current = false;
288+
spaceHeldAtStartRef.current = false;
257289
return;
258290
}
259291

260-
// Assume space is held when recording starts (conservative default)
261-
spaceHeldRef.current = true;
292+
// Use global key state instead of assuming - handles async mic access delay
293+
spaceHeldAtStartRef.current = isSpaceCurrentlyHeld;
262294

263295
const handleKeyUp = (e: KeyboardEvent) => {
264-
if (e.key === " ") spaceHeldRef.current = false;
296+
if (e.key === " ") spaceHeldAtStartRef.current = false;
265297
};
266298

267299
const handleKeyDown = (e: KeyboardEvent) => {
268-
if (e.key === " " && !spaceHeldRef.current) {
300+
if (e.key === " " && !spaceHeldAtStartRef.current) {
269301
e.preventDefault();
270302
stop({ send: true });
271303
} else if (e.key === "Escape") {

0 commit comments

Comments
 (0)