Skip to content

Commit 1b64121

Browse files
committed
fix: move recording keybinds into useVoiceInput hook
More correct by construction: the hook owns the state machine, so it should own the keyboard interactions for that state. - Add useRecordingKeybinds option to useVoiceInput - Track spaceHeld in a ref that persists correctly - Listeners added only when state === 'recording', auto-cleanup on state change - Removes duplicate keybind code from ChatInput
1 parent 38d0f2c commit 1b64121

File tree

2 files changed

+51
-15
lines changed

2 files changed

+51
-15
lines changed

src/browser/components/ChatInput/index.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
173173
},
174174
onSend: () => void handleSend(),
175175
openAIKeySet,
176+
useRecordingKeybinds: true,
176177
});
177178

178179
// Start creation tutorial when entering creation mode
@@ -496,23 +497,9 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
496497
voiceInput.toggle();
497498
};
498499

499-
// Global keybinds only active during recording
500-
const handleKeyDown = (e: KeyboardEvent) => {
501-
if (voiceInput.state !== "recording") return;
502-
if (e.key === " ") {
503-
e.preventDefault();
504-
voiceInput.stop({ send: true });
505-
} else if (e.key === "Escape") {
506-
e.preventDefault();
507-
voiceInput.cancel();
508-
}
509-
};
510-
511500
window.addEventListener(CUSTOM_EVENTS.TOGGLE_VOICE_INPUT, handleToggle as EventListener);
512-
window.addEventListener("keydown", handleKeyDown);
513501
return () => {
514502
window.removeEventListener(CUSTOM_EVENTS.TOGGLE_VOICE_INPUT, handleToggle as EventListener);
515-
window.removeEventListener("keydown", handleKeyDown);
516503
};
517504
}, [voiceInput, setToast]);
518505

@@ -862,9 +849,10 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
862849
return;
863850
}
864851

865-
// Space on empty input starts voice recording
852+
// Space on empty input starts voice recording (ignore key repeat from holding)
866853
if (
867854
e.key === " " &&
855+
!e.repeat &&
868856
input.trim() === "" &&
869857
voiceInput.shouldShowUI &&
870858
voiceInput.isApiKeySet &&

src/browser/hooks/useVoiceInput.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ export interface UseVoiceInputOptions {
1616
/** Called after successful transcription if stop({ send: true }) was used */
1717
onSend?: () => void;
1818
openAIKeySet: boolean;
19+
/**
20+
* When true, hook manages global keybinds during recording:
21+
* - Space: stop and send (requires release after start)
22+
* - Escape: cancel
23+
* - Ctrl+D / Cmd+D: stop without sending
24+
*/
25+
useRecordingKeybinds?: boolean;
1926
}
2027

2128
export interface UseVoiceInputResult {
@@ -236,6 +243,47 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
236243
};
237244
}, [releaseStream]);
238245

246+
// ---------------------------------------------------------------------------
247+
// Recording keybinds (when useRecordingKeybinds is true)
248+
// ---------------------------------------------------------------------------
249+
250+
// Track if space is held to prevent start→send when user holds space
251+
const spaceHeldRef = useRef(false);
252+
253+
useEffect(() => {
254+
if (!options.useRecordingKeybinds || state !== "recording") {
255+
spaceHeldRef.current = false;
256+
return;
257+
}
258+
259+
// Assume space is held when recording starts (conservative default)
260+
spaceHeldRef.current = true;
261+
262+
const handleKeyUp = (e: KeyboardEvent) => {
263+
if (e.key === " ") spaceHeldRef.current = false;
264+
};
265+
266+
const handleKeyDown = (e: KeyboardEvent) => {
267+
if (e.key === " " && !spaceHeldRef.current) {
268+
e.preventDefault();
269+
stop({ send: true });
270+
} else if (e.key === "Escape") {
271+
e.preventDefault();
272+
cancel();
273+
} else if ((e.ctrlKey || e.metaKey) && e.key === "d") {
274+
e.preventDefault();
275+
stop();
276+
}
277+
};
278+
279+
window.addEventListener("keyup", handleKeyUp);
280+
window.addEventListener("keydown", handleKeyDown);
281+
return () => {
282+
window.removeEventListener("keyup", handleKeyUp);
283+
window.removeEventListener("keydown", handleKeyDown);
284+
};
285+
}, [options.useRecordingKeybinds, state, stop, cancel]);
286+
239287
// ---------------------------------------------------------------------------
240288
// Return
241289
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)