Bug
When a user sends a message while the assistant is still streaming a response (mid-tool-call), tool blocks from the previous turn can get stuck showing "Running..." permanently. The assistant's response also gets visually split — part appears before the user message, part after.
Reproduction
- Start a session that triggers multiple sequential tool calls (e.g., writing 3 files)
- While the assistant is still mid-response (between tool calls), send a new message
- Observe: the last tool call(s) from the previous turn show "Running..." and never resolve
Screenshot
The third Write tool pill shows "Running..." even though the file write completed server-side. The user's message appears between the second and third Write calls, splitting the assistant turn.
Root Cause Analysis
MESSAGE_START force-finalizes in-flight messages
In useChatMessages.ts, the MESSAGE_START reducer case immediately finalizes the current streaming message:
case 'MESSAGE_START': {
const base = state.current
? { ...state, messages: [...state.messages, finishCurrent(state.current)] }
: state;
// ...
}
When the user sends a message during streaming:
- Server receives the user's new message (interrupt or queue)
- Server emits MESSAGE_START for the new assistant response
forceFlushPendingMessage closes open blocks server-side
- But the frontend reducer processes MESSAGE_START and finalizes the old
current before the BLOCK_END events for the prior turn arrive
- Late-arriving BLOCK_END events are silently dropped because the block no longer exists in
state.current
BLOCK_END dropped when block not in current
case 'BLOCK_END': {
if (!state.current) return state;
const block = state.current.blocks.get(action.blockId);
if (!block) return state; // ← silently dropped
// ...
}
The block was moved to state.messages (finalized) but BLOCK_END still references the old blockId. Since the finalized message's blocks are a snapshot, the done: false flag is never updated.
Suggested Fix
Two complementary fixes:
1. BLOCK_END should check finalized messages too
When BLOCK_END arrives and the block isn't in state.current, search state.messages for the blockId and update it there:
case 'BLOCK_END': {
if (state.current) {
const block = state.current.blocks.get(action.blockId);
if (block) {
// ... existing logic (update current)
}
}
// Fallback: check recently finalized messages
const msgIdx = state.messages.findLastIndex(m =>
m.blocks.some(b => b.blockId === action.blockId)
);
if (msgIdx >= 0) {
// Update the block in the finalized message
}
return state;
}
2. finishCurrent should mark all blocks as done
When force-finalizing due to a new MESSAGE_START, mark any done: false blocks as done: true:
function finishCurrent(current: StreamingMessage): FinishedMessage {
const blocks = Array.from(current.blockOrder, (id) => {
const b = current.blocks.get(id)!;
return { ...b, done: true }; // ← force done on finalize
});
// ...
}
Impact
- Tool pills stuck in "Running..." state permanently (UX)
- Assistant turns visually split across user messages (confusing)
- Affects any session where the user sends during mid-stream
Bug
When a user sends a message while the assistant is still streaming a response (mid-tool-call), tool blocks from the previous turn can get stuck showing "Running..." permanently. The assistant's response also gets visually split — part appears before the user message, part after.
Reproduction
Screenshot
The third
Writetool pill shows "Running..." even though the file write completed server-side. The user's message appears between the second and third Write calls, splitting the assistant turn.Root Cause Analysis
MESSAGE_START force-finalizes in-flight messages
In
useChatMessages.ts, the MESSAGE_START reducer case immediately finalizes the current streaming message:When the user sends a message during streaming:
forceFlushPendingMessagecloses open blocks server-sidecurrentbefore the BLOCK_END events for the prior turn arrivestate.currentBLOCK_END dropped when block not in current
The block was moved to
state.messages(finalized) but BLOCK_END still references the old blockId. Since the finalized message's blocks are a snapshot, thedone: falseflag is never updated.Suggested Fix
Two complementary fixes:
1. BLOCK_END should check finalized messages too
When BLOCK_END arrives and the block isn't in
state.current, searchstate.messagesfor the blockId and update it there:2. finishCurrent should mark all blocks as done
When force-finalizing due to a new MESSAGE_START, mark any
done: falseblocks asdone: true:Impact