diff --git a/.changeset/agent-ui-mobile-submit.md b/.changeset/agent-ui-mobile-submit.md new file mode 100644 index 0000000000..094c5ce4a9 --- /dev/null +++ b/.changeset/agent-ui-mobile-submit.md @@ -0,0 +1,5 @@ +--- +'@electric-ax/agents-server-ui': patch +--- + +Fix Agent UI message submission on mobile browsers. The Send button now keeps the textarea focused on tap so the on-screen keyboard does not dismiss and reflow the viewport mid-click, and the composer recognises the soft-keyboard return key via `enterKeyHint="send"` plus a `beforeinput` fallback (Android Chrome / GBoard route it as `insertLineBreak` without a matching `keydown`). The Enter handler now also ignores IME composition (`keyCode === 229`). diff --git a/packages/agents-server-ui/src/components/MessageInput.tsx b/packages/agents-server-ui/src/components/MessageInput.tsx index 09b2fa54b9..5687188e8b 100644 --- a/packages/agents-server-ui/src/components/MessageInput.tsx +++ b/packages/agents-server-ui/src/components/MessageInput.tsx @@ -249,8 +249,31 @@ export function MessageInput({ ref={textareaRef} value={value} onChange={(e) => setValue(e.target.value)} + // Tell mobile virtual keyboards that Enter means "send" so the + // GBoard / iOS keyboard surfaces a send-shaped action key and + // — critically on Android Chrome — fires `keydown` with + // `key === 'Enter'` reliably. Without this hint the soft + // keyboard's return key inside a textarea inserts a newline + // and may fire `key === 'Unidentified'` / `keyCode === 229`. + enterKeyHint="send" onKeyDown={(e) => { - if (e.key === `Enter` && !e.shiftKey) { + if (e.key !== `Enter` || e.shiftKey) return + // Don't submit while an IME composition is in progress — + // Enter is committing the candidate, not sending. Android + // Chrome reports composing as `keyCode === 229` rather than + // setting `isComposing`, so check both. + if (e.nativeEvent.isComposing || e.keyCode === 229) return + e.preventDefault() + handleSubmit() + }} + // Fallback for soft keyboards (notably Android Chrome / GBoard) + // that route the return key through `beforeinput` as an + // `insertLineBreak` without firing a `keydown` we can match + // on `key === 'Enter'`. + onBeforeInput={(e) => { + if ( + (e.nativeEvent as InputEvent).inputType === `insertLineBreak` + ) { e.preventDefault() handleSubmit() } @@ -265,6 +288,16 @@ export function MessageInput({ type="button" aria-label={showStop ? `Stop generating` : `Send message`} title={showStop ? `Stop generating` : `Send message`} + // Keep the textarea focused when the user taps Send on a + // touch device. Without this, tapping the button blurs the + // textarea, dismisses the on-screen keyboard, and the + // viewport reflows between pointerdown and pointerup — the + // resulting `click` lands on a different element and the + // send never fires. `preventDefault` here skips the implicit + // focus transfer; the `click` still dispatches normally. + onPointerDown={(e) => { + if (e.pointerType !== `mouse`) e.preventDefault() + }} onClick={handleComposerAction} disabled={showStop ? stopPending : !isButtonActive} className={[