@@ -158,6 +158,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
158158 const [ selectedImages , setSelectedImages ] = useState < string [ ] > ( [ ] )
159159 const [ messageQueue , setMessageQueue ] = useState < QueuedMessage [ ] > ( [ ] )
160160 const isProcessingQueueRef = useRef ( false )
161+ const retryCountRef = useRef < Map < string , number > > ( new Map ( ) )
162+ const MAX_RETRY_ATTEMPTS = 3
161163
162164 // we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
163165 const [ clineAsk , setClineAsk ] = useState < ClineAsk | undefined > ( undefined )
@@ -443,6 +445,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
443445 }
444446 // Reset user response flag for new task
445447 userRespondedRef . current = false
448+
449+ // Clear message queue when starting a new task
450+ setMessageQueue ( [ ] )
451+ // Clear retry counts
452+ retryCountRef . current . clear ( )
446453 } , [ task ?. ts ] )
447454
448455 useEffect ( ( ) => {
@@ -616,36 +623,58 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
616623 )
617624
618625 useEffect ( ( ) => {
619- if ( ! sendingDisabled && messageQueue . length > 0 && ! isProcessingQueueRef . current ) {
620- isProcessingQueueRef . current = true
621-
622- // Use setTimeout to ensure state updates are processed
623- setTimeout ( ( ) => {
624- // Re-check the queue inside the timeout to avoid race conditions
625- setMessageQueue ( ( prev ) => {
626- if ( prev . length > 0 ) {
627- const [ nextMessage , ...remaining ] = prev
628- // Process the message asynchronously to avoid blocking
629- Promise . resolve ( )
630- . then ( ( ) => {
631- handleSendMessage ( nextMessage . text , nextMessage . images , true )
632- } )
633- . catch ( ( error ) => {
634- console . error ( "Failed to send queued message:" , error )
635- // Re-add the message to the queue on error
636- setMessageQueue ( ( current ) => [ nextMessage , ...current ] )
637- } )
638- . finally ( ( ) => {
639- isProcessingQueueRef . current = false
640- } )
641- return remaining
642- }
643- isProcessingQueueRef . current = false
644- return prev
645- } )
646- } , 0 )
626+ // Early return if conditions aren't met
627+ // Also don't process queue if there's an API error (clineAsk === "api_req_failed")
628+ if (
629+ sendingDisabled ||
630+ messageQueue . length === 0 ||
631+ isProcessingQueueRef . current ||
632+ clineAsk === "api_req_failed"
633+ ) {
634+ return
635+ }
636+
637+ // Mark as processing immediately to prevent race conditions
638+ isProcessingQueueRef . current = true
639+
640+ // Process the first message in the queue
641+ const [ nextMessage , ...remaining ] = messageQueue
642+
643+ // Update queue immediately to prevent duplicate processing
644+ setMessageQueue ( remaining )
645+
646+ // Process the message
647+ Promise . resolve ( )
648+ . then ( ( ) => {
649+ handleSendMessage ( nextMessage . text , nextMessage . images , true )
650+ // Clear retry count on success
651+ retryCountRef . current . delete ( nextMessage . id )
652+ } )
653+ . catch ( ( error ) => {
654+ console . error ( "Failed to send queued message:" , error )
655+
656+ // Get current retry count
657+ const retryCount = retryCountRef . current . get ( nextMessage . id ) || 0
658+
659+ // Only re-add if under retry limit
660+ if ( retryCount < MAX_RETRY_ATTEMPTS ) {
661+ retryCountRef . current . set ( nextMessage . id , retryCount + 1 )
662+ // Re-add the message to the end of the queue
663+ setMessageQueue ( ( current ) => [ ...current , nextMessage ] )
664+ } else {
665+ console . error ( `Message ${ nextMessage . id } failed after ${ MAX_RETRY_ATTEMPTS } attempts, discarding` )
666+ retryCountRef . current . delete ( nextMessage . id )
667+ }
668+ } )
669+ . finally ( ( ) => {
670+ isProcessingQueueRef . current = false
671+ } )
672+
673+ // Cleanup function to handle component unmount
674+ return ( ) => {
675+ isProcessingQueueRef . current = false
647676 }
648- } , [ sendingDisabled , messageQueue , handleSendMessage ] )
677+ } , [ sendingDisabled , messageQueue , handleSendMessage , clineAsk ] )
649678
650679 const handleSetChatBoxMessage = useCallback (
651680 ( text : string , images : string [ ] ) => {
@@ -662,6 +691,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
662691 [ inputValue , selectedImages ] ,
663692 )
664693
694+ // Cleanup retry count map on unmount
695+ useEffect ( ( ) => {
696+ // Store refs in variables to avoid stale closure issues
697+ const retryCountMap = retryCountRef . current
698+ const isProcessingRef = isProcessingQueueRef
699+
700+ return ( ) => {
701+ retryCountMap . clear ( )
702+ isProcessingRef . current = false
703+ }
704+ } , [ ] )
705+
665706 const startNewTask = useCallback ( ( ) => vscode . postMessage ( { type : "clearTask" } ) , [ ] )
666707
667708 // This logic depends on the useEffect[messages] above to set clineAsk,
0 commit comments