feat: batch consecutive tool calls in chat UI with shared utility (#11245)

* feat: group consecutive list_files tool calls into single UI block

Consolidate consecutive listFilesTopLevel/listFilesRecursive ask messages
into a single 'Roo wants to view multiple directories' block, matching the
existing read_file batching pattern.

* chore: add missing translation keys for all locales

* refactor: consolidate duplicate listFiles batch-handling blocks in ChatRow

Merge the separate listFilesTopLevel and listFilesRecursive case blocks
into a single combined case with shared batch-detection logic, selecting
the icon and translation key based on the tool type. This removes the
duplicated isBatchDirRequest check and BatchListFilesPermission render.

* feat: batch consecutive file-edit tool calls into single UI block

Add edit-file batching in ChatView groupedMessages that consolidates
consecutive editedExistingFile, appliedDiff, newFileCreated,
insertContent, and searchAndReplace asks into a single BatchDiffApproval
block. Move batchDiffs detection in ChatRow above the switch statement
so it applies to any file-edit tool type.

* 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)

* fix: audit improvements for batch tool-call UI

- Make batchConsecutive() generic instead of ClineMessage-specific
- Add batch-aware button text for edit-file batches ("Save All"/"Deny All")
- Add dedicated list-batch/edit-batch i18n keys (stop reusing read-batch)
- Add JSON.parse defense-in-depth in all three synthesizers
- Fix mixed list_files batch icon to default to FolderTree
- Add 6 missing test cases (all-match, immutability, spy, single-dir)

* chore: minor type cleanup (out-of-scope housekeeping)

- Trim unused recursive/isOutsideWorkspace from DirPermissionItem interface
- Remove 4 pre-existing `as any` casts in ChatView.tsx:
  - window cast → precise inline type
  - checkpoint bracket access → removed unnecessary casts
  - condensing message → `as ClineMessage`
  - debounce cancel → `.clear()` (correct API)
- Update BatchListFilesPermission test data to match trimmed interface

* i18n: add list-batch and edit-batch translations for all locales
This commit is contained in:
Hannes Rudolph 2026-02-09 09:28:04 -07:00 committed by GitHub
parent 5313cb503a
commit 7afa43635f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 850 additions and 142 deletions

View file

@ -849,6 +849,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

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

@ -0,0 +1,45 @@
import { memo } from "react"
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
import { PathTooltip } from "../ui/PathTooltip"
interface DirPermissionItem {
path: string
key: string
}
interface BatchListFilesPermissionProps {
dirs: DirPermissionItem[]
ts: number
}
export const BatchListFilesPermission = memo(({ dirs = [], ts }: BatchListFilesPermissionProps) => {
if (!dirs?.length) {
return null
}
return (
<div className="pt-[5px]">
<div className="flex flex-col gap-0 border border-border rounded-md p-1">
{dirs.map((dir, index) => {
return (
<div key={`${dir.path}-${index}-${ts}`} className="flex items-center gap-2">
<ToolUseBlock className="flex-1">
<ToolUseBlockHeader>
<PathTooltip content={dir.path}>
<span className="whitespace-nowrap overflow-hidden text-ellipsis text-left mr-2 rtl">
{dir.path}
</span>
</PathTooltip>
<div className="flex-grow"></div>
</ToolUseBlockHeader>
</ToolUseBlock>
</div>
)
})}
</div>
</div>
)
})
BatchListFilesPermission.displayName = "BatchListFilesPermission"

View file

@ -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" }}></span>
)
// Handle batch diffs for any file-edit tool type
if (message.type === "ask" && tool.batchDiffs && Array.isArray(tool.batchDiffs)) {
return (
<>
<div style={headerStyle}>
<FileDiff className="w-4 shrink-0" aria-label="Batch diff icon" />
<span style={{ fontWeight: "bold" }}>{t("chat:fileOperations.wantsToApplyBatchChanges")}</span>
</div>
<BatchDiffApproval files={tool.batchDiffs} ts={message.ts} />
</>
)
}
switch (tool.tool as string) {
case "editedExistingFile":
case "appliedDiff":
// Check if this is a batch diff request
if (message.type === "ask" && tool.batchDiffs && Array.isArray(tool.batchDiffs)) {
return (
<>
<div style={headerStyle}>
<FileDiff className="w-4 shrink-0" aria-label="Batch diff icon" />
<span style={{ fontWeight: "bold" }}>
{t("chat:fileOperations.wantsToApplyBatchChanges")}
</span>
</div>
<BatchDiffApproval files={tool.batchDiffs} ts={message.ts} />
</>
)
}
// Regular single file diff
return (
<>
@ -742,45 +741,57 @@ export const ChatRowContent = ({
)
}
case "listFilesTopLevel":
case "listFilesRecursive": {
const isRecursive = tool.tool === "listFilesRecursive"
// Check if this is a batch directory listing request
const isBatchDirRequest = message.type === "ask" && tool.batchDirs && Array.isArray(tool.batchDirs)
// When batching, check if all dirs share the same recursive value
const allTopLevel = tool.batchDirs?.every((d: { recursive: boolean }) => !d.recursive)
const DirIcon = isBatchDirRequest && !allTopLevel ? FolderTree : isRecursive ? FolderTree : ListTree
const dirIconLabel =
isBatchDirRequest && !allTopLevel
? "Folder tree icon"
: isRecursive
? "Folder tree icon"
: "List files icon"
if (isBatchDirRequest) {
return (
<>
<div style={headerStyle}>
<DirIcon className="w-4 shrink-0" aria-label={dirIconLabel} />
<span style={{ fontWeight: "bold" }}>
{t("chat:directoryOperations.wantsToViewMultipleDirectories")}
</span>
</div>
<BatchListFilesPermission dirs={tool.batchDirs || []} ts={message?.ts} />
</>
)
}
const labelKey = isRecursive
? message.type === "ask"
? tool.isOutsideWorkspace
? "chat:directoryOperations.wantsToViewRecursiveOutsideWorkspace"
: "chat:directoryOperations.wantsToViewRecursive"
: tool.isOutsideWorkspace
? "chat:directoryOperations.didViewRecursiveOutsideWorkspace"
: "chat:directoryOperations.didViewRecursive"
: message.type === "ask"
? tool.isOutsideWorkspace
? "chat:directoryOperations.wantsToViewTopLevelOutsideWorkspace"
: "chat:directoryOperations.wantsToViewTopLevel"
: tool.isOutsideWorkspace
? "chat:directoryOperations.didViewTopLevelOutsideWorkspace"
: "chat:directoryOperations.didViewTopLevel"
return (
<>
<div style={headerStyle}>
<ListTree className="w-4 shrink-0" aria-label="List files icon" />
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? tool.isOutsideWorkspace
? t("chat:directoryOperations.wantsToViewTopLevelOutsideWorkspace")
: t("chat:directoryOperations.wantsToViewTopLevel")
: tool.isOutsideWorkspace
? t("chat:directoryOperations.didViewTopLevelOutsideWorkspace")
: t("chat:directoryOperations.didViewTopLevel")}
</span>
</div>
<div className="pl-6">
<CodeAccordian
path={tool.path}
code={tool.content}
language="shell-session"
isExpanded={isExpanded}
onToggleExpand={handleToggleExpand}
/>
</div>
</>
)
case "listFilesRecursive":
return (
<>
<div style={headerStyle}>
<FolderTree className="w-4 shrink-0" aria-label="Folder tree icon" />
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? tool.isOutsideWorkspace
? t("chat:directoryOperations.wantsToViewRecursiveOutsideWorkspace")
: t("chat:directoryOperations.wantsToViewRecursive")
: tool.isOutsideWorkspace
? t("chat:directoryOperations.didViewRecursiveOutsideWorkspace")
: t("chat:directoryOperations.didViewRecursive")}
</span>
<DirIcon className="w-4 shrink-0" aria-label={dirIconLabel} />
<span style={{ fontWeight: "bold" }}>{t(labelKey)}</span>
</div>
<div className="pl-6">
<CodeAccordian
@ -793,6 +804,7 @@ export const ChatRowContent = ({
</div>
</>
)
}
case "searchFiles":
return (
<>

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"
@ -71,8 +72,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const isMountedRef = useRef(true)
const [audioBaseUri] = useState(() => {
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<ChatViewRef, ChatViewPro
case "editedExistingFile":
case "appliedDiff":
case "newFileCreated":
if (tool.batchDiffs && Array.isArray(tool.batchDiffs)) {
setPrimaryButtonText(t("chat:edit-batch.approve.title"))
setSecondaryButtonText(t("chat:edit-batch.deny.title"))
} else {
setPrimaryButtonText(t("chat:save.title"))
setSecondaryButtonText(t("chat:reject.title"))
}
break
case "generateImage":
setPrimaryButtonText(t("chat:save.title"))
setSecondaryButtonText(t("chat:reject.title"))
@ -328,6 +336,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:list-batch.approve.title"))
setSecondaryButtonText(t("chat:list-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"))
@ -957,10 +975,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
if (
msg.say === "user_feedback" &&
msg.checkpoint &&
(msg.checkpoint as any).type === "user_message" &&
(msg.checkpoint as any).hash
msg.checkpoint["type"] === "user_message" &&
msg.checkpoint["hash"]
) {
userMessageCheckpointHashes.add((msg.checkpoint as any).hash)
userMessageCheckpointHashes.add(msg.checkpoint["hash"] as string)
}
})
@ -1154,71 +1172,137 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}
}
// Consolidate consecutive read_file ask messages into batches
const result: 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++
}
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++
}
} else {
result.push(msg)
i++
// Helper to check if a message is a list_files ask that should be batched
const isListFilesAsk = (msg: ClineMessage): boolean => {
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: "" }
}
})
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: "" }
}
})
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<ChatViewRef, ChatViewPro
useEffect(() => {
return () => {
if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === "function") {
;(scrollToBottomSmooth as any).cancel()
}
scrollToBottomSmooth.clear()
}
}, [scrollToBottomSmooth])

View file

@ -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(
<TranslationProvider>
<BatchListFilesPermission dirs={mockDirs} ts={Date.now()} />
</TranslationProvider>,
)
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(
<TranslationProvider>
<BatchListFilesPermission dirs={[]} ts={Date.now()} />
</TranslationProvider>,
)
expect(container.firstChild).toBeNull()
})
it("re-renders when timestamp changes", () => {
const { rerender } = render(
<TranslationProvider>
<BatchListFilesPermission dirs={mockDirs} ts={1000} />
</TranslationProvider>,
)
expect(screen.getByText("apps/cli")).toBeInTheDocument()
rerender(
<TranslationProvider>
<BatchListFilesPermission dirs={mockDirs} ts={2000} />
</TranslationProvider>,
)
expect(screen.getByText("apps/cli")).toBeInTheDocument()
})
it("renders all directories in a single container", () => {
render(
<TranslationProvider>
<BatchListFilesPermission dirs={mockDirs} ts={Date.now()} />
</TranslationProvider>,
)
// 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(
<TranslationProvider>
<BatchListFilesPermission dirs={singleDir} ts={Date.now()} />
</TranslationProvider>,
)
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)
})
})

View file

@ -220,7 +220,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": {
@ -441,6 +442,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}}%",

View file

@ -220,7 +220,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": {
@ -441,6 +442,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}}%",

View file

@ -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"
@ -237,6 +253,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 <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

@ -220,7 +220,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": {
@ -441,6 +442,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}}%",

View file

@ -220,7 +220,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": {
@ -441,6 +442,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}}%",

View file

@ -220,7 +220,8 @@
"wantsToViewTopLevelOutsideWorkspace": "Roo इस निर्देशिका (कार्यक्षेत्र के बाहर) में शीर्ष स्तर की फ़ाइलें देखना चाहता है",
"didViewTopLevelOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में शीर्ष स्तर की फ़ाइलें देखीं",
"wantsToViewRecursiveOutsideWorkspace": "Roo इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखना चाहता है",
"didViewRecursiveOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखा"
"didViewRecursiveOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में सभी फ़ाइलों को पुनरावर्ती रूप से देखा",
"wantsToViewMultipleDirectories": "Roo कई डायरेक्ट्रीज़ देखना चाहता है"
},
"commandOutput": "कमांड आउटपुट",
"commandExecution": {
@ -441,6 +442,22 @@
"title": "सभी अस्वीकार करें"
}
},
"list-batch": {
"approve": {
"title": "सभी स्वीकृत करें"
},
"deny": {
"title": "सभी अस्वीकार करें"
}
},
"edit-batch": {
"approve": {
"title": "सभी सहेजें"
},
"deny": {
"title": "सभी अस्वीकार करें"
}
},
"indexingStatus": {
"ready": "इंडेक्स तैयार",
"indexing": "इंडेक्सिंग {{percentage}}%",

View file

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

View file

@ -220,7 +220,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": {
@ -441,6 +442,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}}%",

View file

@ -220,7 +220,8 @@
"wantsToViewTopLevelOutsideWorkspace": "Rooはこのディレクトリワークスペース外のトップレベルファイルを表示したい",
"didViewTopLevelOutsideWorkspace": "Rooはこのディレクトリワークスペース外のトップレベルファイルを表示しました",
"wantsToViewRecursiveOutsideWorkspace": "Rooはこのディレクトリワークスペース外のすべてのファイルを再帰的に表示したい",
"didViewRecursiveOutsideWorkspace": "Rooはこのディレクトリワークスペース外のすべてのファイルを再帰的に表示しました"
"didViewRecursiveOutsideWorkspace": "Rooはこのディレクトリワークスペース外のすべてのファイルを再帰的に表示しました",
"wantsToViewMultipleDirectories": "Roo は複数のディレクトリを表示したい"
},
"commandOutput": "コマンド出力",
"commandExecution": {
@ -441,6 +442,22 @@
"title": "すべて拒否"
}
},
"list-batch": {
"approve": {
"title": "すべて承認"
},
"deny": {
"title": "すべて拒否"
}
},
"edit-batch": {
"approve": {
"title": "すべて保存"
},
"deny": {
"title": "すべて拒否"
}
},
"indexingStatus": {
"ready": "インデックス準備完了",
"indexing": "インデックス作成中 {{percentage}}%",

View file

@ -220,7 +220,8 @@
"wantsToViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보고 싶어합니다",
"didViewTopLevelOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 최상위 파일을 보았습니다",
"wantsToViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보고 싶어합니다",
"didViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다"
"didViewRecursiveOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)의 모든 파일을 재귀적으로 보았습니다",
"wantsToViewMultipleDirectories": "Roo가 여러 디렉토리를 보려고 합니다"
},
"commandOutput": "명령 출력",
"commandExecution": {
@ -441,6 +442,22 @@
"title": "모두 거부"
}
},
"list-batch": {
"approve": {
"title": "모두 승인"
},
"deny": {
"title": "모두 거부"
}
},
"edit-batch": {
"approve": {
"title": "모두 저장"
},
"deny": {
"title": "모두 거부"
}
},
"indexingStatus": {
"ready": "인덱스 준비됨",
"indexing": "인덱싱 중 {{percentage}}%",

View file

@ -147,14 +147,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."
@ -215,7 +215,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": {
@ -441,6 +442,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}}%",

View file

@ -152,14 +152,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ć."
@ -220,7 +220,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": {
@ -441,6 +442,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}}%",

View file

@ -220,7 +220,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": {
@ -441,6 +442,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}}%",

View file

@ -215,7 +215,8 @@
"wantsToViewTopLevelOutsideWorkspace": "Roo хочет просмотреть файлы верхнего уровня в этой директории (вне рабочего пространства)",
"didViewTopLevelOutsideWorkspace": "Roo просмотрел файлы верхнего уровня в этой директории (вне рабочего пространства)",
"wantsToViewRecursiveOutsideWorkspace": "Roo хочет рекурсивно просмотреть все файлы в этой директории (вне рабочего пространства)",
"didViewRecursiveOutsideWorkspace": "Roo рекурсивно просмотрел все файлы в этой директории (вне рабочего пространства)"
"didViewRecursiveOutsideWorkspace": "Roo рекурсивно просмотрел все файлы в этой директории (вне рабочего пространства)",
"wantsToViewMultipleDirectories": "Roo хочет просмотреть несколько директорий"
},
"commandOutput": "Вывод команды",
"commandExecution": {
@ -442,6 +443,22 @@
"title": "Отклонить все"
}
},
"list-batch": {
"approve": {
"title": "Одобрить все"
},
"deny": {
"title": "Отклонить все"
}
},
"edit-batch": {
"approve": {
"title": "Сохранить все"
},
"deny": {
"title": "Отклонить все"
}
},
"indexingStatus": {
"ready": "Индекс готов",
"indexing": "Индексация {{percentage}}%",

View file

@ -220,7 +220,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": {
@ -442,6 +443,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}}%",

View file

@ -220,7 +220,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": {
@ -442,6 +443,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}}%",

View file

@ -220,7 +220,8 @@
"wantsToViewTopLevelOutsideWorkspace": "需要查看目录文件列表(工作区外)",
"didViewTopLevelOutsideWorkspace": "已查看目录文件列表(工作区外)",
"wantsToViewRecursiveOutsideWorkspace": "需要查看目录所有文件(工作区外)",
"didViewRecursiveOutsideWorkspace": "已查看目录所有文件(工作区外)"
"didViewRecursiveOutsideWorkspace": "已查看目录所有文件(工作区外)",
"wantsToViewMultipleDirectories": "Roo 想要查看多个目录"
},
"commandOutput": "命令输出",
"commandExecution": {
@ -442,6 +443,22 @@
"title": "全部拒绝"
}
},
"list-batch": {
"approve": {
"title": "全部批准"
},
"deny": {
"title": "全部拒绝"
}
},
"edit-batch": {
"approve": {
"title": "全部保存"
},
"deny": {
"title": "全部拒绝"
}
},
"indexingStatus": {
"ready": "索引就绪",
"indexing": "索引中 {{percentage}}%",

View file

@ -74,6 +74,22 @@
"title": "全部拒絕"
}
},
"list-batch": {
"approve": {
"title": "全部核准"
},
"deny": {
"title": "全部拒絕"
}
},
"edit-batch": {
"approve": {
"title": "全部儲存"
},
"deny": {
"title": "全部拒絕"
}
},
"runCommand": {
"title": "執行",
"tooltip": "執行此命令"
@ -158,14 +174,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」以進行驗證。"
@ -243,7 +259,8 @@
"wantsToSearch": "Roo 想要在此目錄中搜尋 <code>{{regex}}</code>",
"didSearch": "Roo 已在此目錄中搜尋 <code>{{regex}}</code>",
"wantsToSearchOutsideWorkspace": "Roo 想要在此目錄(工作區外)中搜尋 <code>{{regex}}</code>",
"didSearchOutsideWorkspace": "Roo 已在此目錄(工作區外)中搜尋 <code>{{regex}}</code>"
"didSearchOutsideWorkspace": "Roo 已在此目錄(工作區外)中搜尋 <code>{{regex}}</code>",
"wantsToViewMultipleDirectories": "Roo 想要查看多個目錄"
},
"codebaseSearch": {
"wantsToSearch": "Roo 想要在程式碼庫中搜尋 <code>{{query}}</code>",

View file

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

View file

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