From 8233f9bbd06ec1ecea12ebaa48800e06290d3f7b Mon Sep 17 00:00:00 2001 From: PierrunoYT Date: Tue, 20 Oct 2026 23:16:58 +0200 Subject: [PATCH] fix(chat): keep API request and reasoning rows after a batched group batchNearby discarded every ignorable message collected while batching, so the api_req_started and reasoning rows after the last batched target (which belong to the next turn) disappeared. Only consume ignorable items between targets and re-emit the trailing ones. Fixes #2061 Co-Authored-By: Claude Sonnet 5.5 --- .../src/utils/__tests__/batchNearby.spec.ts | 58 +++++++++++++++++++ webview-ui/src/utils/batchNearby.ts | 12 ++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/utils/__tests__/batchNearby.spec.ts b/webview-ui/src/utils/__tests__/batchNearby.spec.ts index 5ac35de93d..fb03994354 100644 --- a/webview-ui/src/utils/__tests__/batchNearby.spec.ts +++ b/webview-ui/src/utils/__tests__/batchNearby.spec.ts @@ -450,4 +450,62 @@ describe("batchNearby", () => { expect(result[1].text).toBe("feedback") expect(result[2].text).toBe('BATCH:{"tool":"readFile","path":"c.ts"},{"tool":"readFile","path":"d.ts"}') }) + + describe("ignorable items around a batch", () => { + const options = { + isTarget: isMatch, + isIgnorableBetweenTargets, + isBoundary, + synthesize: synthesizeBatch, + } + + test("consumes ignorable items between targets but keeps the ones after the last target", () => { + const betweenRequest = msg("between request", "say", "api_req_started") + const nextRequest = msg("next request", "say", "api_req_started") + const nextReasoning = msg("next reasoning", "say", "reasoning") + + const result = batchNearby( + [msg("match-1", "ask"), betweenRequest, msg("match-2", "ask"), nextRequest, nextReasoning], + options, + ) + + expect(result.map((m) => m.text)).toEqual(["BATCH:match-1,match-2", "next request", "next reasoning"]) + }) + + test("keeps trailing ignorable items when the batch is followed by a boundary", () => { + const result = batchNearby( + [ + msg("match-1", "ask"), + msg("match-2", "ask"), + msg("next request", "say", "api_req_started"), + msg("visible text", "say", "text"), + ], + options, + ) + + expect(result.map((m) => m.text)).toEqual(["BATCH:match-1,match-2", "next request", "visible text"]) + }) + + test("keeps ignorable items that follow a single target", () => { + const messages = [msg("match-1", "ask"), msg("next request", "say", "api_req_started")] + + expect(batchNearby(messages, options)).toEqual(messages) + }) + + test("does not duplicate ignorable items between three targets", () => { + const result = batchNearby( + [ + msg("match-1", "ask"), + msg("req a", "say", "api_req_started"), + msg("match-2", "ask"), + msg("req b", "say", "api_req_started"), + msg("match-3", "ask"), + msg("req c", "say", "api_req_started"), + ], + options, + ) + + expect(result.map((m) => m.text)).toEqual(["BATCH:match-1,match-2,match-3", "req c"]) + }) + }) }) diff --git a/webview-ui/src/utils/batchNearby.ts b/webview-ui/src/utils/batchNearby.ts index ab49be2b22..f73dc91196 100644 --- a/webview-ui/src/utils/batchNearby.ts +++ b/webview-ui/src/utils/batchNearby.ts @@ -57,6 +57,8 @@ export function batchNearby(items: T[], options: BatchNearbyOptions): T[] break // boundary stops the batch } else if (isTarget(items[j])) { batch.push(items[j]) + // Ignorable items seen so far sit between two targets, so the batch consumes them + pendingIgnorable.length = 0 j++ } else if (isIgnorableBetweenTargets(items[j], batchContext)) { pendingIgnorable.push(items[j]) // track but don't commit yet @@ -67,14 +69,16 @@ export function batchNearby(items: T[], options: BatchNearbyOptions): T[] } if (batch.length > 1) { - // Bridge succeeded — pending ignorable items are metadata consumed by the batch + // Bridge succeeded — ignorable items between targets were consumed by the batch. + // Those left in pendingIgnorable come after the last target and belong to the + // next turn (its api_req_started / reasoning rows), so they must stay visible. result.push(synthesize(batch)) } else { // Bridge failed — restore pending ignorable items to preserve in-order semantics result.push(batch[0]) - if (pendingIgnorable.length > 0) { - result.push(...pendingIgnorable) - } + } + if (pendingIgnorable.length > 0) { + result.push(...pendingIgnorable) } i = j // items[j] was not consumed — re-examine it on next iteration