fix(ui): prevent queued message truncation from stale React closure (#1087)

* fix(ui): read textarea DOM value in queue handler to prevent truncation

When the user types quickly and clicks the Queue button, React may not
have committed the latest `message` state yet, causing handleQueueMessage
to capture a stale closure (often just the first character typed).

Read textareaRef.current.value directly from the DOM instead, which
always reflects the current input regardless of React's render cycle.
Fall back to the React state when the ref is unavailable.

Also recompute hasContent from the DOM value to ensure the guard check
is consistent with the actual message being queued.

* fix(ui): use current input value when sending

---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
pasta-paul
2026-05-01 14:26:14 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent a077735bcf
commit f9094bc3cc
+19 -8
View File
@@ -1251,6 +1251,14 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ 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<ChatInputProps> = ({ 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<ChatInputProps> = ({ 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<ChatInputProps> = ({ 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<ChatInputProps> = ({ 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<ChatInputProps> = ({ 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<HTMLTextAreaElement>) => {
// Early return during IME composition to prevent interference with autocomplete.