fix(chat): clear stuck Processing/Thinking indicators on terminal and error lifecycle paths#483
Conversation
…inking UI Fixes persistent "Processing..." and "Thinking..." indicators by normalizing lifecycle teardown when provider flows end via error, abort, or incomplete terminal signaling. Key changes: - add lifecycle normalization helpers in useChatRealtimeHandlers (flushStreamingState, clearPendingViewSession, finalizeLifecycleForCurrentView) - treat generic backend 'error' as a terminal lifecycle signal - improve session filtering so terminal events can still finalize unbound pending-view requests - route provider terminal paths (claude/cursor/codex/gemini errors, complete/result, codex turn completion/failure, successful abort) through shared teardown - ensure teardown clears loading status, processing-session tracking, pending permissions, and buffered stream state - add small inline observability/context comments in composer and websocket provider (processing start and incoming websocket payload logging) Files: - src/components/chat/hooks/useChatRealtimeHandlers.ts - src/components/chat/hooks/useChatComposerState.ts - src/contexts/WebSocketContext.tsx
📝 WalkthroughWalkthroughThe changes consolidate lifecycle and streaming state management into unified helper functions. Explicit messageType handling improves message classification, and a centralized Changes
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/contexts/WebSocketContext.tsx (1)
66-71:⚠️ Potential issue | 🟠 MajorAvoid unconditional logging of full WebSocket payloads.
This logs every inbound message payload in all environments, which can leak sensitive chat/tool data and creates noisy production diagnostics. Gate it behind an explicit debug flag (or remove it).
Suggested change
- console.log('--->Received WebSocket message:', data); + const debugWebSocket = + typeof window !== 'undefined' && window.localStorage.getItem('debug:websocket') === '1'; + if (debugWebSocket) { + console.debug('[WebSocket] Received message:', data); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/contexts/WebSocketContext.tsx` around lines 66 - 71, The current websocket.onmessage handler unconditionally logs the full inbound payload via console.log('--->Received WebSocket message:', data), which can leak sensitive data; update the handler to remove or gate that logging behind an explicit debug flag (e.g., a context prop or env var like process.env.NODE_ENV === 'development' or a new enableWebsocketDebug flag) before calling console.log, and/or redact sensitive fields before logging; locate this in the websocket.onmessage block where setLatestMessage is called and wrap or remove the console.log accordingly.src/components/chat/hooks/useChatRealtimeHandlers.ts (1)
673-678:⚠️ Potential issue | 🟠 Major
cursor-resultfinalization runs too early for result reconciliation.Calling the shared finalizer before cursor-result-specific merge logic can finalize the streaming message first, so final result text is appended as a new message instead of replacing the in-flight one.
Suggested fix
- finalizeLifecycleForCurrentView( - cursorCompletedSessionId, - currentSessionId, - selectedSession?.id, - pendingCursorSessionId, - ); - try { const resultData = latestMessage.data || {}; const textResult = typeof resultData.result === 'string' ? resultData.result : ''; @@ } catch (error) { console.warn('Error handling cursor-result message:', error); } + + finalizeLifecycleForCurrentView( + cursorCompletedSessionId, + currentSessionId, + selectedSession?.id, + pendingCursorSessionId, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/chat/hooks/useChatRealtimeHandlers.ts` around lines 673 - 678, The finalizeLifecycleForCurrentView call is happening before the cursor-result merge/reconciliation, which causes the streaming message to be finalized too early and the final text to be appended as a new message; move or delay calling finalizeLifecycleForCurrentView(cursorCompletedSessionId, currentSessionId, selectedSession?.id, pendingCursorSessionId) so it runs only after the cursor-result-specific merge/reconciliation completes (i.e., after the cursor-result merge handler finishes updating the in-flight message), or conditionally skip finalization when pendingCursorSessionId/cursorCompletedSessionId indicate a cursor-result flow until reconciliation is done.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/chat/hooks/useChatRealtimeHandlers.ts`:
- Around line 268-280: The mismatch branch currently returns early for messages
missing latestMessage.sessionId which can skip final lifecycle handling for
terminal error events; update the logic in useChatRealtimeHandlers to only
return early when the message is not a terminal lifecycle event (check whatever
flag or field denotes terminal lifecycle on latestMessage), and for terminal
lifecycle messages call handleBackgroundLifecycle using latestMessage.sessionId
if present or the pending-unbound session id from state (use the existing
hasPendingUnboundSession flag to fetch that id) before returning so lifecycle
finalization always runs.
---
Outside diff comments:
In `@src/components/chat/hooks/useChatRealtimeHandlers.ts`:
- Around line 673-678: The finalizeLifecycleForCurrentView call is happening
before the cursor-result merge/reconciliation, which causes the streaming
message to be finalized too early and the final text to be appended as a new
message; move or delay calling
finalizeLifecycleForCurrentView(cursorCompletedSessionId, currentSessionId,
selectedSession?.id, pendingCursorSessionId) so it runs only after the
cursor-result-specific merge/reconciliation completes (i.e., after the
cursor-result merge handler finishes updating the in-flight message), or
conditionally skip finalization when
pendingCursorSessionId/cursorCompletedSessionId indicate a cursor-result flow
until reconciliation is done.
In `@src/contexts/WebSocketContext.tsx`:
- Around line 66-71: The current websocket.onmessage handler unconditionally
logs the full inbound payload via console.log('--->Received WebSocket message:',
data), which can leak sensitive data; update the handler to remove or gate that
logging behind an explicit debug flag (e.g., a context prop or env var like
process.env.NODE_ENV === 'development' or a new enableWebsocketDebug flag)
before calling console.log, and/or redact sensitive fields before logging;
locate this in the websocket.onmessage block where setLatestMessage is called
and wrap or remove the console.log accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e39e37f2-759b-41f3-b281-502a83fc7970
📒 Files selected for processing (3)
src/components/chat/hooks/useChatComposerState.tssrc/components/chat/hooks/useChatRealtimeHandlers.tssrc/contexts/WebSocketContext.tsx
| if (latestMessage.sessionId !== activeViewSessionId) { | ||
| if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) { | ||
| handleBackgroundLifecycle(latestMessage.sessionId); | ||
| const shouldTreatAsPendingViewLifecycle = | ||
| !activeViewSessionId && | ||
| hasPendingUnboundSession && | ||
| latestMessage.sessionId && | ||
| isLifecycleMessage; | ||
|
|
||
| if (!shouldTreatAsPendingViewLifecycle) { | ||
| if (latestMessage.sessionId && isLifecycleMessage) { | ||
| handleBackgroundLifecycle(latestMessage.sessionId); | ||
| } | ||
| return; | ||
| } |
There was a problem hiding this comment.
Pending-unbound terminal errors can still be dropped before teardown.
When latestMessage.sessionId is missing, this mismatch branch can return early even for terminal error events, so lifecycle finalization may not run and indicators can remain stuck.
Suggested fix
+ const normalizedMessageSessionId = latestMessage.sessionId ?? null;
@@
- if (!latestMessage.sessionId && !isUnscopedError && !hasPendingUnboundSession) {
+ if (!normalizedMessageSessionId && !isUnscopedError && !hasPendingUnboundSession) {
return;
}
- if (latestMessage.sessionId !== activeViewSessionId) {
+ if (normalizedMessageSessionId !== activeViewSessionId) {
const shouldTreatAsPendingViewLifecycle =
!activeViewSessionId &&
hasPendingUnboundSession &&
- latestMessage.sessionId &&
- isLifecycleMessage;
+ isLifecycleMessage &&
+ (Boolean(normalizedMessageSessionId) || isUnscopedError || messageType === 'error');
if (!shouldTreatAsPendingViewLifecycle) {
- if (latestMessage.sessionId && isLifecycleMessage) {
- handleBackgroundLifecycle(latestMessage.sessionId);
+ if (normalizedMessageSessionId && isLifecycleMessage) {
+ handleBackgroundLifecycle(normalizedMessageSessionId);
}
return;
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/chat/hooks/useChatRealtimeHandlers.ts` around lines 268 - 280,
The mismatch branch currently returns early for messages missing
latestMessage.sessionId which can skip final lifecycle handling for terminal
error events; update the logic in useChatRealtimeHandlers to only return early
when the message is not a terminal lifecycle event (check whatever flag or field
denotes terminal lifecycle on latestMessage), and for terminal lifecycle
messages call handleBackgroundLifecycle using latestMessage.sessionId if present
or the pending-unbound session id from state (use the existing
hasPendingUnboundSession flag to fetch that id) before returning so lifecycle
finalization always runs.
Summary
This PR fixes a chat lifecycle bug where the Processing... banner and inline Thinking... indicator could remain visible after operations had already terminated (especially on error or incomplete completion signaling).
Problem
What Changed
Behavioral Impact
Processing... and Thinking... now reliably disappear when a request ends via:
Summary by CodeRabbit
Release Notes
Bug Fixes
Chores