diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index fa2f04c0e5d..a0aa6f2e520 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/BatchDiffApproval.tsx b/webview-ui/src/components/chat/BatchDiffApproval.tsx index a88914cd88a..f128e4310d3 100644 --- a/webview-ui/src/components/chat/BatchDiffApproval.tsx +++ b/webview-ui/src/components/chat/BatchDiffApproval.tsx @@ -35,12 +35,12 @@ export const BatchDiffApproval = memo(({ files = [], ts }: BatchDiffApprovalProp return (
- {files.map((file) => { + {files.map((file, index) => { // Use backend-provided unified diff only. Stats also provided by backend. const unified = file.content || "" return ( -
+
{/* Individual files */}
- {files.map((file) => { + {files.map((file, index) => { return ( -
+
vscode.postMessage({ type: "openFile", text: file.content })}> diff --git a/webview-ui/src/components/chat/BatchListFilesPermission.tsx b/webview-ui/src/components/chat/BatchListFilesPermission.tsx new file mode 100644 index 00000000000..a5d08c244bb --- /dev/null +++ b/webview-ui/src/components/chat/BatchListFilesPermission.tsx @@ -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 ( +
+
+ {dirs.map((dir, index) => { + 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 74639ff5ac5..b4342f2edfc 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" @@ -419,24 +420,22 @@ export const ChatRowContent = ({ style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}> ) + // Handle batch diffs for any file-edit tool type + if (message.type === "ask" && tool.batchDiffs && Array.isArray(tool.batchDiffs)) { + return ( + <> +
+ + {t("chat:fileOperations.wantsToApplyBatchChanges")} +
+ + + ) + } + 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 ( - <> -
- - - {t("chat:fileOperations.wantsToApplyBatchChanges")} - -
- - - ) - } - // Regular single file diff return ( <> @@ -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 ( + <> +
+ + + {t("chat:directoryOperations.wantsToViewMultipleDirectories")} + +
+ + + ) + } + + 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 ( <>
- - - {message.type === "ask" - ? tool.isOutsideWorkspace - ? t("chat:directoryOperations.wantsToViewTopLevelOutsideWorkspace") - : t("chat:directoryOperations.wantsToViewTopLevel") - : tool.isOutsideWorkspace - ? t("chat:directoryOperations.didViewTopLevelOutsideWorkspace") - : t("chat:directoryOperations.didViewTopLevel")} - -
-
- -
- - ) - case "listFilesRecursive": - return ( - <> -
- - - {message.type === "ask" - ? tool.isOutsideWorkspace - ? t("chat:directoryOperations.wantsToViewRecursiveOutsideWorkspace") - : t("chat:directoryOperations.wantsToViewRecursive") - : tool.isOutsideWorkspace - ? t("chat:directoryOperations.didViewRecursiveOutsideWorkspace") - : t("chat:directoryOperations.didViewRecursive")} - + + {t(labelKey)}
) + } case "searchFiles": return ( <> diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 5c377eb1f59..05d0c4fe4a3 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -11,6 +11,7 @@ import { Trans } from "react-i18next" import { useDebounceEffect } from "@src/utils/useDebounceEffect" import { appendImages } from "@src/utils/imageUtils" import { getCostBreakdownIfNeeded } from "@src/utils/costFormatting" +import { batchConsecutive } from "@src/utils/batchConsecutive" import type { ClineAsk, ClineSayTool, ClineMessage, ExtensionMessage, AudioType } from "@roo-code/types" @@ -71,8 +72,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - const w = window as any - return w.AUDIO_BASE_URI || "" + return (window as unknown as { AUDIO_BASE_URI?: string }).AUDIO_BASE_URI || "" }) const { t } = useAppTranslation() @@ -311,6 +311,14 @@ 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 + } + } + + // Set of tool names that represent file-editing operations + const editFileTools = new Set([ + "editedExistingFile", + "appliedDiff", + "newFileCreated", + "insertContent", + "searchAndReplace", + ]) + + // Helper to check if a message is a file-edit ask that should be batched + const isEditFileAsk = (msg: ClineMessage): boolean => { + if (msg.type !== "ask" || msg.ask !== "tool") return false + try { + const tool = JSON.parse(msg.text || "{}") + return editFileTools.has(tool.tool) && !tool.batchDiffs // Don't re-batch already batched + } catch { + return false + } + } + + // Synthesize a batch of consecutive read_file asks into a single message + const synthesizeReadFileBatch = (batch: ClineMessage[]): ClineMessage => { + const batchFiles = batch.map((batchMsg) => { + try { + const tool = JSON.parse(batchMsg.text || "{}") + return { + path: tool.path || "", + lineSnippet: tool.reason || "", + isOutsideWorkspace: tool.isOutsideWorkspace || false, + key: `${tool.path}${tool.reason ? ` (${tool.reason})` : ""}`, + content: tool.content || "", + } + } catch { + return { path: "", lineSnippet: "", key: "", content: "" } } + }) - if (batch.length > 1) { - // Create a synthetic batch message - const batchFiles = batch.map((batchMsg) => { - try { - const tool = JSON.parse(batchMsg.text || "{}") - return { - path: tool.path || "", - lineSnippet: tool.reason || "", - isOutsideWorkspace: tool.isOutsideWorkspace || false, - key: `${tool.path}${tool.reason ? ` (${tool.reason})` : ""}`, - content: tool.content || "", - } - } catch { - return { path: "", lineSnippet: "", key: "", content: "" } - } - }) - - // Use the first message as the base, but add batchFiles - const firstTool = JSON.parse(msg.text || "{}") - const syntheticMessage: ClineMessage = { - ...msg, - text: JSON.stringify({ - ...firstTool, - batchFiles, - }), - // Store original messages for response handling - _batchedMessages: batch, - } as ClineMessage & { _batchedMessages: ClineMessage[] } - - result.push(syntheticMessage) - i = j // Skip past all batched messages - } else { - // Single read_file ask, keep as-is - result.push(msg) - i++ + let firstTool + try { + firstTool = JSON.parse(batch[0].text || "{}") + } catch { + return batch[0] + } + return { + ...batch[0], + text: JSON.stringify({ ...firstTool, batchFiles }), + } + } + + // Synthesize a batch of consecutive list_files asks into a single message + const synthesizeListFilesBatch = (batch: ClineMessage[]): ClineMessage => { + 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: "" } } - } else { - result.push(msg) - i++ + }) + + let firstTool + try { + firstTool = JSON.parse(batch[0].text || "{}") + } catch { + return batch[0] + } + return { + ...batch[0], + text: JSON.stringify({ ...firstTool, batchDirs }), + } + } + + // Synthesize a batch of consecutive file-edit asks into a single message + const synthesizeEditFileBatch = (batch: ClineMessage[]): ClineMessage => { + const batchDiffs = batch.map((batchMsg) => { + try { + const tool = JSON.parse(batchMsg.text || "{}") + return { + path: tool.path || "", + changeCount: 1, + key: tool.path || "", + content: tool.content || tool.diff || "", + diffStats: tool.diffStats, + } + } catch { + return { path: "", changeCount: 0, key: "", content: "" } + } + }) + + let firstTool + try { + firstTool = JSON.parse(batch[0].text || "{}") + } catch { + return batch[0] + } + return { + ...batch[0], + text: JSON.stringify({ ...firstTool, batchDiffs }), } } + // Consolidate consecutive ask messages into batches + const readFileBatched = batchConsecutive(filtered, isReadFileAsk, synthesizeReadFileBatch) + const listFilesBatched = batchConsecutive(readFileBatched, isListFilesAsk, synthesizeListFilesBatch) + const result = batchConsecutive(listFilesBatched, isEditFileAsk, synthesizeEditFileBatch) + if (isCondensing) { result.push({ type: "say", say: "condense_context", ts: Date.now(), partial: true, - } as any) + } as ClineMessage) } return result }, [isCondensing, visibleMessages, isBrowserSessionMessage]) @@ -1235,9 +1319,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { return () => { - if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === "function") { - ;(scrollToBottomSmooth as any).cancel() - } + scrollToBottomSmooth.clear() } }, [scrollToBottomSmooth]) 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 00000000000..21ea05192f8 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/BatchListFilesPermission.spec.tsx @@ -0,0 +1,103 @@ +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", + }, + { + key: "apps/web-roo-code", + path: "apps/web-roo-code", + }, + { + key: "packages/core", + path: "packages/core", + }, + ] + + 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) + }) + + it("renders a single directory", () => { + const singleDir = [ + { + key: "apps/cli", + path: "apps/cli", + }, + ] + + render( + + + , + ) + + expect(screen.getByText("apps/cli")).toBeInTheDocument() + + // Single directory should still be rendered inside the container + const bordered = screen.getByText("apps/cli").closest(".border.border-border.rounded-md") + expect(bordered).toBeInTheDocument() + expect(bordered?.querySelectorAll(".flex.items-center.gap-2")).toHaveLength(1) + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 5bba7dc4459..00c9208d93f 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo vol veure els fitxers de nivell superior en aquest directori (fora de l'espai de treball)", "didViewTopLevelOutsideWorkspace": "Roo ha vist els fitxers de nivell superior en aquest directori (fora de l'espai de treball)", "wantsToViewRecursiveOutsideWorkspace": "Roo vol veure recursivament tots els fitxers en aquest directori (fora de l'espai de treball)", - "didViewRecursiveOutsideWorkspace": "Roo ha vist recursivament tots els fitxers en aquest directori (fora de l'espai de treball)" + "didViewRecursiveOutsideWorkspace": "Roo ha vist recursivament tots els fitxers en aquest directori (fora de l'espai de treball)", + "wantsToViewMultipleDirectories": "Roo vol veure diversos directoris" }, "commandOutput": "Sortida de la comanda", "commandExecution": { @@ -439,6 +440,22 @@ "title": "Denegar tot" } }, + "list-batch": { + "approve": { + "title": "Aprovar tot" + }, + "deny": { + "title": "Denegar tot" + } + }, + "edit-batch": { + "approve": { + "title": "Desar tot" + }, + "deny": { + "title": "Denegar tot" + } + }, "indexingStatus": { "ready": "Índex preparat", "indexing": "Indexant {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 58bc85b60cf..082fc967ebd 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo möchte die Dateien auf oberster Ebene in diesem Verzeichnis (außerhalb des Arbeitsbereichs) anzeigen", "didViewTopLevelOutsideWorkspace": "Roo hat die Dateien auf oberster Ebene in diesem Verzeichnis (außerhalb des Arbeitsbereichs) angezeigt", "wantsToViewRecursiveOutsideWorkspace": "Roo möchte rekursiv alle Dateien in diesem Verzeichnis (außerhalb des Arbeitsbereichs) anzeigen", - "didViewRecursiveOutsideWorkspace": "Roo hat rekursiv alle Dateien in diesem Verzeichnis (außerhalb des Arbeitsbereichs) angezeigt" + "didViewRecursiveOutsideWorkspace": "Roo hat rekursiv alle Dateien in diesem Verzeichnis (außerhalb des Arbeitsbereichs) angezeigt", + "wantsToViewMultipleDirectories": "Roo möchte mehrere Verzeichnisse anzeigen" }, "commandOutput": "Befehlsausgabe", "commandExecution": { @@ -439,6 +440,22 @@ "title": "Alle ablehnen" } }, + "list-batch": { + "approve": { + "title": "Alle genehmigen" + }, + "deny": { + "title": "Alle ablehnen" + } + }, + "edit-batch": { + "approve": { + "title": "Alle speichern" + }, + "deny": { + "title": "Alle ablehnen" + } + }, "indexingStatus": { "ready": "Index bereit", "indexing": "Indizierung {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index b9652cfce5c..4313a04db5b 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -74,6 +74,22 @@ "title": "Deny All" } }, + "list-batch": { + "approve": { + "title": "Approve All" + }, + "deny": { + "title": "Deny All" + } + }, + "edit-batch": { + "approve": { + "title": "Save All" + }, + "deny": { + "title": "Deny All" + } + }, "runCommand": { "title": "Run", "tooltip": "Execute this command" @@ -235,6 +251,7 @@ "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", "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}}", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 6c894642c88..a3a66bf7d5f 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo quiere ver los archivos de nivel superior en este directorio (fuera del espacio de trabajo)", "didViewTopLevelOutsideWorkspace": "Roo vio los archivos de nivel superior en este directorio (fuera del espacio de trabajo)", "wantsToViewRecursiveOutsideWorkspace": "Roo quiere ver recursivamente todos los archivos en este directorio (fuera del espacio de trabajo)", - "didViewRecursiveOutsideWorkspace": "Roo vio recursivamente todos los archivos en este directorio (fuera del espacio de trabajo)" + "didViewRecursiveOutsideWorkspace": "Roo vio recursivamente todos los archivos en este directorio (fuera del espacio de trabajo)", + "wantsToViewMultipleDirectories": "Roo quiere ver varios directorios" }, "commandOutput": "Salida del comando", "commandExecution": { @@ -439,6 +440,22 @@ "title": "Denegar todo" } }, + "list-batch": { + "approve": { + "title": "Aprobar todo" + }, + "deny": { + "title": "Denegar todo" + } + }, + "edit-batch": { + "approve": { + "title": "Guardar todo" + }, + "deny": { + "title": "Denegar todo" + } + }, "indexingStatus": { "ready": "Índice listo", "indexing": "Indexando {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 598344de1d7..1dfae130ac1 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo veut voir les fichiers de premier niveau dans ce répertoire (hors espace de travail)", "didViewTopLevelOutsideWorkspace": "Roo a vu les fichiers de premier niveau dans ce répertoire (hors espace de travail)", "wantsToViewRecursiveOutsideWorkspace": "Roo veut voir récursivement tous les fichiers dans ce répertoire (hors espace de travail)", - "didViewRecursiveOutsideWorkspace": "Roo a vu récursivement tous les fichiers dans ce répertoire (hors espace de travail)" + "didViewRecursiveOutsideWorkspace": "Roo a vu récursivement tous les fichiers dans ce répertoire (hors espace de travail)", + "wantsToViewMultipleDirectories": "Roo veut voir plusieurs répertoires" }, "commandOutput": "Sortie de la Commande", "commandExecution": { @@ -439,6 +440,22 @@ "title": "Tout refuser" } }, + "list-batch": { + "approve": { + "title": "Tout approuver" + }, + "deny": { + "title": "Tout refuser" + } + }, + "edit-batch": { + "approve": { + "title": "Tout enregistrer" + }, + "deny": { + "title": "Tout refuser" + } + }, "indexingStatus": { "ready": "Index prêt", "indexing": "Indexation {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 74d5e4e3eef..bac3221048e 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo इस निर्देशिका (कार्यक्षेत्र के बाहर) में शीर्ष स्तर की फ़ाइलें देखना चाहता है", "didViewTopLevelOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में शीर्ष स्तर की फ़ाइलें देखीं", "wantsToViewRecursiveOutsideWorkspace": "Roo इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखना चाहता है", - "didViewRecursiveOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखा" + "didViewRecursiveOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखा", + "wantsToViewMultipleDirectories": "Roo कई डायरेक्ट्रीज़ देखना चाहता है" }, "commandOutput": "कमांड आउटपुट", "commandExecution": { @@ -439,6 +440,22 @@ "title": "सभी अस्वीकार करें" } }, + "list-batch": { + "approve": { + "title": "सभी स्वीकृत करें" + }, + "deny": { + "title": "सभी अस्वीकार करें" + } + }, + "edit-batch": { + "approve": { + "title": "सभी सहेजें" + }, + "deny": { + "title": "सभी अस्वीकार करें" + } + }, "indexingStatus": { "ready": "इंडेक्स तैयार", "indexing": "इंडेक्सिंग {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index f814b9d4a9a..4e220d2666a 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -80,6 +80,22 @@ "title": "Tolak Semua" } }, + "list-batch": { + "approve": { + "title": "Setujui Semua" + }, + "deny": { + "title": "Tolak Semua" + } + }, + "edit-batch": { + "approve": { + "title": "Simpan Semua" + }, + "deny": { + "title": "Tolak Semua" + } + }, "runCommand": { "title": "Perintah", "tooltip": "Jalankan perintah ini" @@ -246,7 +262,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo ingin melihat file tingkat atas di direktori ini (di luar workspace)", "didViewTopLevelOutsideWorkspace": "Roo melihat file tingkat atas di direktori ini (di luar workspace)", "wantsToViewRecursiveOutsideWorkspace": "Roo ingin melihat semua file secara rekursif di direktori ini (di luar workspace)", - "didViewRecursiveOutsideWorkspace": "Roo melihat semua file secara rekursif di direktori ini (di luar workspace)" + "didViewRecursiveOutsideWorkspace": "Roo melihat semua file secara rekursif di direktori ini (di luar workspace)", + "wantsToViewMultipleDirectories": "Roo ingin melihat beberapa direktori" }, "codebaseSearch": { "wantsToSearch": "Roo ingin mencari codebase untuk {{query}}", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index eca4264df20..e77a7f32851 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo vuole visualizzare i file di primo livello in questa directory (fuori dall'area di lavoro)", "didViewTopLevelOutsideWorkspace": "Roo ha visualizzato i file di primo livello in questa directory (fuori dall'area di lavoro)", "wantsToViewRecursiveOutsideWorkspace": "Roo vuole visualizzare ricorsivamente tutti i file in questa directory (fuori dall'area di lavoro)", - "didViewRecursiveOutsideWorkspace": "Roo ha visualizzato ricorsivamente tutti i file in questa directory (fuori dall'area di lavoro)" + "didViewRecursiveOutsideWorkspace": "Roo ha visualizzato ricorsivamente tutti i file in questa directory (fuori dall'area di lavoro)", + "wantsToViewMultipleDirectories": "Roo vuole visualizzare più directory" }, "commandOutput": "Output del Comando", "commandExecution": { @@ -439,6 +440,22 @@ "title": "Nega tutto" } }, + "list-batch": { + "approve": { + "title": "Approva tutto" + }, + "deny": { + "title": "Nega tutto" + } + }, + "edit-batch": { + "approve": { + "title": "Salva tutto" + }, + "deny": { + "title": "Nega tutto" + } + }, "indexingStatus": { "ready": "Indice pronto", "indexing": "Indicizzazione {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 9a0b1b2a35c..f24f528045a 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のトップレベルファイルを表示したい", "didViewTopLevelOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のトップレベルファイルを表示しました", "wantsToViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示したい", - "didViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示しました" + "didViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示しました", + "wantsToViewMultipleDirectories": "Roo は複数のディレクトリを表示したい" }, "commandOutput": "コマンド出力", "commandExecution": { @@ -439,6 +440,22 @@ "title": "すべて拒否" } }, + "list-batch": { + "approve": { + "title": "すべて承認" + }, + "deny": { + "title": "すべて拒否" + } + }, + "edit-batch": { + "approve": { + "title": "すべて保存" + }, + "deny": { + "title": "すべて拒否" + } + }, "indexingStatus": { "ready": "インデックス準備完了", "indexing": "インデックス作成中 {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 3b26c6e6e19..f3e59d8defe 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보고 싶어합니다", "didViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보았습니다", "wantsToViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보고 싶어합니다", - "didViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다" + "didViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다", + "wantsToViewMultipleDirectories": "Roo가 여러 디렉토리를 보려고 합니다" }, "commandOutput": "명령 출력", "commandExecution": { @@ -439,6 +440,22 @@ "title": "모두 거부" } }, + "list-batch": { + "approve": { + "title": "모두 승인" + }, + "deny": { + "title": "모두 거부" + } + }, + "edit-batch": { + "approve": { + "title": "모두 저장" + }, + "deny": { + "title": "모두 거부" + } + }, "indexingStatus": { "ready": "인덱스 준비됨", "indexing": "인덱싱 중 {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 241e9f22166..f1629955336 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -145,14 +145,14 @@ "rateLimitWait": "Snelheidsbeperking", "errorTitle": "Fout van provider {{code}}", "errorMessage": { - "docs": "Documentatie", - "goToSettings": "Instellingen", "400": "De provider kon het verzoek niet verwerken zoals ingediend. Stop de taak en probeer een ander benadering.", "401": "Kon niet authenticeren met provider. Controleer je API-sleutelconfiguratie.", "402": "Het lijkt erop dat je funds/credits op je account op zijn. Ga naar je provider en voeg meer toe om door te gaan.", "403": "Niet geautoriseerd. Je API-sleutel is geldig, maar de provider weigerde dit verzoek in te willigen.", "429": "Te veel verzoeken. Je bent rate-gelimiteerd door de provider. Wacht alsjeblieft even voor je volgende API-aanroep.", "500": "Provider-serverfout. Er is iets mis aan de kant van de provider, er is niets mis met je verzoek.", + "docs": "Documentatie", + "goToSettings": "Instellingen", "unknown": "Onbekende API-fout. Neem alsjeblieft contact op met Roo Code-ondersteuning.", "connection": "Verbindingsfout. Zorg ervoor dat je een werkende internetverbinding hebt.", "claudeCodeNotAuthenticated": "Je moet inloggen om Claude Code te gebruiken. Ga naar Instellingen en klik op \"Inloggen bij Claude Code\" om te authenticeren." @@ -213,7 +213,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo wil de bovenliggende bestanden in deze map (buiten werkruimte) bekijken", "didViewTopLevelOutsideWorkspace": "Roo heeft de bovenliggende bestanden in deze map (buiten werkruimte) bekeken", "wantsToViewRecursiveOutsideWorkspace": "Roo wil alle bestanden in deze map (buiten werkruimte) recursief bekijken", - "didViewRecursiveOutsideWorkspace": "Roo heeft alle bestanden in deze map (buiten werkruimte) recursief bekeken" + "didViewRecursiveOutsideWorkspace": "Roo heeft alle bestanden in deze map (buiten werkruimte) recursief bekeken", + "wantsToViewMultipleDirectories": "Roo wil meerdere mappen bekijken" }, "commandOutput": "Commando-uitvoer", "commandExecution": { @@ -439,6 +440,22 @@ "title": "Alles weigeren" } }, + "list-batch": { + "approve": { + "title": "Alles goedkeuren" + }, + "deny": { + "title": "Alles weigeren" + } + }, + "edit-batch": { + "approve": { + "title": "Alles opslaan" + }, + "deny": { + "title": "Alles weigeren" + } + }, "indexingStatus": { "ready": "Index gereed", "indexing": "Indexeren {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index ae6b5a96ac7..8bd955ee944 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -150,14 +150,14 @@ "rateLimitWait": "Ograniczenie szybkości", "errorTitle": "Błąd dostawcy {{code}}", "errorMessage": { - "docs": "Dokumentacja", - "goToSettings": "Ustawienia", "400": "Dostawca nie mógł przetworzyć żądania. Zatrzymaj zadanie i spróbuj innego podejścia.", "401": "Nie można uwierzytelnić u dostawcy. Sprawdź konfigurację klucza API.", "402": "Wygląda na to, że wyczerpałeś środki/kredyty na swoim koncie. Przejdź do dostawcy i dodaj więcej, aby kontynuować.", "403": "Brak autoryzacji. Twój klucz API jest ważny, ale dostawca odmówił ukończenia tego żądania.", "429": "Zbyt wiele żądań. Dostawca ogranicza Ci szybkość żądań. Poczekaj chwilę przed następnym wywołaniem API.", "500": "Błąd serwera dostawcy. Po stronie dostawcy coś się nie powiodło, w Twoim żądaniu nie ma nic złego.", + "docs": "Dokumentacja", + "goToSettings": "Ustawienia", "unknown": "Nieznany błąd API. Skontaktuj się z pomocą techniczną Roo Code.", "connection": "Błąd połączenia. Upewnij się, że masz działające połączenie internetowe.", "claudeCodeNotAuthenticated": "Musisz się zalogować, aby korzystać z Claude Code. Przejdź do Ustawień i kliknij \"Zaloguj się do Claude Code\", aby się uwierzytelnić." @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo chce zobaczyć pliki najwyższego poziomu w tym katalogu (poza obszarem roboczym)", "didViewTopLevelOutsideWorkspace": "Roo zobaczył pliki najwyższego poziomu w tym katalogu (poza obszarem roboczym)", "wantsToViewRecursiveOutsideWorkspace": "Roo chce rekurencyjnie zobaczyć wszystkie pliki w tym katalogu (poza obszarem roboczym)", - "didViewRecursiveOutsideWorkspace": "Roo rekurencyjnie zobaczył wszystkie pliki w tym katalogu (poza obszarem roboczym)" + "didViewRecursiveOutsideWorkspace": "Roo rekurencyjnie zobaczył wszystkie pliki w tym katalogu (poza obszarem roboczym)", + "wantsToViewMultipleDirectories": "Roo chce wyświetlić wiele katalogów" }, "commandOutput": "Wyjście polecenia", "commandExecution": { @@ -439,6 +440,22 @@ "title": "Odrzuć wszystko" } }, + "list-batch": { + "approve": { + "title": "Zatwierdź wszystko" + }, + "deny": { + "title": "Odrzuć wszystko" + } + }, + "edit-batch": { + "approve": { + "title": "Zapisz wszystko" + }, + "deny": { + "title": "Odrzuć wszystko" + } + }, "indexingStatus": { "ready": "Indeks gotowy", "indexing": "Indeksowanie {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 2ca72ebd970..01f23569300 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo quer visualizar os arquivos de nível superior neste diretório (fora do espaço de trabalho)", "didViewTopLevelOutsideWorkspace": "Roo visualizou os arquivos de nível superior neste diretório (fora do espaço de trabalho)", "wantsToViewRecursiveOutsideWorkspace": "Roo quer visualizar recursivamente todos os arquivos neste diretório (fora do espaço de trabalho)", - "didViewRecursiveOutsideWorkspace": "Roo visualizou recursivamente todos os arquivos neste diretório (fora do espaço de trabalho)" + "didViewRecursiveOutsideWorkspace": "Roo visualizou recursivamente todos os arquivos neste diretório (fora do espaço de trabalho)", + "wantsToViewMultipleDirectories": "Roo quer visualizar vários diretórios" }, "commandOutput": "Saída do comando", "commandExecution": { @@ -439,6 +440,22 @@ "title": "Negar tudo" } }, + "list-batch": { + "approve": { + "title": "Aprovar tudo" + }, + "deny": { + "title": "Negar tudo" + } + }, + "edit-batch": { + "approve": { + "title": "Salvar tudo" + }, + "deny": { + "title": "Negar tudo" + } + }, "indexingStatus": { "ready": "Índice pronto", "indexing": "Indexando {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 347bf1be81e..1f2142ea95e 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -213,7 +213,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo хочет просмотреть файлы верхнего уровня в этой директории (вне рабочего пространства)", "didViewTopLevelOutsideWorkspace": "Roo просмотрел файлы верхнего уровня в этой директории (вне рабочего пространства)", "wantsToViewRecursiveOutsideWorkspace": "Roo хочет рекурсивно просмотреть все файлы в этой директории (вне рабочего пространства)", - "didViewRecursiveOutsideWorkspace": "Roo рекурсивно просмотрел все файлы в этой директории (вне рабочего пространства)" + "didViewRecursiveOutsideWorkspace": "Roo рекурсивно просмотрел все файлы в этой директории (вне рабочего пространства)", + "wantsToViewMultipleDirectories": "Roo хочет просмотреть несколько директорий" }, "commandOutput": "Вывод команды", "commandExecution": { @@ -440,6 +441,22 @@ "title": "Отклонить все" } }, + "list-batch": { + "approve": { + "title": "Одобрить все" + }, + "deny": { + "title": "Отклонить все" + } + }, + "edit-batch": { + "approve": { + "title": "Сохранить все" + }, + "deny": { + "title": "Отклонить все" + } + }, "indexingStatus": { "ready": "Индекс готов", "indexing": "Индексация {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 2301541cdff..f7a2d951f1f 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) üst düzey dosyaları görüntülemek istiyor", "didViewTopLevelOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) üst düzey dosyaları görüntüledi", "wantsToViewRecursiveOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) tüm dosyaları özyinelemeli olarak görüntülemek istiyor", - "didViewRecursiveOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) tüm dosyaları özyinelemeli olarak görüntüledi" + "didViewRecursiveOutsideWorkspace": "Roo bu dizindeki (çalışma alanı dışında) tüm dosyaları özyinelemeli olarak görüntüledi", + "wantsToViewMultipleDirectories": "Roo birden fazla dizini görüntülemek istiyor" }, "commandOutput": "Komut Çıktısı", "commandExecution": { @@ -440,6 +441,22 @@ "title": "Tümünü Reddet" } }, + "list-batch": { + "approve": { + "title": "Tümünü Onayla" + }, + "deny": { + "title": "Tümünü Reddet" + } + }, + "edit-batch": { + "approve": { + "title": "Tümünü Kaydet" + }, + "deny": { + "title": "Tümünü Reddet" + } + }, "indexingStatus": { "ready": "İndeks hazır", "indexing": "İndeksleniyor {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 33b13d9b269..a2c1389e41f 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "Roo muốn xem các tệp cấp cao nhất trong thư mục này (ngoài không gian làm việc)", "didViewTopLevelOutsideWorkspace": "Roo đã xem các tệp cấp cao nhất trong thư mục này (ngoài không gian làm việc)", "wantsToViewRecursiveOutsideWorkspace": "Roo muốn xem đệ quy tất cả các tệp trong thư mục này (ngoài không gian làm việc)", - "didViewRecursiveOutsideWorkspace": "Roo đã xem đệ quy tất cả các tệp trong thư mục này (ngoài không gian làm việc)" + "didViewRecursiveOutsideWorkspace": "Roo đã xem đệ quy tất cả các tệp trong thư mục này (ngoài không gian làm việc)", + "wantsToViewMultipleDirectories": "Roo muốn xem nhiều thư mục" }, "commandOutput": "Kết quả lệnh", "commandExecution": { @@ -440,6 +441,22 @@ "title": "Từ chối tất cả" } }, + "list-batch": { + "approve": { + "title": "Chấp nhận tất cả" + }, + "deny": { + "title": "Từ chối tất cả" + } + }, + "edit-batch": { + "approve": { + "title": "Lưu tất cả" + }, + "deny": { + "title": "Từ chối tất cả" + } + }, "indexingStatus": { "ready": "Chỉ mục sẵn sàng", "indexing": "Đang lập chỉ mục {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 7a94bfb48d7..29783285de5 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -218,7 +218,8 @@ "wantsToViewTopLevelOutsideWorkspace": "需要查看目录文件列表(工作区外)", "didViewTopLevelOutsideWorkspace": "已查看目录文件列表(工作区外)", "wantsToViewRecursiveOutsideWorkspace": "需要查看目录所有文件(工作区外)", - "didViewRecursiveOutsideWorkspace": "已查看目录所有文件(工作区外)" + "didViewRecursiveOutsideWorkspace": "已查看目录所有文件(工作区外)", + "wantsToViewMultipleDirectories": "Roo 想要查看多个目录" }, "commandOutput": "命令输出", "commandExecution": { @@ -440,6 +441,22 @@ "title": "全部拒绝" } }, + "list-batch": { + "approve": { + "title": "全部批准" + }, + "deny": { + "title": "全部拒绝" + } + }, + "edit-batch": { + "approve": { + "title": "全部保存" + }, + "deny": { + "title": "全部拒绝" + } + }, "indexingStatus": { "ready": "索引就绪", "indexing": "索引中 {{percentage}}%", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 926cb105fb4..8cb612dcd22 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -74,6 +74,22 @@ "title": "全部拒絕" } }, + "list-batch": { + "approve": { + "title": "全部核准" + }, + "deny": { + "title": "全部拒絕" + } + }, + "edit-batch": { + "approve": { + "title": "全部儲存" + }, + "deny": { + "title": "全部拒絕" + } + }, "runCommand": { "title": "執行", "tooltip": "執行此命令" @@ -156,14 +172,14 @@ "rateLimitWait": "速率限制", "errorTitle": "供應商錯誤 {{code}}", "errorMessage": { - "docs": "說明文件", - "goToSettings": "設定", "400": "供應商無法按照此方式處理請求。請停止工作並嘗試其他方法。", "401": "無法向供應商進行身份驗證。請檢查您的 API 金鑰設定。", "402": "您的帳戶資金/額度似乎已用盡。請前往供應商增加額度以繼續。", "403": "無權存取。您的 API 金鑰有效,但供應商拒絕完成此請求。", "429": "請求次數過多。供應商已對您的請求進行速率限制。請在下一次 API 呼叫前稍候。", "500": "供應商伺服器錯誤。伺服器端發生問題,您的請求沒有問題。", + "docs": "說明文件", + "goToSettings": "設定", "connection": "連線錯誤。請確保您有可用的網際網路連線。", "unknown": "未知 API 錯誤。請聯絡 Roo Code 技術支援。", "claudeCodeNotAuthenticated": "您需要登入才能使用 Claude Code。前往設定並點選「登入 Claude Code」以進行驗證。" @@ -241,7 +257,8 @@ "wantsToSearch": "Roo 想要在此目錄中搜尋 {{regex}}", "didSearch": "Roo 已在此目錄中搜尋 {{regex}}", "wantsToSearchOutsideWorkspace": "Roo 想要在此目錄(工作區外)中搜尋 {{regex}}", - "didSearchOutsideWorkspace": "Roo 已在此目錄(工作區外)中搜尋 {{regex}}" + "didSearchOutsideWorkspace": "Roo 已在此目錄(工作區外)中搜尋 {{regex}}", + "wantsToViewMultipleDirectories": "Roo 想要查看多個目錄" }, "codebaseSearch": { "wantsToSearch": "Roo 想要在程式碼庫中搜尋 {{query}}", diff --git a/webview-ui/src/utils/__tests__/batchConsecutive.spec.ts b/webview-ui/src/utils/__tests__/batchConsecutive.spec.ts new file mode 100644 index 00000000000..b3919fdbd67 --- /dev/null +++ b/webview-ui/src/utils/__tests__/batchConsecutive.spec.ts @@ -0,0 +1,116 @@ +import { batchConsecutive } from "../batchConsecutive" + +interface TestItem { + ts: number + type: string + text: string +} + +/** Helper: create a minimal test item with an identifiable text field. */ +function msg(text: string, type = "say"): TestItem { + return { ts: Date.now(), type, text } +} + +/** Predicate: matches items whose text starts with "match". */ +const isMatch = (m: TestItem) => !!m.text?.startsWith("match") + +/** Synthesize: merges a batch into a single item with a "BATCH:" marker. */ +const synthesizeBatch = (batch: TestItem[]): TestItem => ({ + ...batch[0], + text: `BATCH:${batch.map((m) => m.text).join(",")}`, +}) + +describe("batchConsecutive", () => { + test("empty input returns empty output", () => { + expect(batchConsecutive([], isMatch, synthesizeBatch)).toEqual([]) + }) + + test("no matches returns passthrough", () => { + const messages = [msg("a"), msg("b"), msg("c")] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toEqual(messages) + }) + + test("single match is passed through without batching", () => { + const messages = [msg("a"), msg("match-1"), msg("b")] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toHaveLength(3) + expect(result[1].text).toBe("match-1") + }) + + test("two consecutive matches produce one synthetic message", () => { + const messages = [msg("a"), msg("match-1"), msg("match-2"), msg("b")] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toHaveLength(3) + expect(result[0].text).toBe("a") + expect(result[1].text).toBe("BATCH:match-1,match-2") + expect(result[2].text).toBe("b") + }) + + test("non-consecutive matches are not batched", () => { + const messages = [msg("match-1"), msg("other"), msg("match-2")] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toHaveLength(3) + expect(result[0].text).toBe("match-1") + expect(result[1].text).toBe("other") + expect(result[2].text).toBe("match-2") + }) + + test("mixed sequences are correctly interleaved", () => { + const messages = [ + msg("match-1"), + msg("match-2"), + msg("match-3"), + msg("other-1"), + msg("match-4"), + msg("other-2"), + msg("match-5"), + msg("match-6"), + ] + const result = batchConsecutive(messages, isMatch, synthesizeBatch) + expect(result).toHaveLength(5) + expect(result[0].text).toBe("BATCH:match-1,match-2,match-3") + expect(result[1].text).toBe("other-1") + expect(result[2].text).toBe("match-4") // single — not batched + expect(result[3].text).toBe("other-2") + expect(result[4].text).toBe("BATCH:match-5,match-6") + }) + + test("all items match → single synthetic message", () => { + const items = [msg("match-1"), msg("match-2"), msg("match-3")] + const result = batchConsecutive(items, isMatch, synthesizeBatch) + expect(result).toHaveLength(1) + expect(result[0].text).toBe("BATCH:match-1,match-2,match-3") + }) + + test("does not mutate the input array", () => { + const items = [msg("match-1"), msg("match-2")] + const original = [...items] + batchConsecutive(items, isMatch, synthesizeBatch) + expect(items).toHaveLength(2) + expect(items).toEqual(original) + }) + + test("returns a new array, not the same reference", () => { + const items = [msg("a"), msg("b")] + const result = batchConsecutive(items, isMatch, synthesizeBatch) + expect(result).not.toBe(items) + }) + + test("synthesize callback receives the correct batches", () => { + const spy = vi.fn(synthesizeBatch) + const items = [msg("match-1"), msg("match-2"), msg("other"), msg("match-3"), msg("match-4")] + batchConsecutive(items, isMatch, spy) + expect(spy).toHaveBeenCalledTimes(2) + expect(spy.mock.calls[0][0]).toHaveLength(2) + expect(spy.mock.calls[1][0]).toHaveLength(2) + }) + + test("batch at the end of the array", () => { + const items = [msg("other"), msg("match-1"), msg("match-2")] + const result = batchConsecutive(items, isMatch, synthesizeBatch) + expect(result).toHaveLength(2) + expect(result[0].text).toBe("other") + expect(result[1].text).toBe("BATCH:match-1,match-2") + }) +}) diff --git a/webview-ui/src/utils/batchConsecutive.ts b/webview-ui/src/utils/batchConsecutive.ts new file mode 100644 index 00000000000..336d8a74a6e --- /dev/null +++ b/webview-ui/src/utils/batchConsecutive.ts @@ -0,0 +1,38 @@ +/** + * Walk an item array and batch runs of consecutive items that match + * `predicate` into synthetic items produced by `synthesize`. + * + * - Runs of length 1 are passed through unchanged. + * - Runs of length >= 2 are replaced by a single synthetic item. + * - Non-matching items are preserved in-order. + */ +export function batchConsecutive(items: T[], predicate: (item: T) => boolean, synthesize: (batch: T[]) => T): T[] { + const result: T[] = [] + let i = 0 + + while (i < items.length) { + if (predicate(items[i])) { + // Collect consecutive matches into a batch + const batch: T[] = [items[i]] + let j = i + 1 + + while (j < items.length && predicate(items[j])) { + batch.push(items[j]) + j++ + } + + if (batch.length > 1) { + result.push(synthesize(batch)) + } else { + result.push(batch[0]) + } + + i = j + } else { + result.push(items[i]) + i++ + } + } + + return result +}