Skip to content

fix(chat): clear stuck Processing/Thinking indicators on terminal and error lifecycle paths#483

Merged
viper151 merged 1 commit into
mainfrom
fix/do-not-show-processing-banner-on-error-response
Mar 4, 2026
Merged

fix(chat): clear stuck Processing/Thinking indicators on terminal and error lifecycle paths#483
viper151 merged 1 commit into
mainfrom
fix/do-not-show-processing-banner-on-error-response

Conversation

@blackmammoth
Copy link
Copy Markdown
Collaborator

@blackmammoth blackmammoth commented Mar 4, 2026

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

  • UI loading state is started when sending a message in src/components/chat/hooks/useChatComposerState.ts:554.
  • Rendering of the two stuck indicators is driven by loading state:
  1. src/components/chat/view/subcomponents/ClaudeStatus.tsx:89
  2. src/components/chat/view/subcomponents/ChatMessagesPane.tsx:267
  • Some provider terminal/error paths did not fully clear loading + processing session lifecycle.
  • Pending unbound sessions could filter out terminal lifecycle messages before teardown, leaving the view in an in-flight state.

What Changed

  • Added normalized lifecycle handling in src/components/chat/hooks/useChatRealtimeHandlers.ts:
  • messageType normalization and lifecycle classification at :137.
  • pending-unbound session detection at :173.
  • shared finalizer finalizeLifecycleForCurrentView at :241.
  • filter updates to allow terminal teardown for pending-unbound views at :255, :259, :264, :271.
  • Applied shared finalizer to terminal/error events:
  1. claude-error at :597
  2. cursor-error at :657
  • completion/result paths at :673, :755, :911, :1037
  1. codex-error at :934
  2. gemini-error at :984
  • generic backend error at :1138
  • Added inline context comments/logging from staged changes:
  1. src/components/chat/hooks/useChatComposerState.ts:554
  2. src/contexts/WebSocketContext.tsx:70

Behavioral Impact

Processing... and Thinking... now reliably disappear when a request ends via:

  1. explicit completion
  2. provider error
  3. successful abort
  4. generic backend terminal error
  5. Stream buffers are flushed/finalized during terminal teardown, reducing partial in-flight UI artifacts.
  6. Processing session markers are cleared consistently to avoid re-triggering loading state.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved error handling and recovery for chat messaging scenarios
    • Enhanced message lifecycle management and streaming state handling
    • Refined session completion and state cleanup processes
  • Chores

    • Added debug logging to WebSocket message handler

…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
@blackmammoth blackmammoth requested a review from viper151 March 4, 2026 19:06
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

The changes consolidate lifecycle and streaming state management into unified helper functions. Explicit messageType handling improves message classification, and a centralized finalizeLifecycleForCurrentView flow replaces scattered state-reset calls across multiple handlers. Error handling expands to cover generic and specific terminal conditions. Minor logging additions support debugging.

Changes

Cohort / File(s) Summary
Lifecycle and Streaming Management
src/components/chat/hooks/useChatRealtimeHandlers.ts
Refactored to consolidate lifecycle finalization with new helper functions (clearPendingViewSession, flushStreamingState, finalizeLifecycleForCurrentView) that handle end-of-view operations (flush streaming, clear loading, complete sessions, reset state). Introduces explicit messageType handling for normalized classification and expands error handling to cover multiple terminal conditions (claude-error, cursor-error, codex-error, gemini-error, etc.). Adds pending-session guards and streaming buffering/flush logic.
Debugging and Monitoring
src/components/chat/hooks/useChatComposerState.ts, src/contexts/WebSocketContext.tsx
Added inline comment clarifying loading state initialization (// Processing banner starts) and WebSocket message console log for debugging received messages.

Possibly related PRs

Suggested reviewers

  • viper151

Poem

🐰 Through sessions we hop, streaming states align,
One finalized flow—no resets to design!
Errors now caught in one unified way,
Lifecycle management hops brighter today! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: clearing stuck Processing/Thinking indicators on terminal and error lifecycle paths, which aligns with the primary behavioral change across the modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/do-not-show-processing-banner-on-error-response

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 | 🟠 Major

Avoid 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-result finalization 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2320e1d and 9235431.

📒 Files selected for processing (3)
  • src/components/chat/hooks/useChatComposerState.ts
  • src/components/chat/hooks/useChatRealtimeHandlers.ts
  • src/contexts/WebSocketContext.tsx

Comment on lines 268 to 280
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;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

@viper151 viper151 merged commit 0590c5c into main Mar 4, 2026
5 checks passed
@blackmammoth blackmammoth deleted the fix/do-not-show-processing-banner-on-error-response branch March 5, 2026 10:49
SuperOuxx pushed a commit to SuperOuxx/coding-agent-ui that referenced this pull request Mar 5, 2026
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.

3 participants