@@ -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"
@@ -1279,10 +1279,64 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
12791279 }
12801280 }
12811281
1282- // Consolidate consecutive ask messages into batches
1283- const readFileBatched = batchConsecutive ( filtered , isReadFileAsk , synthesizeReadFileBatch )
1284- const listFilesBatched = batchConsecutive ( readFileBatched , isListFilesAsk , synthesizeListFilesBatch )
1285- const result = batchConsecutive ( listFilesBatched , isEditFileAsk , synthesizeEditFileBatch )
1282+ // Messages that can be safely skipped over when batching tool asks.
1283+ // These are low-information or invisible messages that don't affect semantics:
1284+ // - api_req_started / api_req_finished (API request metadata rows)
1285+ // - empty text rows (partial streaming with no visible content)
1286+ // - reasoning rows (hidden from user by default)
1287+ const isIgnorableBetweenTargets = ( msg : ClineMessage ) : boolean => {
1288+ if ( msg . type !== "say" ) return false
1289+ return (
1290+ msg . say === "api_req_started" ||
1291+ msg . say === "api_req_finished" ||
1292+ ( msg . say === "text" && ! msg . text ?. trim ( ) ) ||
1293+ msg . say === "reasoning"
1294+ )
1295+ }
1296+
1297+ // Semantic boundaries that stop batching. When we hit one of these,
1298+ // any current batch is finalized and the boundary message is preserved as-is:
1299+ // - user feedback / new user messages
1300+ // - visible assistant text (the model spoke to the user)
1301+ // - completion result (turn ended)
1302+ // - checkpoint saved
1303+ // - errors
1304+ const isBoundary = ( msg : ClineMessage ) : boolean => {
1305+ if ( msg . type !== "say" ) return false
1306+ return (
1307+ msg . say === "user_feedback" ||
1308+ msg . say === "user_feedback_diff" ||
1309+ ( msg . say === "text" && ! ! msg . text ?. trim ( ) ) ||
1310+ msg . say === "completion_result" ||
1311+ msg . say === "checkpoint_saved" ||
1312+ msg . say === "error" ||
1313+ msg . say === "condense_context" ||
1314+ msg . say === "codebase_search_result"
1315+ )
1316+ }
1317+
1318+ // Consolidate tool asks into batches, allowing ignorable messages between targets.
1319+ // Unlike batchConsecutive which only merges truly adjacent items, batchNearby
1320+ // skips over api_req_started/finished, empty text rows, and reasoning rows that
1321+ // models like qwen insert between tool calls during streaming.
1322+ const readFileBatched = batchNearby ( filtered , {
1323+ isTarget : isReadFileAsk ,
1324+ isIgnorableBetweenTargets,
1325+ isBoundary,
1326+ synthesize : synthesizeReadFileBatch ,
1327+ } )
1328+ const listFilesBatched = batchNearby ( readFileBatched , {
1329+ isTarget : isListFilesAsk ,
1330+ isIgnorableBetweenTargets,
1331+ isBoundary,
1332+ synthesize : synthesizeListFilesBatch ,
1333+ } )
1334+ const result = batchNearby ( listFilesBatched , {
1335+ isTarget : isEditFileAsk ,
1336+ isIgnorableBetweenTargets,
1337+ isBoundary,
1338+ synthesize : synthesizeEditFileBatch ,
1339+ } )
12861340
12871341 if ( isCondensing ) {
12881342 result . push ( {
0 commit comments