@@ -8,7 +8,7 @@ import { LRUCache } from "lru-cache"
88import { useDebounceEffect } from "@src/utils/useDebounceEffect"
99import { appendImages } from "@src/utils/imageUtils"
1010import { getCostBreakdownIfNeeded } from "@src/utils/costFormatting"
11- import { batchConsecutive } from "@src/utils/batchConsecutive "
11+ import { batchNearby } from "@src/utils/batchNearby "
1212
1313import type { ClineAsk , ClineSayTool , ClineMessage , ExtensionMessage , AudioType , SuggestionItem } from "@roo-code/types"
1414import { getCompletionCheckpoint , getSuggestionMode , isRetiredProvider } from "@roo-code/types"
@@ -1268,10 +1268,64 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
12681268 }
12691269 }
12701270
1271- // Consolidate consecutive ask messages into batches
1272- const readFileBatched = batchConsecutive ( filtered , isReadFileAsk , synthesizeReadFileBatch )
1273- const listFilesBatched = batchConsecutive ( readFileBatched , isListFilesAsk , synthesizeListFilesBatch )
1274- const result = batchConsecutive ( listFilesBatched , isEditFileAsk , synthesizeEditFileBatch )
1271+ // Messages that can be safely skipped over when batching tool asks.
1272+ // These are low-information or invisible messages that don't affect semantics:
1273+ // - api_req_started / api_req_finished (API request metadata rows)
1274+ // - empty text rows (partial streaming with no visible content)
1275+ // - reasoning rows (hidden from user by default)
1276+ const isIgnorableBetweenTargets = ( msg : ClineMessage ) : boolean => {
1277+ if ( msg . type !== "say" ) return false
1278+ return (
1279+ msg . say === "api_req_started" ||
1280+ msg . say === "api_req_finished" ||
1281+ ( msg . say === "text" && ! msg . text ?. trim ( ) ) ||
1282+ msg . say === "reasoning"
1283+ )
1284+ }
1285+
1286+ // Semantic boundaries that stop batching. When we hit one of these,
1287+ // any current batch is finalized and the boundary message is preserved as-is:
1288+ // - user feedback / new user messages
1289+ // - visible assistant text (the model spoke to the user)
1290+ // - completion result (turn ended)
1291+ // - checkpoint saved
1292+ // - errors
1293+ const isBoundary = ( msg : ClineMessage ) : boolean => {
1294+ if ( msg . type !== "say" ) return false
1295+ return (
1296+ msg . say === "user_feedback" ||
1297+ msg . say === "user_feedback_diff" ||
1298+ ( msg . say === "text" && ! ! msg . text ?. trim ( ) ) ||
1299+ msg . say === "completion_result" ||
1300+ msg . say === "checkpoint_saved" ||
1301+ msg . say === "error" ||
1302+ msg . say === "condense_context" ||
1303+ msg . say === "codebase_search_result"
1304+ )
1305+ }
1306+
1307+ // Consolidate tool asks into batches, allowing ignorable messages between targets.
1308+ // Unlike batchConsecutive which only merges truly adjacent items, batchNearby
1309+ // skips over api_req_started/finished, empty text rows, and reasoning rows that
1310+ // models like qwen insert between tool calls during streaming.
1311+ const readFileBatched = batchNearby ( filtered , {
1312+ isTarget : isReadFileAsk ,
1313+ isIgnorableBetweenTargets,
1314+ isBoundary,
1315+ synthesize : synthesizeReadFileBatch ,
1316+ } )
1317+ const listFilesBatched = batchNearby ( readFileBatched , {
1318+ isTarget : isListFilesAsk ,
1319+ isIgnorableBetweenTargets,
1320+ isBoundary,
1321+ synthesize : synthesizeListFilesBatch ,
1322+ } )
1323+ const result = batchNearby ( listFilesBatched , {
1324+ isTarget : isEditFileAsk ,
1325+ isIgnorableBetweenTargets,
1326+ isBoundary,
1327+ synthesize : synthesizeEditFileBatch ,
1328+ } )
12751329
12761330 if ( isCondensing ) {
12771331 result . push ( {
0 commit comments