Skip to content

Commit 1c3ef7d

Browse files
committed
feat(UI): consolidate consecutive read_file asks into batch
When the model makes multiple parallel read_file tool calls, the UI now consolidates consecutive read_file ask messages into a single batch view showing 'Roo wants to read these files' instead of showing repeated 'Roo wants to read this file' messages for each file. The groupedMessages useMemo in ChatView now detects consecutive read_file asks and creates a synthetic batch message with batchFiles, which triggers the existing BatchFilePermission component to render the files as a group. This improves the UX by reducing visual noise when reading multiple files.
1 parent 7b23f4f commit 1c3ef7d

File tree

6 files changed

+75
-6
lines changed

6 files changed

+75
-6
lines changed

src/core/task/__tests__/Task.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ vi.mock("vscode", () => {
138138

139139
vi.mock("../../mentions", () => ({
140140
parseMentions: vi.fn().mockImplementation((text) => {
141-
return Promise.resolve({ text: `processed: ${text}`, mode: undefined })
141+
return Promise.resolve({ text: `processed: ${text}`, mode: undefined, contentBlocks: [] })
142142
}),
143143
openMention: vi.fn(),
144144
getLatestTerminalOutput: vi.fn(),

src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ vi.mock("vscode", () => {
106106

107107
vi.mock("../../mentions", () => ({
108108
parseMentions: vi.fn().mockImplementation((text) => {
109-
return Promise.resolve(`processed: ${text}`)
109+
return Promise.resolve({ text: `processed: ${text}`, mode: undefined, contentBlocks: [] })
110110
}),
111111
openMention: vi.fn(),
112112
getLatestTerminalOutput: vi.fn(),

src/core/task/__tests__/grace-retry-errors.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ vi.mock("vscode", () => {
111111

112112
vi.mock("../../mentions", () => ({
113113
parseMentions: vi.fn().mockImplementation((text) => {
114-
return Promise.resolve(`processed: ${text}`)
114+
return Promise.resolve({ text: `processed: ${text}`, mode: undefined, contentBlocks: [] })
115115
}),
116116
openMention: vi.fn(),
117117
getLatestTerminalOutput: vi.fn(),

src/core/task/__tests__/grounding-sources.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ vi.mock("fs/promises", () => ({
112112

113113
// Mock mentions
114114
vi.mock("../../mentions", () => ({
115-
parseMentions: vi.fn().mockImplementation((text) => Promise.resolve(text)),
115+
parseMentions: vi.fn().mockImplementation((text) => Promise.resolve({ text, mode: undefined, contentBlocks: [] })),
116116
openMention: vi.fn(),
117117
getLatestTerminalOutput: vi.fn(),
118118
}))

src/core/task/__tests__/reasoning-preservation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ vi.mock("fs/promises", () => ({
112112

113113
// Mock mentions
114114
vi.mock("../../mentions", () => ({
115-
parseMentions: vi.fn().mockImplementation((text) => Promise.resolve(text)),
115+
parseMentions: vi.fn().mockImplementation((text) => Promise.resolve({ text, mode: undefined, contentBlocks: [] })),
116116
openMention: vi.fn(),
117117
getLatestTerminalOutput: vi.fn(),
118118
}))

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

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1136,7 +1136,76 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
11361136

11371137
const groupedMessages = useMemo(() => {
11381138
// Only filter out the launch ask and result messages - browser actions appear in chat
1139-
const result: ClineMessage[] = visibleMessages.filter((msg) => !isBrowserSessionMessage(msg))
1139+
const filtered: ClineMessage[] = visibleMessages.filter((msg) => !isBrowserSessionMessage(msg))
1140+
1141+
// Helper to check if a message is a read_file ask that should be batched
1142+
const isReadFileAsk = (msg: ClineMessage): boolean => {
1143+
if (msg.type !== "ask" || msg.ask !== "tool") return false
1144+
try {
1145+
const tool = JSON.parse(msg.text || "{}")
1146+
return tool.tool === "readFile" && !tool.batchFiles // Don't re-batch already batched
1147+
} catch {
1148+
return false
1149+
}
1150+
}
1151+
1152+
// Consolidate consecutive read_file ask messages into batches
1153+
const result: ClineMessage[] = []
1154+
let i = 0
1155+
while (i < filtered.length) {
1156+
const msg = filtered[i]
1157+
1158+
// Check if this starts a sequence of read_file asks
1159+
if (isReadFileAsk(msg)) {
1160+
// Collect all consecutive read_file asks
1161+
const batch: ClineMessage[] = [msg]
1162+
let j = i + 1
1163+
while (j < filtered.length && isReadFileAsk(filtered[j])) {
1164+
batch.push(filtered[j])
1165+
j++
1166+
}
1167+
1168+
if (batch.length > 1) {
1169+
// Create a synthetic batch message
1170+
const batchFiles = batch.map((batchMsg) => {
1171+
try {
1172+
const tool = JSON.parse(batchMsg.text || "{}")
1173+
return {
1174+
path: tool.path || "",
1175+
lineSnippet: tool.reason || "",
1176+
isOutsideWorkspace: tool.isOutsideWorkspace || false,
1177+
key: `${tool.path}${tool.reason ? ` (${tool.reason})` : ""}`,
1178+
content: tool.content || "",
1179+
}
1180+
} catch {
1181+
return { path: "", lineSnippet: "", key: "", content: "" }
1182+
}
1183+
})
1184+
1185+
// Use the first message as the base, but add batchFiles
1186+
const firstTool = JSON.parse(msg.text || "{}")
1187+
const syntheticMessage: ClineMessage = {
1188+
...msg,
1189+
text: JSON.stringify({
1190+
...firstTool,
1191+
batchFiles,
1192+
}),
1193+
// Store original messages for response handling
1194+
_batchedMessages: batch,
1195+
} as ClineMessage & { _batchedMessages: ClineMessage[] }
1196+
1197+
result.push(syntheticMessage)
1198+
i = j // Skip past all batched messages
1199+
} else {
1200+
// Single read_file ask, keep as-is
1201+
result.push(msg)
1202+
i++
1203+
}
1204+
} else {
1205+
result.push(msg)
1206+
i++
1207+
}
1208+
}
11401209

11411210
if (isCondensing) {
11421211
result.push({

0 commit comments

Comments
 (0)