diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index e002b0fcf3869a..ccd64a886427bf 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -1601,6 +1601,7 @@ export class AICustomizationManagementEditor extends EditorPane { this.customizationsByMigrationCategory = candidatesByCategory; this.customizationMigrationTargetFoldersByType = targetFoldersByType; this.customizationMigrationResultsSettled = true; + this.reconcileMigrationActivity(candidatesByCategory); this.reconcileCustomizationMigrationTargets(); this.refreshCustomizationMigrationUi(); if (this.viewMode === 'migration' && this.activeMigrationCategoryId !== undefined && !candidatesByCategory.has(this.activeMigrationCategoryId)) { @@ -1666,6 +1667,47 @@ export class AICustomizationManagementEditor extends EditorPane { : `file:${customization.storage}:${customization.uri.toString()}`; } + private reconcileMigrationActivity(candidatesByCategory: ReadonlyMap): void { + const candidatesByStorage = new Map(); + for (const storage of [PromptsStorage.user, PromptsStorage.local]) { + candidatesByStorage.set(storage, [...candidatesByCategory.values()].flat().filter(candidate => this.getMigrationCandidateStorage(candidate) === storage)); + } + + for (const [storage, candidates] of candidatesByStorage) { + const candidateKeys = new Set(candidates.map(candidate => this.getMigrationActivityCandidateKey(candidate))); + const legacyCandidateKeys = new Set(candidates.map(candidate => this.getLegacyMigrationActivityKey(candidate))); + const state = this.getMigrationActivityState(storage); + let changed = false; + const activity = state.activity.flatMap(entry => { + const items = entry.items.filter(item => { + const reverted = item.operation !== 'copied' && (item.migrationKey + ? candidateKeys.has(item.migrationKey) + : legacyCandidateKeys.has(JSON.stringify([item.label, item.sourceLabel]))); + changed ||= reverted; + return !reverted; + }); + return items.length ? [{ ...entry, items }] : []; + }); + if (changed) { + this.storeMigrationActivityState(storage, { ...state, activity }); + } + } + } + + private getMigrationActivityCandidateKey(candidate: CustomizationMigrationCandidate): string { + return isMcpServerCustomizationMigrationCandidate(candidate) + ? `mcp:${getMcpServerCustomizationMigrationCandidateKey(candidate)}` + : `file:${candidate.storage}:${getComparisonKey(candidate.uri)}`; + } + + private getLegacyMigrationActivityKey(candidate: CustomizationMigrationCandidate): string { + const label = isMcpServerCustomizationMigrationCandidate(candidate) + ? candidate.name + : candidate.name ?? basename(candidate.uri); + const sourceUri = isMcpServerCustomizationMigrationCandidate(candidate) ? candidate.sourceUri : candidate.uri; + return JSON.stringify([label, this.labelService.getUriLabel(sourceUri)]); + } + private isCustomizationSelectedForMigration(customization: CustomizationMigrationCandidate): boolean { if (isMcpServerCustomizationMigrationCandidate(customization)) { return this.selectedMcpServerMigrationItems.has(getMcpServerCustomizationMigrationCandidateKey(customization)); @@ -1814,6 +1856,7 @@ export class AICustomizationManagementEditor extends EditorPane { sourceLabel: this.labelService.getUriLabel(source.uri), targetLabel: this.labelService.getUriLabel(target.uri), operation: file.type === PromptsType.prompt ? 'converted' as const : deleteOriginalFiles ? 'moved' as const : 'copied' as const, + migrationKey: this.getMigrationActivityCandidateKey(file), }] : []; }); this.recordMigrationActivity(category, context, items); @@ -1886,6 +1929,7 @@ export class AICustomizationManagementEditor extends EditorPane { sourceLabel: this.labelService.getUriLabel(server.sourceUri), targetLabel: this.labelService.getUriLabel(server.targetUri), operation: 'server', + migrationKey: this.getMigrationActivityCandidateKey(server), }))); } await this.refreshCustomizationMigrationInfo(); diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationMigrationDashboard.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationMigrationDashboard.ts index 53e51597f24f4f..c255fc9a47b173 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationMigrationDashboard.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/customizationMigrationDashboard.ts @@ -56,6 +56,7 @@ export interface ICustomizationMigrationDashboardActivity { readonly sourceLabel: string; readonly targetLabel: string; readonly operation: 'converted' | 'moved' | 'copied' | 'server'; + readonly migrationKey?: string; }[]; } diff --git a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationManagementEditor.test.ts b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationManagementEditor.test.ts index cec0180a12e76b..cf1cc96849d3a7 100644 --- a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationManagementEditor.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationManagementEditor.test.ts @@ -1603,7 +1603,13 @@ suite('aiCustomizationManagementEditor', () => { categoryLabel: 'MCP Servers', scopeLabel: 'vscode', storage: PromptsStorage.local, - items: [{ label: 'server', sourceLabel: '/workspace/.vscode/mcp.json', targetLabel: '/workspace/.mcp.json', operation: 'server' }], + items: [{ + label: 'server', + sourceLabel: '/workspace/.vscode/mcp.json', + targetLabel: '/workspace/.mcp.json', + operation: 'server', + migrationKey: 'mcp:["mcp.config.ws0.server","server","file:///workspace/.vscode/mcp.json","file:///workspace/.mcp.json"]', + }], }], }); editor.editorPreviewDisposables.dispose(); @@ -1748,6 +1754,64 @@ suite('aiCustomizationManagementEditor', () => { editor.editorPreviewDisposables.dispose(); }); + test('removes reverted migration activity while preserving copied activity', () => { + const editor = createTestEditor(undefined, createConfigurationServiceStub({ + [ChatConfiguration.ChatCustomizationsPromptMigrationEnabled]: true, + })); + const category = getCustomizationMigrationCategory(CustomizationMigrationCategoryId.PromptFiles); + const context = editor.getMigrationActivityContext(PromptsStorage.local); + const revertedPrompt: MigratableConfiguration = { + uri: URI.file('/workspace/.github/prompts/review.prompt.md'), + name: 'review.prompt.md', + storage: PromptsStorage.local, + type: PromptsType.prompt, + source: PromptFileSource.GitHubWorkspace, + }; + const legacyPrompt: MigratableConfiguration = { + ...revertedPrompt, + uri: URI.file('/workspace/.github/prompts/legacy.prompt.md'), + name: 'legacy.prompt.md', + }; + editor.recordMigrationActivity(category, context, [{ + label: 'review.prompt.md', + sourceLabel: '/workspace/.github/prompts/review.prompt.md', + targetLabel: '/workspace/.github/skills/review/SKILL.md', + operation: 'converted', + migrationKey: `file:${PromptsStorage.local}:${revertedPrompt.uri.toString()}`, + }, { + label: 'legacy.prompt.md', + sourceLabel: '/workspace/.github/prompts/legacy.prompt.md', + targetLabel: '/workspace/.github/skills/legacy/SKILL.md', + operation: 'converted', + }, { + label: 'review.prompt.md', + sourceLabel: '/workspace/.github/prompts/review.prompt.md', + targetLabel: '/workspace/.agents/prompts/review.prompt.md', + operation: 'copied', + migrationKey: `file:${PromptsStorage.local}:${revertedPrompt.uri.toString()}`, + }]); + + editor.setCustomizationsToMigrate(new Map([[category.id, [revertedPrompt, legacyPrompt]]]), new Map()); + + const state = editor.getMigrationActivityState(PromptsStorage.local); + assert.deepStrictEqual({ + activity: state.activity.map(entry => entry.items), + skipped: state.skipped, + started: state.started, + }, { + activity: [[{ + label: 'review.prompt.md', + sourceLabel: '/workspace/.github/prompts/review.prompt.md', + targetLabel: '/workspace/.agents/prompts/review.prompt.md', + operation: 'copied', + migrationKey: `file:${PromptsStorage.local}:${revertedPrompt.uri.toString()}`, + }]], + skipped: false, + started: true, + }); + editor.editorPreviewDisposables.dispose(); + }); + test('persists file migration activity newest first for the initiating profile', () => { const editor = createTestEditor(); const category = getCustomizationMigrationCategory(CustomizationMigrationCategoryId.PromptFiles);