Context is Everything logo

Reconciliation Loading Indicator — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Show a subtle "Syncing messages..." indicator when the system reconciles dropped messages after stream completion, so users know background work is happening.

Architecture: Add isReconciling flag to the messageStreams Redux state via new START_RECONCILE / END_RECONCILE actions. The WebSocket hook dispatches these around the history fetch in reconcileIfThinStream. ChatInterface reads the flag and renders a small pulse-dot indicator distinct from the main processing bar.

Tech Stack: React, Redux reducer (projectReducer.js), existing WebSocket hook


Task 1: Add reducer actions for reconciliation state

Files:

  • Modify: claudecodeui/src/reducers/projectReducer.js:24-35 (ActionTypes)
  • Modify: claudecodeui/src/reducers/projectReducer.js:745-794 (near END_MESSAGE_STREAM)

Step 1: Add ActionTypes

In the ActionTypes object (around line 24), add two new entries after LOAD_SESSION_MESSAGES:

START_RECONCILE: 'START_RECONCILE',
END_RECONCILE: 'END_RECONCILE',

Step 2: Add reducer cases

Add these two cases after the END_MESSAGE_STREAM case (after line ~794):

case ActionTypes.START_RECONCILE: {
  const { sessionId } = action.payload;
  const stream = state.messageStreams[sessionId];
  if (!stream) return state;
  return {
    ...state,
    messageStreams: {
      ...state.messageStreams,
      [sessionId]: {
        ...stream,
        isReconciling: true,
        lastUpdate: Date.now()
      }
    }
  };
}

case ActionTypes.END_RECONCILE: {
  const { sessionId } = action.payload;
  const stream = state.messageStreams[sessionId];
  if (!stream) return state;
  return {
    ...state,
    messageStreams: {
      ...state.messageStreams,
      [sessionId]: {
        ...stream,
        isReconciling: false,
        lastUpdate: Date.now()
      }
    }
  };
}

Step 3: Clear isReconciling in START_MESSAGE_STREAM

In the START_MESSAGE_STREAM case (line ~726), add isReconciling: false to the spread so a new stream always starts clean:

case ActionTypes.START_MESSAGE_STREAM: {
  const { sessionId } = action.payload;
  console.log('🚀 [START_STREAM] Starting message stream for session:', sessionId);
  return {
    ...state,
    messageStreams: {
      ...state.messageStreams,
      [sessionId]: {
        ...(state.messageStreams[sessionId] || { messages: [], lastMessageId: null, lastSeq: 0 }),
        isStreaming: true,
        isReconciling: false,
        lastSeq: 0,
        lastUpdate: Date.now()
      }
    }
  };
}

Step 4: Verify build

Run: cd claudecodeui && npx vite build
Expected: Build succeeds (reducer changes only, no consumers yet)

Step 5: Commit

git add claudecodeui/src/reducers/projectReducer.js
git commit -m "feat(stream): add START_RECONCILE/END_RECONCILE reducer actions"

Task 2: Expose reconcile dispatchers from context

Files:

  • Modify: claudecodeui/src/contexts/ProjectContextV2.jsx:197-202 (near endStream/startStream)

Step 1: Add dispatcher functions

After the endStream dispatcher (line ~202), add:

startReconcile: (sessionId) => {
  dispatch({ type: ActionTypes.START_RECONCILE, payload: { sessionId } });
},
endReconcile: (sessionId) => {
  dispatch({ type: ActionTypes.END_RECONCILE, payload: { sessionId } });
},

Step 2: Verify build

Run: cd claudecodeui && npx vite build
Expected: Build succeeds

Step 3: Commit

git add claudecodeui/src/contexts/ProjectContextV2.jsx
git commit -m "feat(stream): expose startReconcile/endReconcile dispatchers"

Task 3: Dispatch reconcile state from WebSocket hook

Files:

  • Modify: claudecodeui/src/hooks/useProjectWebSocketV2.js

Step 1: Destructure new dispatchers

Find where endStream, startStream, appendStreamChunk etc. are destructured from the context (around line 17-27). Add startReconcile and endReconcile to the destructuring.

Step 2: Dispatch START_RECONCILE at the top of reconcileIfThinStream

In reconcileIfThinStream (line ~553), right after the early-return guards (checking !sid, stream.isStreaming, etc.) and right before the api.sessionMessages call (line ~660), add:

startReconcile(sid);

Place it just before fetchAttempted = true; (line ~659).

Step 3: Dispatch END_RECONCILE in the finally block

In the finally block of reconcileIfThinStream (line ~741), add at the end:

endReconcile(sid);

This ensures the flag is always cleared whether reconciliation succeeds, fails, or errors.

Step 4: Verify build

Run: cd claudecodeui && npx vite build
Expected: Build succeeds

Step 5: Commit

git add claudecodeui/src/hooks/useProjectWebSocketV2.js
git commit -m "feat(stream): dispatch reconcile state around history fetch"

Task 4: Render the reconciliation indicator in ChatInterface

Files:

  • Modify: claudecodeui/src/components/ChatInterface.jsx

Step 1: Derive isReconciling state

Near line 3707 where streamingActive is derived, add:

const isReconciling = selectedSession?.id ? Boolean(messageStreams?.[selectedSession.id]?.isReconciling) : false;

Step 2: Add the indicator JSX

Find the showContentStall indicator block (around line 7784). Right after its closing )}, add the reconciliation indicator:

{isReconciling && !streamingActive && !isLoading && (
  <div className="flex items-center gap-2 py-2 px-3 sm:px-0 animate-in fade-in duration-500">
    <div className="flex gap-1">
      <div className="w-1.5 h-1.5 bg-blue-400/70 rounded-full animate-pulse" style={{ animationDuration: '2s' }}></div>
      <div className="w-1.5 h-1.5 bg-blue-500/70 rounded-full animate-pulse" style={{ animationDelay: '0.3s', animationDuration: '2s' }}></div>
      <div className="w-1.5 h-1.5 bg-blue-600/70 rounded-full animate-pulse" style={{ animationDelay: '0.6s', animationDuration: '2s' }}></div>
    </div>
    <span className="text-xs text-blue-400/80 dark:text-blue-300/70">Syncing messages…</span>
  </div>
)}

Note: Uses blue tones (not orange) to visually distinguish from the main processing/stall indicators.

Step 3: Verify build

Run: cd claudecodeui && npx vite build
Expected: Build succeeds

Step 4: Commit

git add claudecodeui/src/components/ChatInterface.jsx
git commit -m "feat(stream): show subtle indicator during message reconciliation

Fixes: sasha-community-bot/sasha-bug-reports#49"

Task 5: Auto-scroll to reconciliation indicator

Files:

  • Modify: claudecodeui/src/components/ChatInterface.jsx

Step 1: Check existing auto-scroll logic

Find the auto-scroll effect that fires when messages change (search for scrollToBottom or scrollIntoView near the bottom of the chat). Verify it already triggers on message list changes — if so, the reconciliation merge via loadSessionMessages should automatically scroll.

If auto-scroll does NOT trigger on reconciled messages, add isReconciling as a dependency to the scroll effect so the indicator scrolls into view.

Step 2: Verify build

Run: cd claudecodeui && npx vite build
Expected: Build succeeds

Step 3: Commit (only if changes were needed)

git add claudecodeui/src/components/ChatInterface.jsx
git commit -m "fix(stream): ensure auto-scroll during reconciliation"

Task 6: Build and manual test

Step 1: Full build verification

Run: cd claudecodeui && npx vite build
Expected: Build succeeds with no errors

Step 2: Manual test plan

To test the indicator appears correctly:

  1. Build and run docker locally: ./docker-local.sh
  2. Open a session and send a message that triggers a long Claude response
  3. Check browser console for drops-detected-on-complete or force-reconcile logs
  4. Verify the blue "Syncing messages..." indicator appears briefly during reconciliation
  5. Verify it disappears once messages are merged
  6. Verify the main orange processing bar still works normally for regular streaming