UI tool ask batching fails when models insert API rows between tool calls
Problem
When using models like qwen, the UI shows multiple separate prompts for the same type of tool call:
Zoo wants to read this file (a.ts)
Zoo wants to read this file (b.ts)
Zoo wants to edit this file (c.ts)
Instead of a single batched prompt:
Zoo wants to read these files:
- a.ts
- b.ts
Root Cause
The existing batchConsecutive() utility only merges tool asks that are truly adjacent in the message array. However, models like qwen insert low-information messages between tool calls during streaming:
say: "api_req_started" — API request metadata row
say: "api_req_finished" — API request completion row
say: "text" with empty content — partial streaming rows
say: "reasoning" — hidden reasoning text
These messages break the consecutive chain, so batchConsecutive stops merging.
Current Code (ChatView.tsx ~line 1272)
const readFileBatched = batchConsecutive(filtered, isReadFileAsk, synthesizeReadFileBatch)
const listFilesBatched = batchConsecutive(readFileBatched, isListFilesAsk, synthesizeListFilesBatch)
const result = batchConsecutive(listFilesBatched, isEditFileAsk, synthesizeEditFileBatch)
Proposed Solution: batchNearby() utility
A new utility that merges same-type tool asks even when separated by ignorable messages. It uses three predicates:
isTarget — matches the target tool type (readFile, listFiles, editFile)
isIgnorableBetweenTargets — skips over low-info messages without breaking the batch
isBoundary — stops merging when hitting a semantic boundary
Ignorable Messages (skipped during batching)
api_req_started / api_req_finished
- Empty text rows (
say: "text" with no content)
- Reasoning rows (
say: "reasoning")
Semantic Boundaries (stop merging)
- User feedback (
user_feedback, user_feedback_diff)
- Visible assistant text (
text with content)
- Completion result (
completion_result)
- Checkpoint saved (
checkpoint_saved)
- Errors (
error)
- Context condensation (
condense_context)
- Codebase search results (
codebase_search_result)
New Usage in ChatView.tsx
const readFileBatched = batchNearby(filtered, {
isTarget: isReadFileAsk,
isIgnorableBetweenTargets, // api_req_started/finished, empty text, reasoning
isBoundary, // user_feedback, visible text, completion_result, error, etc.
synthesize: synthesizeReadFileBatch,
})
Files Changed
- New:
webview-ui/src/utils/batchNearby.ts — the batching utility function
- New:
webview-ui/src/utils/__tests__/batchNearby.spec.ts — 23 comprehensive tests
- Modified:
webview-ui/src/components/chat/ChatView.tsx — replace batchConsecutive with batchNearby
Testing
All 23 unit tests pass, covering:
- Empty input / no matches / single match passthrough
- Consecutive vs non-consecutive matching
- Ignorable messages between targets (api_req_started/finished, empty text, reasoning)
- Semantic boundaries stopping the merge (user_feedback, visible text, completion_result, checkpoint_saved, error)
- Multiple batches separated by boundaries
- Realistic qwen scenarios with API rows between tool calls
Long-term Improvement (separate issue)
Add toolCallGroupId metadata to backend messages so UI can use explicit grouping instead of heuristic-based merging. This would make batching work consistently across all model providers regardless of streaming format differences.
UI tool ask batching fails when models insert API rows between tool calls
Problem
When using models like qwen, the UI shows multiple separate prompts for the same type of tool call:
Instead of a single batched prompt:
Root Cause
The existing
batchConsecutive()utility only merges tool asks that are truly adjacent in the message array. However, models like qwen insert low-information messages between tool calls during streaming:say: "api_req_started"— API request metadata rowsay: "api_req_finished"— API request completion rowsay: "text"with empty content — partial streaming rowssay: "reasoning"— hidden reasoning textThese messages break the consecutive chain, so
batchConsecutivestops merging.Current Code (ChatView.tsx ~line 1272)
Proposed Solution:
batchNearby()utilityA new utility that merges same-type tool asks even when separated by ignorable messages. It uses three predicates:
isTarget— matches the target tool type (readFile, listFiles, editFile)isIgnorableBetweenTargets— skips over low-info messages without breaking the batchisBoundary— stops merging when hitting a semantic boundaryIgnorable Messages (skipped during batching)
api_req_started/api_req_finishedsay: "text"with no content)say: "reasoning")Semantic Boundaries (stop merging)
user_feedback,user_feedback_diff)textwith content)completion_result)checkpoint_saved)error)condense_context)codebase_search_result)New Usage in ChatView.tsx
Files Changed
webview-ui/src/utils/batchNearby.ts— the batching utility functionwebview-ui/src/utils/__tests__/batchNearby.spec.ts— 23 comprehensive testswebview-ui/src/components/chat/ChatView.tsx— replacebatchConsecutivewithbatchNearbyTesting
All 23 unit tests pass, covering:
Long-term Improvement (separate issue)
Add
toolCallGroupIdmetadata to backend messages so UI can use explicit grouping instead of heuristic-based merging. This would make batching work consistently across all model providers regardless of streaming format differences.