diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 5c25d88..45f1c30 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -136,12 +136,25 @@ } } - // Listen for evdev hotkey events from the Rust backend + // Listen for evdev hotkey events from the Rust backend. + // + // Debounce window: evdev autorepeat, a sticky-key compositor quirk, + // or a user's nervous double-tap can all deliver the same press + // twice within ~100 ms — which, without debouncing, toggles the + // recording into and out of the same frame and loses the capture. + // Matches Handy #1143 ('first press records nothing, second works'). + // This is the UX side of brief item #4; the audio-stream warm-up + // side is owned by Workstream A. + const HOTKEY_DEBOUNCE_MS = 120; + let lastHotkeyAtMs = 0; let unlistenEvdev = null; async function setupEvdevListener() { if (!tauriRuntimeAvailable) return; const { listen } = await import("@tauri-apps/api/event"); unlistenEvdev = await listen("kon:hotkey-pressed", () => { + const now = Date.now(); + if (now - lastHotkeyAtMs < HOTKEY_DEBOUNCE_MS) return; + lastHotkeyAtMs = now; if (page.current !== "dictation") page.current = "dictation"; requestAnimationFrame(() => { window.dispatchEvent(new CustomEvent("kon:toggle-recording"));