From 5391928e2e91fb7a77d0575b9d660ad327c2bedc Mon Sep 17 00:00:00 2001 From: "ScrewTSW (public-projects)" Date: Tue, 18 Aug 2026 22:48:08 +0200 Subject: [PATCH] fix(gui): don't drop batched messages after a block The `...` fast-path in the `streamUpdate` reducer ended in `return` rather than `continue`. Since it sits inside a `for (const message of action.payload)` loop, returning exits the reducer entirely and silently discards every remaining message in the batch, not just the one being handled. The sibling early-exit for redacted thinking uses `continue`, and nothing runs after the loop, so `continue` is the intended control flow here. This went unnoticed because every existing `streamUpdate` test dispatches a single-element payload, where `return` and `continue` are indistinguishable. The added test uses a two-message payload and fails on `return`. Co-Authored-By: Claude Opus 5 --- gui/src/redux/slices/sessionSlice.test.ts | 26 +++++++++++++++++++++++ gui/src/redux/slices/sessionSlice.ts | 6 +++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/gui/src/redux/slices/sessionSlice.test.ts b/gui/src/redux/slices/sessionSlice.test.ts index e933d1dd879..a32a2ab5e15 100644 --- a/gui/src/redux/slices/sessionSlice.test.ts +++ b/gui/src/redux/slices/sessionSlice.test.ts @@ -133,6 +133,32 @@ describe("sessionSlice streamUpdate", () => { ); expect(newState.history[1].message.id).toBe("mock-uuid-1"); }); + + it("should not drop later messages in a batch after a block", () => { + const initialState = createInitialState(); + const action = { + type: "session/streamUpdate", + payload: [ + { + role: "assistant" as const, + content: "Reasoning here.First part.", + }, + { + role: "assistant" as const, + content: " Second part.", + }, + ], + }; + + const newState = sessionSlice.reducer(initialState, action); + + expect(newState.history[0].reasoning?.text).toBe("Reasoning here."); + + // The second message in the same payload must still be appended. + expect(newState.history[1].message.content).toBe( + "First part. Second part.", + ); + }); }); describe("Tool Call With Response", () => { diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts index 8784d0c41dc..60eccf02145 100644 --- a/gui/src/redux/slices/sessionSlice.ts +++ b/gui/src/redux/slices/sessionSlice.ts @@ -580,7 +580,11 @@ export const sessionSlice = createSlice({ handleToolCallsInMessage(message, lastItem); - return; + // `continue`, not `return`: this branch has finished handling + // *this* message, but `action.payload` may contain more. A + // `return` here exits the reducer entirely and silently drops + // every remaining message in the batch. + continue; } }