diff --git a/package-lock.json b/package-lock.json index 1dfa282a26..597c28ec2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13024,7 +13024,6 @@ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "chalk": "^2.4.1", diff --git a/package.json b/package.json index 95e3965608..7562c0022f 100644 --- a/package.json +++ b/package.json @@ -164,6 +164,11 @@ "command": "roo-cline.terminalExplainCommandInCurrentTask", "title": "Explain This Command (Current Task)", "category": "Terminal" + }, + { + "command": "roo-cline.setCustomStoragePath", + "title": "Set Custom Storage Path", + "category": "Roo Code" } ], "menus": { @@ -288,6 +293,11 @@ } }, "description": "Settings for VSCode Language Model API" + }, + "roo-cline.customStoragePath": { + "type": "string", + "default": "", + "description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')" } } } diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index d271a05434..e17e71ad02 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -85,6 +85,10 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt "roo-cline.registerHumanRelayCallback": registerHumanRelayCallback, "roo-cline.unregisterHumanRelayCallback": unregisterHumanRelayCallback, "roo-cline.handleHumanRelayResponse": handleHumanRelayResponse, + "roo-cline.setCustomStoragePath": async () => { + const { promptForCustomStoragePath } = await import("../shared/storagePathManager") + await promptForCustomStoragePath() + }, } } diff --git a/src/core/Cline.ts b/src/core/Cline.ts index b2a0168ee1..bdf27981de 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -294,9 +294,10 @@ export class Cline extends EventEmitter { if (!globalStoragePath) { throw new Error("Global storage uri is invalid") } - const taskDir = path.join(globalStoragePath, "tasks", this.taskId) - await fs.mkdir(taskDir, { recursive: true }) - return taskDir + + // Use storagePathManager to retrieve the task storage directory + const { getTaskDirectoryPath } = await import("../shared/storagePathManager") + return getTaskDirectoryPath(globalStoragePath, this.taskId) } private async getSavedApiConversationHistory(): Promise { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 78c1f76dc5..1a1950959a 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2245,15 +2245,15 @@ export class ClineProvider extends EventEmitter implements } async ensureSettingsDirectoryExists(): Promise { - const settingsDir = path.join(this.contextProxy.globalStorageUri.fsPath, "settings") - await fs.mkdir(settingsDir, { recursive: true }) - return settingsDir + const { getSettingsDirectoryPath } = await import("../../shared/storagePathManager") + const globalStoragePath = this.contextProxy.globalStorageUri.fsPath + return getSettingsDirectoryPath(globalStoragePath) } private async ensureCacheDirectoryExists() { - const cacheDir = path.join(this.contextProxy.globalStorageUri.fsPath, "cache") - await fs.mkdir(cacheDir, { recursive: true }) - return cacheDir + const { getCacheDirectoryPath } = await import("../../shared/storagePathManager") + const globalStoragePath = this.contextProxy.globalStorageUri.fsPath + return getCacheDirectoryPath(globalStoragePath) } private async readModelsFromCache(filename: string): Promise | undefined> { @@ -2383,7 +2383,9 @@ export class ClineProvider extends EventEmitter implements const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || [] const historyItem = history.find((item) => item.id === id) if (historyItem) { - const taskDirPath = path.join(this.contextProxy.globalStorageUri.fsPath, "tasks", id) + const { getTaskDirectoryPath } = await import("../../shared/storagePathManager") + const globalStoragePath = this.contextProxy.globalStorageUri.fsPath + const taskDirPath = await getTaskDirectoryPath(globalStoragePath, id) const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory) const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages) const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath) diff --git a/src/shared/storagePathManager.ts b/src/shared/storagePathManager.ts new file mode 100644 index 0000000000..29c79bd389 --- /dev/null +++ b/src/shared/storagePathManager.ts @@ -0,0 +1,147 @@ +import * as vscode from "vscode" +import * as path from "path" +import * as fs from "fs/promises" + +/** + * Gets the base storage path for conversations + * If a custom path is configured, uses that path + * Otherwise uses the default VSCode extension global storage path + */ +export async function getStorageBasePath(defaultPath: string): Promise { + // Get user-configured custom storage path + let customStoragePath = "" + + try { + // This is the line causing the error in tests + const config = vscode.workspace.getConfiguration("roo-cline") + customStoragePath = config.get("customStoragePath", "") + } catch (error) { + console.warn("Could not access VSCode configuration - using default path") + return defaultPath + } + + // If no custom path is set, use default path + if (!customStoragePath) { + return defaultPath + } + + try { + // Ensure custom path exists + await fs.mkdir(customStoragePath, { recursive: true }) + + // Test if path is writable + const testFile = path.join(customStoragePath, ".write_test") + await fs.writeFile(testFile, "test") + await fs.rm(testFile) + + return customStoragePath + } catch (error) { + // If path is unusable, report error and fall back to default path + console.error(`Custom storage path is unusable: ${error instanceof Error ? error.message : String(error)}`) + if (vscode.window) { + vscode.window.showErrorMessage( + `Custom storage path "${customStoragePath}" is unusable, will use default path`, + ) + } + return defaultPath + } +} + +/** + * Gets the storage directory path for a task + */ +export async function getTaskDirectoryPath(globalStoragePath: string, taskId: string): Promise { + const basePath = await getStorageBasePath(globalStoragePath) + const taskDir = path.join(basePath, "tasks", taskId) + await fs.mkdir(taskDir, { recursive: true }) + return taskDir +} + +/** + * Gets the settings directory path + */ +export async function getSettingsDirectoryPath(globalStoragePath: string): Promise { + const basePath = await getStorageBasePath(globalStoragePath) + const settingsDir = path.join(basePath, "settings") + await fs.mkdir(settingsDir, { recursive: true }) + return settingsDir +} + +/** + * Gets the cache directory path + */ +export async function getCacheDirectoryPath(globalStoragePath: string): Promise { + const basePath = await getStorageBasePath(globalStoragePath) + const cacheDir = path.join(basePath, "cache") + await fs.mkdir(cacheDir, { recursive: true }) + return cacheDir +} + +/** + * Prompts the user to set a custom storage path + * Displays an input box allowing the user to enter a custom path + */ +export async function promptForCustomStoragePath(): Promise { + if (!vscode.window || !vscode.workspace) { + console.error("VS Code API not available") + return + } + + let currentPath = "" + try { + const currentConfig = vscode.workspace.getConfiguration("roo-cline") + currentPath = currentConfig.get("customStoragePath", "") + } catch (error) { + console.error("Could not access configuration") + return + } + + const result = await vscode.window.showInputBox({ + value: currentPath, + placeHolder: "D:\\RooCodeStorage", + prompt: "Enter custom conversation history storage path, leave empty to use default location", + validateInput: (input) => { + if (!input) { + return null // Allow empty value (use default path) + } + + try { + // Validate path format + path.parse(input) + + // Check if path is absolute + if (!path.isAbsolute(input)) { + return "Please enter an absolute path (e.g. D:\\RooCodeStorage or /home/user/storage)" + } + + return null // Path format is valid + } catch (e) { + return "Please enter a valid path" + } + }, + }) + + // If user canceled the operation, result will be undefined + if (result !== undefined) { + try { + const currentConfig = vscode.workspace.getConfiguration("roo-cline") + await currentConfig.update("customStoragePath", result, vscode.ConfigurationTarget.Global) + + if (result) { + try { + // Test if path is accessible + await fs.mkdir(result, { recursive: true }) + vscode.window.showInformationMessage(`Custom storage path set: ${result}`) + } catch (error) { + vscode.window.showErrorMessage( + `Cannot access path ${result}: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } else { + vscode.window.showInformationMessage("Reverted to using default storage path") + } + } catch (error) { + console.error("Failed to update configuration", error) + } + } +}