mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: register importSettings as VSCode command (#5095)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
parent
8455909809
commit
1472c19f8f
26 changed files with 614 additions and 21 deletions
|
|
@ -48,6 +48,7 @@ export const commandIds = [
|
|||
"newTask",
|
||||
|
||||
"setCustomStoragePath",
|
||||
"importSettings",
|
||||
|
||||
"focusInput",
|
||||
"acceptInput",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { focusPanel } from "../utils/focusPanel"
|
|||
import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay"
|
||||
import { handleNewTask } from "./handleTask"
|
||||
import { CodeIndexManager } from "../services/code-index/manager"
|
||||
import { importSettingsWithFeedback } from "../core/config/importExport"
|
||||
import { t } from "../i18n"
|
||||
|
||||
/**
|
||||
* Helper to get the visible ClineProvider instance or log if not found.
|
||||
|
|
@ -171,6 +173,22 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
|
|||
const { promptForCustomStoragePath } = await import("../utils/storage")
|
||||
await promptForCustomStoragePath()
|
||||
},
|
||||
importSettings: async (filePath?: string) => {
|
||||
const visibleProvider = getVisibleProviderOrLog(outputChannel)
|
||||
if (!visibleProvider) {
|
||||
return
|
||||
}
|
||||
|
||||
await importSettingsWithFeedback(
|
||||
{
|
||||
providerSettingsManager: visibleProvider.providerSettingsManager,
|
||||
contextProxy: visibleProvider.contextProxy,
|
||||
customModesManager: visibleProvider.customModesManager,
|
||||
provider: visibleProvider,
|
||||
},
|
||||
filePath,
|
||||
)
|
||||
},
|
||||
focusInput: async () => {
|
||||
try {
|
||||
await focusPanel(tabPanel, sidebarPanel)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import * as vscode from "vscode"
|
|||
import type { ProviderName } from "@roo-code/types"
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { importSettings, exportSettings } from "../importExport"
|
||||
import { importSettings, importSettingsFromFile, importSettingsWithFeedback, exportSettings } from "../importExport"
|
||||
import { ProviderSettingsManager } from "../ProviderSettingsManager"
|
||||
import { ContextProxy } from "../ContextProxy"
|
||||
import { CustomModesManager } from "../CustomModesManager"
|
||||
|
|
@ -20,6 +20,8 @@ vi.mock("vscode", () => ({
|
|||
window: {
|
||||
showOpenDialog: vi.fn(),
|
||||
showSaveDialog: vi.fn(),
|
||||
showErrorMessage: vi.fn(),
|
||||
showInformationMessage: vi.fn(),
|
||||
},
|
||||
Uri: {
|
||||
file: vi.fn((filePath) => ({ fsPath: filePath })),
|
||||
|
|
@ -31,10 +33,20 @@ vi.mock("fs/promises", () => ({
|
|||
readFile: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
access: vi.fn(),
|
||||
constants: {
|
||||
F_OK: 0,
|
||||
R_OK: 4,
|
||||
},
|
||||
},
|
||||
readFile: vi.fn(),
|
||||
mkdir: vi.fn(),
|
||||
writeFile: vi.fn(),
|
||||
access: vi.fn(),
|
||||
constants: {
|
||||
F_OK: 0,
|
||||
R_OK: 4,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("os", () => ({
|
||||
|
|
@ -96,7 +108,7 @@ describe("importExport", () => {
|
|||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: false })
|
||||
expect(result).toEqual({ success: false, error: "User cancelled file selection" })
|
||||
|
||||
expect(vscode.window.showOpenDialog).toHaveBeenCalledWith({
|
||||
filters: { JSON: ["json"] },
|
||||
|
|
@ -146,9 +158,12 @@ describe("importExport", () => {
|
|||
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
|
||||
|
||||
expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({
|
||||
...previousProviderProfiles,
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } },
|
||||
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 })
|
||||
|
|
@ -219,11 +234,12 @@ describe("importExport", () => {
|
|||
expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
|
||||
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
|
||||
expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({
|
||||
...previousProviderProfiles,
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: {
|
||||
default: { apiProvider: "anthropic" as ProviderName, id: "default-id" },
|
||||
test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" },
|
||||
},
|
||||
modeApiConfigs: {},
|
||||
})
|
||||
|
||||
// Should call setValues with an empty object since globalSettings is missing.
|
||||
|
|
@ -297,9 +313,11 @@ describe("importExport", () => {
|
|||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.providerProfiles?.apiConfigs["openai"]).toBeDefined()
|
||||
expect(result.providerProfiles?.apiConfigs["default"]).toBeDefined()
|
||||
expect(result.providerProfiles?.apiConfigs["default"].apiProvider).toBe("anthropic")
|
||||
if (result.success && "providerProfiles" in result) {
|
||||
expect(result.providerProfiles?.apiConfigs["openai"]).toBeDefined()
|
||||
expect(result.providerProfiles?.apiConfigs["default"]).toBeDefined()
|
||||
expect(result.providerProfiles?.apiConfigs["default"].apiProvider).toBe("anthropic")
|
||||
}
|
||||
})
|
||||
|
||||
it("should call updateCustomMode for each custom mode in config", async () => {
|
||||
|
|
@ -337,6 +355,87 @@ describe("importExport", () => {
|
|||
expect(mockCustomModesManager.updateCustomMode).toHaveBeenCalledWith(mode.slug, mode)
|
||||
})
|
||||
})
|
||||
|
||||
it("should import settings from provided file path without showing dialog", async () => {
|
||||
const filePath = "/mock/path/settings.json"
|
||||
const mockFileContent = JSON.stringify({
|
||||
providerProfiles: {
|
||||
currentApiConfigName: "test",
|
||||
apiConfigs: { test: { apiProvider: "openai" as ProviderName, apiKey: "test-key", id: "test-id" } },
|
||||
},
|
||||
globalSettings: { mode: "code", autoApprovalEnabled: true },
|
||||
})
|
||||
|
||||
;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
|
||||
;(fs.access as Mock).mockResolvedValue(undefined) // File exists and is readable
|
||||
|
||||
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 importSettingsFromFile(
|
||||
{
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
},
|
||||
vscode.Uri.file(filePath),
|
||||
)
|
||||
|
||||
expect(vscode.window.showOpenDialog).not.toHaveBeenCalled()
|
||||
expect(fs.readFile).toHaveBeenCalledWith(filePath, "utf-8")
|
||||
expect(result.success).toBe(true)
|
||||
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 })
|
||||
})
|
||||
|
||||
it("should return error when provided file path does not exist", async () => {
|
||||
const filePath = "/nonexistent/path/settings.json"
|
||||
const accessError = new Error("ENOENT: no such file or directory")
|
||||
|
||||
;(fs.access as Mock).mockRejectedValue(accessError)
|
||||
|
||||
// Create a mock provider for the test
|
||||
const mockProvider = {
|
||||
settingsImportedAt: 0,
|
||||
postStateToWebview: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
// Mock the showErrorMessage to capture the error
|
||||
const showErrorMessageSpy = vi.spyOn(vscode.window, "showErrorMessage").mockResolvedValue(undefined)
|
||||
|
||||
await importSettingsWithFeedback(
|
||||
{
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
provider: mockProvider,
|
||||
},
|
||||
filePath,
|
||||
)
|
||||
|
||||
expect(vscode.window.showOpenDialog).not.toHaveBeenCalled()
|
||||
expect(fs.access).toHaveBeenCalledWith(filePath, fs.constants.F_OK | fs.constants.R_OK)
|
||||
expect(fs.readFile).not.toHaveBeenCalled()
|
||||
expect(showErrorMessageSpy).toHaveBeenCalledWith(expect.stringContaining("errors.settings_import_failed"))
|
||||
|
||||
showErrorMessageSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("exportSettings", () => {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { TelemetryService } from "@roo-code/telemetry"
|
|||
import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager"
|
||||
import { ContextProxy } from "./ContextProxy"
|
||||
import { CustomModesManager } from "./CustomModesManager"
|
||||
import { t } from "../../i18n"
|
||||
|
||||
type ImportOptions = {
|
||||
providerSettingsManager: ProviderSettingsManager
|
||||
|
|
@ -24,6 +25,18 @@ type ExportOptions = {
|
|||
contextProxy: ContextProxy
|
||||
}
|
||||
|
||||
type ImportWithProviderOptions = ImportOptions & {
|
||||
provider: {
|
||||
settingsImportedAt?: number
|
||||
postStateToWebview: () => Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import settings from a file using a file dialog
|
||||
* @param options - Import options containing managers and proxy
|
||||
* @returns Promise resolving to import result
|
||||
*/
|
||||
export const importSettings = async ({ providerSettingsManager, contextProxy, customModesManager }: ImportOptions) => {
|
||||
const uris = await vscode.window.showOpenDialog({
|
||||
filters: { JSON: ["json"] },
|
||||
|
|
@ -31,9 +44,22 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu
|
|||
})
|
||||
|
||||
if (!uris) {
|
||||
return { success: false }
|
||||
return { success: false, error: "User cancelled file selection" }
|
||||
}
|
||||
|
||||
return await importSettingsFromFile({ providerSettingsManager, contextProxy, customModesManager }, uris[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* Import settings from a specific file
|
||||
* @param options - Import options containing managers and proxy
|
||||
* @param fileUri - URI of the file to import from
|
||||
* @returns Promise resolving to import result
|
||||
*/
|
||||
export const importSettingsFromFile = async (
|
||||
{ providerSettingsManager, contextProxy, customModesManager }: ImportOptions,
|
||||
fileUri: vscode.Uri,
|
||||
) => {
|
||||
const schema = z.object({
|
||||
providerProfiles: providerProfilesSchema,
|
||||
globalSettings: globalSettingsSchema.optional(),
|
||||
|
|
@ -42,7 +68,7 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu
|
|||
try {
|
||||
const previousProviderProfiles = await providerSettingsManager.export()
|
||||
|
||||
const data = JSON.parse(await fs.readFile(uris[0].fsPath, "utf-8"))
|
||||
const data = JSON.parse(await fs.readFile(fileUri.fsPath, "utf-8"))
|
||||
const { providerProfiles: newProviderProfiles, globalSettings = {} } = schema.parse(data)
|
||||
|
||||
const providerProfiles = {
|
||||
|
|
@ -61,7 +87,7 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu
|
|||
(globalSettings.customModes ?? []).map((mode) => customModesManager.updateCustomMode(mode.slug, mode)),
|
||||
)
|
||||
|
||||
await providerSettingsManager.import(newProviderProfiles)
|
||||
await providerSettingsManager.import(providerProfiles)
|
||||
await contextProxy.setValues(globalSettings)
|
||||
|
||||
// Set the current provider.
|
||||
|
|
@ -120,3 +146,44 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }:
|
|||
await safeWriteJson(uri.fsPath, { providerProfiles, globalSettings })
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import settings with complete UI feedback and provider state updates
|
||||
* @param options - Import options with provider instance
|
||||
* @param filePath - Optional file path to import from. If not provided, a file dialog will be shown.
|
||||
* @returns Promise that resolves when import is complete
|
||||
*/
|
||||
export const importSettingsWithFeedback = async (
|
||||
{ providerSettingsManager, contextProxy, customModesManager, provider }: ImportWithProviderOptions,
|
||||
filePath?: string,
|
||||
) => {
|
||||
let result
|
||||
|
||||
if (filePath) {
|
||||
// Validate file path and check if file exists
|
||||
try {
|
||||
const fileUri = vscode.Uri.file(filePath)
|
||||
// Check if file exists and is readable
|
||||
await fs.access(fileUri.fsPath, fs.constants.F_OK | fs.constants.R_OK)
|
||||
result = await importSettingsFromFile(
|
||||
{ providerSettingsManager, contextProxy, customModesManager },
|
||||
fileUri,
|
||||
)
|
||||
} catch (error) {
|
||||
result = {
|
||||
success: false,
|
||||
error: `Cannot access file at path "${filePath}": ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = await importSettings({ providerSettingsManager, contextProxy, customModesManager })
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
provider.settingsImportedAt = Date.now()
|
||||
await provider.postStateToWebview()
|
||||
await vscode.window.showInformationMessage(t("common:info.settings_imported"))
|
||||
} else if (result.error) {
|
||||
await vscode.window.showErrorMessage(t("common:errors.settings_import_failed", { error: result.error }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { fileExistsAtPath } from "../../utils/fs"
|
|||
import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
|
||||
import { singleCompletionHandler } from "../../utils/single-completion-handler"
|
||||
import { searchCommits } from "../../utils/git"
|
||||
import { exportSettings, importSettings } from "../config/importExport"
|
||||
import { exportSettings, importSettingsWithFeedback } from "../config/importExport"
|
||||
import { getOpenAiModels } from "../../api/providers/openai"
|
||||
import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
|
||||
import { openMention } from "../mentions"
|
||||
|
|
@ -325,20 +325,13 @@ export const webviewMessageHandler = async (
|
|||
provider.exportTaskWithId(message.text!)
|
||||
break
|
||||
case "importSettings": {
|
||||
const result = await importSettings({
|
||||
await importSettingsWithFeedback({
|
||||
providerSettingsManager: provider.providerSettingsManager,
|
||||
contextProxy: provider.contextProxy,
|
||||
customModesManager: provider.customModesManager,
|
||||
provider: provider,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
provider.settingsImportedAt = Date.now()
|
||||
await provider.postStateToWebview()
|
||||
await vscode.window.showInformationMessage(t("common:info.settings_imported"))
|
||||
} else if (result.error) {
|
||||
await vscode.window.showErrorMessage(t("common:errors.settings_import_failed", { error: result.error }))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case "exportSettings":
|
||||
|
|
|
|||
|
|
@ -155,6 +155,11 @@
|
|||
"title": "%command.setCustomStoragePath.title%",
|
||||
"category": "%configuration.title%"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.importSettings",
|
||||
"title": "%command.importSettings.title%",
|
||||
"category": "%configuration.title%"
|
||||
},
|
||||
{
|
||||
"command": "roo-cline.focusInput",
|
||||
"title": "%command.focusInput.title%",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "Obrir en una Nova Pestanya",
|
||||
"command.focusInput.title": "Enfocar Camp d'Entrada",
|
||||
"command.setCustomStoragePath.title": "Establir Ruta d'Emmagatzematge Personalitzada",
|
||||
"command.importSettings.title": "Importar Configuració",
|
||||
"command.terminal.addToContext.title": "Afegir Contingut del Terminal al Context",
|
||||
"command.terminal.fixCommand.title": "Corregir Aquesta Ordre",
|
||||
"command.terminal.explainCommand.title": "Explicar Aquesta Ordre",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "In Neuem Tab Öffnen",
|
||||
"command.focusInput.title": "Eingabefeld Fokussieren",
|
||||
"command.setCustomStoragePath.title": "Benutzerdefinierten Speicherpfad Festlegen",
|
||||
"command.importSettings.title": "Einstellungen Importieren",
|
||||
"command.terminal.addToContext.title": "Terminal-Inhalt zum Kontext Hinzufügen",
|
||||
"command.terminal.fixCommand.title": "Diesen Befehl Reparieren",
|
||||
"command.terminal.explainCommand.title": "Diesen Befehl Erklären",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "Abrir en Nueva Pestaña",
|
||||
"command.focusInput.title": "Enfocar Campo de Entrada",
|
||||
"command.setCustomStoragePath.title": "Establecer Ruta de Almacenamiento Personalizada",
|
||||
"command.importSettings.title": "Importar Configuración",
|
||||
"command.terminal.addToContext.title": "Añadir Contenido de Terminal al Contexto",
|
||||
"command.terminal.fixCommand.title": "Corregir Este Comando",
|
||||
"command.terminal.explainCommand.title": "Explicar Este Comando",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "Ouvrir dans un Nouvel Onglet",
|
||||
"command.focusInput.title": "Focus sur le Champ de Saisie",
|
||||
"command.setCustomStoragePath.title": "Définir le Chemin de Stockage Personnalisé",
|
||||
"command.importSettings.title": "Importer les Paramètres",
|
||||
"command.terminal.addToContext.title": "Ajouter le Contenu du Terminal au Contexte",
|
||||
"command.terminal.fixCommand.title": "Corriger cette Commande",
|
||||
"command.terminal.explainCommand.title": "Expliquer cette Commande",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "नए टैब में खोलें",
|
||||
"command.focusInput.title": "इनपुट फ़ील्ड पर फोकस करें",
|
||||
"command.setCustomStoragePath.title": "कस्टम स्टोरेज पाथ सेट करें",
|
||||
"command.importSettings.title": "सेटिंग्स इम्पोर्ट करें",
|
||||
"command.terminal.addToContext.title": "टर्मिनल सामग्री को संदर्भ में जोड़ें",
|
||||
"command.terminal.fixCommand.title": "यह कमांड ठीक करें",
|
||||
"command.terminal.explainCommand.title": "यह कमांड समझाएं",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"command.addToContext.title": "Tambahkan ke Konteks",
|
||||
"command.focusInput.title": "Fokus ke Field Input",
|
||||
"command.setCustomStoragePath.title": "Atur Path Penyimpanan Kustom",
|
||||
"command.importSettings.title": "Impor Pengaturan",
|
||||
"command.terminal.addToContext.title": "Tambahkan Konten Terminal ke Konteks",
|
||||
"command.terminal.fixCommand.title": "Perbaiki Perintah Ini",
|
||||
"command.terminal.explainCommand.title": "Jelaskan Perintah Ini",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "Apri in Nuova Scheda",
|
||||
"command.focusInput.title": "Focalizza Campo di Input",
|
||||
"command.setCustomStoragePath.title": "Imposta Percorso di Archiviazione Personalizzato",
|
||||
"command.importSettings.title": "Importa Impostazioni",
|
||||
"command.terminal.addToContext.title": "Aggiungi Contenuto del Terminale al Contesto",
|
||||
"command.terminal.fixCommand.title": "Correggi Questo Comando",
|
||||
"command.terminal.explainCommand.title": "Spiega Questo Comando",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"command.addToContext.title": "コンテキストに追加",
|
||||
"command.focusInput.title": "入力フィールドにフォーカス",
|
||||
"command.setCustomStoragePath.title": "カスタムストレージパスの設定",
|
||||
"command.importSettings.title": "設定をインポート",
|
||||
"command.terminal.addToContext.title": "ターミナルの内容をコンテキストに追加",
|
||||
"command.terminal.fixCommand.title": "このコマンドを修正",
|
||||
"command.terminal.explainCommand.title": "このコマンドを説明",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"command.addToContext.title": "Add To Context",
|
||||
"command.focusInput.title": "Focus Input Field",
|
||||
"command.setCustomStoragePath.title": "Set Custom Storage Path",
|
||||
"command.importSettings.title": "Import Settings",
|
||||
"command.terminal.addToContext.title": "Add Terminal Content to Context",
|
||||
"command.terminal.fixCommand.title": "Fix This Command",
|
||||
"command.terminal.explainCommand.title": "Explain This Command",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "새 탭에서 열기",
|
||||
"command.focusInput.title": "입력 필드 포커스",
|
||||
"command.setCustomStoragePath.title": "사용자 지정 저장소 경로 설정",
|
||||
"command.importSettings.title": "설정 가져오기",
|
||||
"command.terminal.addToContext.title": "터미널 내용을 컨텍스트에 추가",
|
||||
"command.terminal.fixCommand.title": "이 명령어 수정",
|
||||
"command.terminal.explainCommand.title": "이 명령어 설명",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"command.addToContext.title": "Toevoegen aan Context",
|
||||
"command.focusInput.title": "Focus op Invoerveld",
|
||||
"command.setCustomStoragePath.title": "Aangepast Opslagpad Instellen",
|
||||
"command.importSettings.title": "Instellingen Importeren",
|
||||
"command.terminal.addToContext.title": "Terminalinhoud aan Context Toevoegen",
|
||||
"command.terminal.fixCommand.title": "Repareer Dit Commando",
|
||||
"command.terminal.explainCommand.title": "Leg Dit Commando Uit",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "Otwórz w Nowej Karcie",
|
||||
"command.focusInput.title": "Fokus na Pole Wprowadzania",
|
||||
"command.setCustomStoragePath.title": "Ustaw Niestandardową Ścieżkę Przechowywania",
|
||||
"command.importSettings.title": "Importuj Ustawienia",
|
||||
"command.terminal.addToContext.title": "Dodaj Zawartość Terminala do Kontekstu",
|
||||
"command.terminal.fixCommand.title": "Napraw tę Komendę",
|
||||
"command.terminal.explainCommand.title": "Wyjaśnij tę Komendę",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "Abrir em Nova Aba",
|
||||
"command.focusInput.title": "Focar Campo de Entrada",
|
||||
"command.setCustomStoragePath.title": "Definir Caminho de Armazenamento Personalizado",
|
||||
"command.importSettings.title": "Importar Configurações",
|
||||
"command.terminal.addToContext.title": "Adicionar Conteúdo do Terminal ao Contexto",
|
||||
"command.terminal.fixCommand.title": "Corrigir Este Comando",
|
||||
"command.terminal.explainCommand.title": "Explicar Este Comando",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"command.addToContext.title": "Добавить в контекст",
|
||||
"command.focusInput.title": "Фокус на поле ввода",
|
||||
"command.setCustomStoragePath.title": "Указать путь хранения",
|
||||
"command.importSettings.title": "Импортировать настройки",
|
||||
"command.terminal.addToContext.title": "Добавить содержимое терминала в контекст",
|
||||
"command.terminal.fixCommand.title": "Исправить эту команду",
|
||||
"command.terminal.explainCommand.title": "Объяснить эту команду",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "Yeni Sekmede Aç",
|
||||
"command.focusInput.title": "Giriş Alanına Odaklan",
|
||||
"command.setCustomStoragePath.title": "Özel Depolama Yolunu Ayarla",
|
||||
"command.importSettings.title": "Ayarları İçe Aktar",
|
||||
"command.terminal.addToContext.title": "Terminal İçeriğini Bağlama Ekle",
|
||||
"command.terminal.fixCommand.title": "Bu Komutu Düzelt",
|
||||
"command.terminal.explainCommand.title": "Bu Komutu Açıkla",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "Mở trong Tab Mới",
|
||||
"command.focusInput.title": "Tập Trung vào Trường Nhập",
|
||||
"command.setCustomStoragePath.title": "Đặt Đường Dẫn Lưu Trữ Tùy Chỉnh",
|
||||
"command.importSettings.title": "Nhập Cài Đặt",
|
||||
"command.terminal.addToContext.title": "Thêm Nội Dung Terminal vào Ngữ Cảnh",
|
||||
"command.terminal.fixCommand.title": "Sửa Lệnh Này",
|
||||
"command.terminal.explainCommand.title": "Giải Thích Lệnh Này",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "在新标签页中打开",
|
||||
"command.focusInput.title": "聚焦输入框",
|
||||
"command.setCustomStoragePath.title": "设置自定义存储路径",
|
||||
"command.importSettings.title": "导入设置",
|
||||
"command.terminal.addToContext.title": "将终端内容添加到上下文",
|
||||
"command.terminal.fixCommand.title": "修复此命令",
|
||||
"command.terminal.explainCommand.title": "解释此命令",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"command.openInNewTab.title": "在新分頁中開啟",
|
||||
"command.focusInput.title": "聚焦輸入框",
|
||||
"command.setCustomStoragePath.title": "設定自訂儲存路徑",
|
||||
"command.importSettings.title": "匯入設定",
|
||||
"command.terminal.addToContext.title": "將終端內容新增到上下文",
|
||||
"command.terminal.fixCommand.title": "修復此命令",
|
||||
"command.terminal.explainCommand.title": "解釋此命令",
|
||||
|
|
|
|||
295
src/utils/__tests__/autoImportSettings.spec.ts
Normal file
295
src/utils/__tests__/autoImportSettings.spec.ts
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
getConfiguration: vi.fn(),
|
||||
},
|
||||
window: {
|
||||
showInformationMessage: vi.fn(),
|
||||
showWarningMessage: vi.fn(),
|
||||
},
|
||||
Uri: {
|
||||
file: vi.fn((path: string) => ({ fsPath: path })),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("fs/promises", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
readFile: vi.fn(),
|
||||
},
|
||||
readFile: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("path", () => ({
|
||||
join: vi.fn((...args: string[]) => args.join("/")),
|
||||
isAbsolute: vi.fn((p: string) => p.startsWith("/")),
|
||||
basename: vi.fn((p: string) => p.split("/").pop() || ""),
|
||||
}))
|
||||
|
||||
vi.mock("os", () => ({
|
||||
homedir: vi.fn(() => "/home/user"),
|
||||
}))
|
||||
|
||||
vi.mock("../fs", () => ({
|
||||
fileExistsAtPath: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("../../core/config/ProviderSettingsManager", async (importOriginal) => {
|
||||
const originalModule = await importOriginal()
|
||||
return {
|
||||
__esModule: true,
|
||||
// We need to mock the class constructor and its methods,
|
||||
// but keep other exports (like schemas) as their original values.
|
||||
...(originalModule || {}), // Spread original exports
|
||||
ProviderSettingsManager: vi.fn().mockImplementation(() => ({
|
||||
// Mock the class
|
||||
export: vi.fn().mockResolvedValue({
|
||||
apiConfigs: {},
|
||||
modeApiConfigs: {},
|
||||
currentApiConfigName: "default",
|
||||
}),
|
||||
import: vi.fn().mockResolvedValue({ success: true }),
|
||||
listConfig: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
}
|
||||
})
|
||||
vi.mock("../../core/config/ContextProxy")
|
||||
vi.mock("../../core/config/CustomModesManager")
|
||||
|
||||
import { autoImportSettings } from "../autoImportSettings"
|
||||
import * as vscode from "vscode"
|
||||
import fsPromises from "fs/promises"
|
||||
import { fileExistsAtPath } from "../fs"
|
||||
|
||||
describe("autoImportSettings", () => {
|
||||
let mockProviderSettingsManager: any
|
||||
let mockContextProxy: any
|
||||
let mockCustomModesManager: any
|
||||
let mockOutputChannel: any
|
||||
let mockProvider: any
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock output channel
|
||||
mockOutputChannel = {
|
||||
appendLine: vi.fn(),
|
||||
}
|
||||
|
||||
// Mock provider settings manager
|
||||
mockProviderSettingsManager = {
|
||||
export: vi.fn().mockResolvedValue({
|
||||
apiConfigs: {},
|
||||
modeApiConfigs: {},
|
||||
currentApiConfigName: "default",
|
||||
}),
|
||||
import: vi.fn().mockResolvedValue({ success: true }),
|
||||
listConfig: vi.fn().mockResolvedValue([]),
|
||||
}
|
||||
|
||||
// Mock context proxy
|
||||
mockContextProxy = {
|
||||
setValues: vi.fn().mockResolvedValue(undefined),
|
||||
setValue: vi.fn().mockResolvedValue(undefined),
|
||||
setProviderSettings: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
// Mock custom modes manager
|
||||
mockCustomModesManager = {
|
||||
updateCustomMode: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
// mockProvider must be initialized AFTER its dependencies
|
||||
mockProvider = {
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
upsertProviderProfile: vi.fn().mockResolvedValue({ success: true }),
|
||||
postStateToWebview: vi.fn().mockResolvedValue({ success: true }),
|
||||
}
|
||||
|
||||
// Reset fs mock
|
||||
vi.mocked(fsPromises.readFile).mockReset()
|
||||
vi.mocked(fileExistsAtPath).mockReset()
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReset()
|
||||
vi.mocked(vscode.window.showInformationMessage).mockReset()
|
||||
vi.mocked(vscode.window.showWarningMessage).mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should skip auto-import when no settings path is specified", async () => {
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
|
||||
get: vi.fn().mockReturnValue(""),
|
||||
} as any)
|
||||
|
||||
await autoImportSettings(mockOutputChannel, {
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
"[AutoImport] No auto-import settings path specified, skipping auto-import",
|
||||
)
|
||||
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should skip auto-import when settings file does not exist", async () => {
|
||||
const settingsPath = "~/Documents/roo-config.json"
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
|
||||
get: vi.fn().mockReturnValue(settingsPath),
|
||||
} as any)
|
||||
|
||||
// Mock fileExistsAtPath to return false
|
||||
vi.mocked(fileExistsAtPath).mockResolvedValue(false)
|
||||
|
||||
await autoImportSettings(mockOutputChannel, {
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
"[AutoImport] Checking for settings file at: /home/user/Documents/roo-config.json",
|
||||
)
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
"[AutoImport] Settings file not found at /home/user/Documents/roo-config.json, skipping auto-import",
|
||||
)
|
||||
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should successfully import settings when file exists and is valid", async () => {
|
||||
const settingsPath = "/absolute/path/to/config.json"
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
|
||||
get: vi.fn().mockReturnValue(settingsPath),
|
||||
} as any)
|
||||
|
||||
// Mock fileExistsAtPath to return true
|
||||
vi.mocked(fileExistsAtPath).mockResolvedValue(true)
|
||||
|
||||
// Mock fs.readFile to return valid config
|
||||
const mockSettings = {
|
||||
providerProfiles: {
|
||||
currentApiConfigName: "test-config",
|
||||
apiConfigs: {
|
||||
"test-config": {
|
||||
apiProvider: "anthropic",
|
||||
anthropicApiKey: "test-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
globalSettings: {
|
||||
customInstructions: "Test instructions",
|
||||
},
|
||||
}
|
||||
|
||||
vi.mocked(fsPromises.readFile).mockResolvedValue(JSON.stringify(mockSettings) as any)
|
||||
|
||||
await autoImportSettings(mockOutputChannel, {
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
"[AutoImport] Checking for settings file at: /absolute/path/to/config.json",
|
||||
)
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
"[AutoImport] Successfully imported settings from /absolute/path/to/config.json",
|
||||
)
|
||||
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("info.auto_import_success")
|
||||
expect(mockProviderSettingsManager.import).toHaveBeenCalled()
|
||||
expect(mockContextProxy.setValues).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle invalid JSON gracefully", async () => {
|
||||
const settingsPath = "~/config.json"
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
|
||||
get: vi.fn().mockReturnValue(settingsPath),
|
||||
} as any)
|
||||
|
||||
// Mock fileExistsAtPath to return true
|
||||
vi.mocked(fileExistsAtPath).mockResolvedValue(true)
|
||||
|
||||
// Mock fs.readFile to return invalid JSON
|
||||
vi.mocked(fsPromises.readFile).mockResolvedValue("invalid json" as any)
|
||||
|
||||
await autoImportSettings(mockOutputChannel, {
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[AutoImport] Failed to import settings:"),
|
||||
)
|
||||
expect(vscode.window.showWarningMessage).toHaveBeenCalledWith("warnings.auto_import_failed")
|
||||
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should resolve home directory paths correctly", async () => {
|
||||
const settingsPath = "~/Documents/config.json"
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
|
||||
get: vi.fn().mockReturnValue(settingsPath),
|
||||
} as any)
|
||||
|
||||
// Mock fileExistsAtPath to return false (so we can check the resolved path)
|
||||
vi.mocked(fileExistsAtPath).mockResolvedValue(false)
|
||||
|
||||
await autoImportSettings(mockOutputChannel, {
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
"[AutoImport] Checking for settings file at: /home/user/Documents/config.json",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle relative paths by resolving them to home directory", async () => {
|
||||
const settingsPath = "Documents/config.json"
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
|
||||
get: vi.fn().mockReturnValue(settingsPath),
|
||||
} as any)
|
||||
|
||||
// Mock fileExistsAtPath to return false (so we can check the resolved path)
|
||||
vi.mocked(fileExistsAtPath).mockResolvedValue(false)
|
||||
|
||||
await autoImportSettings(mockOutputChannel, {
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
"[AutoImport] Checking for settings file at: /home/user/Documents/config.json",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle file system errors gracefully", async () => {
|
||||
const settingsPath = "~/config.json"
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
|
||||
get: vi.fn().mockReturnValue(settingsPath),
|
||||
} as any)
|
||||
|
||||
// Mock fileExistsAtPath to throw an error
|
||||
vi.mocked(fileExistsAtPath).mockRejectedValue(new Error("File system error"))
|
||||
|
||||
await autoImportSettings(mockOutputChannel, {
|
||||
providerSettingsManager: mockProviderSettingsManager,
|
||||
contextProxy: mockContextProxy,
|
||||
customModesManager: mockCustomModesManager,
|
||||
})
|
||||
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[AutoImport] Unexpected error during auto-import:"),
|
||||
)
|
||||
expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
97
src/utils/autoImportSettings.ts
Normal file
97
src/utils/autoImportSettings.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
|
||||
import { Package } from "../shared/package"
|
||||
import { fileExistsAtPath } from "./fs"
|
||||
import { t } from "../i18n"
|
||||
|
||||
import { importSettingsFromFile } from "../core/config/importExport"
|
||||
import { ProviderSettingsManager } from "../core/config/ProviderSettingsManager"
|
||||
import { ContextProxy } from "../core/config/ContextProxy"
|
||||
import { CustomModesManager } from "../core/config/CustomModesManager"
|
||||
|
||||
type ImportOptions = {
|
||||
providerSettingsManager: ProviderSettingsManager
|
||||
contextProxy: ContextProxy
|
||||
customModesManager: CustomModesManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically imports RooCode settings from a specified path if it exists.
|
||||
* This function is called during extension activation to allow users to pre-configure
|
||||
* their settings by placing a settings file at a predefined location.
|
||||
*/
|
||||
export async function autoImportSettings(
|
||||
outputChannel: vscode.OutputChannel,
|
||||
{ providerSettingsManager, contextProxy, customModesManager }: ImportOptions,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Get the auto-import settings path from VSCode settings
|
||||
const settingsPath = vscode.workspace.getConfiguration(Package.name).get<string>("autoImportSettingsPath")
|
||||
|
||||
if (!settingsPath || settingsPath.trim() === "") {
|
||||
outputChannel.appendLine("[AutoImport] No auto-import settings path specified, skipping auto-import")
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the path (handle ~ for home directory and relative paths)
|
||||
const resolvedPath = resolvePath(settingsPath.trim())
|
||||
outputChannel.appendLine(`[AutoImport] Checking for settings file at: ${resolvedPath}`)
|
||||
|
||||
// Check if the file exists
|
||||
if (!(await fileExistsAtPath(resolvedPath))) {
|
||||
outputChannel.appendLine(`[AutoImport] Settings file not found at ${resolvedPath}, skipping auto-import`)
|
||||
return
|
||||
}
|
||||
|
||||
// Attempt to import the configuration
|
||||
const fileUri = vscode.Uri.file(resolvedPath)
|
||||
const result = await importSettingsFromFile(
|
||||
{
|
||||
providerSettingsManager,
|
||||
contextProxy,
|
||||
customModesManager,
|
||||
},
|
||||
fileUri,
|
||||
)
|
||||
|
||||
if (result.success) {
|
||||
outputChannel.appendLine(`[AutoImport] Successfully imported settings from ${resolvedPath}`)
|
||||
|
||||
// Show a notification to the user
|
||||
vscode.window.showInformationMessage(
|
||||
t("common:info.auto_import_success", { filename: path.basename(resolvedPath) }),
|
||||
)
|
||||
} else {
|
||||
outputChannel.appendLine(`[AutoImport] Failed to import settings: ${result.error}`)
|
||||
|
||||
// Show a warning but don't fail the extension activation
|
||||
vscode.window.showWarningMessage(t("common:warnings.auto_import_failed", { error: result.error }))
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
outputChannel.appendLine(`[AutoImport] Unexpected error during auto-import: ${errorMessage}`)
|
||||
|
||||
// Log error but don't fail extension activation
|
||||
console.warn("Auto-import settings error:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a file path, handling home directory expansion and relative paths
|
||||
*/
|
||||
function resolvePath(settingsPath: string): string {
|
||||
// Handle home directory expansion
|
||||
if (settingsPath.startsWith("~/")) {
|
||||
return path.join(os.homedir(), settingsPath.slice(2))
|
||||
}
|
||||
|
||||
// Handle absolute paths
|
||||
if (path.isAbsolute(settingsPath)) {
|
||||
return settingsPath
|
||||
}
|
||||
|
||||
// Handle relative paths (relative to home directory for safety)
|
||||
return path.join(os.homedir(), settingsPath)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue