Skip to content

Commit 944e341

Browse files
committed
feat: space on empty input starts voice, escape cancels
- Pressing space on empty chat input starts voice recording (convenient alternative to Cmd+D) - Pressing escape during recording cancels without transcribing - Add cancel() method to voice hook that sets flag to skip transcribe - Updated overlay text to show all shortcuts
1 parent adcec7c commit 944e341

File tree

2 files changed

+36
-3
lines changed

2 files changed

+36
-3
lines changed

src/browser/components/ChatInput/index.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,19 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
846846
return;
847847
}
848848

849+
// Space on empty input starts voice recording
850+
if (
851+
e.key === " " &&
852+
input.trim() === "" &&
853+
voiceInput.shouldShowUI &&
854+
voiceInput.isApiKeySet &&
855+
voiceInput.state === "idle"
856+
) {
857+
e.preventDefault();
858+
voiceInput.start();
859+
return;
860+
}
861+
849862
// Handle open model selector
850863
if (matchesKeybind(e, KEYBINDS.OPEN_MODEL_SELECTOR)) {
851864
e.preventDefault();
@@ -985,6 +998,9 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
985998
if (e.key === " " && voiceInput.state === "recording") {
986999
e.preventDefault();
9871000
voiceInput.stop({ send: true });
1001+
} else if (e.key === "Escape" && voiceInput.state === "recording") {
1002+
e.preventDefault();
1003+
voiceInput.cancel();
9881004
}
9891005
}}
9901006
disabled={voiceInput.state === "transcribing"}
@@ -1006,7 +1022,7 @@ export const ChatInput: React.FC<ChatInputProps> = (props) => {
10061022
)}
10071023
>
10081024
{voiceInput.state === "recording"
1009-
? `Recording... space to send, ${formatKeybind(KEYBINDS.TOGGLE_VOICE_INPUT)} to stop`
1025+
? `Recording... space to send, ${formatKeybind(KEYBINDS.TOGGLE_VOICE_INPUT)} to stop, esc to cancel`
10101026
: "Transcribing..."}
10111027
</span>
10121028
<WaveformBars

src/browser/hooks/useVoiceInput.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface UseVoiceInputResult {
2424
shouldShowUI: boolean;
2525
start: () => void;
2626
stop: (options?: { send?: boolean }) => void;
27+
/** Cancel recording without transcribing (discard audio) */
28+
cancel: () => void;
2729
toggle: () => void;
2830
}
2931

@@ -42,6 +44,7 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
4244
const audioChunksRef = useRef<Blob[]>([]);
4345
const streamRef = useRef<MediaStream | null>(null);
4446
const sendAfterTranscribeRef = useRef(false);
47+
const cancelledRef = useRef(false);
4548

4649
// Store callbacks in refs to avoid stale closures
4750
const callbacksRef = useRef(options);
@@ -97,11 +100,17 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
97100
};
98101

99102
recorder.onstop = () => {
103+
const wasCancelled = cancelledRef.current;
104+
cancelledRef.current = false;
100105
const blob = new Blob(audioChunksRef.current, { type: mimeType });
101106
audioChunksRef.current = [];
102107
stream.getTracks().forEach((t) => t.stop());
103108
streamRef.current = null;
104-
void transcribe(blob);
109+
if (wasCancelled) {
110+
setState("idle");
111+
} else {
112+
void transcribe(blob);
113+
}
105114
};
106115

107116
recorder.onerror = () => {
@@ -132,7 +141,14 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
132141
mediaRecorderRef.current?.stop();
133142
mediaRecorderRef.current = null;
134143
}
135-
// Note: setState("idle") not called here - transcribe() handles transition
144+
}, []);
145+
146+
const cancel = useCallback(() => {
147+
cancelledRef.current = true;
148+
if (mediaRecorderRef.current?.state !== "inactive") {
149+
mediaRecorderRef.current?.stop();
150+
mediaRecorderRef.current = null;
151+
}
136152
}, []);
137153

138154
const toggle = useCallback(() => {
@@ -158,6 +174,7 @@ export function useVoiceInput(options: UseVoiceInputOptions): UseVoiceInputResul
158174
shouldShowUI: isSupported && !isMobile,
159175
start: () => void start(),
160176
stop,
177+
cancel,
161178
toggle,
162179
};
163180
}

0 commit comments

Comments
 (0)