mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
5313cb503a
commit
7afa43635f
27 changed files with 850 additions and 142 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 })}>
|
||||
|
|
|
|||
45
webview-ui/src/components/chat/BatchListFilesPermission.tsx
Normal file
45
webview-ui/src/components/chat/BatchListFilesPermission.tsx
Normal 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"
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
19
webview-ui/src/i18n/locales/ca/chat.json
generated
19
webview-ui/src/i18n/locales/ca/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/de/chat.json
generated
19
webview-ui/src/i18n/locales/de/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
|
|
@ -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>",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/es/chat.json
generated
19
webview-ui/src/i18n/locales/es/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/fr/chat.json
generated
19
webview-ui/src/i18n/locales/fr/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/hi/chat.json
generated
19
webview-ui/src/i18n/locales/hi/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/id/chat.json
generated
19
webview-ui/src/i18n/locales/id/chat.json
generated
|
|
@ -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>",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/it/chat.json
generated
19
webview-ui/src/i18n/locales/it/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/ja/chat.json
generated
19
webview-ui/src/i18n/locales/ja/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/ko/chat.json
generated
19
webview-ui/src/i18n/locales/ko/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
23
webview-ui/src/i18n/locales/nl/chat.json
generated
23
webview-ui/src/i18n/locales/nl/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
23
webview-ui/src/i18n/locales/pl/chat.json
generated
23
webview-ui/src/i18n/locales/pl/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
19
webview-ui/src/i18n/locales/pt-BR/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/ru/chat.json
generated
19
webview-ui/src/i18n/locales/ru/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/tr/chat.json
generated
19
webview-ui/src/i18n/locales/tr/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/vi/chat.json
generated
19
webview-ui/src/i18n/locales/vi/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
19
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
19
webview-ui/src/i18n/locales/zh-CN/chat.json
generated
|
|
@ -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}}%",
|
||||
|
|
|
|||
23
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
23
webview-ui/src/i18n/locales/zh-TW/chat.json
generated
|
|
@ -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>",
|
||||
|
|
|
|||
116
webview-ui/src/utils/__tests__/batchConsecutive.spec.ts
Normal file
116
webview-ui/src/utils/__tests__/batchConsecutive.spec.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
38
webview-ui/src/utils/batchConsecutive.ts
Normal file
38
webview-ui/src/utils/batchConsecutive.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue