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 4f4d6c4411..2c4c7261d5 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 4794ea5e1d..c94bd8fbd6 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 c014f828cd..edbc739f10 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 2653ac3236..e9b8af9655 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 817ac71018..90cbcf1170 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 2540e8549c..bc9b651228 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",