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:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
a077735bcf
commit
f9094bc3cc
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user