diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts
index fa2f04c0e5..a0aa6f2e52 100644
--- a/packages/types/src/vscode-extension-host.ts
+++ b/packages/types/src/vscode-extension-host.ts
@@ -845,6 +845,12 @@ export interface ClineSayTool {
startLine?: number
}>
}>
+ batchDirs?: Array<{
+ path: string
+ recursive: boolean
+ isOutsideWorkspace?: boolean
+ key: string
+ }>
question?: string
imageData?: string // Base64 encoded image data for generated images
// Properties for runSlashCommand tool
diff --git a/webview-ui/src/components/chat/BatchListFilesPermission.tsx b/webview-ui/src/components/chat/BatchListFilesPermission.tsx
new file mode 100644
index 0000000000..a1d909a524
--- /dev/null
+++ b/webview-ui/src/components/chat/BatchListFilesPermission.tsx
@@ -0,0 +1,47 @@
+import { memo } from "react"
+
+import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
+import { PathTooltip } from "../ui/PathTooltip"
+
+interface DirPermissionItem {
+ path: string
+ recursive: boolean
+ isOutsideWorkspace?: boolean
+ key: string
+}
+
+interface BatchListFilesPermissionProps {
+ dirs: DirPermissionItem[]
+ ts: number
+}
+
+export const BatchListFilesPermission = memo(({ dirs = [], ts }: BatchListFilesPermissionProps) => {
+ if (!dirs?.length) {
+ return null
+ }
+
+ return (
+
+
+ {dirs.map((dir) => {
+ return (
+
+
+
+
+
+ {dir.path}
+
+
+
+
+
+
+ )
+ })}
+
+
+ )
+})
+
+BatchListFilesPermission.displayName = "BatchListFilesPermission"
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index 74639ff5ac..b01b3a2775 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -40,6 +40,7 @@ import { Mention } from "./Mention"
import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
import { FollowUpSuggest } from "./FollowUpSuggest"
import { BatchFilePermission } from "./BatchFilePermission"
+import { BatchListFilesPermission } from "./BatchListFilesPermission"
import { BatchDiffApproval } from "./BatchDiffApproval"
import { ProgressIndicator } from "./ProgressIndicator"
import { Markdown } from "./Markdown"
@@ -741,7 +742,24 @@ export const ChatRowContent = ({
>
)
}
- case "listFilesTopLevel":
+ case "listFilesTopLevel": {
+ // Check if this is a batch directory listing request
+ const isBatchDirRequest = message.type === "ask" && tool.batchDirs && Array.isArray(tool.batchDirs)
+
+ if (isBatchDirRequest) {
+ return (
+ <>
+
+
+
+ {t("chat:directoryOperations.wantsToViewMultipleDirectories")}
+
+
+
+ >
+ )
+ }
+
return (
<>
@@ -767,7 +785,25 @@ export const ChatRowContent = ({
>
)
- case "listFilesRecursive":
+ }
+ case "listFilesRecursive": {
+ // Check if this is a batch directory listing request
+ const isBatchDirRequest = message.type === "ask" && tool.batchDirs && Array.isArray(tool.batchDirs)
+
+ if (isBatchDirRequest) {
+ return (
+ <>
+
+
+
+ {t("chat:directoryOperations.wantsToViewMultipleDirectories")}
+
+
+
+ >
+ )
+ }
+
return (
<>
@@ -793,6 +829,7 @@ export const ChatRowContent = ({
>
)
+ }
case "searchFiles":
return (
<>
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 5c377eb1f5..9920ce15a9 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -1154,8 +1154,21 @@ const ChatViewComponent: React.ForwardRefRenderFunction {
+ if (msg.type !== "ask" || msg.ask !== "tool") return false
+ try {
+ const tool = JSON.parse(msg.text || "{}")
+ return (
+ (tool.tool === "listFilesTopLevel" || tool.tool === "listFilesRecursive") && !tool.batchDirs // Don't re-batch already batched
+ )
+ } catch {
+ return false
+ }
+ }
+
// Consolidate consecutive read_file ask messages into batches
- const result: ClineMessage[] = []
+ const readFileBatched: ClineMessage[] = []
let i = 0
while (i < filtered.length) {
const msg = filtered[i]
@@ -1199,10 +1212,67 @@ const ChatViewComponent: React.ForwardRefRenderFunction 1) {
+ // Create a synthetic batch message
+ const batchDirs = batch.map((batchMsg) => {
+ try {
+ const tool = JSON.parse(batchMsg.text || "{}")
+ return {
+ path: tool.path || "",
+ recursive: tool.tool === "listFilesRecursive",
+ isOutsideWorkspace: tool.isOutsideWorkspace || false,
+ key: tool.path || "",
+ }
+ } catch {
+ return { path: "", recursive: false, key: "" }
+ }
+ })
+
+ // Use the first message as the base, but add batchDirs
+ const firstTool = JSON.parse(msg.text || "{}")
+ const syntheticMessage: ClineMessage = {
+ ...msg,
+ text: JSON.stringify({
+ ...firstTool,
+ batchDirs,
+ }),
+ // Store original messages for response handling
+ _batchedMessages: batch,
+ } as ClineMessage & { _batchedMessages: ClineMessage[] }
+
+ result.push(syntheticMessage)
+ i = j // Skip past all batched messages
+ } else {
+ // Single list_files ask, keep as-is
result.push(msg)
i++
}
diff --git a/webview-ui/src/components/chat/__tests__/BatchListFilesPermission.spec.tsx b/webview-ui/src/components/chat/__tests__/BatchListFilesPermission.spec.tsx
new file mode 100644
index 0000000000..d26205e3fc
--- /dev/null
+++ b/webview-ui/src/components/chat/__tests__/BatchListFilesPermission.spec.tsx
@@ -0,0 +1,87 @@
+import { render, screen } from "@/utils/test-utils"
+
+import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext"
+
+import { BatchListFilesPermission } from "../BatchListFilesPermission"
+
+describe("BatchListFilesPermission", () => {
+ const mockDirs = [
+ {
+ key: "apps/cli",
+ path: "apps/cli",
+ recursive: false,
+ isOutsideWorkspace: false,
+ },
+ {
+ key: "apps/web-roo-code",
+ path: "apps/web-roo-code",
+ recursive: false,
+ isOutsideWorkspace: false,
+ },
+ {
+ key: "packages/core",
+ path: "packages/core",
+ recursive: true,
+ isOutsideWorkspace: false,
+ },
+ ]
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it("renders directory list correctly", () => {
+ render(
+
+
+ ,
+ )
+
+ expect(screen.getByText("apps/cli")).toBeInTheDocument()
+ expect(screen.getByText("apps/web-roo-code")).toBeInTheDocument()
+ expect(screen.getByText("packages/core")).toBeInTheDocument()
+ })
+
+ it("renders nothing when dirs array is empty", () => {
+ const { container } = render(
+
+
+ ,
+ )
+
+ expect(container.firstChild).toBeNull()
+ })
+
+ it("re-renders when timestamp changes", () => {
+ const { rerender } = render(
+
+
+ ,
+ )
+
+ expect(screen.getByText("apps/cli")).toBeInTheDocument()
+
+ rerender(
+
+
+ ,
+ )
+
+ expect(screen.getByText("apps/cli")).toBeInTheDocument()
+ })
+
+ it("renders all directories in a single container", () => {
+ render(
+
+
+ ,
+ )
+
+ // All directories should be within a single bordered container
+ const container = screen.getByText("apps/cli").closest(".border.border-border.rounded-md")
+ expect(container).toBeInTheDocument()
+
+ // All 3 dirs should be inside this container
+ expect(container?.querySelectorAll(".flex.items-center.gap-2")).toHaveLength(mockDirs.length)
+ })
+})
diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json
index b9652cfce5..b60ad59620 100644
--- a/webview-ui/src/i18n/locales/en/chat.json
+++ b/webview-ui/src/i18n/locales/en/chat.json
@@ -235,6 +235,8 @@
"didViewRecursive": "Roo recursively viewed all files in this directory",
"wantsToViewRecursiveOutsideWorkspace": "Roo wants to recursively view all files in this directory (outside workspace)",
"didViewRecursiveOutsideWorkspace": "Roo recursively viewed all files in this directory (outside workspace)",
+ "wantsToViewMultipleDirectories": "Roo wants to view multiple directories",
+ "didViewMultipleDirectories": "Roo viewed multiple directories",
"wantsToSearch": "Roo wants to search this directory for {{regex}}",
"didSearch": "Roo searched this directory for {{regex}}",
"wantsToSearchOutsideWorkspace": "Roo wants to search this directory (outside workspace) for {{regex}}",