|
|
@@ -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++
|
|
|
+ // 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: "" }
|
|
|
}
|
|
|
+ })
|
|
|
|
|
|
- if (batch.length > 1) {
|
|
|
- // Create a synthetic batch message
|
|
|
- const batchFiles = batch.map((batchMsg) => {
|
|
|
- try {
|
|
|
- const tool = JSON.parse(batchMsg.text || "{}")
|
|
|
- return {
|
|
|
- path: tool.path || "",
|
|
|
- lineSnippet: tool.reason || "",
|
|
|
- isOutsideWorkspace: tool.isOutsideWorkspace || false,
|
|
|
- key: `${tool.path}${tool.reason ? ` (${tool.reason})` : ""}`,
|
|
|
- content: tool.content || "",
|
|
|
- }
|
|
|
- } catch {
|
|
|
- return { path: "", lineSnippet: "", key: "", content: "" }
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- // Use the first message as the base, but add batchFiles
|
|
|
- const firstTool = JSON.parse(msg.text || "{}")
|
|
|
- const syntheticMessage: ClineMessage = {
|
|
|
- ...msg,
|
|
|
- text: JSON.stringify({
|
|
|
- ...firstTool,
|
|
|
- batchFiles,
|
|
|
- }),
|
|
|
- // Store original messages for response handling
|
|
|
- _batchedMessages: batch,
|
|
|
- } as ClineMessage & { _batchedMessages: ClineMessage[] }
|
|
|
-
|
|
|
- result.push(syntheticMessage)
|
|
|
- i = j // Skip past all batched messages
|
|
|
- } else {
|
|
|
- // Single read_file ask, keep as-is
|
|
|
- result.push(msg)
|
|
|
- i++
|
|
|
+ let firstTool
|
|
|
+ try {
|
|
|
+ firstTool = JSON.parse(batch[0].text || "{}")
|
|
|
+ } catch {
|
|
|
+ return batch[0]
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ ...batch[0],
|
|
|
+ text: JSON.stringify({ ...firstTool, batchFiles }),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Synthesize a batch of consecutive list_files asks into a single message
|
|
|
+ const synthesizeListFilesBatch = (batch: ClineMessage[]): ClineMessage => {
|
|
|
+ const batchDirs = batch.map((batchMsg) => {
|
|
|
+ try {
|
|
|
+ const tool = JSON.parse(batchMsg.text || "{}")
|
|
|
+ return {
|
|
|
+ path: tool.path || "",
|
|
|
+ recursive: tool.tool === "listFilesRecursive",
|
|
|
+ isOutsideWorkspace: tool.isOutsideWorkspace || false,
|
|
|
+ key: tool.path || "",
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ return { path: "", recursive: false, key: "" }
|
|
|
}
|
|
|
- } else {
|
|
|
- result.push(msg)
|
|
|
- i++
|
|
|
+ })
|
|
|
+
|
|
|
+ let firstTool
|
|
|
+ try {
|
|
|
+ firstTool = JSON.parse(batch[0].text || "{}")
|
|
|
+ } catch {
|
|
|
+ return batch[0]
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ ...batch[0],
|
|
|
+ text: JSON.stringify({ ...firstTool, batchDirs }),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Synthesize a batch of consecutive file-edit asks into a single message
|
|
|
+ const synthesizeEditFileBatch = (batch: ClineMessage[]): ClineMessage => {
|
|
|
+ const batchDiffs = batch.map((batchMsg) => {
|
|
|
+ try {
|
|
|
+ const tool = JSON.parse(batchMsg.text || "{}")
|
|
|
+ return {
|
|
|
+ path: tool.path || "",
|
|
|
+ changeCount: 1,
|
|
|
+ key: tool.path || "",
|
|
|
+ content: tool.content || tool.diff || "",
|
|
|
+ diffStats: tool.diffStats,
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ return { path: "", changeCount: 0, key: "", content: "" }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ let firstTool
|
|
|
+ try {
|
|
|
+ firstTool = JSON.parse(batch[0].text || "{}")
|
|
|
+ } catch {
|
|
|
+ return batch[0]
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ ...batch[0],
|
|
|
+ text: JSON.stringify({ ...firstTool, batchDiffs }),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // Consolidate consecutive ask messages into batches
|
|
|
+ const readFileBatched = batchConsecutive(filtered, isReadFileAsk, synthesizeReadFileBatch)
|
|
|
+ const listFilesBatched = batchConsecutive(readFileBatched, isListFilesAsk, synthesizeListFilesBatch)
|
|
|
+ const result = batchConsecutive(listFilesBatched, isEditFileAsk, synthesizeEditFileBatch)
|
|
|
+
|
|
|
if (isCondensing) {
|
|
|
result.push({
|
|
|
type: "say",
|
|
|
say: "condense_context",
|
|
|
ts: Date.now(),
|
|
|
partial: true,
|
|
|
- } as any)
|
|
|
+ } as ClineMessage)
|
|
|
}
|
|
|
return result
|
|
|
}, [isCondensing, visibleMessages, isBrowserSessionMessage])
|
|
|
@@ -1235,9 +1319,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|
|
|
|
|
useEffect(() => {
|
|
|
return () => {
|
|
|
- if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === "function") {
|
|
|
- ;(scrollToBottomSmooth as any).cancel()
|
|
|
- }
|
|
|
+ scrollToBottomSmooth.clear()
|
|
|
}
|
|
|
}, [scrollToBottomSmooth])
|
|
|
|