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({