fix(app): keep dictation shortcuts responsive (#2268)
The composer mirrored dictation state in a second ref that could remain stale after the recording surface changed. Route shortcut decisions through the dictation lifecycle's synchronous state so finishing or rejecting a recording cannot leave Command-D latched.
This commit is contained in:
@@ -70,6 +70,7 @@ import {
|
||||
resolveComposerSurfacePresentation,
|
||||
runAlternateSendAction,
|
||||
runDefaultSendAction,
|
||||
runMessageInputKeyboardAction,
|
||||
stopRealtimeVoice,
|
||||
} from "./state";
|
||||
|
||||
@@ -496,65 +497,6 @@ function handleDesktopKeyPressImpl(
|
||||
ctx.handleDefaultSendAction();
|
||||
}
|
||||
|
||||
interface KeyboardActionHandlers {
|
||||
textInputRef: React.MutableRefObject<
|
||||
TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
|
||||
>;
|
||||
isDictatingRef: React.MutableRefObject<boolean>;
|
||||
sendAfterTranscriptRef: React.MutableRefObject<boolean>;
|
||||
confirmDictation: () => void | Promise<void>;
|
||||
cancelDictation: () => void | Promise<void>;
|
||||
startDictationIfAvailable: () => Promise<void>;
|
||||
handleToggleRealtimeVoiceShortcut: () => void;
|
||||
isRealtimeVoiceForCurrentAgent: boolean;
|
||||
voice: { toggleMute: () => void } | null | undefined;
|
||||
}
|
||||
|
||||
function runKeyboardActionImpl(
|
||||
action: MessageInputKeyboardActionKind,
|
||||
h: KeyboardActionHandlers,
|
||||
): boolean {
|
||||
if (action === "focus") {
|
||||
h.textInputRef.current?.focus();
|
||||
return true;
|
||||
}
|
||||
if (action === "send" || action === "dictation-confirm") {
|
||||
if (h.isDictatingRef.current) {
|
||||
h.sendAfterTranscriptRef.current = true;
|
||||
void h.confirmDictation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (action === "voice-toggle") {
|
||||
h.handleToggleRealtimeVoiceShortcut();
|
||||
return true;
|
||||
}
|
||||
if (action === "voice-mute-toggle") {
|
||||
if (h.isRealtimeVoiceForCurrentAgent) {
|
||||
h.voice?.toggleMute();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (action === "dictation-cancel") {
|
||||
if (h.isDictatingRef.current) {
|
||||
void h.cancelDictation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (action === "dictation-toggle") {
|
||||
if (h.isDictatingRef.current) {
|
||||
h.sendAfterTranscriptRef.current = true;
|
||||
void h.confirmDictation();
|
||||
} else {
|
||||
void h.startDictationIfAvailable();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTextInputNativeElement(
|
||||
current: TextInput | (TextInput & { getNativeRef?: () => unknown }) | null,
|
||||
): HTMLElement | null {
|
||||
@@ -916,22 +858,18 @@ function toggleRealtimeVoiceImpl(ctx: ToggleRealtimeVoiceContext): void {
|
||||
interface StartDictationContext {
|
||||
dictationUnavailableMessage: string | null | undefined;
|
||||
canStartDictation: () => boolean;
|
||||
isDictatingRef: React.MutableRefObject<boolean>;
|
||||
toast: { error: (msg: string) => void };
|
||||
startDictation: () => Promise<void>;
|
||||
}
|
||||
|
||||
async function startDictationIfAvailableImpl(ctx: StartDictationContext): Promise<void> {
|
||||
if (ctx.dictationUnavailableMessage) {
|
||||
ctx.isDictatingRef.current = false;
|
||||
ctx.toast.error(ctx.dictationUnavailableMessage);
|
||||
return;
|
||||
}
|
||||
if (!ctx.canStartDictation()) {
|
||||
ctx.isDictatingRef.current = false;
|
||||
return;
|
||||
}
|
||||
ctx.isDictatingRef.current = true;
|
||||
await ctx.startDictation();
|
||||
}
|
||||
|
||||
@@ -1276,16 +1214,18 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
textInputRef.current?.blur?.();
|
||||
},
|
||||
runKeyboardAction: (action) =>
|
||||
runKeyboardActionImpl(action, {
|
||||
textInputRef,
|
||||
isDictatingRef,
|
||||
sendAfterTranscriptRef,
|
||||
runMessageInputKeyboardAction(action, {
|
||||
focusInput: () => textInputRef.current?.focus(),
|
||||
isDictationRecording: isDictationActive,
|
||||
markTranscriptForSend: () => {
|
||||
sendAfterTranscriptRef.current = true;
|
||||
},
|
||||
confirmDictation,
|
||||
cancelDictation,
|
||||
startDictationIfAvailable,
|
||||
handleToggleRealtimeVoiceShortcut,
|
||||
isRealtimeVoiceForCurrentAgent,
|
||||
voice,
|
||||
startDictation: startDictationIfAvailable,
|
||||
toggleRealtimeVoice: handleToggleRealtimeVoiceShortcut,
|
||||
isRealtimeVoiceActive: isRealtimeVoiceForCurrentAgent,
|
||||
toggleRealtimeVoiceMute: () => voice?.toggleMute(),
|
||||
}),
|
||||
getNativeElement: () => (isWeb ? getTextInputNativeElement(textInputRef.current) : null),
|
||||
}));
|
||||
@@ -1369,6 +1309,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
|
||||
const {
|
||||
isRecording: isDictating,
|
||||
isRecordingActive: isDictationActive,
|
||||
isProcessing: isDictationProcessing,
|
||||
partialTranscript: _dictationPartialTranscript,
|
||||
volume: dictationVolume,
|
||||
@@ -1389,11 +1330,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
enableDuration: true,
|
||||
});
|
||||
|
||||
const isDictatingRef = useRef(isDictating);
|
||||
useEffect(() => {
|
||||
isDictatingRef.current = isDictating;
|
||||
}, [isDictating]);
|
||||
|
||||
const isRealtimeVoiceForCurrentAgent = computeIsRealtimeVoiceForAgent(
|
||||
voice,
|
||||
voiceServerId,
|
||||
@@ -1420,7 +1356,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
startDictationIfAvailableImpl({
|
||||
dictationUnavailableMessage,
|
||||
canStartDictation,
|
||||
isDictatingRef,
|
||||
toast,
|
||||
startDictation,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user