From 94692aa2243be017462c317aa686a66145832dfa Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:42:46 -0700 Subject: [PATCH 1/5] fix: add sequence counter to loadItems to prevent race conditions Multiple concurrent loadItems() calls can overlap when autoruns fire simultaneously. Without serialization, a slow earlier call can resolve after the correct one and overwrite allItems with stale/empty results. The sequence counter ensures only the latest call's result is applied. --- .../browser/aiCustomization/aiCustomizationListWidget.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index e66527eb39d60..0bc728a80663a 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); @@ -1159,9 +1160,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 +1174,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; From 25198d524690cfb2e52282c03c7b9f4fc3c78705 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:43:09 -0700 Subject: [PATCH 2/5] fix: add missing onDidChangeInstructions subscription The list widget subscribed to onDidChangeCustomAgents, onDidChangeSlashCommands, and onDidChangeSkills but not onDidChangeInstructions. This meant instruction file discovery completing after the initial load never triggered a widget refresh. --- .../chat/browser/aiCustomization/aiCustomizationListWidget.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 0bc728a80663a..3f8d9ef64b210 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -779,6 +779,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 => { From f6f3a5ef3a15505933aae4f9260f163a6bba34cd Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:43:34 -0700 Subject: [PATCH 3/5] fix: re-establish provider onDidChange when harness registers The autorun that subscribes to itemProvider.onDidChange only read activeHarness. If the harness ID was persisted from a previous session, activeHarness never changed when the CLI harness registered, so the subscription was never set up. Now also reads availableHarnesses to re-fire when harnesses are added/removed. --- .../browser/aiCustomization/aiCustomizationListWidget.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 3f8d9ef64b210..2d5ad217e7ea0 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -647,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()); From 681b219cb68da7f6562703e6458b110643240c43 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:44:05 -0700 Subject: [PATCH 4/5] fix: add instruction groups to filterItemsForProvider filterItemsForProvider only had storage-based groups (local, user, extension, builtin). Provider-supplied instruction items have semantic groupKey values like 'context-instructions' and 'on-demand-instructions' which didn't match any group, causing all instruction items to be silently dropped (allItems: 0). Add instruction-semantic groups when the current section is Instructions, matching filterItemsForCore. --- .../aiCustomizationListWidget.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts index 2d5ad217e7ea0..f6f9955e39762 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts @@ -1946,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; From e2ab9cd41ce18bb55bf0e23995667a917932ba22 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:44:29 -0700 Subject: [PATCH 5/5] fix: ignore customization provider API in sessions window The sessions window manages its own harnesses via SessionsCustomizationHarnessService and the remoteAgentHost contribution. Extension-contributed harnesses via the provider API should not be registered in the sessions window. --- src/vs/workbench/api/browser/mainThreadChatAgents2.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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;