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",