From 5391928e2e91fb7a77d0575b9d660ad327c2bedc Mon Sep 17 00:00:00 2001 From: "ScrewTSW (public-projects)" Date: Tue, 18 Aug 2026 22:48:08 +0200 Subject: [PATCH 1/2] 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; } } From 4059cfb5eb694fe2692a236a9df981058ba71625 Mon Sep 17 00:00:00 2001 From: "ScrewTSW (public-projects)" Date: Sat, 22 Aug 2026 11:24:17 +0200 Subject: [PATCH 2/2] test(gui): align think-block fixture with production history layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The batch-drop regression test seeded history with only a user message, but submitEditorAndInitAtIndex (sessionSlice.ts) appends an empty assistant placeholder alongside the user turn before streaming starts. Because streamUpdate attaches `reasoning` to the last history item, the short fixture parked reasoning on the *user* message — a state the reducer can never produce in production: user-only fixture: [0] user "This is a test." reasoning="Reasoning here." [1] assistant "First part. Second part." with placeholder: [0] user "This is a test." [1] assistant "" reasoning="Reasoning here." [2] assistant "First part. Second part." Add the placeholder and shift the assertions to history[1]/history[2]. Also assert history[1].message.role, so the fixture cannot regress to attaching reasoning to a user turn without failing. The fixture is adjusted in this test only; createInitialState() is shared with nine other tests that depend on its current shape. Still mutation-verified: replacing `continue` with `return` fails this test alone ('First part.' vs 'First part. Second part.'), other 9 pass. tsc --noEmit clean. Reported by CodeRabbit on #2. Co-Authored-By: Claude Opus 5 --- gui/src/redux/slices/sessionSlice.test.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/gui/src/redux/slices/sessionSlice.test.ts b/gui/src/redux/slices/sessionSlice.test.ts index a32a2ab5e15..1e70248f373 100644 --- a/gui/src/redux/slices/sessionSlice.test.ts +++ b/gui/src/redux/slices/sessionSlice.test.ts @@ -136,6 +136,18 @@ describe("sessionSlice streamUpdate", () => { it("should not drop later messages in a batch after a block", () => { const initialState = createInitialState(); + // Mirror the production layout: submitEditorAndInitAtIndex appends an + // empty assistant placeholder after the user message, so reasoning is + // attached to that placeholder rather than to the user turn. + initialState.history.push({ + message: { + role: "assistant" as const, + content: "", + id: "initial-assistant-message", + }, + contextItems: [], + }); + const action = { type: "session/streamUpdate", payload: [ @@ -152,10 +164,11 @@ describe("sessionSlice streamUpdate", () => { const newState = sessionSlice.reducer(initialState, action); - expect(newState.history[0].reasoning?.text).toBe("Reasoning here."); + expect(newState.history[1].message.role).toBe("assistant"); + expect(newState.history[1].reasoning?.text).toBe("Reasoning here."); // The second message in the same payload must still be appended. - expect(newState.history[1].message.content).toBe( + expect(newState.history[2].message.content).toBe( "First part. Second part.", ); });