From 875e0c0ff7b9963785bea73e9f8ff81d272b06a4 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 7 May 2026 22:30:58 +1000 Subject: [PATCH] fix: queued message lost when clicking sending now (#9074) --- ui/desktop/src/components/ChatInput.tsx | 108 ++++++++++++++++++------ 1 file changed, 81 insertions(+), 27 deletions(-) diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 9fdeee14..9ee05791 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -52,6 +52,18 @@ interface PastedImage { error?: string; } +const moveQueuedMessageToFront = ( + messages: QueuedMessage[], + messageId: string +): QueuedMessage[] => { + const selectedMessage = messages.find((msg) => msg.id === messageId); + if (!selectedMessage) return messages; + return [selectedMessage, ...messages.filter((msg) => msg.id !== messageId)]; +}; + +const removeQueuedMessage = (messages: QueuedMessage[], messageId: string): QueuedMessage[] => + messages.filter((msg) => msg.id !== messageId); + const MAX_IMAGES_PER_MESSAGE = 10; // Constants for token and tool alerts @@ -220,8 +232,25 @@ export default function ChatInput({ const [queuedMessages, setQueuedMessages] = useState([]); const queuePausedRef = useRef(false); const editingMessageIdRef = useRef(null); + const sendAfterStopMessageIdRef = useRef(null); const [lastInterruption, setLastInterruption] = useState(null); + const pauseRemainingQueue = useCallback(() => { + queuePausedRef.current = true; + }, []); + + const clearPendingSendAfterStop = useCallback((messageId?: string) => { + if (!messageId || sendAfterStopMessageIdRef.current === messageId) { + sendAfterStopMessageIdRef.current = null; + } + }, []); + + const clearQueueState = useCallback(() => { + queuePausedRef.current = false; + sendAfterStopMessageIdRef.current = null; + setLastInterruption(null); + }, []); + const { alerts, addAlert, clearAlerts } = useAlerts(); const dropdownRef: React.RefObject = useRef( null @@ -315,20 +344,40 @@ export default function ChatInput({ // Queue processing useEffect(() => { if (wasLoadingRef.current && !isLoading && queuedMessages.length > 0) { - // After an interruption, we should process the interruption message immediately - // The queue is only truly paused if there was an interruption AND we want to keep it paused - const shouldProcessQueue = !queuePausedRef.current || lastInterruption; + const pendingSendAfterStopId = sendAfterStopMessageIdRef.current; + const messageToSend = pendingSendAfterStopId + ? queuedMessages.find((message) => message.id === pendingSendAfterStopId) + : queuedMessages[0]; + + if (pendingSendAfterStopId && !messageToSend) { + clearPendingSendAfterStop(pendingSendAfterStopId); + wasLoadingRef.current = isLoading; + return; + } + + if (!messageToSend) { + wasLoadingRef.current = isLoading; + return; + } + + const shouldSendAfterStop = pendingSendAfterStopId === messageToSend.id; + const shouldProcessQueue = !queuePausedRef.current || lastInterruption || shouldSendAfterStop; if (shouldProcessQueue) { - const nextMessage = queuedMessages[0]; - LocalMessageStorage.addMessage(nextMessage.content); - handleSubmit({ msg: nextMessage.content, images: nextMessage.images }); + LocalMessageStorage.addMessage(messageToSend.content); + handleSubmit({ msg: messageToSend.content, images: messageToSend.images }); + if (shouldSendAfterStop) { + clearPendingSendAfterStop(messageToSend.id); + } setQueuedMessages((prev) => { - const newQueue = prev.slice(1); + const newQueue = shouldSendAfterStop + ? removeQueuedMessage(prev, messageToSend.id) + : prev.slice(1); // If queue becomes empty after processing, clear the paused state if (newQueue.length === 0) { - queuePausedRef.current = false; - setLastInterruption(null); + clearQueueState(); + } else if (shouldSendAfterStop) { + pauseRemainingQueue(); } return newQueue; }); @@ -338,12 +387,20 @@ export default function ChatInput({ setLastInterruption(null); // Keep the queue paused after sending the interruption message // User can manually resume if they want to continue with queued messages - queuePausedRef.current = true; + pauseRemainingQueue(); } } } wasLoadingRef.current = isLoading; - }, [isLoading, queuedMessages, handleSubmit, lastInterruption]); + }, [ + isLoading, + queuedMessages, + handleSubmit, + lastInterruption, + clearPendingSendAfterStop, + clearQueueState, + pauseRemainingQueue, + ]); const [mentionPopover, setMentionPopover] = useState<{ isOpen: boolean; position: { x: number; y: number }; @@ -934,7 +991,7 @@ export default function ChatInput({ if (interruptionMatch && interruptionMatch.shouldInterrupt) { setLastInterruption(interruptionMatch.matchedText); if (onStop) onStop(); - queuePausedRef.current = true; + pauseRemainingQueue(); // For interruptions, we need to queue the message to be sent after the stop completes // rather than trying to send it immediately while the system is still loading @@ -1227,13 +1284,13 @@ export default function ChatInput({ // Queue management functions - no storage persistence, only in-memory const handleRemoveQueuedMessage = (messageId: string) => { + clearPendingSendAfterStop(messageId); setQueuedMessages((prev) => prev.filter((msg) => msg.id !== messageId)); }; const handleClearQueue = () => { setQueuedMessages([]); - queuePausedRef.current = false; - setLastInterruption(null); + clearQueueState(); }; const handleReorderMessages = (reorderedMessages: QueuedMessage[]) => { @@ -1250,20 +1307,17 @@ export default function ChatInput({ const messageToSend = queuedMessages.find((msg) => msg.id === messageId); if (!messageToSend) return; - // Stop current processing and temporarily pause queue to prevent double-send + if (!isLoading) { + setQueuedMessages((prev) => removeQueuedMessage(prev, messageId)); + LocalMessageStorage.addMessage(messageToSend.content); + handleSubmit({ msg: messageToSend.content, images: messageToSend.images }); + return; + } + + sendAfterStopMessageIdRef.current = messageId; + pauseRemainingQueue(); + setQueuedMessages((prev) => moveQueuedMessageToFront(prev, messageId)); if (onStop) onStop(); - const wasPaused = queuePausedRef.current; - queuePausedRef.current = true; - - // Remove the message from queue and send it immediately - setQueuedMessages((prev) => prev.filter((msg) => msg.id !== messageId)); - LocalMessageStorage.addMessage(messageToSend.content); - handleSubmit({ msg: messageToSend.content, images: messageToSend.images }); - - // Restore previous pause state after a brief delay to prevent race condition - setTimeout(() => { - queuePausedRef.current = wasPaused; - }, 100); }; const handleResumeQueue = () => {