diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 3feb695e104..e6b7a69161c 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1532,6 +1532,21 @@ export class Task extends EventEmitter implements TaskLike { }) } } + + // Mark the last tool-approval ask as answered when user approves (or auto-approval) + if (askResponse === "yesButtonClicked") { + const lastToolAskIndex = findLastIndex( + this.clineMessages, + (msg) => msg.type === "ask" && msg.ask === "tool" && !msg.isAnswered, + ) + if (lastToolAskIndex !== -1) { + this.clineMessages[lastToolAskIndex].isAnswered = true + void this.updateClineMessage(this.clineMessages[lastToolAskIndex]) + this.saveClineMessages().catch((error) => { + console.error("Failed to save answered tool-ask state:", error) + }) + } + } } /** diff --git a/webview-ui/src/__tests__/App.spec.tsx b/webview-ui/src/__tests__/App.spec.tsx index e8e08782da4..e04bc142007 100644 --- a/webview-ui/src/__tests__/App.spec.tsx +++ b/webview-ui/src/__tests__/App.spec.tsx @@ -193,7 +193,7 @@ describe("App", () => { const chatView = screen.getByTestId("chat-view") expect(chatView).toBeInTheDocument() expect(chatView.getAttribute("data-hidden")).toBe("false") - }) + }, 10000) it("switches to settings view when receiving settingsButtonClicked action", async () => { render() diff --git a/webview-ui/src/__tests__/FileChangesPanel.spec.tsx b/webview-ui/src/__tests__/FileChangesPanel.spec.tsx new file mode 100644 index 00000000000..b28102b1fe7 --- /dev/null +++ b/webview-ui/src/__tests__/FileChangesPanel.spec.tsx @@ -0,0 +1,175 @@ +import React from "react" +import { fireEvent, render, screen } from "@/utils/test-utils" +import type { ClineMessage } from "@roo-code/types" +import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" +import FileChangesPanel from "../components/chat/FileChangesPanel" + +const mockPostMessage = vi.fn() + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: (...args: unknown[]) => mockPostMessage(...args), + }, +})) + +// Mock i18n to return readable header with count +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, opts?: { count?: number }) => { + if (key === "chat:fileChangesInConversation.header" && opts?.count != null) { + return `${opts.count} file(s) changed in this conversation` + } + return key + }, + }), +})) + +// Lightweight mock so we don't pull in CodeBlock/DiffView +vi.mock("@src/components/common/CodeAccordian", () => ({ + default: ({ + path, + isExpanded, + onToggleExpand, + }: { + path?: string + isExpanded: boolean + onToggleExpand: () => void + }) => ( +
+ {path} + +
+ ), +})) + +function createFileEditMessage(path: string, diff: string): ClineMessage { + return { + type: "ask", + ask: "tool", + ts: Date.now(), + partial: false, + isAnswered: true, + text: JSON.stringify({ + tool: "appliedDiff", + path, + diff, + }), + } +} + +function renderPanel(messages: ClineMessage[] | undefined) { + return render( + + + , + ) +} + +describe("FileChangesPanel", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("renders nothing when clineMessages is undefined", () => { + const { container } = renderPanel(undefined) + expect(container.firstChild).toBeNull() + }) + + it("renders nothing when clineMessages is empty", () => { + const { container } = renderPanel([]) + expect(container.firstChild).toBeNull() + }) + + it("renders nothing when there are no file-edit messages", () => { + const messages: ClineMessage[] = [ + { + type: "say", + say: "text", + ts: Date.now(), + partial: false, + text: "hello", + }, + { + type: "ask", + ask: "tool", + ts: Date.now(), + partial: false, + text: JSON.stringify({ tool: "read_file", path: "x.ts" }), + }, + ] + const { container } = renderPanel(messages) + expect(container.firstChild).toBeNull() + }) + + it("renders nothing when file-edit ask tool is not approved (isAnswered false or missing)", () => { + const messages: ClineMessage[] = [ + { + type: "ask", + ask: "tool", + ts: Date.now(), + partial: false, + text: JSON.stringify({ + tool: "appliedDiff", + path: "src/foo.ts", + diff: "+line", + }), + }, + ] + const { container } = renderPanel(messages) + expect(container.firstChild).toBeNull() + }) + + it("renders panel with header when there is one file edit", () => { + const messages = [createFileEditMessage("src/foo.ts", "@@ -1 +1 @@\n+line")] + renderPanel(messages) + + expect(screen.getByText("1 file(s) changed in this conversation")).toBeInTheDocument() + // Expand panel so file row is in DOM (CollapsibleContent may not render when closed in some setups) + fireEvent.click(screen.getByText("1 file(s) changed in this conversation").closest("button")!) + expect(screen.getByTestId("accordian-path")).toHaveTextContent("src/foo.ts") + }) + + it("renders one row per unique path when multiple files edited", () => { + const messages = [createFileEditMessage("src/a.ts", "diff a"), createFileEditMessage("src/b.ts", "diff b")] + renderPanel(messages) + + expect(screen.getByText("2 file(s) changed in this conversation")).toBeInTheDocument() + // Expand panel so file rows are rendered + fireEvent.click(screen.getByText("2 file(s) changed in this conversation").closest("button")!) + const paths = screen.getAllByTestId("accordian-path") + expect(paths).toHaveLength(2) + expect(paths.map((el) => el.textContent)).toEqual(expect.arrayContaining(["src/a.ts", "src/b.ts"])) + }) + + it("collapsed by default: panel trigger shows chevron and expanding reveals file rows", () => { + const messages = [createFileEditMessage("src/foo.ts", "diff")] + renderPanel(messages) + + // Header visible + const headerText = screen.getByText("1 file(s) changed in this conversation") + expect(headerText).toBeInTheDocument() + // Trigger is the button that contains the header text + const trigger = headerText.closest("button") + expect(trigger).toBeInTheDocument() + + // Expand panel + fireEvent.click(trigger!) + expect(screen.getByTestId("accordian-path")).toHaveTextContent("src/foo.ts") + }) + + it("toggling a file row expand calls onToggleExpand", () => { + const messages = [createFileEditMessage("src/foo.ts", "diff")] + renderPanel(messages) + + // Expand panel first so the file row is rendered + const headerText = screen.getByText("1 file(s) changed in this conversation") + fireEvent.click(headerText.closest("button")!) + + const accordianToggle = screen.getByTestId("accordian-toggle") + expect(accordianToggle).toHaveTextContent("collapsed") + fireEvent.click(accordianToggle) + expect(accordianToggle).toHaveTextContent("expanded") + }) +}) diff --git a/webview-ui/src/__tests__/fileChangesFromMessages.spec.ts b/webview-ui/src/__tests__/fileChangesFromMessages.spec.ts new file mode 100644 index 00000000000..8fab8b14d50 --- /dev/null +++ b/webview-ui/src/__tests__/fileChangesFromMessages.spec.ts @@ -0,0 +1,280 @@ +import type { ClineMessage } from "@roo-code/types" +import { fileChangesFromMessages } from "../components/chat/utils/fileChangesFromMessages" + +function msg(overrides: Partial & { text: string }): ClineMessage { + return { + type: "say", + say: "tool", + ts: Date.now(), + partial: false, + ...overrides, + } +} + +describe("fileChangesFromMessages", () => { + it("returns empty array for undefined messages", () => { + expect(fileChangesFromMessages(undefined)).toEqual([]) + }) + + it("returns empty array for empty messages", () => { + expect(fileChangesFromMessages([])).toEqual([]) + }) + + it("ignores non-tool messages", () => { + const messages: ClineMessage[] = [ + msg({ type: "say", say: "text", text: "hello" }), + msg({ type: "ask", ask: "followup", text: "world" }), + ] + expect(fileChangesFromMessages(messages)).toEqual([]) + }) + + it("ignores tool messages with non-file-edit tool type", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + text: JSON.stringify({ tool: "read_file", path: "a.ts" }), + }), + ] + expect(fileChangesFromMessages(messages)).toEqual([]) + }) + + it("skips partial messages", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + partial: true, + text: JSON.stringify({ + tool: "appliedDiff", + path: "src/file.ts", + diff: "+x", + }), + }), + ] + expect(fileChangesFromMessages(messages)).toEqual([]) + }) + + it("excludes ask tool file-edit when isAnswered is false or undefined", () => { + const payload = JSON.stringify({ + tool: "appliedDiff", + path: "src/foo.ts", + diff: "+line", + }) + expect(fileChangesFromMessages([msg({ type: "ask", ask: "tool", text: payload, isAnswered: false })])).toEqual( + [], + ) + expect(fileChangesFromMessages([msg({ type: "ask", ask: "tool", text: payload })])).toEqual([]) + }) + + it("includes ask tool file-edit when isAnswered is true", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + isAnswered: true, + text: JSON.stringify({ + tool: "appliedDiff", + path: "src/foo.ts", + diff: "+line", + }), + }), + ] + const result = fileChangesFromMessages(messages) + expect(result).toHaveLength(1) + expect(result[0].path).toBe("src/foo.ts") + }) + + it("extracts single-file edit from ask tool message", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + isAnswered: true, + text: JSON.stringify({ + tool: "appliedDiff", + path: "src/foo.ts", + diff: "@@ -1 +1 @@\n+line", + diffStats: { added: 1, removed: 0 }, + }), + }), + ] + const result = fileChangesFromMessages(messages) + expect(result).toHaveLength(1) + expect(result[0]).toEqual({ + path: "src/foo.ts", + diff: "@@ -1 +1 @@\n+line", + diffStats: { added: 1, removed: 0 }, + }) + }) + + it("extracts single-file edit from say tool message", () => { + const messages: ClineMessage[] = [ + msg({ + type: "say", + say: "tool", + text: JSON.stringify({ + tool: "editedExistingFile", + path: "lib/bar.ts", + diff: "-old\n+new", + }), + }), + ] + const result = fileChangesFromMessages(messages) + expect(result).toHaveLength(1) + expect(result[0].path).toBe("lib/bar.ts") + expect(result[0].diff).toBe("-old\n+new") + }) + + it("uses content when diff is missing for single-file", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + isAnswered: true, + text: JSON.stringify({ + tool: "newFileCreated", + path: "new.ts", + content: "full file content", + }), + }), + ] + const result = fileChangesFromMessages(messages) + expect(result).toHaveLength(1) + expect(result[0].diff).toBe("full file content") + }) + + it("ignores single-file tool when path is missing", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + text: JSON.stringify({ + tool: "appliedDiff", + diff: "something", + }), + }), + ] + expect(fileChangesFromMessages(messages)).toEqual([]) + }) + + it("ignores single-file tool when diff and content are empty", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + text: JSON.stringify({ + tool: "appliedDiff", + path: "x.ts", + }), + }), + ] + expect(fileChangesFromMessages(messages)).toEqual([]) + }) + + it("extracts from batchDiffs", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + isAnswered: true, + text: JSON.stringify({ + tool: "appliedDiff", + batchDiffs: [ + { path: "a.ts", content: "content a" }, + { path: "b.ts", diffs: [{ content: "content b" }] }, + { path: "c.ts" }, // no content + ], + }), + }), + ] + const result = fileChangesFromMessages(messages) + expect(result).toHaveLength(2) + expect(result[0]).toEqual({ path: "a.ts", diff: "content a" }) + expect(result[1].path).toBe("b.ts") + expect(result[1].diff).toBe("content b") + }) + + it("includes diffStats from batchDiffs when present", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + isAnswered: true, + text: JSON.stringify({ + tool: "appliedDiff", + batchDiffs: [ + { + path: "f.ts", + content: "x", + diffStats: { added: 2, removed: 1 }, + }, + ], + }), + }), + ] + const result = fileChangesFromMessages(messages) + expect(result[0].diffStats).toEqual({ added: 2, removed: 1 }) + }) + + it("recognizes all ClineSayTool file-edit tool names (editedExistingFile, appliedDiff, newFileCreated)", () => { + const tools = ["editedExistingFile", "appliedDiff", "newFileCreated"] + for (const tool of tools) { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + isAnswered: true, + text: JSON.stringify({ + tool, + path: "f.ts", + diff: "d", + }), + }), + ] + const result = fileChangesFromMessages(messages) + expect(result).toHaveLength(1) + expect(result[0].path).toBe("f.ts") + } + }) + + it("returns multiple entries for multiple file-edit messages", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + isAnswered: true, + text: JSON.stringify({ + tool: "appliedDiff", + path: "first.ts", + diff: "a", + }), + }), + msg({ + type: "ask", + ask: "tool", + isAnswered: true, + text: JSON.stringify({ + tool: "editedExistingFile", + path: "second.ts", + diff: "b", + }), + }), + ] + const result = fileChangesFromMessages(messages) + expect(result).toHaveLength(2) + expect(result[0].path).toBe("first.ts") + expect(result[1].path).toBe("second.ts") + }) + + it("skips invalid JSON in message text", () => { + const messages: ClineMessage[] = [ + msg({ + type: "ask", + ask: "tool", + text: "not json", + }), + ] + expect(fileChangesFromMessages(messages)).toEqual([]) + }) +}) diff --git a/webview-ui/src/components/__tests__/ErrorBoundary.spec.tsx b/webview-ui/src/components/__tests__/ErrorBoundary.spec.tsx deleted file mode 100644 index 1fbb6774f2d..00000000000 --- a/webview-ui/src/components/__tests__/ErrorBoundary.spec.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from "react" -import { render, screen } from "@testing-library/react" - -import ErrorBoundary from "../ErrorBoundary" - -// Mock telemetryClient -vi.mock("@src/utils/TelemetryClient", () => ({ - telemetryClient: { - capture: vi.fn(), - }, -})) - -// Mock translation -vi.mock("react-i18next", () => ({ - withTranslation: () => (Component: any) => { - Component.defaultProps = { - ...Component.defaultProps, - t: (key: string) => { - // Mock translations for tests - const translations: Record = { - "errorBoundary.title": "Something went wrong", - "errorBoundary.reportText": "Please help us improve by reporting this error on", - "errorBoundary.githubText": "GitHub", - "errorBoundary.copyInstructions": "Please copy and paste the following error message:", - } - return translations[key] || key - }, - } - return Component - }, -})) - -// Test component that throws an error -const ErrorThrowingComponent = ({ shouldThrow = false }) => { - if (shouldThrow) { - throw new Error("Test error") - } - return
Content rendered normally
-} - -describe("ErrorBoundary", () => { - // Suppress console errors during tests - const originalConsoleError = console.error - beforeAll(() => { - console.error = vi.fn() - }) - afterAll(() => { - console.error = originalConsoleError - }) - - test("renders children when no error occurs", () => { - render( - - - , - ) - - expect(screen.getByTestId("normal-render")).toBeInTheDocument() - }) - - test("renders error UI when an error occurs", () => { - // React will log the error to the console - we're just testing the UI behavior - render( - - - , - ) - - // Verify error message is displayed using a more flexible approach - const errorTitle = screen.getByRole("heading", { level: 2 }) - expect(errorTitle.textContent).toContain("Something went wrong") - expect(screen.getByText(/please copy and paste the following error message/i)).toBeInTheDocument() - }) - - test("error boundary renders error UI when component changes but still in error state", () => { - const { rerender } = render( - - - , - ) - - // Verify error message is displayed using a more flexible approach - const errorTitle = screen.getByRole("heading", { level: 2 }) - expect(errorTitle.textContent).toContain("Something went wrong") - - // Update the component to not throw - rerender( - - - , - ) - - // The error boundary should still show the error since it doesn't automatically reset - const errorTitleAfterRerender = screen.getByRole("heading", { level: 2 }) - expect(errorTitleAfterRerender.textContent).toContain("Something went wrong") - }) -}) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index fbd7db07436..c070f8764e6 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -46,6 +46,7 @@ import ProfileViolationWarning from "./ProfileViolationWarning" import { CheckpointWarning } from "./CheckpointWarning" import { QueuedMessages } from "./QueuedMessages" import { WorktreeSelector } from "./WorktreeSelector" +import FileChangesPanel from "./FileChangesPanel" import DismissibleUpsell from "../common/DismissibleUpsell" import { useCloudUpsell } from "@src/hooks/useCloudUpsell" import { Cloud } from "lucide-react" @@ -1700,6 +1701,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction + {areButtonsVisible && (
{ + const { t } = useTranslation() + const [panelExpanded, setPanelExpanded] = useState(false) + const [expandedPaths, setExpandedPaths] = useState>(new Set()) + + // Reset expanded file rows when switching to a different task (clineMessages identity change) + useEffect(() => { + setExpandedPaths(new Set()) + }, [clineMessages]) + + const fileChanges = useMemo(() => fileChangesFromMessages(clineMessages), [clineMessages]) + + // Group by path so we show one row per file (multiple edits to same file combined for display) + const byPath = useMemo(() => { + const map = new Map() + for (const entry of fileChanges) { + const key = entry.path + const list = map.get(key) ?? [] + list.push(entry) + map.set(key, list) + } + return map + }, [fileChanges]) + + const togglePath = useCallback((path: string) => { + setExpandedPaths((prev) => { + const next = new Set(prev) + if (next.has(path)) next.delete(path) + else next.add(path) + return next + }) + }, []) + + if (fileChanges.length === 0) return null + + const fileCount = byPath.size + + return ( + + + {panelExpanded ? ( + + ) : ( + + )} + + + {t("chat:fileChangesInConversation.header", { count: fileCount })} + + + +
+ {Array.from(byPath.entries()).map(([path, entries]) => { + // If multiple edits to same file, concatenate diffs with a separator + const combinedDiff = entries.map((e) => e.diff).join("\n\n") + const combinedStats = entries.reduce( + (acc, e) => ({ + added: acc.added + (e.diffStats?.added ?? 0), + removed: acc.removed + (e.diffStats?.removed ?? 0), + }), + { added: 0, removed: 0 }, + ) + const isExpanded = expandedPaths.has(path) + return ( +
+ togglePath(path)} + diffStats={ + combinedStats.added > 0 || combinedStats.removed > 0 ? combinedStats : undefined + } + onJumpToFile={ + path + ? () => + vscode.postMessage({ + type: "openFile", + text: path.startsWith("./") ? path : "./" + path, + }) + : undefined + } + /> +
+ ) + })} +
+
+
+ ) +}) + +FileChangesPanel.displayName = "FileChangesPanel" + +export default FileChangesPanel diff --git a/webview-ui/src/components/chat/utils/fileChangesFromMessages.ts b/webview-ui/src/components/chat/utils/fileChangesFromMessages.ts new file mode 100644 index 00000000000..6b77833e9d8 --- /dev/null +++ b/webview-ui/src/components/chat/utils/fileChangesFromMessages.ts @@ -0,0 +1,64 @@ +import type { ClineMessage, ClineSayTool } from "@roo-code/types" +import { safeJsonParse } from "@roo/core" + +/** File-edit tool names from ClineSayTool["tool"] (packages/types). */ +const FILE_EDIT_TOOLS = new Set(["editedExistingFile", "appliedDiff", "newFileCreated"]) + +export interface FileChangeEntry { + path: string + diff: string + diffStats?: { added: number; removed: number } +} + +/** + * Derives a list of file changes from clineMessages for the current conversation. + * Includes: + * - type "say" + say "tool" (applied tool results, if any are ever pushed that way) + * - type "ask" + ask "tool" (tool approval messages; after approval the message stays as ask, so this is where file edits appear in the UI) + */ +export function fileChangesFromMessages(messages: ClineMessage[] | undefined): FileChangeEntry[] { + if (!messages?.length) return [] + + const entries: FileChangeEntry[] = [] + + for (const msg of messages) { + // Tool payload can be in say "tool" (rare) or ask "tool" (how file edits are stored after approval) + const isSayTool = msg.type === "say" && msg.say === "tool" + const isAskTool = msg.type === "ask" && msg.ask === "tool" + if ((!isSayTool && !isAskTool) || !msg.text || msg.partial) continue + // Only include ask "tool" file edits that the user (or auto-approval) has approved + if (isAskTool && !msg.isAnswered) continue + + const tool = safeJsonParse(msg.text) + if (!tool || !FILE_EDIT_TOOLS.has(tool.tool as string)) continue + + // Batch diffs + if (tool.batchDiffs && Array.isArray(tool.batchDiffs)) { + for (const file of tool.batchDiffs) { + if (!file.path) continue + const content = file.content ?? file.diffs?.map((d) => d.content).join("\n") ?? "" + if (content) { + entries.push({ + path: file.path, + diff: content, + diffStats: file.diffStats, + }) + } + } + continue + } + + // Single file + if (!tool.path) continue + const diff = tool.diff ?? tool.content ?? "" + if (diff) { + entries.push({ + path: tool.path, + diff, + diffStats: tool.diffStats, + }) + } + } + + return entries +} diff --git a/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx b/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx index a0b6857a373..8e41eefa14a 100644 --- a/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx +++ b/webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx @@ -34,7 +34,7 @@ describe("MarkdownBlock", () => { // Check that the period is outside the link const paragraph = container.querySelector("p") expect(paragraph?.textContent).toBe("Check out this link: https://example.com.") - }) + }, 10000) it("should render unordered lists with proper styling", async () => { const markdown = `Here are some items: diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 566d0139bea..d4df0cd44bd 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Desfixar", "pin": "Fixar", + "fileChangesInConversation": { + "header": "{{count}} fitxer(s) canviat(s) en aquesta conversa" + }, "tokenProgress": { "availableSpace": "Espai disponible: {{amount}} tokens", "tokensUsed": "Tokens utilitzats: {{used}} de {{total}}", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 9568dce40dc..6c952ea7534 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Lösen von oben", "pin": "Anheften", + "fileChangesInConversation": { + "header": "{{count}} Datei(en) in dieser Unterhaltung geändert" + }, "tokenProgress": { "availableSpace": "Verfügbarer Speicher: {{amount}} Tokens", "tokensUsed": "Verwendete Tokens: {{used}} von {{total}}", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index dd21792e43e..d8019f4a821 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Unpin", "pin": "Pin", + "fileChangesInConversation": { + "header": "{{count}} file(s) changed in this conversation" + }, "retry": { "title": "Retry", "tooltip": "Try the operation again" diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 69837b7ec90..bd9980f8ec1 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Desfijar", "pin": "Fijar", + "fileChangesInConversation": { + "header": "{{count}} archivo(s) modificado(s) en esta conversación" + }, "retry": { "title": "Reintentar", "tooltip": "Intenta la operación de nuevo" diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index e3c7d772736..9fe822fbb2d 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Désépingler", "pin": "Épingler", + "fileChangesInConversation": { + "header": "{{count}} fichier(s) modifié(s) dans cette conversation" + }, "tokenProgress": { "availableSpace": "Espace disponible : {{amount}} tokens", "tokensUsed": "Tokens utilisés : {{used}} sur {{total}}", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 6e17b7d314b..15e80ad00d9 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -33,6 +33,9 @@ }, "unpin": "पिन करें", "pin": "अवपिन करें", + "fileChangesInConversation": { + "header": "इस वार्तालाप में {{count}} फ़ाइल(ें) बदली गईं" + }, "tokenProgress": { "availableSpace": "उपलब्ध स्थान: {{amount}} tokens", "tokensUsed": "प्रयुक्त tokens: {{used}} / {{total}}", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 9a4473cb015..67434d9c666 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -36,6 +36,9 @@ }, "unpin": "Lepas Pin", "pin": "Pin", + "fileChangesInConversation": { + "header": "{{count}} file diubah dalam percakapan ini" + }, "retry": { "title": "Coba Lagi", "tooltip": "Coba operasi lagi" diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 4c4190c7c1e..c16f77b2b55 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Rilascia", "pin": "Fissa", + "fileChangesInConversation": { + "header": "{{count}} file modificati in questa conversazione" + }, "tokenProgress": { "availableSpace": "Spazio disponibile: {{amount}} tokens", "tokensUsed": "Tokens utilizzati: {{used}} di {{total}}", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 3c39a9e95bb..91fbdd81fc0 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -33,6 +33,9 @@ }, "unpin": "ピン留めを解除", "pin": "ピン留め", + "fileChangesInConversation": { + "header": "この会話で {{count}} 個のファイルが変更されました" + }, "tokenProgress": { "availableSpace": "利用可能な空き容量: {{amount}} トークン", "tokensUsed": "使用トークン: {{used}} / {{total}}", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 73621f86049..bd216e8d285 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -33,6 +33,9 @@ }, "unpin": "고정 해제하기", "pin": "고정하기", + "fileChangesInConversation": { + "header": "이 대화에서 {{count}}개 파일이 변경됨" + }, "tokenProgress": { "availableSpace": "사용 가능한 공간: {{amount}} 토큰", "tokensUsed": "사용된 토큰: {{used}} / {{total}}", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 94ea4720c0a..f669a260cc0 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Losmaken", "pin": "Vastmaken", + "fileChangesInConversation": { + "header": "{{count}} bestand(en) gewijzigd in dit gesprek" + }, "retry": { "title": "Opnieuw proberen", "tooltip": "Probeer de bewerking opnieuw" diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 8d27348d498..702c84a41f3 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Odepnij", "pin": "Przypnij", + "fileChangesInConversation": { + "header": "{{count}} plik(ów) zmienionych w tej rozmowie" + }, "tokenProgress": { "availableSpace": "Dostępne miejsce: {{amount}} tokenów", "tokensUsed": "Wykorzystane tokeny: {{used}} z {{total}}", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index d4c7a92f561..9af645b8161 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Desfixar", "pin": "Fixar", + "fileChangesInConversation": { + "header": "{{count}} arquivo(s) alterado(s) nesta conversa" + }, "tokenProgress": { "availableSpace": "Espaço disponível: {{amount}} tokens", "tokensUsed": "Tokens usados: {{used}} de {{total}}", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index e8d17600978..3aa3fc587df 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Открепить", "pin": "Закрепить", + "fileChangesInConversation": { + "header": "{{count}} файл(ов) изменено в этом разговоре" + }, "retry": { "title": "Повторить", "tooltip": "Попробовать выполнить операцию снова" diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 2d884eaa92d..622329d0cec 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Sabitlemeyi iptal et", "pin": "Sabitle", + "fileChangesInConversation": { + "header": "Bu sohbette {{count}} dosya değiştirildi" + }, "tokenProgress": { "availableSpace": "Kullanılabilir alan: {{amount}} token", "tokensUsed": "Kullanılan tokenlar: {{used}} / {{total}}", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index ac448074842..86d8d2dc283 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -33,6 +33,9 @@ }, "unpin": "Bỏ ghim khỏi đầu", "pin": "Ghim lên đầu", + "fileChangesInConversation": { + "header": "{{count}} tệp đã thay đổi trong cuộc hội thoại này" + }, "tokenProgress": { "availableSpace": "Không gian khả dụng: {{amount}} tokens", "tokensUsed": "Tokens đã sử dụng: {{used}} trong {{total}}", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 0912c0bcac6..afb48daccc4 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -33,6 +33,9 @@ }, "unpin": "取消置顶", "pin": "置顶", + "fileChangesInConversation": { + "header": "此对话中已更改 {{count}} 个文件" + }, "tokenProgress": { "availableSpace": "可用: {{amount}}", "tokensUsed": "已使用: {{used}} / {{total}}", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 6f7066648a1..12ec53d6d46 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -33,6 +33,9 @@ }, "unpin": "取消釘選", "pin": "釘選", + "fileChangesInConversation": { + "header": "此對話中已變更 {{count}} 個檔案" + }, "retry": { "title": "重試", "tooltip": "再次嘗試操作"