From 3b86e9491a893f597d189cd5e5e7c3beb3cece2c Mon Sep 17 00:00:00 2001 From: Ammar Date: Tue, 2 Dec 2025 23:03:49 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20fix:=20spacebar=20voice=20input?= =?UTF-8?q?=20race=20condition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/browser/hooks/useVoiceInput.ts | 46 +++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/browser/hooks/useVoiceInput.ts b/src/browser/hooks/useVoiceInput.ts index b5ce2e51cd..09719b13f6 100644 --- a/src/browser/hooks/useVoiceInput.ts +++ b/src/browser/hooks/useVoiceInput.ts @@ -62,6 +62,38 @@ const HAS_MEDIA_RECORDER = typeof window !== "undefined" && typeof MediaRecorder const HAS_GET_USER_MEDIA = typeof window !== "undefined" && typeof navigator.mediaDevices?.getUserMedia === "function"; +// ============================================================================= +// Global Key State Tracking +// ============================================================================= + +/** + * Track whether space is currently pressed at the module level. + * This runs outside React's render cycle, so it captures key state + * accurately even during async operations like microphone access. + */ +let isSpaceCurrentlyHeld = false; + +if (typeof window !== "undefined") { + window.addEventListener( + "keydown", + (e) => { + if (e.key === " ") isSpaceCurrentlyHeld = true; + }, + true + ); + window.addEventListener( + "keyup", + (e) => { + if (e.key === " ") isSpaceCurrentlyHeld = false; + }, + true + ); + // Also reset on blur (user switches window while holding space) + window.addEventListener("blur", () => { + isSpaceCurrentlyHeld = false; + }); +} + // ============================================================================= // Hook // ============================================================================= @@ -248,24 +280,24 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul // Recording keybinds (when useRecordingKeybinds is true) // --------------------------------------------------------------------------- - // Track if space is held to prevent start→send when user holds space - const spaceHeldRef = useRef(false); + // Track if space was held when recording started to prevent immediate send + const spaceHeldAtStartRef = useRef(false); useEffect(() => { if (!options.useRecordingKeybinds || state !== "recording") { - spaceHeldRef.current = false; + spaceHeldAtStartRef.current = false; return; } - // Assume space is held when recording starts (conservative default) - spaceHeldRef.current = true; + // Use global key state instead of assuming - handles async mic access delay + spaceHeldAtStartRef.current = isSpaceCurrentlyHeld; const handleKeyUp = (e: KeyboardEvent) => { - if (e.key === " ") spaceHeldRef.current = false; + if (e.key === " ") spaceHeldAtStartRef.current = false; }; const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === " " && !spaceHeldRef.current) { + if (e.key === " " && !spaceHeldAtStartRef.current) { e.preventDefault(); stop({ send: true }); } else if (e.key === "Escape") {