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)
This commit is contained in:
Hannes Rudolph 2026-02-05 23:33:33 -07:00
parent 2e02d04d7d
commit 3034b55f1d
25 changed files with 214 additions and 200 deletions

View file

@ -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}

View file

@ -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 })}>

View file

@ -23,9 +23,9 @@ export const BatchListFilesPermission = memo(({ dirs = [], ts }: BatchListFilesP
return (
<div className="pt-[5px]">
<div className="flex flex-col gap-0 border border-border rounded-md p-1">
{dirs.map((dir) => {
{dirs.map((dir, index) => {
return (
<div key={`${dir.path}-${ts}`} className="flex items-center gap-2">
<div key={`${dir.path}-${index}-${ts}`} className="flex items-center gap-2">
<ToolUseBlock className="flex-1">
<ToolUseBlockHeader>
<PathTooltip content={dir.path}>

View file

@ -789,7 +789,7 @@ export const ChatRowContent = ({
<CodeAccordian
path={tool.path}
code={tool.content}
language={isRecursive ? "shellsession" : "shell-session"}
language="shellsession"
isExpanded={isExpanded}
onToggleExpand={handleToggleExpand}
/>

View file

@ -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<ChatViewRef, ChatViewPro
setSecondaryButtonText(t("chat:reject.title"))
}
break
case "listFilesTopLevel":
case "listFilesRecursive":
if (tool.batchDirs && Array.isArray(tool.batchDirs)) {
setPrimaryButtonText(t("chat:read-batch.approve.title"))
setSecondaryButtonText(t("chat:read-batch.deny.title"))
} else {
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
}
break
default:
setPrimaryButtonText(t("chat:approve.title"))
setSecondaryButtonText(t("chat:reject.title"))
@ -1187,179 +1198,82 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}
}
// Consolidate consecutive read_file ask messages into batches
const readFileBatched: ClineMessage[] = []
let i = 0
while (i < filtered.length) {
const msg = filtered[i]
// Check if this starts a sequence of read_file asks
if (isReadFileAsk(msg)) {
// Collect all consecutive read_file asks
const batch: ClineMessage[] = [msg]
let j = i + 1
while (j < filtered.length && isReadFileAsk(filtered[j])) {
batch.push(filtered[j])
j++
// 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[] }
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",

View file

@ -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": {

View file

@ -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": {

View file

@ -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 <code>{{regex}}</code>",
"didSearch": "Roo searched this directory for <code>{{regex}}</code>",
"wantsToSearchOutsideWorkspace": "Roo wants to search this directory (outside workspace) for <code>{{regex}}</code>",

View file

@ -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": {

View file

@ -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": {

View file

@ -219,8 +219,7 @@
"didViewTopLevelOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में शीर्ष स्तर की फ़ाइलें देखीं",
"wantsToViewRecursiveOutsideWorkspace": "Roo इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखना चाहता है",
"didViewRecursiveOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखा",
"wantsToViewMultipleDirectories": "Roo कई डायरेक्ट्रीज़ देखना चाहता है",
"didViewMultipleDirectories": "Roo ने कई डायरेक्ट्रीज़ देखीं"
"wantsToViewMultipleDirectories": "Roo कई डायरेक्ट्रीज़ देखना चाहता है"
},
"commandOutput": "कमांड आउटपुट",
"commandExecution": {

View file

@ -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 <code>{{query}}</code>",

View file

@ -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": {

View file

@ -219,8 +219,7 @@
"didViewTopLevelOutsideWorkspace": "Rooはこのディレクトリワークスペース外のトップレベルファイルを表示しました",
"wantsToViewRecursiveOutsideWorkspace": "Rooはこのディレクトリワークスペース外のすべてのファイルを再帰的に表示したい",
"didViewRecursiveOutsideWorkspace": "Rooはこのディレクトリワークスペース外のすべてのファイルを再帰的に表示しました",
"wantsToViewMultipleDirectories": "Roo は複数のディレクトリを表示したい",
"didViewMultipleDirectories": "Roo は複数のディレクトリを表示しました"
"wantsToViewMultipleDirectories": "Roo は複数のディレクトリを表示したい"
},
"commandOutput": "コマンド出力",
"commandExecution": {

View file

@ -219,8 +219,7 @@
"didViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보았습니다",
"wantsToViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보고 싶어합니다",
"didViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다",
"wantsToViewMultipleDirectories": "Roo가 여러 디렉토리를 보려고 합니다",
"didViewMultipleDirectories": "Roo가 여러 디렉토리를 확인했습니다"
"wantsToViewMultipleDirectories": "Roo가 여러 디렉토리를 보려고 합니다"
},
"commandOutput": "명령 출력",
"commandExecution": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -214,8 +214,7 @@
"didViewTopLevelOutsideWorkspace": "Roo просмотрел файлы верхнего уровня в этой директории (вне рабочего пространства)",
"wantsToViewRecursiveOutsideWorkspace": "Roo хочет рекурсивно просмотреть все файлы в этой директории (вне рабочего пространства)",
"didViewRecursiveOutsideWorkspace": "Roo рекурсивно просмотрел все файлы в этой директории (вне рабочего пространства)",
"wantsToViewMultipleDirectories": "Roo хочет просмотреть несколько директорий",
"didViewMultipleDirectories": "Roo просмотрел несколько директорий"
"wantsToViewMultipleDirectories": "Roo хочет просмотреть несколько директорий"
},
"commandOutput": "Вывод команды",
"commandExecution": {

View file

@ -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": {

View file

@ -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": {

View file

@ -219,8 +219,7 @@
"didViewTopLevelOutsideWorkspace": "已查看目录文件列表(工作区外)",
"wantsToViewRecursiveOutsideWorkspace": "需要查看目录所有文件(工作区外)",
"didViewRecursiveOutsideWorkspace": "已查看目录所有文件(工作区外)",
"wantsToViewMultipleDirectories": "Roo 想要查看多个目录",
"didViewMultipleDirectories": "Roo 查看了多个目录"
"wantsToViewMultipleDirectories": "Roo 想要查看多个目录"
},
"commandOutput": "命令输出",
"commandExecution": {

View file

@ -242,8 +242,7 @@
"didSearch": "Roo 已在此目錄中搜尋 <code>{{regex}}</code>",
"wantsToSearchOutsideWorkspace": "Roo 想要在此目錄(工作區外)中搜尋 <code>{{regex}}</code>",
"didSearchOutsideWorkspace": "Roo 已在此目錄(工作區外)中搜尋 <code>{{regex}}</code>",
"wantsToViewMultipleDirectories": "Roo 想要查看多個目錄",
"didViewMultipleDirectories": "Roo 查看了多個目錄"
"wantsToViewMultipleDirectories": "Roo 想要查看多個目錄"
},
"codebaseSearch": {
"wantsToSearch": "Roo 想要在程式碼庫中搜尋 <code>{{query}}</code>",

View file

@ -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")
})
})

View file

@ -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
}