From 3034b55f1dedbddf8c7bbba4b0053786346f7d3e Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Thu, 5 Feb 2026 23:33:33 -0700 Subject: [PATCH] refactor: extract batchConsecutive utility, fix batch UI issues - Extract generic batchConsecutive() utility from 3 identical while-loops - Fix React key collisions in BatchListFilesPermission, BatchFilePermission, BatchDiffApproval - Normalize language prop to "shellsession" (was "shell-session" for top-level) - Remove unused _batchedMessages property from synthetic messages - Remove dead didViewMultipleDirectories i18n key from all 18 locale files - Add batch button text for listFilesTopLevel/listFilesRecursive - Add batchConsecutive utility tests (6 cases) --- .../src/components/chat/BatchDiffApproval.tsx | 4 +- .../components/chat/BatchFilePermission.tsx | 4 +- .../chat/BatchListFilesPermission.tsx | 4 +- webview-ui/src/components/chat/ChatRow.tsx | 2 +- webview-ui/src/components/chat/ChatView.tsx | 230 ++++++------------ webview-ui/src/i18n/locales/ca/chat.json | 3 +- webview-ui/src/i18n/locales/de/chat.json | 3 +- webview-ui/src/i18n/locales/en/chat.json | 1 - webview-ui/src/i18n/locales/es/chat.json | 3 +- webview-ui/src/i18n/locales/fr/chat.json | 3 +- webview-ui/src/i18n/locales/hi/chat.json | 3 +- webview-ui/src/i18n/locales/id/chat.json | 3 +- webview-ui/src/i18n/locales/it/chat.json | 3 +- webview-ui/src/i18n/locales/ja/chat.json | 3 +- webview-ui/src/i18n/locales/ko/chat.json | 3 +- webview-ui/src/i18n/locales/nl/chat.json | 3 +- webview-ui/src/i18n/locales/pl/chat.json | 3 +- webview-ui/src/i18n/locales/pt-BR/chat.json | 3 +- webview-ui/src/i18n/locales/ru/chat.json | 3 +- webview-ui/src/i18n/locales/tr/chat.json | 3 +- webview-ui/src/i18n/locales/vi/chat.json | 3 +- webview-ui/src/i18n/locales/zh-CN/chat.json | 3 +- webview-ui/src/i18n/locales/zh-TW/chat.json | 3 +- .../utils/__tests__/batchConsecutive.spec.ts | 74 ++++++ webview-ui/src/utils/batchConsecutive.ts | 44 ++++ 25 files changed, 214 insertions(+), 200 deletions(-) create mode 100644 webview-ui/src/utils/__tests__/batchConsecutive.spec.ts create mode 100644 webview-ui/src/utils/batchConsecutive.ts diff --git a/webview-ui/src/components/chat/BatchDiffApproval.tsx b/webview-ui/src/components/chat/BatchDiffApproval.tsx index a88914cd88..f128e4310d 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 index a1d909a524..1645bc85d1 100644 --- a/webview-ui/src/components/chat/BatchListFilesPermission.tsx +++ b/webview-ui/src/components/chat/BatchListFilesPermission.tsx @@ -23,9 +23,9 @@ export const BatchListFilesPermission = memo(({ dirs = [], ts }: BatchListFilesP return (
- {dirs.map((dir) => { + {dirs.map((dir, index) => { return ( -
+
diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index a57328427f..0772d52f15 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -789,7 +789,7 @@ export const ChatRowContent = ({ diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 8a01105273..cc9560250f 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" @@ -328,6 +329,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + 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[] } - - readFileBatched.push(syntheticMessage) - i = j // Skip past all batched messages - } else { - // Single read_file ask, keep as-is - readFileBatched.push(msg) - i++ - } - } else { - readFileBatched.push(msg) - i++ + const firstTool = JSON.parse(batch[0].text || "{}") + return { + ...batch[0], + text: JSON.stringify({ ...firstTool, batchFiles }), } } - // Consolidate consecutive list_files ask messages into batches - const listFilesBatched: ClineMessage[] = [] - i = 0 - while (i < readFileBatched.length) { - const msg = readFileBatched[i] - - // Check if this starts a sequence of list_files asks - if (isListFilesAsk(msg)) { - // Collect all consecutive list_files asks - const batch: ClineMessage[] = [msg] - let j = i + 1 - while (j < readFileBatched.length && isListFilesAsk(readFileBatched[j])) { - batch.push(readFileBatched[j]) - j++ + // 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: "" } } + }) - if (batch.length > 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[] } - - listFilesBatched.push(syntheticMessage) - i = j // Skip past all batched messages - } else { - // Single list_files ask, keep as-is - listFilesBatched.push(msg) - i++ - } - } else { - listFilesBatched.push(msg) - i++ + const firstTool = JSON.parse(batch[0].text || "{}") + return { + ...batch[0], + text: JSON.stringify({ ...firstTool, batchDirs }), } } - // Consolidate consecutive file-edit ask messages into batches - const result: ClineMessage[] = [] - i = 0 - while (i < listFilesBatched.length) { - const msg = listFilesBatched[i] - - // Check if this starts a sequence of file-edit asks - if (isEditFileAsk(msg)) { - // Collect all consecutive file-edit asks - const batch: ClineMessage[] = [msg] - let j = i + 1 - while (j < listFilesBatched.length && isEditFileAsk(listFilesBatched[j])) { - batch.push(listFilesBatched[j]) - j++ + // 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: "" } } + }) - if (batch.length > 1) { - // Create a synthetic batch message with batchDiffs - 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: "" } - } - }) - - // Use the first message as the base, but add batchDiffs - const firstTool = JSON.parse(msg.text || "{}") - const syntheticMessage: ClineMessage = { - ...msg, - text: JSON.stringify({ - ...firstTool, - batchDiffs, - }), - // Store original messages for response handling - _batchedMessages: batch, - } as ClineMessage & { _batchedMessages: ClineMessage[] } - - result.push(syntheticMessage) - i = j // Skip past all batched messages - } else { - // Single file-edit ask, keep as-is - result.push(msg) - i++ - } - } else { - result.push(msg) - i++ + const firstTool = JSON.parse(batch[0].text || "{}") + 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", diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index bb9eb34478..5adf053b30 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -219,8 +219,7 @@ "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)", - "wantsToViewMultipleDirectories": "Roo vol veure diversos directoris", - "didViewMultipleDirectories": "Roo ha vist diversos directoris" + "wantsToViewMultipleDirectories": "Roo vol veure diversos directoris" }, "commandOutput": "Sortida de la comanda", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 054c8b5e03..e0f93dfe32 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -219,8 +219,7 @@ "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", - "wantsToViewMultipleDirectories": "Roo möchte mehrere Verzeichnisse anzeigen", - "didViewMultipleDirectories": "Roo hat mehrere Verzeichnisse angezeigt" + "wantsToViewMultipleDirectories": "Roo möchte mehrere Verzeichnisse anzeigen" }, "commandOutput": "Befehlsausgabe", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index b60ad59620..04d8769c0b 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -236,7 +236,6 @@ "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}}", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index d4e4272455..b7329cdab1 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -219,8 +219,7 @@ "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)", - "wantsToViewMultipleDirectories": "Roo quiere ver varios directorios", - "didViewMultipleDirectories": "Roo vio varios directorios" + "wantsToViewMultipleDirectories": "Roo quiere ver varios directorios" }, "commandOutput": "Salida del comando", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 95dc15a67b..74283e7d4a 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -219,8 +219,7 @@ "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)", - "wantsToViewMultipleDirectories": "Roo veut voir plusieurs répertoires", - "didViewMultipleDirectories": "Roo a vu plusieurs répertoires" + "wantsToViewMultipleDirectories": "Roo veut voir plusieurs répertoires" }, "commandOutput": "Sortie de la Commande", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index cf385e7daf..a6c7683c84 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -219,8 +219,7 @@ "didViewTopLevelOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में शीर्ष स्तर की फ़ाइलें देखीं", "wantsToViewRecursiveOutsideWorkspace": "Roo इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखना चाहता है", "didViewRecursiveOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखा", - "wantsToViewMultipleDirectories": "Roo कई डायरेक्ट्रीज़ देखना चाहता है", - "didViewMultipleDirectories": "Roo ने कई डायरेक्ट्रीज़ देखीं" + "wantsToViewMultipleDirectories": "Roo कई डायरेक्ट्रीज़ देखना चाहता है" }, "commandOutput": "कमांड आउटपुट", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 66ef4b30fe..4dfc357bf2 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -247,8 +247,7 @@ "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)", - "wantsToViewMultipleDirectories": "Roo ingin melihat beberapa direktori", - "didViewMultipleDirectories": "Roo melihat beberapa direktori" + "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 b2078f37e9..8aaa2ebac3 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -219,8 +219,7 @@ "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)", - "wantsToViewMultipleDirectories": "Roo vuole visualizzare più directory", - "didViewMultipleDirectories": "Roo ha visualizzato più directory" + "wantsToViewMultipleDirectories": "Roo vuole visualizzare più directory" }, "commandOutput": "Output del Comando", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index eee2b888fc..48b13cfd66 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -219,8 +219,7 @@ "didViewTopLevelOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のトップレベルファイルを表示しました", "wantsToViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示したい", "didViewRecursiveOutsideWorkspace": "Rooはこのディレクトリ(ワークスペース外)のすべてのファイルを再帰的に表示しました", - "wantsToViewMultipleDirectories": "Roo は複数のディレクトリを表示したい", - "didViewMultipleDirectories": "Roo は複数のディレクトリを表示しました" + "wantsToViewMultipleDirectories": "Roo は複数のディレクトリを表示したい" }, "commandOutput": "コマンド出力", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 1227dd6c15..eb037eefe1 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -219,8 +219,7 @@ "didViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보았습니다", "wantsToViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보고 싶어합니다", "didViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다", - "wantsToViewMultipleDirectories": "Roo가 여러 디렉토리를 보려고 합니다", - "didViewMultipleDirectories": "Roo가 여러 디렉토리를 확인했습니다" + "wantsToViewMultipleDirectories": "Roo가 여러 디렉토리를 보려고 합니다" }, "commandOutput": "명령 출력", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index c639180525..77d2531000 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -214,8 +214,7 @@ "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", - "wantsToViewMultipleDirectories": "Roo wil meerdere mappen bekijken", - "didViewMultipleDirectories": "Roo heeft meerdere mappen bekeken" + "wantsToViewMultipleDirectories": "Roo wil meerdere mappen bekijken" }, "commandOutput": "Commando-uitvoer", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 7471169553..66801899e9 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -219,8 +219,7 @@ "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)", - "wantsToViewMultipleDirectories": "Roo chce wyświetlić wiele katalogów", - "didViewMultipleDirectories": "Roo wyświetlił wiele katalogów" + "wantsToViewMultipleDirectories": "Roo chce wyświetlić wiele katalogów" }, "commandOutput": "Wyjście polecenia", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 649d196c34..d563aa6b36 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -219,8 +219,7 @@ "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)", - "wantsToViewMultipleDirectories": "Roo quer visualizar vários diretórios", - "didViewMultipleDirectories": "Roo visualizou vários diretórios" + "wantsToViewMultipleDirectories": "Roo quer visualizar vários diretórios" }, "commandOutput": "Saída do comando", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index b1187f49f4..f599bacaa1 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -214,8 +214,7 @@ "didViewTopLevelOutsideWorkspace": "Roo просмотрел файлы верхнего уровня в этой директории (вне рабочего пространства)", "wantsToViewRecursiveOutsideWorkspace": "Roo хочет рекурсивно просмотреть все файлы в этой директории (вне рабочего пространства)", "didViewRecursiveOutsideWorkspace": "Roo рекурсивно просмотрел все файлы в этой директории (вне рабочего пространства)", - "wantsToViewMultipleDirectories": "Roo хочет просмотреть несколько директорий", - "didViewMultipleDirectories": "Roo просмотрел несколько директорий" + "wantsToViewMultipleDirectories": "Roo хочет просмотреть несколько директорий" }, "commandOutput": "Вывод команды", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 53540ae988..e4a9bf71f0 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -219,8 +219,7 @@ "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", - "wantsToViewMultipleDirectories": "Roo birden fazla dizini görüntülemek istiyor", - "didViewMultipleDirectories": "Roo birden fazla dizini görüntüledi" + "wantsToViewMultipleDirectories": "Roo birden fazla dizini görüntülemek istiyor" }, "commandOutput": "Komut Çıktısı", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 5b8f514af0..7823801766 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -219,8 +219,7 @@ "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)", - "wantsToViewMultipleDirectories": "Roo muốn xem nhiều thư mục", - "didViewMultipleDirectories": "Roo đã xem nhiều thư mục" + "wantsToViewMultipleDirectories": "Roo muốn xem nhiều thư mục" }, "commandOutput": "Kết quả lệnh", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 83b3ec70fd..4e5002b33d 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -219,8 +219,7 @@ "didViewTopLevelOutsideWorkspace": "已查看目录文件列表(工作区外)", "wantsToViewRecursiveOutsideWorkspace": "需要查看目录所有文件(工作区外)", "didViewRecursiveOutsideWorkspace": "已查看目录所有文件(工作区外)", - "wantsToViewMultipleDirectories": "Roo 想要查看多个目录", - "didViewMultipleDirectories": "Roo 查看了多个目录" + "wantsToViewMultipleDirectories": "Roo 想要查看多个目录" }, "commandOutput": "命令输出", "commandExecution": { diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 3510b433b2..49b459d54b 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -242,8 +242,7 @@ "didSearch": "Roo 已在此目錄中搜尋 {{regex}}", "wantsToSearchOutsideWorkspace": "Roo 想要在此目錄(工作區外)中搜尋 {{regex}}", "didSearchOutsideWorkspace": "Roo 已在此目錄(工作區外)中搜尋 {{regex}}", - "wantsToViewMultipleDirectories": "Roo 想要查看多個目錄", - "didViewMultipleDirectories": "Roo 查看了多個目錄" + "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 0000000000..3bb5a69d2f --- /dev/null +++ b/webview-ui/src/utils/__tests__/batchConsecutive.spec.ts @@ -0,0 +1,74 @@ +import type { ClineMessage } from "@roo-code/types" + +import { batchConsecutive } from "../batchConsecutive" + +/** Helper: create a minimal ClineMessage with an identifiable text field. */ +function msg(text: string, type: ClineMessage["type"] = "say"): ClineMessage { + return { ts: Date.now(), type, text } +} + +/** Predicate: matches messages whose text starts with "match". */ +const isMatch = (m: ClineMessage) => !!m.text?.startsWith("match") + +/** Synthesize: merges a batch into a single message with a "BATCH:" marker. */ +const synthesizeBatch = (batch: ClineMessage[]): ClineMessage => ({ + ...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") + }) +}) diff --git a/webview-ui/src/utils/batchConsecutive.ts b/webview-ui/src/utils/batchConsecutive.ts new file mode 100644 index 0000000000..e030ad0c70 --- /dev/null +++ b/webview-ui/src/utils/batchConsecutive.ts @@ -0,0 +1,44 @@ +import type { ClineMessage } from "@roo-code/types" + +/** + * Walk a message array and batch runs of consecutive messages that match + * `predicate` into synthetic messages produced by `synthesize`. + * + * - Runs of length 1 are passed through unchanged. + * - Runs of length >= 2 are replaced by a single synthetic message. + * - Non-matching messages are preserved in-order. + */ +export function batchConsecutive( + messages: ClineMessage[], + predicate: (msg: ClineMessage) => boolean, + synthesize: (batch: ClineMessage[]) => ClineMessage, +): ClineMessage[] { + const result: ClineMessage[] = [] + let i = 0 + + while (i < messages.length) { + if (predicate(messages[i])) { + // Collect consecutive matches into a batch + const batch: ClineMessage[] = [messages[i]] + let j = i + 1 + + while (j < messages.length && predicate(messages[j])) { + batch.push(messages[j]) + j++ + } + + if (batch.length > 1) { + result.push(synthesize(batch)) + } else { + result.push(batch[0]) + } + + i = j + } else { + result.push(messages[i]) + i++ + } + } + + return result +}