Skip to content

Commit 09f4d8c

Browse files
joshspicerCopilot
andauthored
Fix delete action in AI Customizations editor (microsoft#301630)
* Fix delete action visibility for read-only customizations - Add context key overlays (type, storage, URI) when rendering list items and context menus so when-clauses are properly evaluated - Add when-clause to delete menu items: hide for extension, plugin, and built-in storage types - Update delete action handler to also block BUILTIN_STORAGE items - Move item context key constants to shared aiCustomizationManagement.ts - Fixes Run Prompt and Reveal in OS actions that were also broken because the context keys were never set on the scoped service Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix delete failing when telemetry throws and trash is unsupported Two issues caused the delete action to silently fail: 1. The telemetry call (publicLog2) was throwing an exception that propagated uncaught, preventing the fileService.del() call from ever executing. Wrap telemetry in try/catch so it cannot block deletion. 2. fileService.del() was called with useTrash: true unconditionally, but some file system providers don't support trash, causing an error. Check hasCapability() before choosing useTrash. Fixes both the management editor and sessions tree view delete actions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 970a8d3 commit 09f4d8c

4 files changed

Lines changed: 65 additions & 18 deletions

File tree

src/vs/sessions/contrib/aiCustomizationTreeView/browser/aiCustomizationTreeView.contribution.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { Codicon } from '../../../../base/common/codicons.js';
1414
import { ICommandService } from '../../../../platform/commands/common/commands.js';
1515
import { URI } from '../../../../base/common/uri.js';
1616
import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';
17-
import { IFileService } from '../../../../platform/files/common/files.js';
17+
import { IFileService, FileSystemProviderCapabilities } from '../../../../platform/files/common/files.js';
1818
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
1919
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
2020

@@ -108,7 +108,8 @@ registerAction2(class extends Action2 {
108108
});
109109

110110
if (confirmation.confirmed) {
111-
await fileService.del(uri, { useTrash: true, recursive: true });
111+
const useTrash = fileService.hasCapability(uri, FileSystemProviderCapabilities.Trash);
112+
await fileService.del(uri, { useTrash, recursive: true });
112113
}
113114
}
114115
});

src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { IPromptsService, PromptsStorage, IPromptPath } from '../../common/promp
2222
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
2323
import { AGENT_MD_FILENAME } from '../../common/promptSyntax/config/promptFileLocations.js';
2424
import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, userIcon, workspaceIcon, extensionIcon, pluginIcon, builtinIcon } from './aiCustomizationIcons.js';
25-
import { AICustomizationManagementItemMenuId, AICustomizationManagementSection, BUILTIN_STORAGE } from './aiCustomizationManagement.js';
25+
import { AI_CUSTOMIZATION_ITEM_STORAGE_KEY, AI_CUSTOMIZATION_ITEM_TYPE_KEY, AI_CUSTOMIZATION_ITEM_URI_KEY, AICustomizationManagementItemMenuId, AICustomizationManagementSection, BUILTIN_STORAGE } from './aiCustomizationManagement.js';
2626
import { InputBox } from '../../../../../base/browser/ui/inputbox/inputBox.js';
2727
import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
2828
import { Delayer } from '../../../../../base/common/async.js';
@@ -353,8 +353,15 @@ class AICustomizationItemRenderer implements IListRenderer<IFileItemEntry, IAICu
353353
storage: element.storage,
354354
};
355355

356+
// Create scoped context key service with item-specific keys for when-clause filtering
357+
const overlay = this.contextKeyService.createOverlay([
358+
[AI_CUSTOMIZATION_ITEM_TYPE_KEY, element.promptType],
359+
[AI_CUSTOMIZATION_ITEM_STORAGE_KEY, element.storage],
360+
[AI_CUSTOMIZATION_ITEM_URI_KEY, element.uri.toString()],
361+
]);
362+
356363
const menu = templateData.elementDisposables.add(
357-
this.menuService.createMenu(AICustomizationManagementItemMenuId, this.contextKeyService)
364+
this.menuService.createMenu(AICustomizationManagementItemMenuId, overlay)
358365
);
359366

360367
const updateActions = () => {
@@ -615,8 +622,15 @@ export class AICustomizationListWidget extends Disposable {
615622
storage: item.storage,
616623
};
617624

625+
// Create scoped context key service with item-specific keys for when-clause filtering
626+
const overlay = this.contextKeyService.createOverlay([
627+
[AI_CUSTOMIZATION_ITEM_TYPE_KEY, item.promptType],
628+
[AI_CUSTOMIZATION_ITEM_STORAGE_KEY, item.storage],
629+
[AI_CUSTOMIZATION_ITEM_URI_KEY, item.uri.toString()],
630+
]);
631+
618632
// Get menu actions, excluding inline actions to avoid duplicates
619-
const actions = this.menuService.getMenuActions(AICustomizationManagementItemMenuId, this.contextKeyService, {
633+
const actions = this.menuService.getMenuActions(AICustomizationManagementItemMenuId, overlay, {
620634
arg: context,
621635
shouldForwardArgs: true,
622636
});

src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.contribution.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ import { AICustomizationManagementEditorInput } from './aiCustomizationManagemen
2020
import {
2121
AI_CUSTOMIZATION_MANAGEMENT_EDITOR_ID,
2222
AI_CUSTOMIZATION_MANAGEMENT_EDITOR_INPUT_ID,
23+
AI_CUSTOMIZATION_ITEM_STORAGE_KEY,
24+
AI_CUSTOMIZATION_ITEM_TYPE_KEY,
25+
AI_CUSTOMIZATION_ITEM_URI_KEY,
2326
AICustomizationManagementCommands,
2427
AICustomizationManagementItemMenuId,
2528
AICustomizationManagementSection,
29+
BUILTIN_STORAGE,
2630
} from './aiCustomizationManagement.js';
2731
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js';
2832
import { Codicon } from '../../../../../base/common/codicons.js';
@@ -33,7 +37,7 @@ import { PromptsStorage } from '../../common/promptSyntax/service/promptsService
3337
import { IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js';
3438
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
3539
import { ChatConfiguration } from '../../common/constants.js';
36-
import { IFileService } from '../../../../../platform/files/common/files.js';
40+
import { IFileService, FileSystemProviderCapabilities } from '../../../../../platform/files/common/files.js';
3741
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
3842
import { basename, dirname } from '../../../../../base/common/resources.js';
3943
import { Schemas } from '../../../../../base/common/network.js';
@@ -227,8 +231,8 @@ registerAction2(class extends Action2 {
227231
// For skills, use the parent folder name since skills are structured as <skillname>/SKILL.md.
228232
const fileName = isSkill ? basename(dirname(uri)) : basename(uri);
229233

230-
// Extension and plugin files cannot be deleted
231-
if (storage === PromptsStorage.extension || storage === PromptsStorage.plugin) {
234+
// Extension, plugin, and built-in files cannot be deleted
235+
if (storage === PromptsStorage.extension || storage === PromptsStorage.plugin || storage === BUILTIN_STORAGE) {
232236
await dialogService.info(
233237
localize('cannotDeleteExtension', "Cannot Delete Extension File"),
234238
localize('cannotDeleteExtensionDetail', "Files provided by extensions cannot be deleted. You can disable the extension if you no longer want to use this customization.")
@@ -248,16 +252,21 @@ registerAction2(class extends Action2 {
248252
});
249253

250254
if (confirmation.confirmed) {
251-
const telemetryService = accessor.get(ITelemetryService);
252-
telemetryService.publicLog2<CustomizationEditorDeleteItemEvent, CustomizationEditorDeleteItemClassification>('chatCustomizationEditor.deleteItem', {
253-
promptType: promptType ?? '',
254-
storage: storage ?? '',
255-
});
255+
try {
256+
const telemetryService = accessor.get(ITelemetryService);
257+
telemetryService.publicLog2<CustomizationEditorDeleteItemEvent, CustomizationEditorDeleteItemClassification>('chatCustomizationEditor.deleteItem', {
258+
promptType: promptType ?? '',
259+
storage: storage ?? '',
260+
});
261+
} catch {
262+
// Telemetry must not block deletion
263+
}
256264

257265
// For skills, delete the parent folder (e.g. .github/skills/my-skill/)
258266
// since each skill is a folder containing SKILL.md.
259267
const deleteTarget = isSkill ? dirname(uri) : uri;
260-
await fileService.del(deleteTarget, { useTrash: true, recursive: isSkill });
268+
const useTrash = fileService.hasCapability(deleteTarget, FileSystemProviderCapabilities.Trash);
269+
await fileService.del(deleteTarget, { useTrash, recursive: isSkill });
261270

262271
// Commit the deletion to git (sessions: main repo + worktree)
263272
if (storage === PromptsStorage.local) {
@@ -289,8 +298,14 @@ registerAction2(class extends Action2 {
289298
}
290299
});
291300

292-
// Context Key for prompt type to conditionally show "Run Prompt"
293-
const AI_CUSTOMIZATION_ITEM_TYPE_KEY = 'aiCustomizationManagementItemType';
301+
/**
302+
* When clause that hides an action for read-only (extension, plugin, built-in) items.
303+
*/
304+
const WHEN_ITEM_IS_DELETABLE = ContextKeyExpr.and(
305+
ContextKeyExpr.notEquals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, PromptsStorage.extension),
306+
ContextKeyExpr.notEquals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, PromptsStorage.plugin),
307+
ContextKeyExpr.notEquals(AI_CUSTOMIZATION_ITEM_STORAGE_KEY, BUILTIN_STORAGE),
308+
);
294309

295310
// Register context menu items
296311

@@ -305,6 +320,7 @@ MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
305320
command: { id: DELETE_AI_CUSTOMIZATION_ID, title: localize('delete', "Delete"), icon: Codicon.trash },
306321
group: 'inline',
307322
order: 10,
323+
when: WHEN_ITEM_IS_DELETABLE,
308324
});
309325

310326
// Context menu items (shown on right-click)
@@ -326,15 +342,16 @@ MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
326342
group: '3_file',
327343
order: 1,
328344
when: ContextKeyExpr.or(
329-
ContextKeyExpr.regex('aiCustomizationManagementItemUri', new RegExp(`^${Schemas.file}:`)),
330-
ContextKeyExpr.regex('aiCustomizationManagementItemUri', new RegExp(`^${Schemas.vscodeUserData}:`))
345+
ContextKeyExpr.regex(AI_CUSTOMIZATION_ITEM_URI_KEY, new RegExp(`^${Schemas.file}:`)),
346+
ContextKeyExpr.regex(AI_CUSTOMIZATION_ITEM_URI_KEY, new RegExp(`^${Schemas.vscodeUserData}:`))
331347
),
332348
});
333349

334350
MenuRegistry.appendMenuItem(AICustomizationManagementItemMenuId, {
335351
command: { id: DELETE_AI_CUSTOMIZATION_ID, title: localize('delete', "Delete") },
336352
group: '4_modify',
337353
order: 1,
354+
when: WHEN_ITEM_IS_DELETABLE,
338355
});
339356

340357
//#endregion

src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagement.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ export const AICustomizationManagementTitleMenuId = MenuId.for('AICustomizationM
7272
*/
7373
export const AICustomizationManagementItemMenuId = MenuId.for('AICustomizationManagementEditorItem');
7474

75+
/**
76+
* Context key for the item prompt type (e.g. 'prompt', 'agent') used in when-clause filtering.
77+
*/
78+
export const AI_CUSTOMIZATION_ITEM_TYPE_KEY = 'aiCustomizationManagementItemType';
79+
80+
/**
81+
* Context key for the item storage type (e.g. 'local', 'user', 'extension') used in when-clause filtering.
82+
*/
83+
export const AI_CUSTOMIZATION_ITEM_STORAGE_KEY = 'aiCustomizationManagementItemStorage';
84+
85+
/**
86+
* Context key for the item URI used in when-clause filtering.
87+
*/
88+
export const AI_CUSTOMIZATION_ITEM_URI_KEY = 'aiCustomizationManagementItemUri';
89+
7590
/**
7691
* Storage key for persisting the selected section.
7792
*/

0 commit comments

Comments
 (0)