Skip to content

Commit 456bd03

Browse files
authored
🤖 fix: prevent UI freeze during reasoning stream by bypassing useDeferredValue (#1158)
## Summary Fixes a bug where stream updates stopped showing in the UI during thinking blocks, while the token counter continued showing activity. ## Root Cause The `useDeferredValue` hook can defer rendering indefinitely when React keeps receiving new work (rapid streaming deltas). The previous logic only showed immediate messages when the message array length changed: ```tsx const deferredMessages = transformedMessages.length !== deferredTransformedMessages.length ? transformedMessages : deferredTransformedMessages; ``` During reasoning block streaming, the array length stays constant - only the content changes within existing reasoning messages. This caused React to keep deferring indefinitely. ## Symptoms - Token counter kept updating (reads aggregator directly during render, bypassing React state) - Message UI appeared frozen (useDeferredValue kept deferring updates) - Reload fixed it (cleared the deferral queue) ## Fix Added a `hasActiveStream` check that bypasses deferred rendering when any message has `isStreaming=true`, ensuring real-time UI updates during streaming while still benefiting from deferred rendering for idle scrollback: ```tsx const hasActiveStream = transformedMessages.some((m) => "isStreaming" in m && m.isStreaming); const deferredMessages = hasActiveStream || transformedMessages.length !== deferredTransformedMessages.length ? transformedMessages : deferredTransformedMessages; ``` --- _Generated with `mux` • Model: `anthropic:claude-opus-4-5` • Thinking: `medium`_
1 parent e277b21 commit 456bd03

File tree

1 file changed

+7
-4
lines changed

1 file changed

+7
-4
lines changed

‎src/browser/components/AIView.tsx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,14 @@ const AIViewInner: React.FC<AIViewProps> = ({
173173
const transformedMessages = useMemo(() => mergeConsecutiveStreamErrors(messages), [messages]);
174174
const deferredTransformedMessages = useDeferredValue(transformedMessages);
175175

176-
// CRITICAL: When message count changes (new message sent/received), show immediately.
177-
// Only defer content changes within existing messages (streaming deltas).
178-
// This ensures user messages appear instantly while keeping streaming performant.
176+
// CRITICAL: Show immediate messages when streaming or when message count changes.
177+
// useDeferredValue can defer indefinitely if React keeps getting new work (rapid deltas).
178+
// During active streaming (reasoning, text), we MUST show immediate updates or the UI
179+
// appears frozen while only the token counter updates (reads aggregator directly).
180+
// Only use deferred messages when the stream is idle and no content is changing.
181+
const hasActiveStream = transformedMessages.some((m) => "isStreaming" in m && m.isStreaming);
179182
const deferredMessages =
180-
transformedMessages.length !== deferredTransformedMessages.length
183+
hasActiveStream || transformedMessages.length !== deferredTransformedMessages.length
181184
? transformedMessages
182185
: deferredTransformedMessages;
183186

0 commit comments

Comments
 (0)