diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 7c41e88e605c9..beef61f2322f1 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -50,6 +50,7 @@ import { ICustomizationHarnessService, IExternalCustomizationItem, IExternalCust import { AICustomizationManagementSection, BUILTIN_STORAGE } from '../../contrib/chat/common/aiCustomizationWorkspaceService.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; import { IAgentPluginService } from '../../contrib/chat/common/plugins/agentPluginService.js'; +import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js'; interface AgentData { dispose: () => void; @@ -136,6 +137,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @IConfigurationService private readonly _configurationService: IConfigurationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IAgentPluginService private readonly _agentPluginService: IAgentPluginService, + @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -655,6 +657,11 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA } async $registerChatSessionCustomizationProvider(handle: number, chatSessionType: string, metadata: IChatSessionCustomizationProviderMetadataDto, extensionId: ExtensionIdentifier): Promise { + if (this._environmentService.isSessionsWindow) { + this._logService.trace(`[MainThreadChatAgents2] Sessions window does not use the customization provider API, ignoring registration from ${extensionId.value}`); + return; + } + if (!this._configurationService.getValue('chat.customizations.providerApi.enabled')) { this._logService.trace(`[MainThreadChatAgents2] Customization provider API is disabled, ignoring registration from ${extensionId.value}`); return; diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index e66527eb39d60..f6f9955e39762 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -586,6 +586,7 @@ export class AICustomizationListWidget extends Disposable { private searchQuery: string = ''; private readonly collapsedGroups = new Set(); private readonly dropdownActionDisposables = this._register(new DisposableStore()); + private _loadItemsSeq = 0; private readonly delayedFilter = new Delayer(200); @@ -646,11 +647,16 @@ export class AICustomizationListWidget extends Disposable { this.refresh(); })); - // Subscribe to the active provider's onDidChange event + // Subscribe to the active provider's onDidChange event. + // Read both activeHarness and availableHarnesses so that the + // subscription is re-established when a new provider harness + // registers (availableHarnesses changes) even if activeHarness + // was already set to the harness id from persisted state. const providerChangeDisposable = this._register(new MutableDisposable()); const syncChangeDisposable = this._register(new MutableDisposable()); this._register(autorun(reader => { this.harnessService.activeHarness.read(reader); + this.harnessService.availableHarnesses.read(reader); const activeDescriptor = this.harnessService.getActiveDescriptor(); if (activeDescriptor.itemProvider) { providerChangeDisposable.value = activeDescriptor.itemProvider.onDidChange(() => this.refresh()); @@ -778,6 +784,7 @@ export class AICustomizationListWidget extends Disposable { this._register(this.promptsService.onDidChangeCustomAgents(() => this.refresh())); this._register(this.promptsService.onDidChangeSlashCommands(() => this.refresh())); this._register(this.promptsService.onDidChangeSkills(() => this.refresh())); + this._register(this.promptsService.onDidChangeInstructions(() => this.refresh())); // Refresh on file deletions so the list updates after inline delete actions this._register(this.fileService.onDidFilesChange(e => { @@ -1159,9 +1166,12 @@ export class AICustomizationListWidget extends Disposable { /** * Loads items for the current section. + * Uses a sequence counter so that stale results from concurrent + * calls (e.g. overlapping autorun refreshes) are discarded. */ private async loadItems(): Promise { const section = this.currentSection; + const seq = ++this._loadItemsSeq; let items: IAICustomizationListItem[]; try { items = await this.fetchItemsForSection(section); @@ -1170,8 +1180,8 @@ export class AICustomizationListWidget extends Disposable { items = []; } - if (this.currentSection !== section) { - return; // section changed while loading + if (this.currentSection !== section || this._loadItemsSeq !== seq) { + return; // section changed or a newer load started while loading } this.allItems = items; @@ -1936,13 +1946,26 @@ export class AICustomizationListWidget extends Disposable { this.displayEntries = entries; } else { - // Standard provider layout: group by inferred storage/groupKey - const groups: { groupKey: string; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] = [ - { groupKey: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, description: localize('workspaceGroupDescription', "Customizations stored as files in your project folder and shared with your team via version control."), items: [] }, - { groupKey: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, - { groupKey: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, description: localize('extensionGroupDescription', "Read-only customizations provided by installed extensions."), items: [] }, - { groupKey: BUILTIN_STORAGE, label: localize('builtinGroup', "Built-in"), icon: builtinIcon, description: localize('builtinGroupDescription', "Built-in customizations shipped with the application."), items: [] }, - ]; + // Standard provider layout: group by inferred storage/groupKey. + // Instructions use semantic categories (matching core path) so + // that provider-supplied groupKeys like 'context-instructions' + // are routed to the correct collapsible header. + const groups: { groupKey: string; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] = + this.currentSection === AICustomizationManagementSection.Instructions + ? [ + { groupKey: 'agent-instructions', label: localize('agentInstructionsGroup', "Agent Instructions"), icon: instructionsIcon, description: localize('agentInstructionsGroupDescription', "Instruction files automatically loaded for all agent interactions (e.g. AGENTS.md, CLAUDE.md, copilot-instructions.md)."), items: [] }, + { groupKey: 'context-instructions', label: localize('contextInstructionsGroup', "Included Based on Context"), icon: instructionsIcon, description: localize('contextInstructionsGroupDescription', "Instructions automatically loaded when matching files are part of the context."), items: [] }, + { groupKey: 'on-demand-instructions', label: localize('onDemandInstructionsGroup', "Loaded on Demand"), icon: instructionsIcon, description: localize('onDemandInstructionsGroupDescription', "Instructions loaded only when explicitly referenced."), items: [] }, + { groupKey: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, description: localize('workspaceGroupDescription', "Customizations stored as files in your project folder and shared with your team via version control."), items: [] }, + { groupKey: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, + { groupKey: BUILTIN_STORAGE, label: localize('builtinGroup', "Built-in"), icon: builtinIcon, description: localize('builtinGroupDescription', "Built-in customizations shipped with the application."), items: [] }, + ] + : [ + { groupKey: PromptsStorage.local, label: localize('workspaceGroup', "Workspace"), icon: workspaceIcon, description: localize('workspaceGroupDescription', "Customizations stored as files in your project folder and shared with your team via version control."), items: [] }, + { groupKey: PromptsStorage.user, label: localize('userGroup', "User"), icon: userIcon, description: localize('userGroupDescription', "Customizations stored locally on your machine in a central location. Private to you and available across all projects."), items: [] }, + { groupKey: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, description: localize('extensionGroupDescription', "Read-only customizations provided by installed extensions."), items: [] }, + { groupKey: BUILTIN_STORAGE, label: localize('builtinGroup', "Built-in"), icon: builtinIcon, description: localize('builtinGroupDescription', "Built-in customizations shipped with the application."), items: [] }, + ]; for (const item of matchedItems) { const key = item.groupKey ?? item.storage ?? PromptsStorage.local;