diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index 1a8f5645..7616c8f5 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -47,6 +47,16 @@ interface ChatInputProps { const isPrimaryMode = (mode?: string) => mode === 'primary' || mode === 'all' || mode === undefined || mode === null; +/** + * Detects if a keyboard event is part of IME composition. + * Uses both isComposing and keyCode === 229 (MDN recommended). + * WebKit may fire compositionend before keydown, causing isComposing to be false + * while keyCode remains 229, so both checks are needed. + */ +const isIMECompositionEvent = (e: React.KeyboardEvent): boolean => { + return e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229; +}; + export const ChatInput: React.FC = ({ onOpenSettings, scrollToBottom }) => { const [message, setMessage] = React.useState(''); const [isDragging, setIsDragging] = React.useState(false); @@ -476,6 +486,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }, [sessionPhase, queuedMessages.length, currentSessionId, currentProviderId, currentModelId, sessionAbortFlags]); const handleKeyDown = (e: React.KeyboardEvent) => { + // Early return during IME composition to prevent interference with autocomplete + // Uses keyCode === 229 fallback for WebKit where compositionend fires before keydown + if (isIMECompositionEvent(e)) return; if (showCommandAutocomplete && commandRef.current) { if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') { @@ -508,7 +521,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } // Handle Enter/Ctrl+Enter based on queue mode - if (e.key === 'Enter' && !e.shiftKey && !isMobile) { + if (e.key === 'Enter' && !e.shiftKey && !isMobile && !isIMECompositionEvent(e)) { e.preventDefault(); const isCtrlEnter = e.ctrlKey || e.metaKey; diff --git a/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx b/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx index ac0322b9..9e324d87 100644 --- a/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx +++ b/packages/ui/src/components/views/agent-manager/AgentManagerEmptyState.tsx @@ -23,6 +23,16 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024; /** Max number of concurrent runs */ const MAX_MODELS = 5; +/** + * Detects if a keyboard event is part of IME composition. + * Uses both isComposing and keyCode === 229 (MDN recommended). + * WebKit may fire compositionend before keydown, causing isComposing to be false + * while keyCode remains 229, so both checks are needed. + */ +const isIMECompositionEvent = (e: React.KeyboardEvent): boolean => { + return e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229; +}; + /** Attached file for agent manager */ interface AttachedFile { id: string; @@ -177,8 +187,11 @@ export const AgentManagerEmptyState: React.FC = ({ }; const handleKeyDown = (e: React.KeyboardEvent) => { + // Early return during IME composition + if (isIMECompositionEvent(e)) return; + // Enter submits if valid, Shift+Enter adds newline - if (e.key === 'Enter' && !e.shiftKey) { + if (e.key === 'Enter' && !e.shiftKey && !isIMECompositionEvent(e)) { e.preventDefault(); if (isValid && !isSubmittingOrCreating) { handleSubmit(e as unknown as React.FormEvent);