From 273eeac68c8e0f448b1a8646a0d2714d36d55726 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Sat, 16 May 2026 05:55:34 +0000 Subject: [PATCH] feat(mobile): chat input keybinds + safe-area + tap targets + overflow safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChatInput: e.nativeEvent.isComposing early-return added (CJK IME safety โ€” first Enter of a composition no longer submits). Bare-Enter send path gated by !isMobile so mobile inserts a newline; send is button-only. Cmd/Ctrl+Enter and Shift+Cmd/Ctrl+Enter retained as desktop secondary bindings. Placeholder is now viewport-aware. Outer wrapper gets paddingBottom: env(safe-area-inset-bottom) so iOS home indicator doesn't overlap. - MessageBubble: ActionRow buttons (Copy / Regenerate / Fork / Trash) bumped to max-md min-h/min-w 44px; opacity-100 on mobile so actions don't hide behind a hover-to-reveal pattern. User bubble and assistant content wrapper gain break-words + min-w-0 so long unbreakable strings (URLs / paths) wrap rather than blowing out the column on narrow viewports. - ChatPane: queued-message dropdown + close X + Stop-generating button hit max-md 44px sizing. - ChatTabBar: per-tab X, +/History/Close-pane action buttons hit max-md 44px. Tab close X is force-visible on mobile (no hover-to-reveal). - M8: CodeBlock / Markdown tables / ToolCallCard already wrap overflow-x-auto pre-existing โ€” no source change needed there; the break-words + min-w-0 additions above are the new defensive layer. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/src/components/ChatInput.tsx | 19 ++++++++++++++++--- apps/web/src/components/ChatTabBar.tsx | 8 ++++---- apps/web/src/components/MessageBubble.tsx | 14 +++++++------- apps/web/src/components/panes/ChatPane.tsx | 6 +++--- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/apps/web/src/components/ChatInput.tsx b/apps/web/src/components/ChatInput.tsx index 22baf48..3de3c8c 100644 --- a/apps/web/src/components/ChatInput.tsx +++ b/apps/web/src/components/ChatInput.tsx @@ -9,6 +9,7 @@ import { AttachmentPreviewModal } from '@/components/AttachmentPreviewModal'; import { FileMentionPopover } from '@/components/FileMentionPopover'; import { api } from '@/api/client'; import { sessionEvents } from '@/hooks/sessionEvents'; +import { useViewport } from '@/hooks/useViewport'; interface Props { disabled?: boolean; @@ -18,6 +19,7 @@ interface Props { } export function ChatInput({ disabled, projectId, onSend, onForceSend }: Props) { + const { isMobile } = useViewport(); const [value, setValue] = useState(''); const [busy, setBusy] = useState(false); const [attachments, setAttachments] = useState([]); @@ -185,6 +187,11 @@ export function ChatInput({ disabled, projectId, onSend, onForceSend }: Props) { function onKeyDown(e: KeyboardEvent) { if (mentionState?.open) return; + // IME safety: never act on Enter while an IME composition is in flight + // (CJK input methods commit composition via Enter). Without this, the + // first Enter of a Japanese/Chinese/Korean composition would submit + // instead of finalizing the candidate. + if (e.nativeEvent.isComposing) return; if (e.key === 'Enter' && e.shiftKey && (e.metaKey || e.ctrlKey) && onForceSend) { e.preventDefault(); void forceSubmit(); @@ -195,7 +202,9 @@ export function ChatInput({ disabled, projectId, onSend, onForceSend }: Props) { void submit(); return; } - if (e.key === 'Enter' && !e.shiftKey) { + // Bare Enter: sends on desktop, inserts a newline on mobile (per spec โ€” + // send is via the dedicated button on touch devices). + if (e.key === 'Enter' && !e.shiftKey && !isMobile) { e.preventDefault(); void submit(); } @@ -219,7 +228,7 @@ export function ChatInput({ disabled, projectId, onSend, onForceSend }: Props) { } return ( -
+
{attachments.length > 0 && (
@@ -239,7 +248,11 @@ export function ChatInput({ disabled, projectId, onSend, onForceSend }: Props) { value={value} onChange={handleChange} onKeyDown={onKeyDown} - placeholder="Ask about this project. Enter to send, Shift+Enter for newline." + placeholder={ + isMobile + ? 'Ask about this project. Tap send to submit.' + : 'Ask about this project. Enter to send ยท Shift+Enter for newline.' + } disabled={disabled || busy} rows={3} className="resize-none min-h-[68px] max-h-[240px]" diff --git a/apps/web/src/components/ChatTabBar.tsx b/apps/web/src/components/ChatTabBar.tsx index dbb63cf..6a730b1 100644 --- a/apps/web/src/components/ChatTabBar.tsx +++ b/apps/web/src/components/ChatTabBar.tsx @@ -115,7 +115,7 @@ export function ChatTabBar({ e.stopPropagation(); onRemoveTab(chat.id); }} - className="p-0.5 hover:bg-muted rounded opacity-0 group-hover:opacity-60 hover:!opacity-100 shrink-0" + className="inline-flex items-center justify-center p-0.5 hover:bg-muted rounded opacity-0 group-hover:opacity-60 hover:!opacity-100 shrink-0 max-md:min-h-[44px] max-md:min-w-[44px] max-md:opacity-100" aria-label="Close tab" > @@ -161,7 +161,7 @@ export function ChatTabBar({