Skip to content

Commit 64d6e37

Browse files
easonLiangWorldedtechtaltaseasonliang28
authored
feat(webview): add batchNearby utility for smarter tool ask batching (#1005)
* feat(webview): add batchNearby utility for smarter tool ask batching Replace batchConsecutive with batchNearby in ChatView.tsx to allow merging same-type tool asks even when separated by ignorable messages (api_req_started/finished, empty text rows, reasoning). Semantic boundaries (user_feedback, visible text, completion_result, checkpoint_saved, error) stop the merge, preserving correct ordering. This fixes UX where models like qwen insert API request rows between tool calls, causing UI to show multiple 'Zoo wants to read this file' instead of a single batched prompt. * fix: batchNearby restores dropped ignorable items when bridge fails When a single target is followed by ignorable messages and then a boundary (no second target found), the original code silently dropped those ignorable items — contradicting the documented contract that all items are preserved in-order. Fix: track skipped ignorable items in pendingIgnorable, flush them after the batch result so they're never lost. When bridge succeeds (second target found), pending items are consumed into the synthesized batch as expected. * fix(chat): keep nearby batching boundaries in sync * fix(chat): share tool batching predicates --------- Co-authored-by: T <[email protected]> Co-authored-by: Eason Liang <[email protected]>
1 parent f589ce7 commit 64d6e37

7 files changed

Lines changed: 590 additions & 159 deletions

File tree

‎webview-ui/src/components/chat/ChatView.tsx‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { LRUCache } from "lru-cache"
88
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
99
import { appendImages } from "@src/utils/imageUtils"
1010
import { getCostBreakdownIfNeeded } from "@src/utils/costFormatting"
11-
import { batchConsecutive } from "@src/utils/batchConsecutive"
11+
import { batchNearby } from "@src/utils/batchNearby"
12+
import { isBoundary, isIgnorableBetweenTargets } from "@src/utils/chatBatchingPredicates"
1213

1314
import type { ClineAsk, ClineSayTool, ClineMessage, ExtensionMessage, AudioType, SuggestionItem } from "@roo-code/types"
1415
import { getCompletionCheckpoint, getSuggestionMode, isRetiredProvider } from "@roo-code/types"
@@ -1279,10 +1280,27 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
12791280
}
12801281
}
12811282

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)
1283+
// Consolidate tool asks into batches, allowing ignorable messages between targets.
1284+
// batchNearby skips over api_req_started, empty text rows, and reasoning rows that
1285+
// models like qwen insert between tool calls during streaming.
1286+
const readFileBatched = batchNearby(filtered, {
1287+
isTarget: isReadFileAsk,
1288+
isIgnorableBetweenTargets,
1289+
isBoundary,
1290+
synthesize: synthesizeReadFileBatch,
1291+
})
1292+
const listFilesBatched = batchNearby(readFileBatched, {
1293+
isTarget: isListFilesAsk,
1294+
isIgnorableBetweenTargets,
1295+
isBoundary,
1296+
synthesize: synthesizeListFilesBatch,
1297+
})
1298+
const result = batchNearby(listFilesBatched, {
1299+
isTarget: isEditFileAsk,
1300+
isIgnorableBetweenTargets,
1301+
isBoundary,
1302+
synthesize: synthesizeEditFileBatch,
1303+
})
12861304

12871305
if (isCondensing) {
12881306
result.push({

‎webview-ui/src/components/chat/__tests__/ChatView.spec.tsx‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const mockVirtuosoState = vi.hoisted(() => ({
1616
defaultItemHeight?: number
1717
increaseViewportBy?: number | { top?: number; bottom?: number }
1818
} | null,
19+
lastData: [] as ClineMessage[],
1920
}))
2021

2122
// Define minimal types needed for testing
@@ -111,6 +112,7 @@ vi.mock("react-virtuoso", () => ({
111112
defaultItemHeight,
112113
increaseViewportBy,
113114
}
115+
mockVirtuosoState.lastData = data
114116

115117
return (
116118
<div data-testid="virtuoso-item-list">
@@ -287,6 +289,22 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
287289
</button>
288290
)
289291
},
292+
VSCodeCheckbox: function MockVSCodeCheckbox({
293+
children,
294+
checked,
295+
onChange,
296+
}: {
297+
children: React.ReactNode
298+
checked?: boolean
299+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
300+
}) {
301+
return (
302+
<label>
303+
<input type="checkbox" checked={checked} onChange={onChange} />
304+
{children}
305+
</label>
306+
)
307+
},
290308
VSCodeTextField: function MockVSCodeTextField({
291309
value,
292310
onInput,
@@ -308,6 +326,9 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
308326
VSCodeLink: function MockVSCodeLink({ children, href }: { children: React.ReactNode; href?: string }) {
309327
return <a href={href}>{children}</a>
310328
},
329+
VSCodeProgressRing: function MockVSCodeProgressRing() {
330+
return <div data-testid="vscode-progress-ring" />
331+
},
311332
}))
312333

313334
// Mock window.postMessage to trigger state hydration
@@ -349,6 +370,55 @@ const renderChatView = (props: Partial<ChatViewProps> = {}) => {
349370
)
350371
}
351372

373+
describe("ChatView - Tool Batching Tests", () => {
374+
beforeEach(() => vi.clearAllMocks())
375+
376+
it("batches readFile asks separated by an API request row", async () => {
377+
renderChatView()
378+
379+
mockPostMessage({
380+
clineMessages: [
381+
{
382+
type: "say",
383+
say: "task",
384+
ts: 1,
385+
text: "Initial task",
386+
},
387+
{
388+
type: "ask",
389+
ask: "tool",
390+
ts: 2,
391+
text: JSON.stringify({ tool: "readFile", path: "a.ts" }),
392+
},
393+
{
394+
type: "say",
395+
say: "api_req_started",
396+
ts: 3,
397+
text: JSON.stringify({ apiProtocol: "anthropic" }),
398+
},
399+
{
400+
type: "ask",
401+
ask: "tool",
402+
ts: 4,
403+
text: JSON.stringify({ tool: "readFile", path: "b.ts" }),
404+
},
405+
],
406+
})
407+
408+
await waitFor(() => {
409+
const toolRows = mockVirtuosoState.lastData.filter(
410+
(message) => message.type === "ask" && message.ask === "tool",
411+
)
412+
const [toolRow] = toolRows
413+
414+
expect(toolRows).toHaveLength(1)
415+
expect(toolRow?.text).toContain('"batchFiles"')
416+
expect(toolRow?.text).toContain('"path":"a.ts"')
417+
expect(toolRow?.text).toContain('"path":"b.ts"')
418+
})
419+
})
420+
})
421+
352422
describe("ChatView - Sound Playing Tests", () => {
353423
beforeEach(() => vi.clearAllMocks())
354424

‎webview-ui/src/utils/__tests__/batchConsecutive.spec.ts‎

Lines changed: 0 additions & 116 deletions
This file was deleted.

0 commit comments

Comments
 (0)