Jelajahi Sumber

Support customize storage path (#1941)

Co-authored-by: Your Name <[email protected]>
Jiayuan Chen 11 bulan lalu
induk
melakukan
1c9daf5a48

+ 0 - 1
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",

+ 10 - 0
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')"
 				}
 			}
 		}

+ 4 - 0
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()
+		},
 	}
 }
 

+ 4 - 3
src/core/Cline.ts

@@ -294,9 +294,10 @@ export class Cline extends EventEmitter<ClineEvents> {
 		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<Anthropic.MessageParam[]> {

+ 9 - 7
src/core/webview/ClineProvider.ts

@@ -2245,15 +2245,15 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
 	}
 
 	async ensureSettingsDirectoryExists(): Promise<string> {
-		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<Record<string, ModelInfo> | undefined> {
@@ -2383,7 +2383,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> 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)

+ 147 - 0
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<string> {
+	// 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<string>("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<string> {
+	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<string> {
+	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<string> {
+	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<void> {
+	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<string>("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)
+		}
+	}
+}