Skip to content
6 changes: 6 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/components/chat/BatchDiffApproval.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ export const BatchDiffApproval = memo(({ files = [], ts }: BatchDiffApprovalProp
return (
<div className="pt-[5px]">
<div className="flex flex-col gap-0 border border-border rounded-md p-1">
{files.map((file) => {
{files.map((file, index) => {
// Use backend-provided unified diff only. Stats also provided by backend.
const unified = file.content || ""

return (
<div key={`${file.path}-${ts}`}>
<div key={`${file.path}-${index}-${ts}`}>
<CodeAccordian
path={file.path}
code={unified}
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/components/chat/BatchFilePermission.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ export const BatchFilePermission = memo(({ files = [], onPermissionResponse, ts
<div className="pt-[5px]">
{/* Individual files */}
<div className="flex flex-col gap-0 border border-border rounded-md p-1">
{files.map((file) => {
{files.map((file, index) => {
return (
<div key={`${file.path}-${ts}`} className="flex items-center gap-2">
<div key={`${file.path}-${index}-${ts}`} className="flex items-center gap-2">
<ToolUseBlock className="flex-1">
<ToolUseBlockHeader
onClick={() => vscode.postMessage({ type: "openFile", text: file.content })}>
Expand Down
45 changes: 45 additions & 0 deletions webview-ui/src/components/chat/BatchListFilesPermission.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { memo } from "react"

import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
import { PathTooltip } from "../ui/PathTooltip"

interface DirPermissionItem {
path: string
key: string
}

interface BatchListFilesPermissionProps {
dirs: DirPermissionItem[]
ts: number
}

export const BatchListFilesPermission = memo(({ dirs = [], ts }: BatchListFilesPermissionProps) => {
if (!dirs?.length) {
return null
}

return (
<div className="pt-[5px]">
<div className="flex flex-col gap-0 border border-border rounded-md p-1">
{dirs.map((dir, index) => {
return (
<div key={`${dir.path}-${index}-${ts}`} className="flex items-center gap-2">
<ToolUseBlock className="flex-1">
<ToolUseBlockHeader>
<PathTooltip content={dir.path}>
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
{dir.path}
</span>
</PathTooltip>
<div className="flex-grow"></div>
</ToolUseBlockHeader>
</ToolUseBlock>
</div>
)
})}
</div>
</div>
)
})

BatchListFilesPermission.displayName = "BatchListFilesPermission"
114 changes: 63 additions & 51 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -419,24 +420,22 @@ export const ChatRowContent = ({
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
)

// Handle batch diffs for any file-edit tool type
if (message.type === "ask" && tool.batchDiffs && Array.isArray(tool.batchDiffs)) {
return (
<>
<div style={headerStyle}>
<FileDiff className="w-4 shrink-0" aria-label="Batch diff icon" />
<span style={{ fontWeight: "bold" }}>{t("chat:fileOperations.wantsToApplyBatchChanges")}</span>
</div>
<BatchDiffApproval files={tool.batchDiffs} ts={message.ts} />
</>
)
}

switch (tool.tool as string) {
case "editedExistingFile":
case "appliedDiff":
// Check if this is a batch diff request
if (message.type === "ask" && tool.batchDiffs && Array.isArray(tool.batchDiffs)) {
return (
<>
<div style={headerStyle}>
<FileDiff className="w-4 shrink-0" aria-label="Batch diff icon" />
<span style={{ fontWeight: "bold" }}>
{t("chat:fileOperations.wantsToApplyBatchChanges")}
</span>
</div>
<BatchDiffApproval files={tool.batchDiffs} ts={message.ts} />
</>
)
}

// Regular single file diff
return (
<>
Expand Down Expand Up @@ -742,45 +741,57 @@ export const ChatRowContent = ({
)
}
case "listFilesTopLevel":
case "listFilesRecursive": {
const isRecursive = tool.tool === "listFilesRecursive"

// Check if this is a batch directory listing request
const isBatchDirRequest = message.type === "ask" && tool.batchDirs && Array.isArray(tool.batchDirs)

// When batching, check if all dirs share the same recursive value
const allTopLevel = tool.batchDirs?.every((d: { recursive: boolean }) => !d.recursive)
const DirIcon = isBatchDirRequest && !allTopLevel ? FolderTree : isRecursive ? FolderTree : ListTree
const dirIconLabel =
isBatchDirRequest && !allTopLevel
? "Folder tree icon"
: isRecursive
? "Folder tree icon"
: "List files icon"

if (isBatchDirRequest) {
return (
<>
<div style={headerStyle}>
<DirIcon className="w-4 shrink-0" aria-label={dirIconLabel} />
<span style={{ fontWeight: "bold" }}>
{t("chat:directoryOperations.wantsToViewMultipleDirectories")}
</span>
</div>
<BatchListFilesPermission dirs={tool.batchDirs || []} ts={message?.ts} />
</>
)
}

const labelKey = isRecursive
? message.type === "ask"
? tool.isOutsideWorkspace
? "chat:directoryOperations.wantsToViewRecursiveOutsideWorkspace"
: "chat:directoryOperations.wantsToViewRecursive"
: tool.isOutsideWorkspace
? "chat:directoryOperations.didViewRecursiveOutsideWorkspace"
: "chat:directoryOperations.didViewRecursive"
: message.type === "ask"
? tool.isOutsideWorkspace
? "chat:directoryOperations.wantsToViewTopLevelOutsideWorkspace"
: "chat:directoryOperations.wantsToViewTopLevel"
: tool.isOutsideWorkspace
? "chat:directoryOperations.didViewTopLevelOutsideWorkspace"
: "chat:directoryOperations.didViewTopLevel"

return (
<>
<div style={headerStyle}>
<ListTree className="w-4 shrink-0" aria-label="List files icon" />
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? tool.isOutsideWorkspace
? t("chat:directoryOperations.wantsToViewTopLevelOutsideWorkspace")
: t("chat:directoryOperations.wantsToViewTopLevel")
: tool.isOutsideWorkspace
? t("chat:directoryOperations.didViewTopLevelOutsideWorkspace")
: t("chat:directoryOperations.didViewTopLevel")}
</span>
</div>
<div className="pl-6">
<CodeAccordian
path={tool.path}
code={tool.content}
language="shell-session"
isExpanded={isExpanded}
onToggleExpand={handleToggleExpand}
/>
</div>
</>
)
case "listFilesRecursive":
return (
<>
<div style={headerStyle}>
<FolderTree className="w-4 shrink-0" aria-label="Folder tree icon" />
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? tool.isOutsideWorkspace
? t("chat:directoryOperations.wantsToViewRecursiveOutsideWorkspace")
: t("chat:directoryOperations.wantsToViewRecursive")
: tool.isOutsideWorkspace
? t("chat:directoryOperations.didViewRecursiveOutsideWorkspace")
: t("chat:directoryOperations.didViewRecursive")}
</span>
<DirIcon className="w-4 shrink-0" aria-label={dirIconLabel} />
<span style={{ fontWeight: "bold" }}>{t(labelKey)}</span>
</div>
<div className="pl-6">
<CodeAccordian
Expand All @@ -793,6 +804,7 @@ export const ChatRowContent = ({
</div>
</>
)
}
case "searchFiles":
return (
<>
Expand Down
Loading
Loading