Skip to content

Commit ec038bf

Browse files
authored
fix: customization provider API rendering bugs and sessions window isolation (#307745)
* 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. * 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. * 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. * 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. * 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.
1 parent 46f8d7e commit ec038bf

2 files changed

Lines changed: 40 additions & 10 deletions

File tree

src/vs/workbench/api/browser/mainThreadChatAgents2.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { ICustomizationHarnessService, IExternalCustomizationItem, IExternalCust
5050
import { AICustomizationManagementSection, BUILTIN_STORAGE } from '../../contrib/chat/common/aiCustomizationWorkspaceService.js';
5151
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
5252
import { IAgentPluginService } from '../../contrib/chat/common/plugins/agentPluginService.js';
53+
import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';
5354

5455
interface AgentData {
5556
dispose: () => void;
@@ -136,6 +137,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
136137
@IConfigurationService private readonly _configurationService: IConfigurationService,
137138
@ITelemetryService private readonly _telemetryService: ITelemetryService,
138139
@IAgentPluginService private readonly _agentPluginService: IAgentPluginService,
140+
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
139141
) {
140142
super();
141143
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2);
@@ -655,6 +657,11 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
655657
}
656658

657659
async $registerChatSessionCustomizationProvider(handle: number, chatSessionType: string, metadata: IChatSessionCustomizationProviderMetadataDto, extensionId: ExtensionIdentifier): Promise<void> {
660+
if (this._environmentService.isSessionsWindow) {
661+
this._logService.trace(`[MainThreadChatAgents2] Sessions window does not use the customization provider API, ignoring registration from ${extensionId.value}`);
662+
return;
663+
}
664+
658665
if (!this._configurationService.getValue<boolean>('chat.customizations.providerApi.enabled')) {
659666
this._logService.trace(`[MainThreadChatAgents2] Customization provider API is disabled, ignoring registration from ${extensionId.value}`);
660667
return;

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

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ export class AICustomizationListWidget extends Disposable {
586586
private searchQuery: string = '';
587587
private readonly collapsedGroups = new Set<string>();
588588
private readonly dropdownActionDisposables = this._register(new DisposableStore());
589+
private _loadItemsSeq = 0;
589590

590591
private readonly delayedFilter = new Delayer<void>(200);
591592

@@ -646,11 +647,16 @@ export class AICustomizationListWidget extends Disposable {
646647
this.refresh();
647648
}));
648649

649-
// Subscribe to the active provider's onDidChange event
650+
// Subscribe to the active provider's onDidChange event.
651+
// Read both activeHarness and availableHarnesses so that the
652+
// subscription is re-established when a new provider harness
653+
// registers (availableHarnesses changes) even if activeHarness
654+
// was already set to the harness id from persisted state.
650655
const providerChangeDisposable = this._register(new MutableDisposable());
651656
const syncChangeDisposable = this._register(new MutableDisposable());
652657
this._register(autorun(reader => {
653658
this.harnessService.activeHarness.read(reader);
659+
this.harnessService.availableHarnesses.read(reader);
654660
const activeDescriptor = this.harnessService.getActiveDescriptor();
655661
if (activeDescriptor.itemProvider) {
656662
providerChangeDisposable.value = activeDescriptor.itemProvider.onDidChange(() => this.refresh());
@@ -778,6 +784,7 @@ export class AICustomizationListWidget extends Disposable {
778784
this._register(this.promptsService.onDidChangeCustomAgents(() => this.refresh()));
779785
this._register(this.promptsService.onDidChangeSlashCommands(() => this.refresh()));
780786
this._register(this.promptsService.onDidChangeSkills(() => this.refresh()));
787+
this._register(this.promptsService.onDidChangeInstructions(() => this.refresh()));
781788

782789
// Refresh on file deletions so the list updates after inline delete actions
783790
this._register(this.fileService.onDidFilesChange(e => {
@@ -1159,9 +1166,12 @@ export class AICustomizationListWidget extends Disposable {
11591166

11601167
/**
11611168
* Loads items for the current section.
1169+
* Uses a sequence counter so that stale results from concurrent
1170+
* calls (e.g. overlapping autorun refreshes) are discarded.
11621171
*/
11631172
private async loadItems(): Promise<void> {
11641173
const section = this.currentSection;
1174+
const seq = ++this._loadItemsSeq;
11651175
let items: IAICustomizationListItem[];
11661176
try {
11671177
items = await this.fetchItemsForSection(section);
@@ -1170,8 +1180,8 @@ export class AICustomizationListWidget extends Disposable {
11701180
items = [];
11711181
}
11721182

1173-
if (this.currentSection !== section) {
1174-
return; // section changed while loading
1183+
if (this.currentSection !== section || this._loadItemsSeq !== seq) {
1184+
return; // section changed or a newer load started while loading
11751185
}
11761186

11771187
this.allItems = items;
@@ -1936,13 +1946,26 @@ export class AICustomizationListWidget extends Disposable {
19361946

19371947
this.displayEntries = entries;
19381948
} else {
1939-
// Standard provider layout: group by inferred storage/groupKey
1940-
const groups: { groupKey: string; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] = [
1941-
{ 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: [] },
1942-
{ 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: [] },
1943-
{ groupKey: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, description: localize('extensionGroupDescription', "Read-only customizations provided by installed extensions."), items: [] },
1944-
{ groupKey: BUILTIN_STORAGE, label: localize('builtinGroup', "Built-in"), icon: builtinIcon, description: localize('builtinGroupDescription', "Built-in customizations shipped with the application."), items: [] },
1945-
];
1949+
// Standard provider layout: group by inferred storage/groupKey.
1950+
// Instructions use semantic categories (matching core path) so
1951+
// that provider-supplied groupKeys like 'context-instructions'
1952+
// are routed to the correct collapsible header.
1953+
const groups: { groupKey: string; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] =
1954+
this.currentSection === AICustomizationManagementSection.Instructions
1955+
? [
1956+
{ 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: [] },
1957+
{ 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: [] },
1958+
{ groupKey: 'on-demand-instructions', label: localize('onDemandInstructionsGroup', "Loaded on Demand"), icon: instructionsIcon, description: localize('onDemandInstructionsGroupDescription', "Instructions loaded only when explicitly referenced."), items: [] },
1959+
{ 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: [] },
1960+
{ 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: [] },
1961+
{ groupKey: BUILTIN_STORAGE, label: localize('builtinGroup', "Built-in"), icon: builtinIcon, description: localize('builtinGroupDescription', "Built-in customizations shipped with the application."), items: [] },
1962+
]
1963+
: [
1964+
{ 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: [] },
1965+
{ 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: [] },
1966+
{ groupKey: PromptsStorage.extension, label: localize('extensionGroup', "Extensions"), icon: extensionIcon, description: localize('extensionGroupDescription', "Read-only customizations provided by installed extensions."), items: [] },
1967+
{ groupKey: BUILTIN_STORAGE, label: localize('builtinGroup', "Built-in"), icon: builtinIcon, description: localize('builtinGroupDescription', "Built-in customizations shipped with the application."), items: [] },
1968+
];
19461969

19471970
for (const item of matchedItems) {
19481971
const key = item.groupKey ?? item.storage ?? PromptsStorage.local;

0 commit comments

Comments
 (0)