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:
- Build and run docker locally:
./docker-local.sh - Open a session and send a message that triggers a long Claude response
- Check browser console for
drops-detected-on-completeorforce-reconcilelogs - Verify the blue "Syncing messages..." indicator appears briefly during reconciliation
- Verify it disappears once messages are merged
- Verify the main orange processing bar still works normally for regular streaming
