From 809e5fbbdce7c0091ddf999d76be4a6de8ff1de8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 5 Apr 2026 14:53:10 +0700 Subject: [PATCH] feat(app): Enter key confirms and sends dictation When dictating and focused on an agent, pressing Enter now confirms the dictation and sends the message, matching the behavior of clicking the submit button. --- .../app/src/components/agent-input-area.tsx | 18 +++++++++++++++ packages/app/src/components/message-input.tsx | 22 ++++++++++++++----- .../app/src/hooks/use-keyboard-shortcuts.ts | 10 +++++++++ packages/app/src/keyboard/actions.ts | 1 + .../keyboard/keyboard-action-dispatcher.ts | 4 ++++ .../app/src/keyboard/keyboard-shortcuts.ts | 8 +++++++ 6 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index 4f4d6c441..2c4c7261d 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -74,6 +74,8 @@ interface AgentInputAreaProps { autoFocus?: boolean; /** Callback to expose the addImages function to parent components */ onAddImages?: (addImages: (images: ImageAttachment[]) => void) => void; + /** Callback to expose a focus function to parent components (desktop only). */ + onFocusInput?: (focus: () => void) => void; /** Optional draft context for listing commands before an agent exists. */ commandDraftConfig?: DraftCommandConfig; /** Called when a message is about to be sent (any path: keyboard, dictation, queued). */ @@ -103,6 +105,7 @@ export function AgentInputArea({ clearDraft, autoFocus = false, onAddImages, + onFocusInput, commandDraftConfig, onMessageSent, onComposerHeightChange, @@ -208,6 +211,15 @@ export function AgentInputArea({ onAddImages?.(addImages); }, [addImages, onAddImages]); + const focusInput = useCallback(() => { + if (Platform.OS !== "web") return; + messageInputRef.current?.focus(); + }, []); + + useEffect(() => { + onFocusInput?.(focusInput); + }, [focusInput, onFocusInput]); + const submitMessage = useCallback( async (text: string, images?: ImageAttachment[]) => { onMessageSent?.(); @@ -402,6 +414,10 @@ export function AgentInputArea({ } switch (action.id) { + case "message-input.send": + return messageInputRef.current?.runKeyboardAction("send") ?? false; + case "message-input.dictation-confirm": + return messageInputRef.current?.runKeyboardAction("dictation-confirm") ?? false; case "message-input.focus": if (Platform.OS !== "web") { messageInputRef.current?.focus(); @@ -440,8 +456,10 @@ export function AgentInputArea({ handlerId: keyboardHandlerIdRef.current, actions: [ "message-input.focus", + "message-input.send", "message-input.dictation-toggle", "message-input.dictation-cancel", + "message-input.dictation-confirm", "message-input.voice-toggle", "message-input.voice-mute-toggle", ], diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx index 4794ea5e1..c94bd8fbd 100644 --- a/packages/app/src/components/message-input.tsx +++ b/packages/app/src/components/message-input.tsx @@ -100,7 +100,7 @@ export interface MessageInputProps { export interface MessageInputRef { focus: () => void; blur: () => void; - runKeyboardAction: (action: MessageInputKeyboardActionKind) => void; + runKeyboardAction: (action: MessageInputKeyboardActionKind) => boolean; /** * Web-only: return the underlying DOM element for focus assertions/retries. * May return null if not mounted or on native. @@ -242,26 +242,35 @@ export const MessageInput = forwardRef(funct runKeyboardAction: (action) => { if (action === "focus") { textInputRef.current?.focus(); - return; + return true; + } + + if (action === "send" || action === "dictation-confirm") { + if (isDictatingRef.current) { + sendAfterTranscriptRef.current = true; + confirmDictation(); + return true; + } + return false; } if (action === "voice-toggle") { handleToggleRealtimeVoiceShortcut(); - return; + return true; } if (action === "voice-mute-toggle") { if (isRealtimeVoiceForCurrentAgent) { voice?.toggleMute(); } - return; + return true; } if (action === "dictation-cancel") { if (isDictatingRef.current) { cancelDictation(); } - return; + return true; } if (action === "dictation-toggle") { @@ -271,7 +280,10 @@ export const MessageInput = forwardRef(funct } else { void startDictationIfAvailable(); } + return true; } + + return false; }, getNativeElement: () => { if (!IS_WEB) return null; diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index c014f828c..edbc739f1 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -129,6 +129,11 @@ export function useKeyboardShortcuts({ id: "message-input.focus", scope: "message-input", }); + case "send": + return keyboardActionDispatcher.dispatch({ + id: "message-input.send", + scope: "message-input", + }); case "dictation-toggle": return keyboardActionDispatcher.dispatch({ id: "message-input.dictation-toggle", @@ -139,6 +144,11 @@ export function useKeyboardShortcuts({ id: "message-input.dictation-cancel", scope: "message-input", }); + case "dictation-confirm": + return keyboardActionDispatcher.dispatch({ + id: "message-input.dictation-confirm", + scope: "message-input", + }); case "voice-toggle": return keyboardActionDispatcher.dispatch({ id: "message-input.voice-toggle", diff --git a/packages/app/src/keyboard/actions.ts b/packages/app/src/keyboard/actions.ts index 2653ac323..e9b8af965 100644 --- a/packages/app/src/keyboard/actions.ts +++ b/packages/app/src/keyboard/actions.ts @@ -11,6 +11,7 @@ export type MessageInputKeyboardActionKind = | "queue" | "dictation-toggle" | "dictation-cancel" + | "dictation-confirm" | "voice-toggle" | "voice-mute-toggle"; diff --git a/packages/app/src/keyboard/keyboard-action-dispatcher.ts b/packages/app/src/keyboard/keyboard-action-dispatcher.ts index 817ac7101..90cbcf117 100644 --- a/packages/app/src/keyboard/keyboard-action-dispatcher.ts +++ b/packages/app/src/keyboard/keyboard-action-dispatcher.ts @@ -2,8 +2,10 @@ export type KeyboardActionScope = "global" | "message-input" | "sidebar" | "work export type KeyboardActionId = | "message-input.focus" + | "message-input.send" | "message-input.dictation-toggle" | "message-input.dictation-cancel" + | "message-input.dictation-confirm" | "message-input.voice-toggle" | "message-input.voice-mute-toggle" | "workspace.tab.new" @@ -27,8 +29,10 @@ export type KeyboardActionId = export type KeyboardActionDefinition = | { id: "message-input.focus"; scope: KeyboardActionScope } + | { id: "message-input.send"; scope: KeyboardActionScope } | { id: "message-input.dictation-toggle"; scope: KeyboardActionScope } | { id: "message-input.dictation-cancel"; scope: KeyboardActionScope } + | { id: "message-input.dictation-confirm"; scope: KeyboardActionScope } | { id: "message-input.voice-toggle"; scope: KeyboardActionScope } | { id: "message-input.voice-mute-toggle"; scope: KeyboardActionScope } | { id: "workspace.tab.new"; scope: KeyboardActionScope } diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts index 2540e8549..bc9b65122 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.ts @@ -860,6 +860,14 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ }, }, + { + id: "message-input-dictation-confirm-enter", + action: "message-input.action", + combo: "Enter", + when: { commandCenter: false, terminal: false }, + payload: { type: "message-input", kind: "dictation-confirm" }, + }, + { id: "message-input-voice-mute-toggle", action: "message-input.action",