From 61ab73e762b0c1588d1dbd629e7107f4d5646ed6 Mon Sep 17 00:00:00 2001 From: Elliott de Launay Date: Wed, 17 Jun 2026 02:49:01 +0000 Subject: [PATCH 1/4] feat: adding i18n strings and handling no tasks to import edge case --- packages/types/src/vscode-extension-host.ts | 11 + .../__tests__/importRooTaskHistory.spec.ts | 468 ++++++++++++++++++ .../task-persistence/importRooTaskHistory.ts | 291 +++++++++++ ...iewMessageHandler.importRooHistory.spec.ts | 253 ++++++++++ src/core/webview/webviewMessageHandler.ts | 81 +++ src/i18n/locales/ca/common.json | 14 +- src/i18n/locales/de/common.json | 14 +- src/i18n/locales/en/common.json | 12 +- src/i18n/locales/es/common.json | 14 +- src/i18n/locales/fr/common.json | 14 +- src/i18n/locales/hi/common.json | 14 +- src/i18n/locales/id/common.json | 14 +- src/i18n/locales/it/common.json | 14 +- src/i18n/locales/ja/common.json | 14 +- src/i18n/locales/ko/common.json | 14 +- src/i18n/locales/nl/common.json | 14 +- src/i18n/locales/pl/common.json | 14 +- src/i18n/locales/pt-BR/common.json | 14 +- src/i18n/locales/ru/common.json | 14 +- src/i18n/locales/tr/common.json | 14 +- src/i18n/locales/vi/common.json | 14 +- src/i18n/locales/zh-CN/common.json | 14 +- src/i18n/locales/zh-TW/common.json | 14 +- webview-ui/src/components/settings/About.tsx | 158 +++++- .../settings/__tests__/About.spec.tsx | 152 +++++- webview-ui/src/i18n/locales/ca/settings.json | 17 + webview-ui/src/i18n/locales/de/settings.json | 17 + webview-ui/src/i18n/locales/en/settings.json | 17 + webview-ui/src/i18n/locales/es/settings.json | 17 + webview-ui/src/i18n/locales/fr/settings.json | 17 + webview-ui/src/i18n/locales/hi/settings.json | 17 + webview-ui/src/i18n/locales/id/settings.json | 17 + webview-ui/src/i18n/locales/it/settings.json | 17 + webview-ui/src/i18n/locales/ja/settings.json | 17 + webview-ui/src/i18n/locales/ko/settings.json | 17 + webview-ui/src/i18n/locales/nl/settings.json | 17 + webview-ui/src/i18n/locales/pl/settings.json | 17 + .../src/i18n/locales/pt-BR/settings.json | 17 + webview-ui/src/i18n/locales/ru/settings.json | 17 + webview-ui/src/i18n/locales/tr/settings.json | 17 + webview-ui/src/i18n/locales/vi/settings.json | 17 + .../src/i18n/locales/zh-CN/settings.json | 17 + .../src/i18n/locales/zh-TW/settings.json | 17 + 43 files changed, 1929 insertions(+), 41 deletions(-) create mode 100644 src/core/task-persistence/__tests__/importRooTaskHistory.spec.ts create mode 100644 src/core/task-persistence/importRooTaskHistory.ts create mode 100644 src/core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 94c9011041..c0afd5f6ed 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -100,6 +100,7 @@ export interface ExtensionMessage { | "folderSelected" | "skills" | "fileContent" + | "rooHistoryImportProgress" text?: string /** For fileContent: { path, content, error? } */ fileContent?: { path: string; content: string | null; error?: string } @@ -178,6 +179,15 @@ export interface ExtensionMessage { tools?: SerializedCustomToolDefinition[] // For customToolsResult skills?: SkillMetadata[] // For skills response modes?: { slug: string; name: string }[] // For modes response + rooHistoryImportProgress?: { + status: "starting" | "copying" | "finished" | "failed" + copiedFileCount: number + totalFileCount: number + importedTaskCount: number + totalTaskCount: number + currentTaskId?: string + currentFileName?: string + } aggregatedCosts?: { // For taskWithAggregatedCosts response totalCost: number @@ -587,6 +597,7 @@ export interface WebviewMessage { | "removeInstalledMarketplaceItem" | "marketplaceInstallResult" | "shareTaskSuccess" + | "importRooHistory" // Skills messages | "requestSkills" | "createSkill" diff --git a/src/core/task-persistence/__tests__/importRooTaskHistory.spec.ts b/src/core/task-persistence/__tests__/importRooTaskHistory.spec.ts new file mode 100644 index 0000000000..30210a1171 --- /dev/null +++ b/src/core/task-persistence/__tests__/importRooTaskHistory.spec.ts @@ -0,0 +1,468 @@ +import * as fs from "fs/promises" +import * as os from "os" +import * as path from "path" +import * as vscode from "vscode" + +import { importRooTaskHistory, resolveRooHistoryImportPaths } from "../importRooTaskHistory" + +vi.mock("vscode", () => ({ + workspace: { + getConfiguration: vi.fn(), + }, +})) + +describe("importRooTaskHistory", () => { + let tempRoot: string + + const mockStorageConfiguration = ({ + roo = "", + zoo = "", + throwOnRoo = false, + }: { + roo?: string + zoo?: string + throwOnRoo?: boolean + } = {}) => { + const getConfigurationMock = vi.mocked(vscode.workspace.getConfiguration) + + getConfigurationMock.mockImplementation((section?: string) => { + const resolvedSection = section ?? "" + return { + get: vi.fn().mockImplementation(() => { + if (resolvedSection === "roo-cline" && throwOnRoo) { + throw new Error("roo config unavailable") + } + + if (resolvedSection === "roo-cline") { + return roo + } + + if (resolvedSection === "zoo-code") { + return zoo + } + + return "" + }), + } as any + }) + } + + beforeEach(async () => { + tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "roo-history-import-")) + vi.clearAllMocks() + }) + + afterEach(async () => { + await fs.rm(tempRoot, { recursive: true, force: true }) + }) + + it("resolves Roo and Zoo storage roots from extension domains and configured custom paths", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooCustomStoragePath = path.join(tempRoot, "roo-custom") + const zooCustomStoragePath = path.join(tempRoot, "zoo-custom") + + mockStorageConfiguration({ + roo: rooCustomStoragePath, + zoo: zooCustomStoragePath, + }) + + const result = await resolveRooHistoryImportPaths(zooGlobalStoragePath) + + expect(result.rooExtensionDomain).toBe("RooVeterinaryInc.roo-cline") + expect(result.zooExtensionDomain).toBe("ZooCodeOrganization.zoo-code") + expect(result.rooStorageRoots).toEqual([ + path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline"), + rooCustomStoragePath, + ]) + expect(result.zooStorageRoot).toBe(zooCustomStoragePath) + }) + + it("falls back to the default Roo storage root when reading Roo custom storage fails", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + + mockStorageConfiguration({ throwOnRoo: true }) + + const result = await resolveRooHistoryImportPaths(zooGlobalStoragePath) + + expect(result.rooStorageRoots).toEqual([path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline")]) + expect(result.zooStorageRoot).toBe(zooGlobalStoragePath) + }) + + it("dedupes Roo storage roots when the custom path matches the default Roo storage root", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration({ roo: rooDefaultStorageRoot }) + + const result = await resolveRooHistoryImportPaths(zooGlobalStoragePath) + + expect(result.rooStorageRoots).toEqual([rooDefaultStorageRoot]) + }) + + it("copies Roo task directories into the active Zoo storage root", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const rooCustomStorageRoot = path.join(tempRoot, "roo-custom") + const zooCustomStorageRoot = path.join(tempRoot, "zoo-custom") + + mockStorageConfiguration({ + roo: rooCustomStorageRoot, + zoo: zooCustomStorageRoot, + }) + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-default"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-default", "history_item.json"), + JSON.stringify({ id: "task-default" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-default", "ui_messages.json"), "default") + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "_index.json"), "{}") + + await fs.mkdir(path.join(rooCustomStorageRoot, "tasks", "task-custom"), { recursive: true }) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-custom", "history_item.json"), + JSON.stringify({ id: "task-custom" }), + ) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-custom", "api_conversation_history.json"), + "custom", + ) + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.foundTaskCount).toBe(2) + expect(result.importedTaskCount).toBe(2) + expect(result.importedFileCount).toBe(4) + expect( + await fs.readFile(path.join(zooCustomStorageRoot, "tasks", "task-default", "ui_messages.json"), "utf8"), + ).toBe("default") + expect( + await fs.readFile( + path.join(zooCustomStorageRoot, "tasks", "task-custom", "api_conversation_history.json"), + "utf8", + ), + ).toBe("custom") + await expect(fs.access(path.join(zooCustomStorageRoot, "tasks", "_index.json"))).rejects.toMatchObject({ + code: "ENOENT", + }) + }) + + it("does not overwrite an existing Zoo task directory when the same Roo history is imported again", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-repeat"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-repeat", "history_item.json"), + JSON.stringify({ id: "task-repeat", source: "first-import" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-repeat", "ui_messages.json"), "first-ui") + + const firstImportResult = await importRooTaskHistory(zooGlobalStoragePath) + + expect(firstImportResult.importedTaskCount).toBe(1) + expect(firstImportResult.importedFileCount).toBe(2) + + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-repeat", "history_item.json"), + JSON.stringify({ id: "task-repeat", source: "second-import" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-repeat", "ui_messages.json"), "second-ui") + + const secondImportResult = await importRooTaskHistory(zooGlobalStoragePath) + + expect(secondImportResult.foundTaskCount).toBe(1) + expect(secondImportResult.importedTaskCount).toBe(0) + expect(secondImportResult.importedFileCount).toBe(0) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-repeat", "history_item.json"), "utf8"), + ).toBe(JSON.stringify({ id: "task-repeat", source: "first-import" })) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-repeat", "ui_messages.json"), "utf8"), + ).toBe("first-ui") + }) + + it("deterministically keeps the first importable Roo task when duplicate task IDs exist across roots", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const rooCustomStorageRoot = path.join(tempRoot, "roo-custom") + + mockStorageConfiguration({ roo: rooCustomStorageRoot }) + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-shared"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-shared", "history_item.json"), + JSON.stringify({ id: "task-shared", source: "default-root" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-shared", "ui_messages.json"), "default-ui") + + await fs.mkdir(path.join(rooCustomStorageRoot, "tasks", "task-shared"), { recursive: true }) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-shared", "history_item.json"), + JSON.stringify({ id: "task-shared", source: "custom-root" }), + ) + await fs.writeFile(path.join(rooCustomStorageRoot, "tasks", "task-shared", "ui_messages.json"), "custom-ui") + + await fs.mkdir(path.join(rooCustomStorageRoot, "tasks", "task-custom-only"), { recursive: true }) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-custom-only", "history_item.json"), + JSON.stringify({ id: "task-custom-only", source: "custom-root" }), + ) + await fs.writeFile( + path.join(rooCustomStorageRoot, "tasks", "task-custom-only", "ui_messages.json"), + "custom-only-ui", + ) + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(2) + expect(result.importedFileCount).toBe(4) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-shared", "history_item.json"), "utf8"), + ).toBe(JSON.stringify({ id: "task-shared", source: "default-root" })) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-shared", "ui_messages.json"), "utf8"), + ).toBe("default-ui") + expect( + await fs.readFile( + path.join(zooGlobalStoragePath, "tasks", "task-custom-only", "history_item.json"), + "utf8", + ), + ).toBe(JSON.stringify({ id: "task-custom-only", source: "custom-root" })) + }) + + it("reports Roo history import progress as files are copied", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const onProgress = vi.fn() + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-progress"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-progress", "history_item.json"), + JSON.stringify({ id: "task-progress" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-progress", "ui_messages.json"), "ui") + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-progress", "api_conversation_history.json"), + "api", + ) + + await importRooTaskHistory(zooGlobalStoragePath, onProgress) + + expect(onProgress.mock.calls).toEqual([ + [ + { + copiedFileCount: 0, + totalFileCount: 3, + importedTaskCount: 0, + totalTaskCount: 1, + currentTaskId: undefined, + currentFileName: undefined, + }, + ], + [ + { + copiedFileCount: 1, + totalFileCount: 3, + importedTaskCount: 1, + totalTaskCount: 1, + currentTaskId: "task-progress", + currentFileName: "history_item.json", + }, + ], + [ + { + copiedFileCount: 2, + totalFileCount: 3, + importedTaskCount: 1, + totalTaskCount: 1, + currentTaskId: "task-progress", + currentFileName: "ui_messages.json", + }, + ], + [ + { + copiedFileCount: 3, + totalFileCount: 3, + importedTaskCount: 1, + totalTaskCount: 1, + currentTaskId: "task-progress", + currentFileName: "api_conversation_history.json", + }, + ], + ]) + }) + + it("imports only top-level task history files and skips checkpoint directories", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const zooCustomStorageRoot = path.join(tempRoot, "shared-storage") + + mockStorageConfiguration({ + roo: zooCustomStorageRoot, + zoo: zooCustomStorageRoot, + }) + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-visible", "checkpoints", ".git", "objects"), { + recursive: true, + }) + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", ".task-hidden"), { recursive: true }) + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "_task-hidden"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-visible", "history_item.json"), + JSON.stringify({ id: "task-visible" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-visible", "ui_messages.json"), "visible-ui") + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-visible", "api_conversation_history.json"), + "visible-api", + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-visible", "task_metadata.json"), "metadata") + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "loose.json"), "loose") + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-visible", "checkpoints", ".git", "objects", "object"), + "git-object", + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", ".task-hidden", "history_item.json"), "hidden-dir") + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "_task-hidden", "history_item.json"), "hidden-dir") + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.rooStorageRoots).toEqual([rooDefaultStorageRoot]) + expect(result.importedTaskCount).toBe(1) + expect(result.importedFileCount).toBe(4) + expect( + await fs.readFile(path.join(zooCustomStorageRoot, "tasks", "task-visible", "ui_messages.json"), "utf8"), + ).toBe("visible-ui") + expect( + await fs.readFile( + path.join(zooCustomStorageRoot, "tasks", "task-visible", "api_conversation_history.json"), + "utf8", + ), + ).toBe("visible-api") + expect( + await fs.readFile(path.join(zooCustomStorageRoot, "tasks", "task-visible", "task_metadata.json"), "utf8"), + ).toBe("metadata") + await expect(fs.access(path.join(zooCustomStorageRoot, "tasks", ".task-hidden"))).rejects.toMatchObject({ + code: "ENOENT", + }) + await expect(fs.access(path.join(zooCustomStorageRoot, "tasks", "_task-hidden"))).rejects.toMatchObject({ + code: "ENOENT", + }) + await expect( + fs.access( + path.join(zooCustomStorageRoot, "tasks", "task-visible", "checkpoints", ".git", "objects", "object"), + ), + ).rejects.toMatchObject({ + code: "ENOENT", + }) + await expect(fs.access(path.join(zooCustomStorageRoot, "tasks", "loose.json"))).rejects.toMatchObject({ + code: "ENOENT", + }) + }) + + it("ignores missing Roo task roots while still importing from available roots", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const rooMissingCustomStorageRoot = path.join(tempRoot, "roo-missing") + + mockStorageConfiguration({ roo: rooMissingCustomStorageRoot }) + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-default"), { recursive: true }) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-default", "history_item.json"), "default") + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.rooStorageRoots).toEqual([rooDefaultStorageRoot, rooMissingCustomStorageRoot]) + expect(result.importedTaskCount).toBe(1) + expect(result.importedFileCount).toBe(1) + expect( + await fs.readFile(path.join(zooGlobalStoragePath, "tasks", "task-default", "history_item.json"), "utf8"), + ).toBe("default") + }) + + it("skips tasks that do not have an importable history_item.json", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-missing-history"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-missing-history", "ui_messages.json"), + "ui only", + ) + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(0) + expect(result.importedFileCount).toBe(0) + await expect(fs.access(path.join(zooGlobalStoragePath, "tasks", "task-missing-history"))).rejects.toMatchObject( + { + code: "ENOENT", + }, + ) + }) + + it("does not delete an existing Zoo task when the Roo task is missing history_item.json", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const existingZooTaskDirectory = path.join(zooGlobalStoragePath, "tasks", "task-existing") + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-existing"), { recursive: true }) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-existing", "ui_messages.json"), "ui only") + await fs.mkdir(existingZooTaskDirectory, { recursive: true }) + await fs.writeFile(path.join(existingZooTaskDirectory, "history_item.json"), "existing") + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(0) + expect(result.importedFileCount).toBe(0) + expect(await fs.readFile(path.join(existingZooTaskDirectory, "history_item.json"), "utf8")).toBe("existing") + }) + + it("does not overwrite an existing Zoo task when the Roo task is otherwise importable", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + const existingZooTaskDirectory = path.join(zooGlobalStoragePath, "tasks", "task-existing") + + mockStorageConfiguration() + + await fs.mkdir(path.join(rooDefaultStorageRoot, "tasks", "task-existing"), { recursive: true }) + await fs.writeFile( + path.join(rooDefaultStorageRoot, "tasks", "task-existing", "history_item.json"), + JSON.stringify({ id: "task-existing", source: "roo" }), + ) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks", "task-existing", "ui_messages.json"), "roo-ui") + await fs.mkdir(existingZooTaskDirectory, { recursive: true }) + await fs.writeFile(path.join(existingZooTaskDirectory, "history_item.json"), "existing") + await fs.writeFile(path.join(existingZooTaskDirectory, "ui_messages.json"), "existing-ui") + + const result = await importRooTaskHistory(zooGlobalStoragePath) + + expect(result.importedTaskCount).toBe(0) + expect(result.importedFileCount).toBe(0) + expect(await fs.readFile(path.join(existingZooTaskDirectory, "history_item.json"), "utf8")).toBe("existing") + expect(await fs.readFile(path.join(existingZooTaskDirectory, "ui_messages.json"), "utf8")).toBe("existing-ui") + }) + + it("rethrows unexpected task-root errors while importing Roo history", async () => { + const zooGlobalStoragePath = path.join(tempRoot, "globalStorage", "zoocodeorganization.zoo-code") + const rooDefaultStorageRoot = path.join(tempRoot, "globalStorage", "rooveterinaryinc.roo-cline") + + mockStorageConfiguration() + + await fs.mkdir(rooDefaultStorageRoot, { recursive: true }) + await fs.writeFile(path.join(rooDefaultStorageRoot, "tasks"), "not a directory") + + await expect(importRooTaskHistory(zooGlobalStoragePath)).rejects.toMatchObject({ + code: "ENOTDIR", + }) + }) +}) diff --git a/src/core/task-persistence/importRooTaskHistory.ts b/src/core/task-persistence/importRooTaskHistory.ts new file mode 100644 index 0000000000..434c401237 --- /dev/null +++ b/src/core/task-persistence/importRooTaskHistory.ts @@ -0,0 +1,291 @@ +import type { Dirent } from "fs" +import * as fs from "fs/promises" +import * as path from "path" +import * as vscode from "vscode" + +import { GlobalFileNames } from "../../shared/globalFileNames" +import { Package } from "../../shared/package" +import { getStorageBasePath } from "../../utils/storage" + +const ROO_EXTENSION_DOMAIN = "RooVeterinaryInc.roo-cline" +const ROO_STORAGE_DIRECTORY = ROO_EXTENSION_DOMAIN.toLowerCase() +const ROO_CONFIGURATION_SECTION = "roo-cline" +const IMPORTABLE_TASK_FILE_NAMES = [ + GlobalFileNames.historyItem, + GlobalFileNames.uiMessages, + GlobalFileNames.apiConversationHistory, + GlobalFileNames.taskMetadata, +] + +export interface RooHistoryImportPaths { + rooExtensionDomain: string + zooExtensionDomain: string + rooStorageRoots: string[] + zooStorageRoot: string +} + +export interface RooHistoryImportResult extends RooHistoryImportPaths { + foundTaskCount: number + importedTaskCount: number + importedFileCount: number +} + +export interface RooHistoryImportProgress { + copiedFileCount: number + totalFileCount: number + importedTaskCount: number + totalTaskCount: number + currentTaskId?: string + currentFileName?: string +} + +interface ImportableTaskPlan { + taskId: string + sourceTaskDirectory: string + fileNames: string[] +} + +const toComparablePath = (candidatePath: string) => { + const resolvedPath = path.resolve(candidatePath) + return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath +} + +const dedupePaths = (paths: string[]) => { + const seen = new Set() + return paths.filter((candidatePath) => { + const comparablePath = toComparablePath(candidatePath) + if (seen.has(comparablePath)) { + return false + } + seen.add(comparablePath) + return true + }) +} + +const getConfiguredCustomStoragePath = (configurationSection: string) => { + try { + const configuredPath = vscode.workspace + .getConfiguration(configurationSection) + .get("customStoragePath", "") + .trim() + return configuredPath || undefined + } catch { + return undefined + } +} + +const isSkippableImportError = (error: unknown) => { + const nodeError = error as NodeJS.ErrnoException + return nodeError.code === "ENOENT" || nodeError.code === "EACCES" || nodeError.code === "EPERM" +} + +const copyTaskFileIfPresent = async ( + sourceTaskDirectory: string, + destinationTaskDirectory: string, + fileName: string, +) => { + try { + await fs.mkdir(destinationTaskDirectory, { recursive: true }) + await fs.copyFile(path.join(sourceTaskDirectory, fileName), path.join(destinationTaskDirectory, fileName)) + return true + } catch (error) { + if (isSkippableImportError(error)) { + return false + } + + throw error + } +} + +const pathExists = async (candidatePath: string) => { + try { + await fs.access(candidatePath) + return true + } catch { + return false + } +} + +const getImportableTaskFileNames = async (sourceTaskDirectory: string) => { + const fileNames: string[] = [] + + for (const fileName of IMPORTABLE_TASK_FILE_NAMES) { + try { + await fs.access(path.join(sourceTaskDirectory, fileName)) + fileNames.push(fileName) + } catch (error) { + if (isSkippableImportError(error)) { + continue + } + + throw error + } + } + + return fileNames +} + +const collectImportableTaskPlans = async (sourceRoots: string[]) => { + const taskPlans: ImportableTaskPlan[] = [] + const taskIds = new Set() + + for (const sourceRoot of sourceRoots) { + const sourceTasksRoot = path.join(sourceRoot, "tasks") + let entries: Dirent[] + + try { + entries = await fs.readdir(sourceTasksRoot, { withFileTypes: true }) + } catch (error) { + const nodeError = error as NodeJS.ErrnoException + if (nodeError.code === "ENOENT") { + continue + } + throw error + } + + for (const entry of entries) { + if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) { + continue + } + + // Preserve source-root priority: the first importable occurrence of a task ID wins. + if (taskIds.has(entry.name)) { + continue + } + + const sourceTaskDirectory = path.join(sourceTasksRoot, entry.name) + const fileNames = await getImportableTaskFileNames(sourceTaskDirectory) + + if (!fileNames.includes(GlobalFileNames.historyItem)) { + continue + } + + taskPlans.push({ + taskId: entry.name, + sourceTaskDirectory, + fileNames, + }) + taskIds.add(entry.name) + } + } + + return { + taskPlans, + totalTaskCount: taskPlans.length, + } +} + +export const resolveRooHistoryImportPaths = async (globalStoragePath: string): Promise => { + const zooExtensionDomain = `${Package.publisher}.${Package.name}` + const zooStorageRoot = await getStorageBasePath(globalStoragePath) + const rooDefaultStorageRoot = path.join(path.dirname(globalStoragePath), ROO_STORAGE_DIRECTORY) + const rooCustomStorageRoot = getConfiguredCustomStoragePath(ROO_CONFIGURATION_SECTION) + + return { + rooExtensionDomain: ROO_EXTENSION_DOMAIN, + zooExtensionDomain, + rooStorageRoots: dedupePaths([rooDefaultStorageRoot, ...(rooCustomStorageRoot ? [rooCustomStorageRoot] : [])]), + zooStorageRoot, + } +} + +export const importRooTaskHistory = async ( + globalStoragePath: string, + onProgress?: (progress: RooHistoryImportProgress) => Promise | void, +): Promise => { + const paths = await resolveRooHistoryImportPaths(globalStoragePath) + const destinationComparablePath = toComparablePath(paths.zooStorageRoot) + const sourceRoots = paths.rooStorageRoots.filter( + (sourceRoot) => toComparablePath(sourceRoot) !== destinationComparablePath, + ) + const destinationTasksRoot = path.join(paths.zooStorageRoot, "tasks") + const { taskPlans, totalTaskCount: foundTaskCount } = await collectImportableTaskPlans(sourceRoots) + const importedTaskIds = new Set() + let importedFileCount = 0 + let copiedFileCount = 0 + const importableTaskPlans: ImportableTaskPlan[] = [] + + await fs.mkdir(destinationTasksRoot, { recursive: true }) + + for (const taskPlan of taskPlans) { + const destinationTaskDirectory = path.join(destinationTasksRoot, taskPlan.taskId) + if (await pathExists(destinationTaskDirectory)) { + continue + } + + importableTaskPlans.push(taskPlan) + } + + const totalTaskCount = importableTaskPlans.length + let totalFileCount = importableTaskPlans.reduce((count, taskPlan) => count + taskPlan.fileNames.length, 0) + + const reportProgress = async (currentTaskId?: string, currentFileName?: string) => { + if (!onProgress) { + return + } + + await onProgress({ + copiedFileCount, + totalFileCount, + importedTaskCount: importedTaskIds.size, + totalTaskCount, + currentTaskId, + currentFileName, + }) + } + + await reportProgress() + + for (const taskPlan of importableTaskPlans) { + const destinationTaskDirectory = path.join(destinationTasksRoot, taskPlan.taskId) + const destinationTaskDirectoryExisted = await pathExists(destinationTaskDirectory) + + if (destinationTaskDirectoryExisted) { + totalFileCount -= taskPlan.fileNames.length + continue + } + + const historyItemCopied = await copyTaskFileIfPresent( + taskPlan.sourceTaskDirectory, + destinationTaskDirectory, + GlobalFileNames.historyItem, + ) + + if (!historyItemCopied) { + totalFileCount -= taskPlan.fileNames.length + if (!destinationTaskDirectoryExisted) { + await fs.rm(destinationTaskDirectory, { recursive: true, force: true }) + } + await reportProgress(taskPlan.taskId, GlobalFileNames.historyItem) + continue + } + + importedTaskIds.add(taskPlan.taskId) + importedFileCount += 1 + copiedFileCount += 1 + await reportProgress(taskPlan.taskId, GlobalFileNames.historyItem) + + for (const fileName of taskPlan.fileNames) { + if (fileName === GlobalFileNames.historyItem) { + continue + } + + if (await copyTaskFileIfPresent(taskPlan.sourceTaskDirectory, destinationTaskDirectory, fileName)) { + importedFileCount += 1 + copiedFileCount += 1 + } else { + totalFileCount -= 1 + } + + await reportProgress(taskPlan.taskId, fileName) + } + } + + return { + ...paths, + rooStorageRoots: sourceRoots, + foundTaskCount, + importedTaskCount: importedTaskIds.size, + importedFileCount, + } +} diff --git a/src/core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts new file mode 100644 index 0000000000..2c68d3469c --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.importRooHistory.spec.ts @@ -0,0 +1,253 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string) => key), + changeLanguage: vi.fn(), +})) + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +vi.mock("vscode", () => ({ + window: { + showErrorMessage: vi.fn(), + showWarningMessage: vi.fn(), + showInformationMessage: vi.fn(), + }, + workspace: { + workspaceFolders: undefined, + getConfiguration: vi.fn(() => ({ + get: vi.fn(), + update: vi.fn(), + })), + }, + env: { + clipboard: { writeText: vi.fn() }, + openExternal: vi.fn(), + }, + commands: { + executeCommand: vi.fn(), + }, + Uri: { + parse: vi.fn((s: string) => ({ toString: () => s })), + file: vi.fn((p: string) => ({ fsPath: p })), + }, + ConfigurationTarget: { + Global: 1, + Workspace: 2, + WorkspaceFolder: 3, + }, +})) + +const importRooTaskHistoryMock = vi.fn() +vi.mock("../../task-persistence/importRooTaskHistory", () => ({ + importRooTaskHistory: (...args: any[]) => importRooTaskHistoryMock(...args), +})) + +import * as vscode from "vscode" + +describe("webviewMessageHandler - importRooHistory", () => { + let mockProvider: ClineProvider & { + contextProxy: any + taskHistoryStore: { + invalidateAll: ReturnType + reconcile: ReturnType + flushIndex: ReturnType + } + postMessageToWebview: ReturnType + postStateToWebview: ReturnType + log: ReturnType + } + + beforeEach(() => { + vi.clearAllMocks() + + mockProvider = { + contextProxy: { + getValue: vi.fn(), + setValue: vi.fn(), + globalStorageUri: { fsPath: "/mock/storage" }, + }, + taskHistoryStore: { + invalidateAll: vi.fn(), + reconcile: vi.fn().mockResolvedValue(undefined), + flushIndex: vi.fn().mockResolvedValue(undefined), + }, + postMessageToWebview: vi.fn().mockResolvedValue(undefined), + postStateToWebview: vi.fn().mockResolvedValue(undefined), + log: vi.fn(), + } as any + }) + + it("refreshes task history, streams progress, and shows a success message after importing Roo history", async () => { + importRooTaskHistoryMock.mockImplementation(async (_globalStoragePath, onProgress) => { + await onProgress?.({ + copiedFileCount: 2, + totalFileCount: 4, + importedTaskCount: 1, + totalTaskCount: 2, + currentTaskId: "task-1", + currentFileName: "ui_messages.json", + }) + + return { + rooExtensionDomain: "RooVeterinaryInc.roo-cline", + zooExtensionDomain: "ZooCodeOrganization.zoo-code", + rooStorageRoots: ["/mock/roo-storage"], + zooStorageRoot: "/mock/storage", + foundTaskCount: 2, + importedTaskCount: 2, + importedFileCount: 4, + } + }) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + expect(importRooTaskHistoryMock).toHaveBeenCalledWith("/mock/storage", expect.any(Function)) + expect(mockProvider.taskHistoryStore.invalidateAll).toHaveBeenCalledTimes(1) + expect(mockProvider.taskHistoryStore.reconcile).toHaveBeenCalledTimes(1) + expect(mockProvider.taskHistoryStore.flushIndex).toHaveBeenCalledTimes(1) + expect(mockProvider.postStateToWebview).toHaveBeenCalledTimes(1) + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(1, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "starting", + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + }, + }) + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "copying", + copiedFileCount: 2, + totalFileCount: 4, + importedTaskCount: 1, + totalTaskCount: 2, + currentTaskId: "task-1", + currentFileName: "ui_messages.json", + }, + }) + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(3, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + copiedFileCount: 4, + totalFileCount: 4, + importedTaskCount: 2, + totalTaskCount: 2, + currentTaskId: "task-1", + currentFileName: "ui_messages.json", + }, + }) + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("common:info.rooHistoryImport.success") + expect(vscode.window.showWarningMessage).not.toHaveBeenCalled() + }) + + it("uses the singular success message when one Roo task history is imported", async () => { + importRooTaskHistoryMock.mockResolvedValue({ + rooExtensionDomain: "RooVeterinaryInc.roo-cline", + zooExtensionDomain: "ZooCodeOrganization.zoo-code", + rooStorageRoots: ["/mock/roo-storage"], + zooStorageRoot: "/mock/storage", + foundTaskCount: 1, + importedTaskCount: 1, + importedFileCount: 2, + }) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("common:info.rooHistoryImport.success") + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + copiedFileCount: 2, + totalFileCount: 2, + importedTaskCount: 1, + totalTaskCount: 1, + }, + }) + expect(vscode.window.showWarningMessage).not.toHaveBeenCalled() + }) + + it("shows a 'not found' warning when no Roo history exists at all", async () => { + importRooTaskHistoryMock.mockResolvedValue({ + rooExtensionDomain: "RooVeterinaryInc.roo-cline", + zooExtensionDomain: "ZooCodeOrganization.zoo-code", + rooStorageRoots: ["/mock/roo-storage"], + zooStorageRoot: "/mock/storage", + foundTaskCount: 0, + importedTaskCount: 0, + importedFileCount: 0, + }) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + expect(importRooTaskHistoryMock).toHaveBeenCalledWith("/mock/storage", expect.any(Function)) + expect(mockProvider.taskHistoryStore.invalidateAll).not.toHaveBeenCalled() + expect(mockProvider.taskHistoryStore.reconcile).not.toHaveBeenCalled() + expect(mockProvider.taskHistoryStore.flushIndex).not.toHaveBeenCalled() + expect(mockProvider.postStateToWebview).not.toHaveBeenCalled() + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + }, + }) + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("common:warnings.rooHistoryImport.nothingFound") + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled() + }) + + it("shows an 'already imported' warning when all Roo tasks are already in Zoo", async () => { + importRooTaskHistoryMock.mockResolvedValue({ + rooExtensionDomain: "RooVeterinaryInc.roo-cline", + zooExtensionDomain: "ZooCodeOrganization.zoo-code", + rooStorageRoots: ["/mock/roo-storage"], + zooStorageRoot: "/mock/storage", + foundTaskCount: 3, + importedTaskCount: 0, + importedFileCount: 0, + }) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + expect(mockProvider.taskHistoryStore.invalidateAll).not.toHaveBeenCalled() + expect(mockProvider.postStateToWebview).not.toHaveBeenCalled() + expect(vscode.window.showWarningMessage).toHaveBeenCalledWith( + "common:warnings.rooHistoryImport.alreadyImported", + ) + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled() + }) + + it("shows an error without refreshing task history when the import throws", async () => { + importRooTaskHistoryMock.mockRejectedValue(new Error("permission denied")) + + await webviewMessageHandler(mockProvider as any, { type: "importRooHistory" } as any) + + expect(mockProvider.taskHistoryStore.invalidateAll).not.toHaveBeenCalled() + expect(mockProvider.taskHistoryStore.reconcile).not.toHaveBeenCalled() + expect(mockProvider.taskHistoryStore.flushIndex).not.toHaveBeenCalled() + expect(mockProvider.postStateToWebview).not.toHaveBeenCalled() + expect(mockProvider.log).toHaveBeenCalledWith("[importRooHistory] failed: permission denied") + expect(mockProvider.postMessageToWebview).toHaveBeenNthCalledWith(2, { + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "failed", + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + }, + }) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.rooHistoryImport") + expect(vscode.window.showInformationMessage).not.toHaveBeenCalled() + expect(vscode.window.showWarningMessage).not.toHaveBeenCalled() + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 40a8bfed8b..c693bcae0b 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -28,6 +28,7 @@ import { TelemetryService } from "@roo-code/telemetry" import { type ApiMessage } from "../task-persistence/apiMessages" import { saveTaskMessages } from "../task-persistence" +import { importRooTaskHistory } from "../task-persistence/importRooTaskHistory" import { ClineProvider } from "./ClineProvider" import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler" @@ -903,6 +904,86 @@ export const webviewMessageHandler = async ( break } + case "importRooHistory": { + let latestProgress = { + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + } + + try { + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "starting", + ...latestProgress, + }, + }) + + const result = await importRooTaskHistory( + provider.contextProxy.globalStorageUri.fsPath, + async (progress) => { + latestProgress = progress + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "copying", + ...progress, + }, + }) + }, + ) + + if (result.importedTaskCount === 0) { + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + ...latestProgress, + }, + }) + const warningMessage = + result.foundTaskCount === 0 + ? t("common:warnings.rooHistoryImport.nothingFound", { domain: result.rooExtensionDomain }) + : t("common:warnings.rooHistoryImport.alreadyImported", { count: result.foundTaskCount }) + vscode.window.showWarningMessage(warningMessage) + break + } + + provider.taskHistoryStore.invalidateAll() + await provider.taskHistoryStore.reconcile() + await provider.taskHistoryStore.flushIndex() + await provider.postStateToWebview() + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "finished", + ...latestProgress, + copiedFileCount: result.importedFileCount, + totalFileCount: latestProgress.totalFileCount || result.importedFileCount, + importedTaskCount: result.importedTaskCount, + totalTaskCount: latestProgress.totalTaskCount || result.importedTaskCount, + }, + }) + + vscode.window.showInformationMessage( + t("common:info.rooHistoryImport.success", { count: result.importedTaskCount }), + ) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + provider.log(`[importRooHistory] failed: ${message}`) + await provider.postMessageToWebview({ + type: "rooHistoryImportProgress", + rooHistoryImportProgress: { + status: "failed", + ...latestProgress, + }, + }) + vscode.window.showErrorMessage(t("common:errors.rooHistoryImport", { error: message })) + } + break + } case "exportSettings": await exportSettings({ providerSettingsManager: provider.providerSettingsManager, diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 0a1c99f1f8..f988fa0c37 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -145,12 +145,18 @@ "manual_url_missing_params": "URL de callback no vàlida: falten paràmetres requerits (code i state)", "manual_url_auth_failed": "Autenticació manual per URL ha fallat", "manual_url_auth_error": "Autenticació fallida", - "mode_import_failed": "Ha fallat la importació del mode: {{error}}" + "mode_import_failed": "Ha fallat la importació del mode: {{error}}", + "rooHistoryImport": "No s'ha pogut importar l'historial de Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "No s'ha seleccionat contingut de terminal", "missing_task_files": "Els fitxers d'aquesta tasca falten. Vols eliminar-la de la llista de tasques?", - "auto_import_failed": "Ha fallat la importació automàtica de la configuració de ZooCode: {{error}}" + "auto_import_failed": "Ha fallat la importació automàtica de la configuració de ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "No s'ha trobat historial de tasques de Roo Code per importar de {{domain}}.", + "alreadyImported_one": "Tot l'historial de tasca de Roo Code ({{count}}) ja s'ha importat a Zoo Code.", + "alreadyImported_other": "Tots els {{count}} historials de tasques de Roo Code ja s'han importat a Zoo Code." + } }, "info": { "no_changes": "No s'han trobat canvis.", @@ -169,6 +175,10 @@ "mode_imported": "Mode importat correctament", "roo": { "signInUnavailable": "L'inici de sessió de Roo Code Cloud no està disponible en aquest moment. Configura un altre proveïdor per continuar." + }, + "rooHistoryImport": { + "success_one": "S'ha importat {{count}} historial de tasca de Roo Code a Zoo Code.", + "success_other": "S'han importat {{count}} historials de tasques de Roo Code a Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 02f27925a5..03dc94ca22 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -141,12 +141,18 @@ "manual_url_no_query": "Ungültige Callback-URL: Query-Parameter fehlen", "manual_url_missing_params": "Ungültige Callback-URL: erforderliche Parameter (code und state) fehlen", "manual_url_auth_failed": "Manuelle URL-Authentifizierung fehlgeschlagen", - "manual_url_auth_error": "Authentifizierung fehlgeschlagen" + "manual_url_auth_error": "Authentifizierung fehlgeschlagen", + "rooHistoryImport": "Fehler beim Importieren des Roo Code-Verlaufs: {{error}}" }, "warnings": { "no_terminal_content": "Kein Terminal-Inhalt ausgewählt", "missing_task_files": "Die Dateien dieser Aufgabe fehlen. Möchtest du sie aus der Aufgabenliste entfernen?", - "auto_import_failed": "Fehler beim automatischen Import der ZooCode-Einstellungen: {{error}}" + "auto_import_failed": "Fehler beim automatischen Import der ZooCode-Einstellungen: {{error}}", + "rooHistoryImport": { + "nothingFound": "Es wurde kein Roo Code-Aufgabenverlauf zum Importieren von {{domain}} gefunden.", + "alreadyImported_one": "Der {{count}} Roo Code-Aufgabenverlauf wurde bereits in Zoo Code importiert.", + "alreadyImported_other": "Alle {{count}} Roo Code-Aufgabenverläufe wurden bereits in Zoo Code importiert." + } }, "info": { "no_changes": "Keine Änderungen gefunden.", @@ -165,6 +171,10 @@ "mode_imported": "Modus erfolgreich importiert", "roo": { "signInUnavailable": "Die Anmeldung bei Roo Code Cloud ist derzeit nicht verfügbar. Konfiguriere einen anderen Anbieter, um fortzufahren." + }, + "rooHistoryImport": { + "success_one": "{{count}} Roo Code-Aufgabenverlauf in Zoo Code importiert.", + "success_other": "{{count}} Roo Code-Aufgabenverläufe in Zoo Code importiert." } }, "answers": { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 5abed998a5..190af9180a 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -78,6 +78,7 @@ "share_not_enabled": "Task sharing is not enabled for this organization.", "share_task_not_found": "Task not found or access denied.", "mode_import_failed": "Failed to import mode: {{error}}", + "rooHistoryImport": "Failed to import Roo Code history: {{error}}", "delete_rules_folder_failed": "Failed to delete rules folder: {{rulesFolderPath}}. Error: {{error}}", "command_not_found": "Command '{{name}}' not found", "open_command_file": "Failed to open command file", @@ -146,7 +147,12 @@ "warnings": { "no_terminal_content": "No terminal content selected", "missing_task_files": "This task's files are missing. Would you like to remove it from the task list?", - "auto_import_failed": "Failed to auto-import ZooCode settings: {{error}}" + "auto_import_failed": "Failed to auto-import ZooCode settings: {{error}}", + "rooHistoryImport": { + "nothingFound": "No Roo Code task history was found to import from {{domain}}.", + "alreadyImported_one": "All {{count}} Roo Code task history has already been imported into Zoo Code.", + "alreadyImported_other": "All {{count}} Roo Code task histories have already been imported into Zoo Code." + } }, "info": { "no_changes": "No changes found.", @@ -163,6 +169,10 @@ "image_saved": "Image saved to {{path}}", "mode_exported": "Mode '{{mode}}' exported successfully", "mode_imported": "Mode imported successfully", + "rooHistoryImport": { + "success_one": "Imported {{count}} Roo Code task history into Zoo Code.", + "success_other": "Imported {{count}} Roo Code task histories into Zoo Code." + }, "roo": { "signInUnavailable": "Roo Code Cloud sign-in is currently unavailable. Configure another provider to continue." } diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 3c5d6ea754..4fcbd82a85 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Error al importar el historial de Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "No hay contenido de terminal seleccionado", "missing_task_files": "Los archivos de esta tarea faltan. ¿Deseas eliminarla de la lista de tareas?", - "auto_import_failed": "Error al importar automáticamente la configuración de ZooCode: {{error}}" + "auto_import_failed": "Error al importar automáticamente la configuración de ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "No se encontró historial de tareas de Roo Code para importar desde {{domain}}.", + "alreadyImported_one": "El historial de {{count}} tarea de Roo Code ya ha sido importado en Zoo Code.", + "alreadyImported_other": "Los {{count}} historiales de tareas de Roo Code ya han sido importados en Zoo Code." + } }, "info": { "no_changes": "No se encontraron cambios.", @@ -165,6 +171,10 @@ "mode_imported": "Modo importado correctamente", "roo": { "signInUnavailable": "El inicio de sesión de Roo Code Cloud no está disponible en este momento. Configura otro proveedor para continuar." + }, + "rooHistoryImport": { + "success_one": "Se importó {{count}} historial de tarea de Roo Code en Zoo Code.", + "success_other": "Se importaron {{count}} historiales de tareas de Roo Code en Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 7f9e466f04..ece55c9b11 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Échec de l'importation de l'historique Roo Code : {{error}}" }, "warnings": { "no_terminal_content": "Aucun contenu de terminal sélectionné", "missing_task_files": "Les fichiers de cette tâche sont introuvables. Souhaitez-vous la supprimer de la liste des tâches ?", - "auto_import_failed": "Échec de l'importation automatique des paramètres ZooCode : {{error}}" + "auto_import_failed": "Échec de l'importation automatique des paramètres ZooCode : {{error}}", + "rooHistoryImport": { + "nothingFound": "Aucun historique de tâches Roo Code trouvé à importer depuis {{domain}}.", + "alreadyImported_one": "L'historique de {{count}} tâche Roo Code a déjà été importé dans Zoo Code.", + "alreadyImported_other": "Les {{count}} historiques de tâches Roo Code ont déjà été importés dans Zoo Code." + } }, "info": { "no_changes": "Aucun changement trouvé.", @@ -165,6 +171,10 @@ "mode_imported": "Mode importé avec succès", "roo": { "signInUnavailable": "La connexion à Roo Code Cloud n'est pas disponible pour le moment. Configure un autre fournisseur pour continuer." + }, + "rooHistoryImport": { + "success_one": "{{count}} historique de tâche Roo Code importé dans Zoo Code.", + "success_other": "{{count}} historiques de tâches Roo Code importés dans Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 3c567dac08..42e46f0e9e 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Roo Code इतिहास आयात करने में विफल: {{error}}" }, "warnings": { "no_terminal_content": "कोई टर्मिनल सामग्री चयनित नहीं", "missing_task_files": "इस टास्क की फाइलें गायब हैं। क्या आप इसे टास्क सूची से हटाना चाहते हैं?", - "auto_import_failed": "ZooCode सेटिंग्स का स्वचालित आयात विफल: {{error}}" + "auto_import_failed": "ZooCode सेटिंग्स का स्वचालित आयात विफल: {{error}}", + "rooHistoryImport": { + "nothingFound": "{{domain}} से आयात करने के लिए कोई Roo Code कार्य इतिहास नहीं मिला।", + "alreadyImported_one": "सभी {{count}} Roo Code कार्य इतिहास पहले से ही Zoo Code में आयात किया जा चुका है।", + "alreadyImported_other": "सभी {{count}} Roo Code कार्य इतिहास पहले से ही Zoo Code में आयात किए जा चुके हैं।" + } }, "info": { "no_changes": "कोई परिवर्तन नहीं मिला।", @@ -165,6 +171,10 @@ "mode_imported": "मोड सफलतापूर्वक आयात किया गया", "roo": { "signInUnavailable": "Roo Code Cloud साइन-इन अभी उपलब्ध नहीं है। जारी रखने के लिए कोई दूसरा प्रदाता कॉन्फ़िगर करें।" + }, + "rooHistoryImport": { + "success_one": "{{count}} Roo Code कार्य इतिहास Zoo Code में आयात किया।", + "success_other": "{{count}} Roo Code कार्य इतिहास Zoo Code में आयात किए।" } }, "answers": { diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 7208ea9507..92dbf24171 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Gagal mengimpor riwayat Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Tidak ada konten terminal yang dipilih", "missing_task_files": "File tugas ini hilang. Apakah kamu ingin menghapusnya dari daftar tugas?", - "auto_import_failed": "Gagal mengimpor pengaturan ZooCode secara otomatis: {{error}}" + "auto_import_failed": "Gagal mengimpor pengaturan ZooCode secara otomatis: {{error}}", + "rooHistoryImport": { + "nothingFound": "Tidak ada riwayat tugas Roo Code yang ditemukan untuk diimpor dari {{domain}}.", + "alreadyImported_one": "Semua {{count}} riwayat tugas Roo Code sudah diimpor ke Zoo Code.", + "alreadyImported_other": "Semua {{count}} riwayat tugas Roo Code sudah diimpor ke Zoo Code." + } }, "info": { "no_changes": "Tidak ada perubahan ditemukan.", @@ -165,6 +171,10 @@ "mode_imported": "Mode berhasil diimpor", "roo": { "signInUnavailable": "Masuk ke Roo Code Cloud saat ini tidak tersedia. Konfigurasikan penyedia lain untuk melanjutkan." + }, + "rooHistoryImport": { + "success_one": "{{count}} riwayat tugas Roo Code berhasil diimpor ke Zoo Code.", + "success_other": "{{count}} riwayat tugas Roo Code berhasil diimpor ke Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index a69a96092c..fd739f7d8d 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Impossibile importare la cronologia di Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Nessun contenuto del terminale selezionato", "missing_task_files": "I file di questa attività sono mancanti. Vuoi rimuoverla dall'elenco delle attività?", - "auto_import_failed": "Importazione automatica delle impostazioni ZooCode fallita: {{error}}" + "auto_import_failed": "Importazione automatica delle impostazioni ZooCode fallita: {{error}}", + "rooHistoryImport": { + "nothingFound": "Nessuna cronologia di attività Roo Code trovata da importare da {{domain}}.", + "alreadyImported_one": "La cronologia di {{count}} attività Roo Code è già stata importata in Zoo Code.", + "alreadyImported_other": "Tutte le {{count}} cronologie di attività Roo Code sono già state importate in Zoo Code." + } }, "info": { "no_changes": "Nessuna modifica trovata.", @@ -165,6 +171,10 @@ "mode_imported": "Modalità importata con successo", "roo": { "signInUnavailable": "L'accesso a Roo Code Cloud non è attualmente disponibile. Configura un altro provider per continuare." + }, + "rooHistoryImport": { + "success_one": "Importata {{count}} cronologia di attività Roo Code in Zoo Code.", + "success_other": "Importate {{count}} cronologie di attività Roo Code in Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index bea4db707c..06c8fd4a05 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Roo Codeの履歴のインポートに失敗しました: {{error}}" }, "warnings": { "no_terminal_content": "選択されたターミナルコンテンツがありません", "missing_task_files": "このタスクのファイルが見つかりません。タスクリストから削除しますか?", - "auto_import_failed": "ZooCode設定の自動インポートに失敗しました:{{error}}" + "auto_import_failed": "ZooCode設定の自動インポートに失敗しました:{{error}}", + "rooHistoryImport": { + "nothingFound": "{{domain}}からインポートするRoo Codeのタスク履歴が見つかりませんでした。", + "alreadyImported_one": "{{count}}件のRoo Codeタスク履歴はすでにZoo Codeにインポート済みです。", + "alreadyImported_other": "{{count}}件のRoo Codeタスク履歴はすでにZoo Codeにインポート済みです。" + } }, "info": { "no_changes": "変更は見つかりませんでした。", @@ -165,6 +171,10 @@ "mode_imported": "モードが正常にインポートされました", "roo": { "signInUnavailable": "Roo Code Cloud へのサインインは現在利用できません。続行するには別のプロバイダーを設定してください。" + }, + "rooHistoryImport": { + "success_one": "{{count}}件のRoo CodeタスクをZoo Codeにインポートしました。", + "success_other": "{{count}}件のRoo CodeタスクをZoo Codeにインポートしました。" } }, "answers": { diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index daf9f69e4c..4b2944cfd9 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Roo Code 기록 가져오기 실패: {{error}}" }, "warnings": { "no_terminal_content": "선택된 터미널 내용이 없습니다", "missing_task_files": "이 작업의 파일이 누락되었습니다. 작업 목록에서 제거하시겠습니까?", - "auto_import_failed": "ZooCode 설정 자동 가져오기 실패: {{error}}" + "auto_import_failed": "ZooCode 설정 자동 가져오기 실패: {{error}}", + "rooHistoryImport": { + "nothingFound": "{{domain}}에서 가져올 Roo Code 작업 기록을 찾을 수 없습니다.", + "alreadyImported_one": "{{count}}개의 Roo Code 작업 기록이 이미 Zoo Code로 가져와졌습니다.", + "alreadyImported_other": "{{count}}개의 Roo Code 작업 기록이 이미 Zoo Code로 가져와졌습니다." + } }, "info": { "no_changes": "변경 사항이 없습니다.", @@ -165,6 +171,10 @@ "mode_imported": "모드를 성공적으로 가져왔습니다", "roo": { "signInUnavailable": "Roo Code Cloud 로그인은 현재 사용할 수 없습니다. 계속하려면 다른 제공업체를 구성하세요." + }, + "rooHistoryImport": { + "success_one": "Roo Code 작업 기록 {{count}}개를 Zoo Code로 가져왔습니다.", + "success_other": "Roo Code 작업 기록 {{count}}개를 Zoo Code로 가져왔습니다." } }, "answers": { diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 3f4c1e984d..96a921c6c1 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Importeren van Roo Code-geschiedenis mislukt: {{error}}" }, "warnings": { "no_terminal_content": "Geen terminalinhoud geselecteerd", "missing_task_files": "De bestanden van deze taak ontbreken. Wil je deze uit de takenlijst verwijderen?", - "auto_import_failed": "Automatisch importeren van ZooCode-instellingen mislukt: {{error}}" + "auto_import_failed": "Automatisch importeren van ZooCode-instellingen mislukt: {{error}}", + "rooHistoryImport": { + "nothingFound": "Geen Roo Code-taakgeschiedenis gevonden om te importeren van {{domain}}.", + "alreadyImported_one": "De {{count}} Roo Code-taakgeschiedenis is al geïmporteerd in Zoo Code.", + "alreadyImported_other": "Alle {{count}} Roo Code-taakgeschiedenissen zijn al geïmporteerd in Zoo Code." + } }, "info": { "no_changes": "Geen wijzigingen gevonden.", @@ -165,6 +171,10 @@ "mode_imported": "Modus succesvol geïmporteerd", "roo": { "signInUnavailable": "Inloggen bij Roo Code Cloud is momenteel niet beschikbaar. Configureer een andere provider om door te gaan." + }, + "rooHistoryImport": { + "success_one": "{{count}} Roo Code-taakgeschiedenis geïmporteerd in Zoo Code.", + "success_other": "{{count}} Roo Code-taakgeschiedenissen geïmporteerd in Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index c88aabcaa5..f3e789e842 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Nie udało się zaimportować historii Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Nie wybrano zawartości terminala", "missing_task_files": "Pliki tego zadania są brakujące. Czy chcesz usunąć je z listy zadań?", - "auto_import_failed": "Nie udało się automatycznie zaimportować ustawień ZooCode: {{error}}" + "auto_import_failed": "Nie udało się automatycznie zaimportować ustawień ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "Nie znaleziono historii zadań Roo Code do zaimportowania z {{domain}}.", + "alreadyImported_one": "Cała {{count}} historia zadań Roo Code została już zaimportowana do Zoo Code.", + "alreadyImported_other": "Wszystkie {{count}} historii zadań Roo Code zostały już zaimportowane do Zoo Code." + } }, "info": { "no_changes": "Nie znaleziono zmian.", @@ -165,6 +171,10 @@ "mode_imported": "Tryb pomyślnie zaimportowany", "roo": { "signInUnavailable": "Logowanie do Roo Code Cloud jest obecnie niedostępne. Skonfiguruj innego dostawcę, aby kontynuować." + }, + "rooHistoryImport": { + "success_one": "Zaimportowano {{count}} historię zadania Roo Code do Zoo Code.", + "success_other": "Zaimportowano {{count}} historii zadań Roo Code do Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index ce4c51e08d..ac5ec44f6b 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -145,12 +145,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Falha ao importar o histórico do Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Nenhum conteúdo do terminal selecionado", "missing_task_files": "Os arquivos desta tarefa estão faltando. Deseja removê-la da lista de tarefas?", - "auto_import_failed": "Falha ao importar automaticamente as configurações do ZooCode: {{error}}" + "auto_import_failed": "Falha ao importar automaticamente as configurações do ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "Nenhum histórico de tarefas do Roo Code encontrado para importar de {{domain}}.", + "alreadyImported_one": "O histórico de {{count}} tarefa do Roo Code já foi importado para o Zoo Code.", + "alreadyImported_other": "Todos os {{count}} históricos de tarefas do Roo Code já foram importados para o Zoo Code." + } }, "info": { "no_changes": "Nenhuma alteração encontrada.", @@ -169,6 +175,10 @@ "mode_imported": "Modo importado com sucesso", "roo": { "signInUnavailable": "O login do Roo Code Cloud não está disponível no momento. Configure outro provedor para continuar." + }, + "rooHistoryImport": { + "success_one": "{{count}} histórico de tarefa do Roo Code importado para o Zoo Code.", + "success_other": "{{count}} históricos de tarefas do Roo Code importados para o Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 9d096fb346..26d4b2032f 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Ошибка импорта истории Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Не выбрано содержимое терминала", "missing_task_files": "Файлы этой задачи отсутствуют. Хотите удалить её из списка задач?", - "auto_import_failed": "Не удалось автоматически импортировать настройки ZooCode: {{error}}" + "auto_import_failed": "Не удалось автоматически импортировать настройки ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "История задач Roo Code для импорта из {{domain}} не найдена.", + "alreadyImported_one": "{{count}} история задач Roo Code уже импортирована в Zoo Code.", + "alreadyImported_other": "Все {{count}} истории задач Roo Code уже импортированы в Zoo Code." + } }, "info": { "no_changes": "Изменения не найдены.", @@ -165,6 +171,10 @@ "mode_imported": "Режим успешно импортирован", "roo": { "signInUnavailable": "Вход в Roo Code Cloud сейчас недоступен. Настрой другого провайдера, чтобы продолжить." + }, + "rooHistoryImport": { + "success_one": "{{count}} история задач Roo Code импортирована в Zoo Code.", + "success_other": "{{count}} историй задач Roo Code импортировано в Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 3d83d28c32..bde000783a 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Roo Code geçmişi içe aktarılamadı: {{error}}" }, "warnings": { "no_terminal_content": "Seçili terminal içeriği yok", "missing_task_files": "Bu görevin dosyaları eksik. Görev listesinden kaldırmak istiyor musunuz?", - "auto_import_failed": "ZooCode ayarları otomatik olarak içe aktarılamadı: {{error}}" + "auto_import_failed": "ZooCode ayarları otomatik olarak içe aktarılamadı: {{error}}", + "rooHistoryImport": { + "nothingFound": "{{domain}} kaynağından içe aktarılacak Roo Code görev geçmişi bulunamadı.", + "alreadyImported_one": "{{count}} Roo Code görev geçmişinin tamamı zaten Zoo Code'a aktarılmış.", + "alreadyImported_other": "{{count}} Roo Code görev geçmişinin tamamı zaten Zoo Code'a aktarılmış." + } }, "info": { "no_changes": "Değişiklik bulunamadı.", @@ -165,6 +171,10 @@ "mode_imported": "Mod başarıyla içe aktarıldı", "roo": { "signInUnavailable": "Roo Code Cloud girişi şu anda kullanılamıyor. Devam etmek için farklı bir sağlayıcı yapılandırın." + }, + "rooHistoryImport": { + "success_one": "{{count}} Roo Code görev geçmişi Zoo Code'a aktarıldı.", + "success_other": "{{count}} Roo Code görev geçmişi Zoo Code'a aktarıldı." } }, "answers": { diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index d111ade94b..2a9433c506 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "Không thể nhập lịch sử Roo Code: {{error}}" }, "warnings": { "no_terminal_content": "Không có nội dung terminal được chọn", "missing_task_files": "Các tệp của nhiệm vụ này bị thiếu. Bạn có muốn xóa nó khỏi danh sách nhiệm vụ không?", - "auto_import_failed": "Không thể tự động nhập cài đặt ZooCode: {{error}}" + "auto_import_failed": "Không thể tự động nhập cài đặt ZooCode: {{error}}", + "rooHistoryImport": { + "nothingFound": "Không tìm thấy lịch sử nhiệm vụ Roo Code nào để nhập từ {{domain}}.", + "alreadyImported_one": "{{count}} lịch sử nhiệm vụ Roo Code đã được nhập vào Zoo Code.", + "alreadyImported_other": "Tất cả {{count}} lịch sử nhiệm vụ Roo Code đã được nhập vào Zoo Code." + } }, "info": { "no_changes": "Không tìm thấy thay đổi nào.", @@ -165,6 +171,10 @@ "mode_imported": "Chế độ đã được nhập thành công", "roo": { "signInUnavailable": "Đăng nhập Roo Code Cloud hiện không khả dụng. Hãy cấu hình nhà cung cấp khác để tiếp tục." + }, + "rooHistoryImport": { + "success_one": "Đã nhập {{count}} lịch sử nhiệm vụ Roo Code vào Zoo Code.", + "success_other": "Đã nhập {{count}} lịch sử nhiệm vụ Roo Code vào Zoo Code." } }, "answers": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 1ff8d330cf..a1d0b06e89 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -146,12 +146,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "导入 Roo Code 历史记录失败:{{error}}" }, "warnings": { "no_terminal_content": "没有选择终端内容", "missing_task_files": "此任务的文件丢失。您想从任务列表中删除它吗?", - "auto_import_failed": "自动导入 ZooCode 设置失败:{{error}}" + "auto_import_failed": "自动导入 ZooCode 设置失败:{{error}}", + "rooHistoryImport": { + "nothingFound": "未找到可从 {{domain}} 导入的 Roo Code 任务历史记录。", + "alreadyImported_one": "全部 {{count}} 条 Roo Code 任务历史记录已导入 Zoo Code。", + "alreadyImported_other": "全部 {{count}} 条 Roo Code 任务历史记录已导入 Zoo Code。" + } }, "info": { "no_changes": "未找到更改。", @@ -170,6 +176,10 @@ "mode_imported": "模式已成功导入", "roo": { "signInUnavailable": "Roo Code Cloud 登录当前不可用。请配置其他提供商以继续。" + }, + "rooHistoryImport": { + "success_one": "已将 {{count}} 条 Roo Code 任务历史记录导入 Zoo Code。", + "success_other": "已将 {{count}} 条 Roo Code 任务历史记录导入 Zoo Code。" } }, "answers": { diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 74e661e280..92435e42ff 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -141,12 +141,18 @@ "streamProcessingError": "Error processing response stream: {{message}}", "unexpectedStreamError": "Unexpected error processing response stream", "completionError": "OpenAI Codex completion error: {{message}}" - } + }, + "rooHistoryImport": "匯入 Roo Code 歷史記錄失敗:{{error}}" }, "warnings": { "no_terminal_content": "沒有選擇終端機內容", "missing_task_files": "此工作的檔案遺失。您想從工作列表中刪除它嗎?", - "auto_import_failed": "自動匯入 ZooCode 設定失敗:{{error}}" + "auto_import_failed": "自動匯入 ZooCode 設定失敗:{{error}}", + "rooHistoryImport": { + "nothingFound": "未找到可從 {{domain}} 匯入的 Roo Code 工作歷史記錄。", + "alreadyImported_one": "全部 {{count}} 筆 Roo Code 工作歷史記錄已匯入 Zoo Code。", + "alreadyImported_other": "全部 {{count}} 筆 Roo Code 工作歷史記錄已匯入 Zoo Code。" + } }, "info": { "no_changes": "沒有找到更改。", @@ -165,6 +171,10 @@ "mode_imported": "模式已成功匯入", "roo": { "signInUnavailable": "Roo Code Cloud 登入目前無法使用。請設定其他提供者以繼續。" + }, + "rooHistoryImport": { + "success_one": "已將 {{count}} 筆 Roo Code 工作歷史記錄匯入 Zoo Code。", + "success_other": "已將 {{count}} 筆 Roo Code 工作歷史記錄匯入 Zoo Code。" } }, "answers": { diff --git a/webview-ui/src/components/settings/About.tsx b/webview-ui/src/components/settings/About.tsx index c33063e047..efc95e07be 100644 --- a/webview-ui/src/components/settings/About.tsx +++ b/webview-ui/src/components/settings/About.tsx @@ -1,10 +1,10 @@ -import { HTMLAttributes } from "react" +import { HTMLAttributes, useEffect, useState } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { Trans } from "react-i18next" -import { Download, Upload, TriangleAlert, Bug, Lightbulb, Shield, MessagesSquare } from "lucide-react" -import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { ArrowRightLeft, Download, Upload, TriangleAlert, Bug, Lightbulb, Shield, MessagesSquare } from "lucide-react" +import { VSCodeButton, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import type { TelemetrySetting } from "@roo-code/types" +import type { ExtensionMessage, TelemetrySetting } from "@roo-code/types" import { Package } from "@roo/package" @@ -17,6 +17,8 @@ import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" +type RooHistoryImportProgress = NonNullable + type AboutProps = HTMLAttributes & { telemetrySetting: TelemetrySetting setTelemetrySetting: (setting: TelemetrySetting) => void @@ -26,6 +28,74 @@ type AboutProps = HTMLAttributes & { export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug, className, ...props }: AboutProps) => { const { t } = useAppTranslation() + const [rooHistoryImportProgress, setRooHistoryImportProgress] = useState(null) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type !== "rooHistoryImportProgress" || !message.rooHistoryImportProgress) { + return + } + + const progress = message.rooHistoryImportProgress + if (progress.status === "finished" && progress.totalFileCount === 0) { + setRooHistoryImportProgress(null) + return + } + + setRooHistoryImportProgress(progress) + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, []) + + const isImporting = + rooHistoryImportProgress?.status === "starting" || rooHistoryImportProgress?.status === "copying" + const isImportFailed = rooHistoryImportProgress?.status === "failed" + const isImportSuccessful = + rooHistoryImportProgress?.status === "finished" && rooHistoryImportProgress.totalFileCount > 0 + const shouldShowImportProgress = !!rooHistoryImportProgress && (isImporting || isImportFailed || isImportSuccessful) + const importProgressPercent = + rooHistoryImportProgress && rooHistoryImportProgress.totalFileCount > 0 + ? Math.round((rooHistoryImportProgress.copiedFileCount / rooHistoryImportProgress.totalFileCount) * 100) + : 0 + const importProgressSummary = !rooHistoryImportProgress + ? "" + : isImportFailed + ? rooHistoryImportProgress.totalFileCount > 0 + ? t("settings:about.rooHistoryImport.summaryFailedWithFiles", { + copied: rooHistoryImportProgress.copiedFileCount, + total: rooHistoryImportProgress.totalFileCount, + }) + : t("settings:about.rooHistoryImport.summaryFailedNoFiles") + : t("settings:about.rooHistoryImport.summaryCopied", { + copied: rooHistoryImportProgress.copiedFileCount, + total: rooHistoryImportProgress.totalFileCount, + }) + const importTaskCount = Math.max( + rooHistoryImportProgress?.totalTaskCount ?? 0, + rooHistoryImportProgress?.importedTaskCount ?? 0, + ) + const importProgressDetail = isImportFailed + ? t("settings:about.rooHistoryImport.detailFailed") + : rooHistoryImportProgress && rooHistoryImportProgress.importedTaskCount > 0 + ? t("settings:about.rooHistoryImport.detailTasksImported", { + count: importTaskCount, + total: importTaskCount, + }) + : t("settings:about.rooHistoryImport.detailPreparing") + + const handleImportRooHistory = () => { + setRooHistoryImportProgress({ + status: "starting", + copiedFileCount: 0, + totalFileCount: 0, + importedTaskCount: 0, + totalTaskCount: 0, + }) + vscode.postMessage({ type: "importRooHistory" }) + } return (
@@ -149,6 +219,86 @@ export const About = ({ telemetrySetting, setTelemetrySetting, debug, setDebug,
+ +
+ +
+
+
+ +
+
+
+ {t("settings:about.rooHistoryImport.cardTitle")} +
+
+ {t("settings:about.rooHistoryImport.cardDescription")} +
+
+
+ {shouldShowImportProgress && ( +
+
+
+ {isImporting ? ( + + ) : isImportFailed ? ( + + ) : ( + + )} + + {isImporting + ? t("settings:about.rooHistoryImport.statusImporting") + : isImportFailed + ? t("settings:about.rooHistoryImport.statusFailed") + : t("settings:about.rooHistoryImport.statusComplete")} + +
+
+ {importProgressPercent}% +
+
+
+
+
+
+
{importProgressSummary}
+
{importProgressDetail}
+
+
+ )} + + {isImporting + ? t("settings:about.rooHistoryImport.buttonImporting") + : t("settings:about.rooHistoryImport.buttonIdle")} + +
+ +
) } diff --git a/webview-ui/src/components/settings/__tests__/About.spec.tsx b/webview-ui/src/components/settings/__tests__/About.spec.tsx index ce6b7feb95..ec00875817 100644 --- a/webview-ui/src/components/settings/__tests__/About.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/About.spec.tsx @@ -1,8 +1,9 @@ import React from "react" -import { render, screen } from "@/utils/test-utils" +import { act, fireEvent, render, screen } from "@/utils/test-utils" import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext" import { EXTERNAL_LINKS } from "@/constants/externalLinks" +import { vscode } from "@/utils/vscode" import { About } from "../About" @@ -11,6 +12,16 @@ vi.mock("@/utils/vscode", () => ({ })) vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeButton: ({ + children, + onClick, + disabled, + ...props + }: React.ButtonHTMLAttributes & { appearance?: string }) => ( + + ), VSCodeCheckbox: ({ children, ...props }: React.InputHTMLAttributes) => (