feat(mobile): chat input keybinds + safe-area + tap targets + overflow safety
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Attachment[]>([]);
|
||||
@@ -185,6 +187,11 @@ export function ChatInput({ disabled, projectId, onSend, onForceSend }: Props) {
|
||||
|
||||
function onKeyDown(e: KeyboardEvent<HTMLTextAreaElement>) {
|
||||
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 (
|
||||
<div className="border-t">
|
||||
<div className="border-t" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
|
||||
<div className="max-w-[1000px] mx-auto w-full">
|
||||
{attachments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 px-4 pt-3">
|
||||
@@ -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]"
|
||||
|
||||
Reference in New Issue
Block a user