Skip to content

fix(chat): prevent duplicate first message send and UI flicker#185

Open
Saul-Gomez-J wants to merge 1 commit intolevante-hub:developfrom
Saul-Gomez-J:feat/fix-double-message-send
Open

fix(chat): prevent duplicate first message send and UI flicker#185
Saul-Gomez-J wants to merge 1 commit intolevante-hub:developfrom
Saul-Gomez-J:feat/fix-double-message-send

Conversation

@Saul-Gomez-J
Copy link
Contributor

Summary

Fixes a critical race condition in ChatPage that caused the first message of a new chat session to be sent multiple times, resulting in:

  • Duplicate API calls and wasted tokens/credits
  • Multiple identical messages stored in database
  • Confusing duplicate assistant responses
  • UI flickering showing empty state during message processing

Root Cause

The useEffect that processes pendingFirstMessage would execute multiple times if the component re-rendered during async message sending (e.g., from state updates like setIsStreaming, currentSession changes, or Zustand updates).

Since pendingFirstMessage was cleared immediately at the start, subsequent re-renders would find it null - but if execution was already in progress, the async operation would continue, leading to duplicate sends.

Changes

1. Processing Guard with Ref (pendingMessageProcessingRef)

  • Prevents concurrent executions of the message sending logic
  • Set to true when processing starts
  • Checked in useEffect guard condition
  • Cleared in finally block to ensure cleanup even on errors

Code:

const pendingMessageProcessingRef = useRef(false);

useEffect(() => {
  if (pendingFirstMessage === null || !currentSession || pendingMessageProcessingRef.current) {
    return; // Skip if already processing
  }
  
  pendingMessageProcessingRef.current = true;
  
  try {
    // ... send message logic
  } finally {
    pendingMessageProcessingRef.current = false;
  }
}, [pendingFirstMessage, currentSession]);

2. Deferred State Clearing

  • pendingFirstMessage and pendingFirstAttachments now cleared AFTER sendMessageAI completes successfully
  • Also cleared on error to allow user to retry with restored input
  • Prevents premature UI state transition

Before:

const messageText = pendingFirstMessage;
setPendingFirstMessage(null); // ❌ Cleared immediately
setPendingFirstAttachments(null);
await sendMessageAI(...);

After:

const messageText = pendingFirstMessage;
// Keep pendingFirstMessage until after sending
await sendMessageAI(...);
setPendingFirstMessage(null); // ✅ Cleared after success
setPendingFirstAttachments(null);

3. Updated isChatEmpty Logic

Now considers pendingFirstMessage when determining if chat is empty:

// Before
const isChatEmpty = messages.length === 0 && status !== 'streaming';

// After
const isChatEmpty = messages.length === 0 && status !== 'streaming' && pendingFirstMessage === null;

This prevents showing the BreathingLogo/empty state while the first message is being processed.

Impact

Before this fix:

  • First message could send 2+ times (wasting API credits)
  • UI would flicker showing empty state
  • Database would contain duplicate messages
  • User would see multiple identical responses

After this fix:

  • First message sends exactly once ✅
  • Smooth UI transition with consistent state ✅
  • No duplicate messages in database ✅
  • Proper error handling with input restoration ✅

Testing

Manually tested scenarios:

  • ✅ Send first message in new chat - sends once
  • ✅ Rapid state changes during send - no duplicates
  • ✅ No UI flicker during processing
  • ✅ Error handling restores input correctly
  • ✅ Fast re-renders don't trigger multiple sends

Files Changed

  • src/renderer/pages/ChatPage.tsx (+21, -4)
    • Added pendingMessageProcessingRef for execution guard
    • Moved state clearing to after successful send
    • Updated isChatEmpty to consider pending message

🤖 Generated with Claude Code

Fixes race condition that caused the pending first message to be sent
multiple times when the component re-rendered during processing.

## Issues Fixed

1. **Duplicate Message Sending**: The useEffect processing pendingFirstMessage
   would execute multiple times if re-renders occurred, causing:
   - Same message sent 2+ times to AI API
   - Unnecessary token/credit consumption
   - Duplicate messages in database
   - Multiple assistant responses

2. **UI Flicker**: Clearing pendingFirstMessage immediately caused the chat
   to show empty state (BreathingLogo) briefly during message processing,
   creating poor visual feedback

## Solution

### Processing Guard (pendingMessageProcessingRef)
- New ref prevents concurrent executions of message sending logic
- Checked before processing, set during, cleared in finally block
- Ensures one-time execution per pending message

### Deferred State Clearing
- pendingFirstMessage now cleared AFTER sendMessageAI completes
- Also cleared on error to allow retry with restored input
- Prevents premature transition to empty state

### Updated isChatEmpty Logic
```typescript
// Before
const isChatEmpty = messages.length === 0 && status !== 'streaming';

// After
const isChatEmpty = messages.length === 0 && status !== 'streaming' && pendingFirstMessage === null;
```
Now considers pending message when determining empty state

## Testing

Manually verified:
- First message sends exactly once
- No UI flicker during send
- Error handling restores input correctly
- Re-renders don't trigger duplicate sends

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant