fix: properly implement stable project IDs for Sprint 1

- Fix generateProjectId to check for existing ID before creating new one
- Add workspace filtering to task history based on project ID/workspace path
- Add migration logic when generating project ID for existing workspace
- Add missing translation keys
- Update tests to cover new functionality

This ensures that task history actually follows the project when it's moved,
which was the core requirement that was missing from the original implementation.
This commit is contained in:
Roo Code 2025-08-03 04:42:42 +00:00
parent 64fbf59d86
commit fc7b68b261
5 changed files with 91 additions and 9 deletions

View file

@ -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(

View file

@ -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<number> {
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 {

View file

@ -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",

View file

@ -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")

View file

@ -38,11 +38,18 @@ export async function getProjectId(workspaceRoot: string): Promise<string | null
/**
* Generates a new project ID and writes it to the .rooprojectid file.
* If a project ID already exists, returns the existing one.
*
* @param workspaceRoot The root directory of the workspace
* @returns The generated project ID
* @returns The generated or existing project ID
*/
export async function generateProjectId(workspaceRoot: string): Promise<string> {
// 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)