diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 41c127333d..81cb6be90b 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -190,7 +190,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt customModesManager: visibleProvider.customModesManager, provider: visibleProvider, }, - filePath, + filePath ? { filePath } : undefined, ) }, focusInput: async () => { diff --git a/src/core/config/__tests__/importExport.spec.ts b/src/core/config/__tests__/importExport.spec.ts index 361d6b23b0..590eecc8d3 100644 --- a/src/core/config/__tests__/importExport.spec.ts +++ b/src/core/config/__tests__/importExport.spec.ts @@ -1,5 +1,6 @@ // npx vitest src/core/config/__tests__/importExport.spec.ts +import { describe, it, expect, vi, beforeEach } from "vitest" import fs from "fs/promises" import * as path from "path" @@ -8,7 +9,13 @@ import * as vscode from "vscode" import type { ProviderName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { importSettings, importSettingsFromFile, importSettingsWithFeedback, exportSettings } from "../importExport" +import { + importSettings, + importSettingsFromFile, + importSettingsFromContent, + importSettingsWithFeedback, + exportSettings, +} from "../importExport" import { ProviderSettingsManager } from "../ProviderSettingsManager" import { ContextProxy } from "../ContextProxy" import { CustomModesManager } from "../CustomModesManager" @@ -263,7 +270,9 @@ describe("importExport", () => { }) expect(result.success).toBe(false) - expect(result.error).toMatch(/^Expected property name or '}' in JSON at position 2/) + if (!result.success) { + expect(result.error).toMatch(/^Expected property name or '}' in JSON at position 2/) + } expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8") expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() expect(mockContextProxy.setValues).not.toHaveBeenCalled() @@ -426,7 +435,7 @@ describe("importExport", () => { customModesManager: mockCustomModesManager, provider: mockProvider, }, - filePath, + { filePath }, ) expect(vscode.window.showOpenDialog).not.toHaveBeenCalled() @@ -436,6 +445,178 @@ describe("importExport", () => { showErrorMessageSpy.mockRestore() }) + + it("should import settings successfully from provided fileContents", async () => { + const mockFileContent = JSON.stringify({ + providerProfiles: { + currentApiConfigName: "test", + apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } }, + }, + globalSettings: { mode: "code", autoApprovalEnabled: true }, + }) + + const previousProviderProfiles = { + currentApiConfigName: "default", + apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } }, + } + + mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { name: "test", id: "test-id", apiProvider: "openai" as ProviderName }, + { name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName }, + ]) + mockContextProxy.export.mockResolvedValue({ mode: "code" }) + + const result = await importSettingsFromContent(mockFileContent, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(result.success).toBe(true) + expect(mockProviderSettingsManager.export).toHaveBeenCalled() + expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({ + currentApiConfigName: "test", + apiConfigs: { + default: { apiProvider: "anthropic" as ProviderName, id: "default-id" }, + test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" }, + }, + modeApiConfigs: {}, + }) + expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true }) + expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "test") + expect(mockContextProxy.setValue).toHaveBeenCalledWith("listApiConfigMeta", [ + { name: "test", id: "test-id", apiProvider: "openai" as ProviderName }, + { name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName }, + ]) + }) + + it("should return success: false when fileContents is not valid JSON", async () => { + const mockInvalidJson = "{ this is not valid JSON }" + + const result = await importSettingsFromContent(mockInvalidJson, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error).toMatch(/^Expected property name or '}' in JSON at position 2/) + } + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + expect(mockContextProxy.setValues).not.toHaveBeenCalled() + }) + + it("should return success: false when fileContents is missing required fields", async () => { + // Invalid content (missing required fields) + const mockInvalidContent = JSON.stringify({ + providerProfiles: { apiConfigs: {} }, + globalSettings: {}, + }) + + const result = await importSettingsFromContent(mockInvalidContent, { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error).toBe("[providerProfiles.currentApiConfigName]: Required") + } + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + expect(mockContextProxy.setValues).not.toHaveBeenCalled() + }) + + it("should import settings successfully using importSettingsWithFeedback with fileContents", async () => { + const mockFileContent = JSON.stringify({ + providerProfiles: { + currentApiConfigName: "test", + apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } }, + }, + globalSettings: { mode: "code", autoApprovalEnabled: true }, + }) + + const previousProviderProfiles = { + currentApiConfigName: "default", + apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } }, + } + + mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles) + mockProviderSettingsManager.listConfig.mockResolvedValue([ + { name: "test", id: "test-id", apiProvider: "openai" as ProviderName }, + { name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName }, + ]) + mockContextProxy.export.mockResolvedValue({ mode: "code" }) + + const mockProvider = { + settingsImportedAt: 0, + postStateToWebview: vi.fn().mockResolvedValue(undefined), + } + + const showInformationMessageSpy = vi + .spyOn(vscode.window, "showInformationMessage") + .mockResolvedValue(undefined) + + await importSettingsWithFeedback( + { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + provider: mockProvider, + }, + { fileContents: mockFileContent }, + ) + + expect(vscode.window.showOpenDialog).not.toHaveBeenCalled() + expect(fs.readFile).not.toHaveBeenCalled() + expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({ + currentApiConfigName: "test", + apiConfigs: { + default: { apiProvider: "anthropic" as ProviderName, id: "default-id" }, + test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" }, + }, + modeApiConfigs: {}, + }) + expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true }) + expect(mockProvider.settingsImportedAt).toBeGreaterThan(0) + expect(mockProvider.postStateToWebview).toHaveBeenCalled() + expect(showInformationMessageSpy).toHaveBeenCalledWith(expect.stringContaining("info.settings_imported")) + + showInformationMessageSpy.mockRestore() + }) + + it("should show error message when importSettingsWithFeedback fails with invalid fileContents", async () => { + const mockInvalidJson = "{ this is not valid JSON }" + + const mockProvider = { + settingsImportedAt: 0, + postStateToWebview: vi.fn().mockResolvedValue(undefined), + } + + const showErrorMessageSpy = vi.spyOn(vscode.window, "showErrorMessage").mockResolvedValue(undefined) + + await importSettingsWithFeedback( + { + providerSettingsManager: mockProviderSettingsManager, + contextProxy: mockContextProxy, + customModesManager: mockCustomModesManager, + provider: mockProvider, + }, + { fileContents: mockInvalidJson }, + ) + + expect(vscode.window.showOpenDialog).not.toHaveBeenCalled() + expect(fs.readFile).not.toHaveBeenCalled() + expect(mockProviderSettingsManager.import).not.toHaveBeenCalled() + expect(mockContextProxy.setValues).not.toHaveBeenCalled() + expect(mockProvider.settingsImportedAt).toBe(0) + expect(mockProvider.postStateToWebview).not.toHaveBeenCalled() + expect(showErrorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("errors.settings_import_failed")) + + showErrorMessageSpy.mockRestore() + }) }) describe("exportSettings", () => { diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts index c3d6f9c215..0e677b27ac 100644 --- a/src/core/config/importExport.ts +++ b/src/core/config/importExport.ts @@ -31,75 +31,93 @@ type ImportWithProviderOptions = ImportOptions & { } } +export type ImportSource = { filePath: string } | { fileContents: string } + /** * Imports configuration from a specific file path * Shares base functionality for import settings for both the manual * and automatic settings importing */ -export async function importSettingsFromPath( - filePath: string, +const importSettingsSchema = z.object({ + providerProfiles: providerProfilesSchema, + globalSettings: globalSettingsSchema.optional(), +}) + +type ImportSettingsPayload = z.infer + +const toImportError = (e: unknown) => { + let error = "Unknown error" + + if (e instanceof ZodError) { + error = e.issues.map((issue) => `[${issue.path.join(".")}]: ${issue.message}`).join("\n") + TelemetryService.instance.captureSchemaValidationError({ schemaName: "ImportExport", error: e }) + } else if (e instanceof Error) { + error = e.message + } + + return { success: false as const, error } +} + +const applyImportedSettings = async ( + { providerProfiles: newProviderProfiles, globalSettings = {} }: ImportSettingsPayload, { providerSettingsManager, contextProxy, customModesManager }: ImportOptions, -) { - const schema = z.object({ - providerProfiles: providerProfilesSchema, - globalSettings: globalSettingsSchema.optional(), - }) +) => { + const previousProviderProfiles = await providerSettingsManager.export() + + const providerProfiles = { + currentApiConfigName: newProviderProfiles.currentApiConfigName, + apiConfigs: { + ...previousProviderProfiles.apiConfigs, + ...newProviderProfiles.apiConfigs, + }, + modeApiConfigs: { + ...previousProviderProfiles.modeApiConfigs, + ...newProviderProfiles.modeApiConfigs, + }, + } - try { - const previousProviderProfiles = await providerSettingsManager.export() - - const { providerProfiles: newProviderProfiles, globalSettings = {} } = schema.parse( - JSON.parse(await fs.readFile(filePath, "utf-8")), - ) - - const providerProfiles = { - currentApiConfigName: newProviderProfiles.currentApiConfigName, - apiConfigs: { - ...previousProviderProfiles.apiConfigs, - ...newProviderProfiles.apiConfigs, - }, - modeApiConfigs: { - ...previousProviderProfiles.modeApiConfigs, - ...newProviderProfiles.modeApiConfigs, - }, - } + await Promise.all( + (globalSettings.customModes ?? []).map((mode) => customModesManager.updateCustomMode(mode.slug, mode)), + ) - await Promise.all( - (globalSettings.customModes ?? []).map((mode) => customModesManager.updateCustomMode(mode.slug, mode)), - ) + // OpenAI Compatible settings are now correctly stored in codebaseIndexConfig + // They will be imported automatically with the config - no special handling needed - // OpenAI Compatible settings are now correctly stored in codebaseIndexConfig - // They will be imported automatically with the config - no special handling needed + await providerSettingsManager.import(providerProfiles) + await contextProxy.setValues(globalSettings) - await providerSettingsManager.import(providerProfiles) - await contextProxy.setValues(globalSettings) + // Set the current provider. + const currentProviderName = providerProfiles.currentApiConfigName + const currentProvider = providerProfiles.apiConfigs[currentProviderName] + contextProxy.setValue("currentApiConfigName", currentProviderName) - // Set the current provider. - const currentProviderName = providerProfiles.currentApiConfigName - const currentProvider = providerProfiles.apiConfigs[currentProviderName] - contextProxy.setValue("currentApiConfigName", currentProviderName) + // TODO: It seems like we don't need to have the provider settings in + // the proxy; we can just use providerSettingsManager as the source of + // truth. + if (currentProvider) { + contextProxy.setProviderSettings(currentProvider) + } - // TODO: It seems like we don't need to have the provider settings in - // the proxy; we can just use providerSettingsManager as the source of - // truth. - if (currentProvider) { - contextProxy.setProviderSettings(currentProvider) - } + contextProxy.setValue("listApiConfigMeta", await providerSettingsManager.listConfig()) - contextProxy.setValue("listApiConfigMeta", await providerSettingsManager.listConfig()) + return { providerProfiles, globalSettings, success: true as const } +} - return { providerProfiles, globalSettings, success: true } +export async function importSettingsFromContent(jsonContent: string, options: ImportOptions) { + try { + const payload = importSettingsSchema.parse(JSON.parse(jsonContent)) + return await applyImportedSettings(payload, options) } catch (e) { - let error = "Unknown error" - - if (e instanceof ZodError) { - error = e.issues.map((issue) => `[${issue.path.join(".")}]: ${issue.message}`).join("\n") - TelemetryService.instance.captureSchemaValidationError({ schemaName: "ImportExport", error: e }) - } else if (e instanceof Error) { - error = e.message - } + return toImportError(e) + } +} - return { success: false, error } +export async function importSettingsFromPath(filePath: string, options: ImportOptions) { + try { + const fileContents = await fs.readFile(filePath, "utf-8") + return await importSettingsFromContent(fileContents, options) + } catch (e) { + return toImportError(e) } } @@ -184,20 +202,18 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }: */ export const importSettingsWithFeedback = async ( { providerSettingsManager, contextProxy, customModesManager, provider }: ImportWithProviderOptions, - filePath?: string, + source?: ImportSource, ) => { let result + const baseImportOptions = { providerSettingsManager, contextProxy, customModesManager } - if (filePath) { - // Validate file path and check if file exists + if (source && "fileContents" in source) { + result = await importSettingsFromContent(source.fileContents, baseImportOptions) + } else if (source && "filePath" in source) { + const { filePath } = source try { - // Check if file exists and is readable await fs.access(filePath, fs.constants.F_OK | fs.constants.R_OK) - result = await importSettingsFromPath(filePath, { - providerSettingsManager, - contextProxy, - customModesManager, - }) + result = await importSettingsFromPath(filePath, baseImportOptions) } catch (error) { result = { success: false, @@ -205,7 +221,7 @@ export const importSettingsWithFeedback = async ( } } } else { - result = await importSettings({ providerSettingsManager, contextProxy, customModesManager }) + result = await importSettings(baseImportOptions) } if (result.success) { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index af5f9925c3..924255bad2 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -730,12 +730,65 @@ export const webviewMessageHandler = async ( provider.exportTaskWithId(message.text!) break case "importSettings": { - await importSettingsWithFeedback({ - providerSettingsManager: provider.providerSettingsManager, - contextProxy: provider.contextProxy, - customModesManager: provider.customModesManager, - provider: provider, - }) + // In remote workspaces, offer two options: + // 1) Pick a JSON file from the remote/workspace filesystem (VS Code Open Dialog) + // 2) Pick a JSON file from the local machine via the webview (browser file picker) + const isRemote = !!vscode.env.remoteName + if (!isRemote) { + await importSettingsWithFeedback({ + providerSettingsManager: provider.providerSettingsManager, + contextProxy: provider.contextProxy, + customModesManager: provider.customModesManager, + provider: provider, + }) + } else { + const selection = await vscode.window.showQuickPick( + [ + { + label: t("common:settingsFooter.importFromLocalFile"), + description: t("common:settingsFooter.importFromLocalFileDescription"), + id: "local", + }, + { + label: t("common:settingsFooter.importFromRemoteFile"), + description: t("common:settingsFooter.importFromRemoteFileDescription"), + id: "remote", + }, + ], + { placeHolder: t("common:settingsFooter.chooseImportSource") }, + ) + if (!selection) break + + if (selection.id === "remote") { + await importSettingsWithFeedback({ + providerSettingsManager: provider.providerSettingsManager, + contextProxy: provider.contextProxy, + customModesManager: provider.customModesManager, + provider: provider, + }) + } else { + // Ask the webview to trigger a local file picker + await provider.postMessageToWebview({ type: "requestLocalSettingsFile" }) + } + } + break + } + case "importSettingsFromLocal": { + const content = message.text || "" + if (!content) { + vscode.window.showErrorMessage(t("common:errors.settings_import_no_content")) + break + } + + await importSettingsWithFeedback( + { + providerSettingsManager: provider.providerSettingsManager, + contextProxy: provider.contextProxy, + customModesManager: provider.customModesManager, + provider: provider, + }, + { fileContents: content }, + ) break } diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index a1f528ef97..debb556486 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -54,6 +54,7 @@ "custom_storage_path_unusable": "La ruta d'emmagatzematge personalitzada \"{{path}}\" no és utilitzable, s'utilitzarà la ruta predeterminada", "cannot_access_path": "No es pot accedir a la ruta {{path}}: {{error}}", "settings_import_failed": "Ha fallat la importació de la configuració: {{error}}.", + "settings_import_no_content": "No s'ha rebut cap contingut per importar la configuració.", "mistake_limit_guidance": "Això pot indicar un error en el procés de pensament del model o la incapacitat d'utilitzar una eina correctament, que es pot mitigar amb orientació de l'usuari (p. ex. \"Prova de dividir la tasca en passos més petits\").", "violated_organization_allowlist": "Ha fallat l'execució de la tasca: el perfil actual no és compatible amb la configuració de la teva organització", "condense_failed": "Ha fallat la condensació del context", @@ -148,6 +149,13 @@ "mode_exported": "Mode '{{mode}}' exportat correctament", "mode_imported": "Mode importat correctament" }, + "settingsFooter": { + "importFromRemoteFile": "Importa des del sistema de fitxers remot", + "importFromRemoteFileDescription": "Navega i selecciona des del sistema de fitxers remot/workspace", + "importFromLocalFile": "Importa des del teu ordinador local", + "importFromLocalFileDescription": "Navega i selecciona des del teu ordinador local mitjançant el navegador", + "chooseImportSource": "Tria la font d'importació" + }, "answers": { "yes": "Sí", "no": "No", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index dbd9452e60..aa57b663b3 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Benutzerdefinierter Speicherpfad \"{{path}}\" ist nicht verwendbar, Standardpfad wird verwendet", "cannot_access_path": "Zugriff auf Pfad {{path}} nicht möglich: {{error}}", "settings_import_failed": "Fehler beim Importieren der Einstellungen: {{error}}.", + "settings_import_no_content": "Für den Einstellungsimport wurde kein Inhalt empfangen.", "mistake_limit_guidance": "Dies kann auf einen Fehler im Denkprozess des Modells oder die Unfähigkeit hinweisen, ein Tool richtig zu verwenden, was durch Benutzerführung behoben werden kann (z.B. \"Versuche, die Aufgabe in kleinere Schritte zu unterteilen\").", "violated_organization_allowlist": "Aufgabe konnte nicht ausgeführt werden: Das aktuelle Profil ist nicht kompatibel mit den Einstellungen deiner Organisation", "condense_failed": "Fehler beim Verdichten des Kontexts", @@ -144,6 +145,13 @@ "mode_exported": "Modus '{{mode}}' erfolgreich exportiert", "mode_imported": "Modus erfolgreich importiert" }, + "settingsFooter": { + "importFromRemoteFile": "Aus dem entfernten Dateisystem importieren", + "importFromRemoteFileDescription": "Durchsuchen und im Remote-/Workspace-Dateisystem auswählen", + "importFromLocalFile": "Von Ihrem lokalen Computer importieren", + "importFromLocalFileDescription": "Durchsuchen und von Ihrem lokalen Computer über den Browser auswählen", + "chooseImportSource": "Importquelle auswählen" + }, "answers": { "yes": "Ja", "no": "Nein", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 3a613cc1c2..2ea929b1fa 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Custom storage path \"{{path}}\" is unusable, will use default path", "cannot_access_path": "Cannot access path {{path}}: {{error}}", "settings_import_failed": "Settings import failed: {{error}}.", + "settings_import_no_content": "No content received for settings import.", "mistake_limit_guidance": "This may indicate a failure in the model's thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. \"Try breaking down the task into smaller steps\").", "violated_organization_allowlist": "Failed to run task: the current profile isn't compatible with your organization settings", "condense_failed": "Failed to condense context", @@ -144,6 +145,13 @@ "mode_exported": "Mode '{{mode}}' exported successfully", "mode_imported": "Mode imported successfully" }, + "settingsFooter": { + "importFromRemoteFile": "Import from remote file system", + "importFromRemoteFileDescription": "Browse and select from the remote/workspace filesystem", + "importFromLocalFile": "Import from your local machine", + "importFromLocalFileDescription": "Browse and select from your local computer via browser", + "chooseImportSource": "Choose import source" + }, "answers": { "yes": "Yes", "no": "No", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 49dcfe98c5..abf251167e 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "La ruta de almacenamiento personalizada \"{{path}}\" no es utilizable, se usará la ruta predeterminada", "cannot_access_path": "No se puede acceder a la ruta {{path}}: {{error}}", "settings_import_failed": "Error al importar la configuración: {{error}}.", + "settings_import_no_content": "No se recibió contenido para importar la configuración.", "mistake_limit_guidance": "Esto puede indicar un fallo en el proceso de pensamiento del modelo o la incapacidad de usar una herramienta correctamente, lo cual puede mitigarse con orientación del usuario (ej. \"Intenta dividir la tarea en pasos más pequeños\").", "violated_organization_allowlist": "Error al ejecutar la tarea: el perfil actual no es compatible con la configuración de tu organización", "condense_failed": "Error al condensar el contexto", @@ -144,6 +145,13 @@ "mode_exported": "Modo '{{mode}}' exportado correctamente", "mode_imported": "Modo importado correctamente" }, + "settingsFooter": { + "importFromRemoteFile": "Importar desde el sistema de archivos remoto", + "importFromRemoteFileDescription": "Navega y selecciona desde el sistema de archivos remoto/workspace", + "importFromLocalFile": "Importar desde tu equipo local", + "importFromLocalFileDescription": "Navega y selecciona desde tu equipo local mediante el navegador", + "chooseImportSource": "Elige fuente de importación" + }, "answers": { "yes": "Sí", "no": "No", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 260bbbf13b..ef81f6ea3e 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Le chemin de stockage personnalisé \"{{path}}\" est inutilisable, le chemin par défaut sera utilisé", "cannot_access_path": "Impossible d'accéder au chemin {{path}} : {{error}}", "settings_import_failed": "Échec de l'importation des paramètres : {{error}}", + "settings_import_no_content": "Aucun contenu reçu pour l'importation des paramètres.", "mistake_limit_guidance": "Cela peut indiquer un échec dans le processus de réflexion du modèle ou une incapacité à utiliser un outil correctement, ce qui peut être atténué avec des conseils de l'utilisateur (par ex. \"Essaie de diviser la tâche en étapes plus petites\").", "violated_organization_allowlist": "Échec de l'exécution de la tâche : le profil actuel n'est pas compatible avec les paramètres de votre organisation", "condense_failed": "Échec de la condensation du contexte", @@ -144,6 +145,13 @@ "mode_exported": "Mode '{{mode}}' exporté avec succès", "mode_imported": "Mode importé avec succès" }, + "settingsFooter": { + "importFromRemoteFile": "Importer depuis le système de fichiers distant", + "importFromRemoteFileDescription": "Parcourir et sélectionner depuis le système de fichiers distant/espace de travail", + "importFromLocalFile": "Importer depuis votre machine locale", + "importFromLocalFileDescription": "Parcourir et sélectionner depuis votre ordinateur local via le navigateur", + "chooseImportSource": "Choisir la source d'importation" + }, "answers": { "yes": "Oui", "no": "Non", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index ab7d594e8f..f3f18aa0d4 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "कस्टम स्टोरेज पाथ \"{{path}}\" उपयोग योग्य नहीं है, डिफ़ॉल्ट पाथ का उपयोग किया जाएगा", "cannot_access_path": "पाथ {{path}} तक पहुंच नहीं पा रहे हैं: {{error}}", "settings_import_failed": "सेटिंग्स इम्पोर्ट करने में विफल: {{error}}।", + "settings_import_no_content": "सेटिंग्स आयात के लिए कोई सामग्री प्राप्त नहीं हुई।", "mistake_limit_guidance": "यह मॉडल की सोच प्रक्रिया में विफलता या किसी टूल का सही उपयोग न कर पाने का संकेत हो सकता है, जिसे उपयोगकर्ता के मार्गदर्शन से ठीक किया जा सकता है (जैसे \"कार्य को छोटे चरणों में बांटने की कोशिश करें\")।", "violated_organization_allowlist": "कार्य चलाने में विफल: वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स के साथ संगत नहीं है", "condense_failed": "संदर्भ को संक्षिप्त करने में विफल", @@ -144,6 +145,13 @@ "mode_exported": "मोड '{{mode}}' सफलतापूर्वक निर्यात किया गया", "mode_imported": "मोड सफलतापूर्वक आयात किया गया" }, + "settingsFooter": { + "importFromRemoteFile": "दूरस्थ फ़ाइल सिस्टम से आयात करें", + "importFromRemoteFileDescription": "दूरस्थ/वर्कस्पेस फ़ाइल सिस्टम से ब्राउज़ करें और चयन करें", + "importFromLocalFile": "अपने लोकल मशीन से आयात करें", + "importFromLocalFileDescription": "ब्राउज़र के माध्यम से अपने लोकल कंप्यूटर से ब्राउज़ करें और चयन करें", + "chooseImportSource": "आयात स्रोत चुनें" + }, "answers": { "yes": "हां", "no": "नहीं", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index ddd549b6f0..083b0517ac 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Path penyimpanan kustom \"{{path}}\" tidak dapat digunakan, akan menggunakan path default", "cannot_access_path": "Tidak dapat mengakses path {{path}}: {{error}}", "settings_import_failed": "Impor pengaturan gagal: {{error}}.", + "settings_import_no_content": "Tidak ada konten yang diterima untuk impor pengaturan.", "mistake_limit_guidance": "Ini mungkin menunjukkan kegagalan dalam proses pemikiran model atau ketidakmampuan untuk menggunakan tool dengan benar, yang dapat diatasi dengan beberapa panduan pengguna (misalnya \"Coba bagi tugas menjadi langkah-langkah yang lebih kecil\").", "violated_organization_allowlist": "Gagal menjalankan tugas: profil saat ini tidak kompatibel dengan pengaturan organisasi kamu", "condense_failed": "Gagal mengompres konteks", @@ -144,6 +145,13 @@ "mode_exported": "Mode '{{mode}}' berhasil diekspor", "mode_imported": "Mode berhasil diimpor" }, + "settingsFooter": { + "importFromRemoteFile": "Impor dari sistem berkas jarak jauh", + "importFromRemoteFileDescription": "Jelajahi dan pilih dari sistem berkas jarak jauh/ruang kerja", + "importFromLocalFile": "Impor dari mesin lokal Anda", + "importFromLocalFileDescription": "Jelajahi dan pilih dari komputer lokal Anda melalui peramban", + "chooseImportSource": "Pilih sumber impor" + }, "answers": { "yes": "Ya", "no": "Tidak", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 80e8e0633b..c291c5b23a 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Il percorso di archiviazione personalizzato \"{{path}}\" non è utilizzabile, verrà utilizzato il percorso predefinito", "cannot_access_path": "Impossibile accedere al percorso {{path}}: {{error}}", "settings_import_failed": "Importazione delle impostazioni fallita: {{error}}.", + "settings_import_no_content": "Nessun contenuto ricevuto per l'importazione delle impostazioni.", "mistake_limit_guidance": "Questo può indicare un fallimento nel processo di pensiero del modello o l'incapacità di utilizzare correttamente uno strumento, che può essere mitigato con la guida dell'utente (ad es. \"Prova a suddividere l'attività in passaggi più piccoli\").", "violated_organization_allowlist": "Impossibile eseguire l'attività: il profilo corrente non è compatibile con le impostazioni della tua organizzazione", "condense_failed": "Impossibile condensare il contesto", @@ -144,6 +145,13 @@ "mode_exported": "Modalità '{{mode}}' esportata con successo", "mode_imported": "Modalità importata con successo" }, + "settingsFooter": { + "importFromRemoteFile": "Importa dal file system remoto", + "importFromRemoteFileDescription": "Sfoglia e seleziona dal file system remoto/workspace", + "importFromLocalFile": "Importa dal tuo computer locale", + "importFromLocalFileDescription": "Sfoglia e seleziona dal tuo computer locale tramite il browser", + "chooseImportSource": "Scegli la fonte di importazione" + }, "answers": { "yes": "Sì", "no": "No", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index accba790a2..da34995fb0 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "カスタムストレージパス \"{{path}}\" が使用できないため、デフォルトパスを使用します", "cannot_access_path": "パス {{path}} にアクセスできません:{{error}}", "settings_import_failed": "設定のインポートに失敗しました:{{error}}", + "settings_import_no_content": "設定のインポート用のコンテンツが受信されませんでした。", "mistake_limit_guidance": "これは、モデルの思考プロセスの失敗やツールを適切に使用できないことを示している可能性があり、ユーザーのガイダンスによって軽減できます(例:「タスクをより小さなステップに分割してみてください」)。", "violated_organization_allowlist": "タスクの実行に失敗しました: 現在のプロファイルは組織の設定と互換性がありません", "condense_failed": "コンテキストの圧縮に失敗しました", @@ -144,6 +145,13 @@ "mode_exported": "モード「{{mode}}」が正常にエクスポートされました", "mode_imported": "モードが正常にインポートされました" }, + "settingsFooter": { + "importFromRemoteFile": "リモートファイルシステムからインポート", + "importFromRemoteFileDescription": "リモート/ワークスペースのファイルシステムを参照して選択します", + "importFromLocalFile": "ローカルマシンからインポート", + "importFromLocalFileDescription": "ブラウザを介してローカルコンピュータから参照して選択します", + "chooseImportSource": "インポート元を選択" + }, "answers": { "yes": "はい", "no": "いいえ", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index acb7bd47d7..336112c5d2 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "사용자 지정 저장 경로 \"{{path}}\"를 사용할 수 없어 기본 경로를 사용합니다", "cannot_access_path": "경로 {{path}}에 접근할 수 없습니다: {{error}}", "settings_import_failed": "설정 가져오기 실패: {{error}}.", + "settings_import_no_content": "설정 가져오기용 콘텐츠를 받지 못했습니다.", "mistake_limit_guidance": "이는 모델의 사고 과정 실패나 도구를 제대로 사용하지 못하는 것을 나타낼 수 있으며, 사용자 가이드를 통해 완화할 수 있습니다 (예: \"작업을 더 작은 단계로 나누어 시도해보세요\").", "violated_organization_allowlist": "작업 실행 실패: 현재 프로필이 조직 설정과 호환되지 않습니다", "condense_failed": "컨텍스트 압축에 실패했습니다", @@ -144,6 +145,13 @@ "mode_exported": "'{{mode}}' 모드가 성공적으로 내보내졌습니다", "mode_imported": "모드를 성공적으로 가져왔습니다" }, + "settingsFooter": { + "importFromRemoteFile": "원격 파일 시스템에서 가져오기", + "importFromRemoteFileDescription": "원격/작업공간 파일 시스템에서 찾아서 선택합니다", + "importFromLocalFile": "로컬 컴퓨터에서 가져오기", + "importFromLocalFileDescription": "브라우저를 통해 로컬 컴퓨터에서 찾아서 선택합니다", + "chooseImportSource": "가져오기 소스 선택" + }, "answers": { "yes": "예", "no": "아니오", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index d43690c435..a18d931396 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Aangepast opslagpad \"{{path}}\" is onbruikbaar, standaardpad wordt gebruikt", "cannot_access_path": "Kan pad {{path}} niet openen: {{error}}", "settings_import_failed": "Importeren van instellingen mislukt: {{error}}.", + "settings_import_no_content": "Er is geen inhoud ontvangen voor het importeren van instellingen.", "mistake_limit_guidance": "Dit kan duiden op een fout in het denkproces van het model of het onvermogen om een tool correct te gebruiken, wat kan worden verminderd met gebruikersbegeleiding (bijv. \"Probeer de taak op te delen in kleinere stappen\").", "violated_organization_allowlist": "Taak uitvoeren mislukt: het huidige profiel is niet compatibel met de instellingen van uw organisatie", "condense_failed": "Comprimeren van context mislukt", @@ -144,6 +145,13 @@ "mode_exported": "Modus '{{mode}}' succesvol geëxporteerd", "mode_imported": "Modus succesvol geïmporteerd" }, + "settingsFooter": { + "importFromRemoteFile": "Importeren vanaf het externe bestandssysteem", + "importFromRemoteFileDescription": "Bladeren en selecteren vanaf het externe/workspace bestandssysteem", + "importFromLocalFile": "Importeren vanaf uw lokale machine", + "importFromLocalFileDescription": "Bladeren en selecteren vanaf uw lokale computer via de browser", + "chooseImportSource": "Kies importbron" + }, "answers": { "yes": "Ja", "no": "Nee", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 56c076f785..2cba7ec735 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Niestandardowa ścieżka przechowywania \"{{path}}\" nie jest użyteczna, zostanie użyta domyślna ścieżka", "cannot_access_path": "Nie można uzyskać dostępu do ścieżki {{path}}: {{error}}", "settings_import_failed": "Nie udało się zaimportować ustawień: {{error}}.", + "settings_import_no_content": "Nie otrzymano treści do importu ustawień.", "mistake_limit_guidance": "To może wskazywać na błąd w procesie myślowym modelu lub niezdolność do prawidłowego użycia narzędzia, co można złagodzić poprzez wskazówki użytkownika (np. \"Spróbuj podzielić zadanie na mniejsze kroki\").", "violated_organization_allowlist": "Nie udało się uruchomić zadania: bieżący profil nie jest kompatybilny z ustawieniami Twojej organizacji", "condense_failed": "Nie udało się skondensować kontekstu", @@ -144,6 +145,13 @@ "mode_exported": "Tryb '{{mode}}' pomyślnie wyeksportowany", "mode_imported": "Tryb pomyślnie zaimportowany" }, + "settingsFooter": { + "importFromRemoteFile": "Importuj z zdalnego systemu plików", + "importFromRemoteFileDescription": "Przeglądaj i wybieraj z zdalnego/systemu plików workspace", + "importFromLocalFile": "Importuj ze swojego lokalnego komputera", + "importFromLocalFileDescription": "Przeglądaj i wybieraj ze swojego lokalnego komputera przez przeglądarkę", + "chooseImportSource": "Wybierz źródło importu" + }, "answers": { "yes": "Tak", "no": "Nie", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index c2cd63255f..d5ca656c41 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -54,6 +54,7 @@ "custom_storage_path_unusable": "O caminho de armazenamento personalizado \"{{path}}\" não pode ser usado, será usado o caminho padrão", "cannot_access_path": "Não é possível acessar o caminho {{path}}: {{error}}", "settings_import_failed": "Falha ao importar configurações: {{error}}", + "settings_import_no_content": "Nenhum conteúdo recebido para importar configurações.", "mistake_limit_guidance": "Isso pode indicar uma falha no processo de pensamento do modelo ou incapacidade de usar uma ferramenta adequadamente, o que pode ser mitigado com orientação do usuário (ex. \"Tente dividir a tarefa em etapas menores\").", "violated_organization_allowlist": "Falha ao executar a tarefa: o perfil atual não é compatível com as configurações da sua organização", "condense_failed": "Falha ao condensar o contexto", @@ -148,6 +149,13 @@ "mode_exported": "Modo '{{mode}}' exportado com sucesso", "mode_imported": "Modo importado com sucesso" }, + "settingsFooter": { + "importFromRemoteFile": "Importar do sistema de arquivos remoto", + "importFromRemoteFileDescription": "Navegue e selecione do sistema de arquivos remoto/workspace", + "importFromLocalFile": "Importar do seu computador local", + "importFromLocalFileDescription": "Navegue e selecione do seu computador local através do navegador", + "chooseImportSource": "Escolha a fonte de importação" + }, "answers": { "yes": "Sim", "no": "Não", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 9595f1f276..2ee7497af3 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Пользовательский путь хранения \"{{path}}\" непригоден, будет использован путь по умолчанию", "cannot_access_path": "Невозможно получить доступ к пути {{path}}: {{error}}", "settings_import_failed": "Не удалось импортировать настройки: {{error}}.", + "settings_import_no_content": "Не получено содержимого для импорта настроек.", "mistake_limit_guidance": "Это может указывать на сбой в процессе мышления модели или неспособность правильно использовать инструмент, что можно смягчить с помощью руководства пользователя (например, \"Попробуйте разбить задачу на более мелкие шаги\").", "violated_organization_allowlist": "Не удалось выполнить задачу: текущий профиль несовместим с настройками вашей организации", "condense_failed": "Не удалось сжать контекст", @@ -144,6 +145,13 @@ "mode_exported": "Режим '{{mode}}' успешно экспортирован", "mode_imported": "Режим успешно импортирован" }, + "settingsFooter": { + "importFromRemoteFile": "Импорт из удаленной файловой системы", + "importFromRemoteFileDescription": "Просматривать и выбирать из удаленной/рабочей файловой системы", + "importFromLocalFile": "Импорт с вашего локального компьютера", + "importFromLocalFileDescription": "Просматривать и выбирать с вашего локального компьютера через браузер", + "chooseImportSource": "Выберите источник импорта" + }, "answers": { "yes": "Да", "no": "Нет", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index aa11041110..406c7eb8a0 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Özel depolama yolu \"{{path}}\" kullanılamıyor, varsayılan yol kullanılacak", "cannot_access_path": "{{path}} yoluna erişilemiyor: {{error}}", "settings_import_failed": "Ayarlar içe aktarılamadı: {{error}}.", + "settings_import_no_content": "Ayarlar içe aktarma için içerik alınmadı.", "mistake_limit_guidance": "Bu, modelin düşünce sürecindeki bir başarısızlığı veya bir aracı düzgün kullanamama durumunu gösterebilir, bu da kullanıcı rehberliği ile hafifletilebilir (örn. \"Görevi daha küçük adımlara bölmeyi deneyin\").", "violated_organization_allowlist": "Görev yürütülemedi: Geçerli profil kuruluşunuzun ayarlarıyla uyumlu değil", "condense_failed": "Bağlam sıkıştırılamadı", @@ -144,6 +145,13 @@ "mode_exported": "'{{mode}}' modu başarıyla dışa aktarıldı", "mode_imported": "Mod başarıyla içe aktarıldı" }, + "settingsFooter": { + "importFromRemoteFile": "Uzak dosya sisteminden içe aktar", + "importFromRemoteFileDescription": "Uzak/çalışma alanı dosya sisteminde gezinin ve seçin", + "importFromLocalFile": "Yerel makinenizden içe aktar", + "importFromLocalFileDescription": "Tarayıcı aracılığıyla yerel bilgisayarınızdan gezinin ve seçin", + "chooseImportSource": "İçe aktarma kaynağını seçin" + }, "answers": { "yes": "Evet", "no": "Hayır", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index b4b92b373e..077c3b436c 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "Đường dẫn lưu trữ tùy chỉnh \"{{path}}\" không thể sử dụng được, sẽ sử dụng đường dẫn mặc định", "cannot_access_path": "Không thể truy cập đường dẫn {{path}}: {{error}}", "settings_import_failed": "Nhập cài đặt thất bại: {{error}}.", + "settings_import_no_content": "Không nhận được nội dung để nhập cài đặt.", "mistake_limit_guidance": "Điều này có thể cho thấy sự thất bại trong quá trình suy nghĩ của mô hình hoặc không thể sử dụng công cụ đúng cách, có thể được giảm thiểu bằng hướng dẫn của người dùng (ví dụ: \"Hãy thử chia nhỏ nhiệm vụ thành các bước nhỏ hơn\").", "violated_organization_allowlist": "Không thể chạy tác vụ: hồ sơ hiện tại không tương thích với cài đặt của tổ chức của bạn", "condense_failed": "Không thể nén ngữ cảnh", @@ -150,6 +151,13 @@ "remove": "Xóa", "keep": "Giữ" }, + "settingsFooter": { + "importFromRemoteFile": "Nhập từ hệ thống tệp từ xa", + "importFromRemoteFileDescription": "Duyệt và chọn từ hệ thống tệp từ xa/workspace", + "importFromLocalFile": "Nhập từ máy cục bộ của bạn", + "importFromLocalFileDescription": "Duyệt và chọn từ máy tính cục bộ của bạn qua trình duyệt", + "chooseImportSource": "Chọn nguồn nhập" + }, "buttons": { "save": "Lưu", "edit": "Chỉnh sửa", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 8c42744484..a9ceb9a799 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -55,6 +55,7 @@ "custom_storage_path_unusable": "自定义存储路径 \"{{path}}\" 不可用,将使用默认路径", "cannot_access_path": "无法访问路径 {{path}}:{{error}}", "settings_import_failed": "设置导入失败:{{error}}。", + "settings_import_no_content": "未收到任何内容,无法导入设置。", "mistake_limit_guidance": "这可能表明模型思维过程失败或无法正确使用工具,可通过用户指导来缓解(例如\"尝试将任务分解为更小的步骤\")。", "violated_organization_allowlist": "执行任务失败:当前配置文件与您的组织设置不兼容", "condense_failed": "压缩上下文失败", @@ -149,6 +150,13 @@ "mode_exported": "模式 '{{mode}}' 已成功导出", "mode_imported": "模式已成功导入" }, + "settingsFooter": { + "importFromRemoteFile": "从远程文件系统导入", + "importFromRemoteFileDescription": "浏览并从远程/工作区文件系统中选择", + "importFromLocalFile": "从本地计算机导入", + "importFromLocalFileDescription": "通过浏览器浏览并从本地计算机中选择", + "chooseImportSource": "选择导入来源" + }, "answers": { "yes": "是", "no": "否", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 5a31d601b4..72d7b602fd 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -50,6 +50,7 @@ "custom_storage_path_unusable": "自訂儲存路徑 \"{{path}}\" 無法使用,將使用預設路徑", "cannot_access_path": "無法存取路徑 {{path}}:{{error}}", "settings_import_failed": "設定匯入失敗:{{error}}。", + "settings_import_no_content": "未收到任何內容,無法匯入設定。", "mistake_limit_guidance": "這可能表明模型思維過程失敗或無法正確使用工具,可透過使用者指導來緩解(例如「嘗試將工作分解為更小的步驟」)。", "violated_organization_allowlist": "執行工作失敗:目前設定檔與您的組織設定不相容", "condense_failed": "壓縮上下文失敗", @@ -144,6 +145,13 @@ "mode_exported": "模式 '{{mode}}' 已成功匯出", "mode_imported": "模式已成功匯入" }, + "settingsFooter": { + "importFromRemoteFile": "從遠端檔案系統匯入", + "importFromRemoteFileDescription": "瀏覽並從遠端/工作區檔案系統中選擇", + "importFromLocalFile": "從本機匯入", + "importFromLocalFileDescription": "透過瀏覽器從您的本機電腦瀏覽並選擇", + "chooseImportSource": "選擇匯入來源" + }, "answers": { "yes": "是", "no": "否", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 66f389f81c..26778f29bc 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -125,6 +125,7 @@ export interface ExtensionMessage { | "commands" | "insertTextIntoTextarea" | "dismissedUpsells" + | "requestLocalSettingsFile" | "organizationSwitchResult" text?: string payload?: any // Add a generic payload for now, can refine later diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d43a2fce04..ff968e8af7 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -62,6 +62,7 @@ export interface WebviewMessage { | "deleteTaskWithId" | "exportTaskWithId" | "importSettings" + | "importSettingsFromLocal" | "exportSettings" | "resetState" | "flushRouterModels" diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 220c8cf3af..bdf189bdff 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -7,6 +7,7 @@ import TranslationProvider from "./i18n/TranslationContext" import { MarketplaceViewStateManager } from "./components/marketplace/MarketplaceViewStateManager" import { vscode } from "./utils/vscode" +import { pickFileAsText } from "./utils/filePicker" import { telemetryClient } from "./utils/TelemetryClient" import { TelemetryEventName } from "@roo-code/types" import { initializeSourceMaps, exposeSourceMapsForDebugging } from "./utils/sourceMapInitializer" @@ -188,6 +189,17 @@ const App = () => { if (message.type === "acceptInput") { chatViewRef.current?.acceptInput() } + + if (message.type === "requestLocalSettingsFile") { + const importLocalSettings = async () => { + const text = await pickFileAsText({ accept: ".json,application/json" }) + if (text) { + vscode.postMessage({ type: "importSettingsFromLocal", text }) + } + } + + importLocalSettings() + } }, [switchTab], ) diff --git a/webview-ui/src/utils/filePicker.ts b/webview-ui/src/utils/filePicker.ts new file mode 100644 index 0000000000..5d623debc4 --- /dev/null +++ b/webview-ui/src/utils/filePicker.ts @@ -0,0 +1,46 @@ +export interface PickFileAsTextOptions { + accept?: string +} + +// Choose a file and read the text content +export function pickFileAsText({ accept }: PickFileAsTextOptions = {}): Promise { + return new Promise((resolve) => { + const input = document.createElement("input") + input.type = "file" + + if (accept) { + input.accept = accept + } + + const cleanup = () => { + input.onchange = null + } + + input.onchange = () => { + const file = input.files?.[0] + + if (!file) { + cleanup() + resolve(undefined) + return + } + + const reader = new FileReader() + + reader.onload = () => { + cleanup() + const text = typeof reader.result === "string" ? reader.result : "" + resolve(text) + } + + reader.onerror = () => { + cleanup() + resolve(undefined) + } + + reader.readAsText(file) + } + + input.click() + }) +}