diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 8f0529574a..ae9080101c 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -228,13 +228,38 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt } try { - const projectId = await generateProjectId(workspacePath) - vscode.window.showInformationMessage(t("common:info.project_id_generated", { projectId })) + // Check if project ID already exists + const { getProjectId } = await import("../utils/projectId") + const existingId = await getProjectId(workspacePath) - // Notify the provider to update any cached state + if (existingId) { + vscode.window.showInformationMessage( + t("common:info.project_id_already_exists", { projectId: existingId }), + ) + return + } + + const projectId = await generateProjectId(workspacePath) + + // Migrate existing tasks to use the new project ID const visibleProvider = getVisibleProviderOrLog(outputChannel) if (visibleProvider) { + const migrated = await visibleProvider.migrateTasksToProjectId(workspacePath, projectId) + + if (migrated > 0) { + vscode.window.showInformationMessage( + t("common:info.project_id_generated_with_migration", { + projectId, + count: migrated, + }), + ) + } else { + vscode.window.showInformationMessage(t("common:info.project_id_generated", { projectId })) + } + await visibleProvider.postStateToWebview() + } else { + vscode.window.showInformationMessage(t("common:info.project_id_generated", { projectId })) } } catch (error) { vscode.window.showErrorMessage( diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ed8f8a27d1..5b5ad68028 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1643,6 +1643,16 @@ export class ClineProvider const currentMode = mode ?? defaultModeSlug const hasSystemPromptOverride = await this.hasFileBasedSystemPromptOverride(currentMode) + // Get the workspace storage key (project ID or workspace path) + const { getWorkspaceStorageKey } = await import("../../utils/projectId") + const workspaceStorageKey = await getWorkspaceStorageKey(cwd) + + // Filter task history to only show tasks for the current workspace + const filteredTaskHistory = (taskHistory || []) + .filter((item: HistoryItem) => item.workspace === workspaceStorageKey) + .filter((item: HistoryItem) => item.ts && item.task) + .sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts) + return { version: this.context.extension?.packageJSON?.version ?? "", apiConfiguration, @@ -1664,12 +1674,10 @@ export class ClineProvider autoCondenseContextPercent: autoCondenseContextPercent ?? 100, uriScheme: vscode.env.uriScheme, currentTaskItem: this.getCurrentCline()?.taskId - ? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId) + ? filteredTaskHistory.find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId) : undefined, clineMessages: this.getCurrentCline()?.clineMessages || [], - taskHistory: (taskHistory || []) - .filter((item: HistoryItem) => item.ts && item.task) - .sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts), + taskHistory: filteredTaskHistory, soundEnabled: soundEnabled ?? false, ttsEnabled: ttsEnabled ?? false, ttsSpeed: ttsSpeed ?? 1.0, @@ -2114,6 +2122,32 @@ export class ClineProvider ...gitInfo, } } + + /** + * Migrate existing tasks to use the new project ID + * @param workspacePath The workspace path to migrate from + * @param projectId The new project ID to migrate to + * @returns The number of tasks migrated + */ + async migrateTasksToProjectId(workspacePath: string, projectId: string): Promise { + const taskHistory = this.getGlobalState("taskHistory") ?? [] + let migratedCount = 0 + + // Update all tasks that match the workspace path + const updatedHistory = taskHistory.map((item: HistoryItem) => { + if (item.workspace === workspacePath) { + migratedCount++ + return { ...item, workspace: projectId } + } + return item + }) + + if (migratedCount > 0) { + await this.updateGlobalState("taskHistory", updatedHistory) + } + + return migratedCount + } } class OrganizationAllowListViolationError extends Error { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index c8deee5cf4..7b5c61c22e 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -120,7 +120,11 @@ "image_copied_to_clipboard": "Image data URI copied to clipboard", "image_saved": "Image saved to {{path}}", "mode_exported": "Mode '{{mode}}' exported successfully", - "mode_imported": "Mode imported successfully" + "mode_imported": "Mode imported successfully", + "project_id_generated": "Project ID generated: {{projectId}}", + "project_id_already_exists": "Project ID already exists: {{projectId}}", + "project_id_generated_with_migration": "Project ID generated: {{projectId}}. Migrated {{count}} task(s) to use the new ID.", + "project_id_generation_failed": "Failed to generate project ID: {{error}}" }, "answers": { "yes": "Yes", diff --git a/src/utils/__tests__/projectId.test.ts b/src/utils/__tests__/projectId.test.ts index 3598fe61f2..8ea43dbe0a 100644 --- a/src/utils/__tests__/projectId.test.ts +++ b/src/utils/__tests__/projectId.test.ts @@ -76,6 +76,7 @@ describe("projectId", () => { describe("generateProjectId", () => { it("should generate and save a new project ID", async () => { + vi.mocked(fileExistsAtPath).mockResolvedValue(false) vi.mocked(fs.writeFile).mockResolvedValue() const result = await generateProjectId(mockWorkspaceRoot) @@ -84,7 +85,18 @@ describe("projectId", () => { expect(fs.writeFile).toHaveBeenCalledWith(mockProjectIdPath, result, "utf8") }) + it("should return existing project ID if already exists", async () => { + vi.mocked(fileExistsAtPath).mockResolvedValue(true) + vi.mocked(fs.readFile).mockResolvedValue(mockProjectId) + + const result = await generateProjectId(mockWorkspaceRoot) + + expect(result).toBe(mockProjectId) + expect(fs.writeFile).not.toHaveBeenCalled() + }) + it("should throw error if write fails", async () => { + vi.mocked(fileExistsAtPath).mockResolvedValue(false) vi.mocked(fs.writeFile).mockRejectedValue(new Error("Write failed")) await expect(generateProjectId(mockWorkspaceRoot)).rejects.toThrow("Write failed") diff --git a/src/utils/projectId.ts b/src/utils/projectId.ts index 5d022a6e51..e231e311a5 100644 --- a/src/utils/projectId.ts +++ b/src/utils/projectId.ts @@ -38,11 +38,18 @@ export async function getProjectId(workspaceRoot: string): Promise { + // Check if project ID already exists + const existingId = await getProjectId(workspaceRoot) + if (existingId) { + return existingId + } + const projectId = uuidv4() const projectIdPath = path.join(workspaceRoot, PROJECT_ID_FILENAME)