diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx index fea8b17e..da009be6 100644 --- a/packages/ui/src/components/chat/ChatInput.tsx +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -1251,6 +1251,14 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const canAbort = sessionPhase !== 'idle'; + const getCurrentInputSnapshot = React.useCallback(() => { + const currentMessage = textareaRef.current?.value ?? message; + return { + message: currentMessage, + hasContent: currentMessage.trim().length > 0 || sendableAttachedFiles.length > 0 || hasDrafts, + }; + }, [hasDrafts, message, sendableAttachedFiles.length]); + // Keep a ref to handleSubmit so callbacks don't depend on it. type SubmitOptions = { queuedOnly?: boolean; @@ -1259,11 +1267,12 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo // Add message to queue instead of sending const handleQueueMessage = React.useCallback(() => { - if (!hasContent || !currentSessionId) return; + const inputSnapshot = getCurrentInputSnapshot(); + if (!inputSnapshot.hasContent || !currentSessionId) return; const drafts = consumeDrafts(currentSessionId); - let messageToQueue = message.replace(/^\n+|\n+$/g, ''); + let messageToQueue = inputSnapshot.message.replace(/^\n+|\n+$/g, ''); if (drafts.length > 0) { messageToQueue = appendInlineComments(messageToQueue, drafts); } @@ -1292,7 +1301,7 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo if (!isMobile) { textareaRef.current?.focus(); } - }, [hasContent, currentSessionId, message, sendableAttachedFiles, sanitizeAttachmentsForSend, addToQueue, clearAttachedFiles, isMobile, consumeDrafts, currentProviderId, currentModelId, currentAgentName, currentVariant]); + }, [getCurrentInputSnapshot, currentSessionId, sendableAttachedFiles, sanitizeAttachmentsForSend, addToQueue, clearAttachedFiles, isMobile, consumeDrafts, currentProviderId, currentModelId, currentAgentName, currentVariant]); const handleQueuedMessageEdit = React.useCallback((content: string) => { setMessage(content); @@ -1319,10 +1328,11 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo const handleSubmit = async (options?: SubmitOptions) => { const queuedOnly = options?.queuedOnly ?? false; + const inputSnapshot = getCurrentInputSnapshot(); if (queuedOnly) { if (!hasQueuedMessages || !currentSessionId) return; - } else if (!canSend || (!currentSessionId && !newSessionDraftOpen)) { + } else if ((!inputSnapshot.hasContent && !hasQueuedMessages) || (!currentSessionId && !newSessionDraftOpen)) { return; } @@ -1369,8 +1379,8 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo } // Add current input (skip for queued-only auto-send) - if (!queuedOnly && hasContent) { - const messageToSend = message.replace(/^\n+|\n+$/g, ''); + if (!queuedOnly && inputSnapshot.hasContent) { + const messageToSend = inputSnapshot.message.replace(/^\n+|\n+$/g, ''); const { sanitizedText, mention } = parseAgentMentions(messageToSend, agents); const { sanitizedText: messageText, attachments: mentionAttachments } = extractInlineFileMentions(sanitizedText); const attachmentsToSend = sanitizeAttachmentsForSend(sendableAttachedFiles); @@ -1655,13 +1665,14 @@ const ChatInputComponent: React.FC = ({ onOpenSettings, scrollTo // Primary action for send button - respects queue mode setting const handlePrimaryAction = React.useCallback(() => { - const canQueue = inputMode === 'normal' && hasContent && currentSessionId && sessionPhase !== 'idle'; + const inputSnapshot = getCurrentInputSnapshot(); + const canQueue = inputMode === 'normal' && inputSnapshot.hasContent && currentSessionId && sessionPhase !== 'idle'; if (queueModeEnabled && canQueue) { handleQueueMessage(); } else { void handleSubmitRef.current(); } - }, [inputMode, hasContent, currentSessionId, sessionPhase, queueModeEnabled, handleQueueMessage]); + }, [inputMode, getCurrentInputSnapshot, currentSessionId, sessionPhase, queueModeEnabled, handleQueueMessage]); const handleKeyDown = (e: React.KeyboardEvent) => { // Early return during IME composition to prevent interference with autocomplete.