Skip to content

fix(chat): prevent streaming text from appearing in bursts after citations#7745

Merged
nmgarza5 merged 1 commit intomainfrom
nikg/fix-streaming-bursts
Jan 24, 2026
Merged

fix(chat): prevent streaming text from appearing in bursts after citations#7745
nmgarza5 merged 1 commit intomainfrom
nikg/fix-streaming-bursts

Conversation

@nmgarza5
Copy link
Copy Markdown
Contributor

@nmgarza5 nmgarza5 commented Jan 24, 2026

Description

Fixes a regression where AI response text appeared in large bursts instead of streaming smoothly.

Root Cause

The AIMessage component uses React.memo with a custom arePropsEqual function that compared prev.rawPackets.length === next.rawPackets.length. However, the packets array was being mutated in place with .push() and the same array reference was passed to the store on each update.

This violated React's immutability expectation: prev.rawPackets and next.rawPackets pointed to the same object, so the length comparison always returned true (comparing identical objects), preventing re-renders during streaming.

Solution

Added a packetsVersion counter that increments with each new packet. The memo comparison now checks prev.packetsVersion === next.packetsVersion instead of comparing array lengths.

Why Version Counter Over Array Copying?

An alternative fix would be to create a new array on each update: packets: [...packets]. However:

Approach Complexity per packet Memory
[...packets] O(n) - copies entire array Creates n arrays of sizes 1, 2, 3, ..., n
packetsVersion++ O(1) - increment integer Single integer

For a response with 500 packets, array copying would perform ~125,000 copy operations total, while the version counter approach performs 500 increments. This matters for long AI responses.

How Has This Been Tested?

2026-01-24 10 28 05

Additional Options

  • [Optional] Override Linear Check

@nmgarza5 nmgarza5 requested a review from a team as a code owner January 24, 2026 18:18
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 24, 2026

Greptile Overview

Greptile Summary

Fixes streaming regression by introducing a packetsVersion counter that increments with each packet, enabling React.memo to detect updates. Previously, AIMessage's arePropsEqual compared rawPackets.length, but the shallow copy in upsertMessages preserved the same array reference, causing all length comparisons to be equal and blocking re-renders.

Key changes:

  • Added packetsVersion counter in useChatController.ts that increments on each packet push
  • Updated arePropsEqual in AIMessage.tsx to compare packetsVersion instead of rawPackets.length
  • Removed noisy console.debug statements during streaming
  • Cleaned up outdated TODO comment about packets being append-only

Technical approach: Instead of creating new array copies on each update (memory overhead), uses an integer version counter (primitive value) that gets copied during shallow cloning, triggering proper React re-renders.

Confidence Score: 4/5

  • Safe to merge - fixes a real regression with a sound technical approach
  • The fix addresses the root cause correctly by ensuring React detects packet updates through a version counter. The approach is memory-efficient and aligns with the existing shallow-copy pattern in upsertMessages. Minor improvement suggested for consistency in SharedChatDisplay.tsx.
  • No files require special attention - the implementation is straightforward and consistent across all modified files

Important Files Changed

Filename Overview
web/src/app/chat/hooks/useChatController.ts Added packetsVersion counter to track packet updates, incremented on each packet push, and removed debug logging
web/src/app/chat/interfaces.ts Added optional packetsVersion field to Message interface with clear documentation
web/src/app/chat/message/messageComponents/AIMessage.tsx Updated memo comparison to use packetsVersion instead of rawPackets.length and removed outdated TODO comment
web/src/components/chat/MessageList.tsx Passed packetsVersion prop to AIMessage component

Sequence Diagram

sequenceDiagram
    participant Stream as Backend Stream
    participant Hook as useChatController
    participant Tree as MessageTree
    participant List as MessageList
    participant AI as AIMessage (React.memo)
    
    Stream->>Hook: packet arrives
    Hook->>Hook: packets.push(packet)
    Hook->>Hook: packetsVersion++
    Hook->>Tree: upsertToCompleteMessageTree({packets, packetsVersion})
    Tree->>Tree: shallow copy message ({...msg})
    Note over Tree: packetsVersion copied (primitive)<br/>packets ref copied (same array)
    Tree->>List: messageTree updated
    List->>AI: render with rawPackets, packetsVersion
    AI->>AI: arePropsEqual(prev, next)
    Note over AI: prev.packetsVersion !== next.packetsVersion<br/>returns false → re-render!
    AI->>AI: process new packets from lastProcessedIndex
Loading

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

@nmgarza5 nmgarza5 force-pushed the nikg/fix-streaming-bursts branch from 0af3591 to 9b69c36 Compare January 24, 2026 18:28
The AIMessage component uses React.memo with a custom arePropsEqual
that compared rawPackets.length. However, the packets array was being
mutated in place with .push() and the same reference was passed to the
store on each update. This caused prev.rawPackets and next.rawPackets
to be the same object, making the length comparison always return true
and preventing re-renders during streaming.

Fix: Add a packetsVersion counter that increments with each packet.
The memo comparison now checks this version instead of array length.
This approach was chosen over creating a new array ([...packets]) on
each update because it's O(1) vs O(n) per packet - significant for
long responses with hundreds of packets.
@nmgarza5 nmgarza5 force-pushed the nikg/fix-streaming-bursts branch from 9b69c36 to f5ce38d Compare January 24, 2026 18:28
@nmgarza5
Copy link
Copy Markdown
Contributor Author

@greptile

@nmgarza5 nmgarza5 changed the title fix(chat): prevent streaming text from appearing in bursts fix(chat): prevent streaming text from appearing in bursts after citations Jan 24, 2026
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Jan 24, 2026

Additional Comments (1)

web/src/app/chat/shared/[chatId]/SharedChatDisplay.tsx
also pass packetsVersion={message.packetsVersion} here for consistency with MessageList.tsx

                    rawPackets={message.packets}
                    packetsVersion={message.packetsVersion}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/app/chat/shared/[chatId]/SharedChatDisplay.tsx
Line: 98:98

Comment:
also pass `packetsVersion={message.packetsVersion}` here for consistency with `MessageList.tsx`

```suggestion
                    rawPackets={message.packets}
                    packetsVersion={message.packetsVersion}
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@nmgarza5 nmgarza5 enabled auto-merge January 24, 2026 18:42
@nmgarza5 nmgarza5 disabled auto-merge January 24, 2026 18:57
@nmgarza5
Copy link
Copy Markdown
Contributor Author

Failing exa test is unrelated. going to merge

@nmgarza5 nmgarza5 merged commit d9db849 into main Jan 24, 2026
291 of 303 checks passed
@nmgarza5 nmgarza5 deleted the nikg/fix-streaming-bursts branch January 24, 2026 19:48
@wenxi-onyx wenxi-onyx mentioned this pull request Jan 26, 2026
1 task
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.

2 participants