sessions: enable customizations UI for all session types with deep-link navigation (#312398)

Previously the customizations sidebar only worked for copilotcli sessions.
This change extends support to any session type that has a registered content
provider, making it work for claude-code and other AHP harnesses.

- Remove hardcoded copilotcli gate in mainThreadChatAgents2; now any session
  type with a registered content provider schema is accepted
- Wire ICustomizationHarnessService into count widgets so they use the active
  harness's itemProvider when available (avoids count mismatches for remote
  item providers)
- Add getActiveItemProvider() utility and update getCustomizationTotalCount()
  to accept an optional itemProvider for harness-backed sessions
- Each sidebar customization link now deep-links to its section in the
  management editor via selectSectionById
- Use reader.store pattern instead of MutableDisposable + manual rebind for
  itemProvider change subscriptions
- Add tests for getActiveItemProvider and getCustomizationTotalCount with
  itemProvider; add ICustomizationHarnessService mock to widget fixture
- Update AI_CUSTOMIZATIONS.md to reflect the new multi-harness sessions behavior
This commit is contained in:
Tyler James Leonhardt
2026-04-24 10:29:55 -07:00
committed by GitHub
parent 4651bee042
commit 439c85ab2d
7 changed files with 250 additions and 26 deletions

View File

@@ -51,7 +51,7 @@ Sessions-specific overrides:
```
src/vs/sessions/contrib/chat/browser/
├── aiCustomizationWorkspaceService.ts # Sessions workspace service override
├── customizationHarnessService.ts # Sessions harness service (CLI harness only)
├── customizationHarnessService.ts # Sessions harness service (accepts any content-provider-backed session type)
└── promptsService.ts # AgenticPromptsService (CLI user roots)
src/vs/sessions/contrib/sessions/browser/
├── aiCustomizationShortcutsWidget.ts # Shortcuts widget
@@ -92,7 +92,7 @@ Available harnesses:
| `claude` | Claude | Restricts user roots to `~/.claude`; hides Prompts + Plugins sections |
In core VS Code, all three harnesses are registered but CLI and Claude only appear when their respective agents are registered (`requiredAgentId` checked via `IChatAgentService`). VS Code is the default.
In sessions, only CLI is registered (single harness, toggle bar hidden).
In sessions, harnesses are accepted for any session type that has a registered content provider (checked via `IChatSessionsService.getContentProviderSchemes()`). AHP remote servers register directly via `registerExternalHarness`.
### IHarnessDescriptor
@@ -220,7 +220,7 @@ Skills that are directly invoked by UI elements (toolbar buttons, menu items) ar
### Count Consistency
`customizationCounts.ts` uses the **same data sources** as the list widget. Both go through the active harness's `ICustomizationItemProvider` (or the `PromptsServiceCustomizationItemProvider` fallback), ensuring counts match what the list displays.
`customizationCounts.ts` uses the **same data sources** as the list widget. When a harness with an `itemProvider` is active (determined by `getActiveItemProvider()`), counts come from that provider's `provideChatSessionCustomizations()`. Otherwise, both counts and the list go through the `PromptsServiceCustomizationItemProvider` fallback, ensuring counts match what the list displays.
### Item Badges

View File

@@ -21,8 +21,10 @@ import { IPromptsService } from '../../../../workbench/contrib/chat/common/promp
import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.js';
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { Menus } from '../../../browser/menus.js';
import { getCustomizationTotalCount } from './customizationCounts.js';
import { getCustomizationTotalCount, getActiveItemProvider } from './customizationCounts.js';
import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js';
import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
const $ = DOM.$;
@@ -46,6 +48,8 @@ export class AICustomizationShortcutsWidget extends Disposable {
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService,
@IAgentPluginService private readonly agentPluginService: IAgentPluginService,
@ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService,
@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,
) {
super();
@@ -101,9 +105,10 @@ export class AICustomizationShortcutsWidget extends Disposable {
}));
let updateCountRequestId = 0;
const updateHeaderTotalCount = async () => {
const requestId = ++updateCountRequestId;
const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService, this.agentPluginService);
const totalCount = await getCustomizationTotalCount(this.promptsService, this.mcpService, this.workspaceService, this.workspaceContextService, this.agentPluginService, getActiveItemProvider(this.sessionsManagementService, this.harnessService));
if (requestId !== updateCountRequestId) {
return;
}
@@ -123,6 +128,15 @@ export class AICustomizationShortcutsWidget extends Disposable {
this.workspaceService.activeProjectRoot.read(reader);
updateHeaderTotalCount();
}));
this._register(autorun(reader => {
this.sessionsManagementService.activeSession.read(reader);
this.harnessService.availableHarnesses.read(reader);
const provider = getActiveItemProvider(this.sessionsManagementService, this.harnessService);
if (provider) {
reader.store.add(provider.onDidChange(() => updateHeaderTotalCount()));
}
updateHeaderTotalCount();
}));
updateHeaderTotalCount();
// Toggle collapse on header click

View File

@@ -15,7 +15,9 @@ import { IMcpService } from '../../../../workbench/contrib/mcp/common/mcpTypes.j
import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { parseHooksFromFile } from '../../../../workbench/contrib/chat/common/promptSyntax/hookCompatibility.js';
import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js';
import { ICustomizationHarnessService, ICustomizationItemProvider } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { parse as parseJSONC } from '../../../../base/common/jsonc.js';
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
export interface ISourceCounts {
readonly workspace: number;
@@ -136,19 +138,41 @@ export async function getSourceCounts(
};
}
const PROMPT_TYPES: PromptsType[] = [PromptsType.agent, PromptsType.skill, PromptsType.instructions, PromptsType.hook];
const PROMPT_TYPE_SET = new Set<string>(PROMPT_TYPES);
export async function getCustomizationTotalCount(
promptsService: IPromptsService,
mcpService: IMcpService,
workspaceService: IAICustomizationWorkspaceService,
workspaceContextService: IWorkspaceContextService,
agentPluginService?: IAgentPluginService,
itemProvider?: ICustomizationItemProvider,
): Promise<number> {
const types: PromptsType[] = [PromptsType.agent, PromptsType.skill, PromptsType.instructions, PromptsType.hook];
const results = await Promise.all(types.map(type => {
const filter = workspaceService.getStorageSourceFilter(type);
return getSourceCounts(promptsService, type, filter, workspaceContextService, workspaceService)
.then(counts => getSourceCountsTotal(counts, filter));
}));
let promptTotal: number;
if (itemProvider) {
const allItems = await itemProvider.provideChatSessionCustomizations(CancellationToken.None);
promptTotal = allItems?.filter(item => PROMPT_TYPE_SET.has(item.type)).length ?? 0;
} else {
const results = await Promise.all(PROMPT_TYPES.map(type => {
const filter = workspaceService.getStorageSourceFilter(type);
return getSourceCounts(promptsService, type, filter, workspaceContextService, workspaceService)
.then(counts => getSourceCountsTotal(counts, filter));
}));
promptTotal = results.reduce((sum, n) => sum + n, 0);
}
const pluginCount = agentPluginService?.plugins.get().length ?? 0;
return results.reduce((sum, n) => sum + n, 0) + mcpService.servers.get().length + pluginCount;
return promptTotal + mcpService.servers.get().length + pluginCount;
}
export function getActiveItemProvider(
sessionsManagementService: ISessionsManagementService,
harnessService: ICustomizationHarnessService,
): ICustomizationItemProvider | undefined {
const sessionType = sessionsManagementService.activeSession.get()?.sessionType;
if (sessionType) {
return harnessService.findHarnessById(sessionType)?.itemProvider;
}
return undefined;
}

View File

@@ -5,6 +5,7 @@
import '../../../browser/media/sidebarActionButton.css';
import './media/customizationsToolbar.css';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { localize, localize2 } from '../../../../nls.js';
@@ -12,6 +13,7 @@ import { Action2, registerAction2 } from '../../../../platform/actions/common/ac
import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
import { AICustomizationManagementEditor } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.js';
import { AICustomizationManagementEditorInput } from '../../../../workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditorInput.js';
import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { PromptsType } from '../../../../workbench/contrib/chat/common/promptSyntax/promptTypes.js';
@@ -27,16 +29,18 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
import { IFileService } from '../../../../platform/files/common/files.js';
import { Button } from '../../../../base/browser/ui/button/button.js';
import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { getSourceCounts, getSourceCountsTotal } from './customizationCounts.js';
import { getSourceCounts, getSourceCountsTotal, getActiveItemProvider } from './customizationCounts.js';
import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js';
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { AICustomizationManagementSection, IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js';
import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
export interface ICustomizationItemConfig {
readonly id: string;
readonly label: string;
readonly icon: ThemeIcon;
readonly section: typeof AICustomizationManagementSection[keyof typeof AICustomizationManagementSection];
readonly promptType?: PromptsType;
readonly isMcp?: boolean;
readonly isPlugins?: boolean;
@@ -47,36 +51,42 @@ export const CUSTOMIZATION_ITEMS: ICustomizationItemConfig[] = [
id: 'sessions.customization.agents',
label: localize('agents', "Agents"),
icon: agentIcon,
section: AICustomizationManagementSection.Agents,
promptType: PromptsType.agent,
},
{
id: 'sessions.customization.skills',
label: localize('skills', "Skills"),
icon: skillIcon,
section: AICustomizationManagementSection.Skills,
promptType: PromptsType.skill,
},
{
id: 'sessions.customization.instructions',
label: localize('instructions', "Instructions"),
icon: instructionsIcon,
section: AICustomizationManagementSection.Instructions,
promptType: PromptsType.instructions,
},
{
id: 'sessions.customization.hooks',
label: localize('hooks', "Hooks"),
icon: hookIcon,
section: AICustomizationManagementSection.Hooks,
promptType: PromptsType.hook,
},
{
id: 'sessions.customization.mcpServers',
label: localize('mcpServers', "MCP Servers"),
icon: mcpServerIcon,
section: AICustomizationManagementSection.McpServers,
isMcp: true,
},
{
id: 'sessions.customization.plugins',
label: localize('plugins', "Plugins"),
icon: pluginIcon,
section: AICustomizationManagementSection.Plugins,
isPlugins: true,
},
];
@@ -103,6 +113,7 @@ export class CustomizationLinkViewItem extends ActionViewItem {
@IAICustomizationWorkspaceService private readonly _workspaceService: IAICustomizationWorkspaceService,
@IFileService private readonly _fileService: IFileService,
@IAgentPluginService private readonly _agentPluginService: IAgentPluginService,
@ICustomizationHarnessService private readonly _harnessService: ICustomizationHarnessService,
) {
super(undefined, action, { ...options, icon: false, label: false });
this._viewItemDisposables = this._register(new DisposableStore());
@@ -153,6 +164,11 @@ export class CustomizationLinkViewItem extends ActionViewItem {
this._viewItemDisposables.add(this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._updateCounts()));
this._viewItemDisposables.add(autorun(reader => {
this._activeSessionService.activeSession.read(reader);
this._harnessService.availableHarnesses.read(reader);
const provider = getActiveItemProvider(this._activeSessionService, this._harnessService);
if (provider) {
reader.store.add(provider.onDidChange(() => this._updateCounts()));
}
this._updateCounts();
}));
@@ -168,16 +184,26 @@ export class CustomizationLinkViewItem extends ActionViewItem {
}
const requestId = ++this._updateCountsRequestId;
const itemProvider = getActiveItemProvider(this._activeSessionService, this._harnessService);
if (this._config.promptType) {
const type = this._config.promptType;
const filter = this._workspaceService.getStorageSourceFilter(type);
const counts = await getSourceCounts(this._promptsService, type, filter, this._workspaceContextService, this._workspaceService, this._fileService);
if (requestId !== this._updateCountsRequestId) {
return;
if (itemProvider) {
const allItems = await itemProvider.provideChatSessionCustomizations(CancellationToken.None);
if (requestId !== this._updateCountsRequestId) {
return;
}
const total = allItems?.filter(item => item.type === this._config.promptType).length ?? 0;
this._renderTotalCount(this._countContainer, total);
} else {
const type = this._config.promptType;
const filter = this._workspaceService.getStorageSourceFilter(type);
const counts = await getSourceCounts(this._promptsService, type, filter, this._workspaceContextService, this._workspaceService, this._fileService);
if (requestId !== this._updateCountsRequestId) {
return;
}
const total = getSourceCountsTotal(counts, filter);
this._renderTotalCount(this._countContainer, total);
}
const total = getSourceCountsTotal(counts, filter);
this._renderTotalCount(this._countContainer, total);
} else if (this._config.isMcp) {
const total = this._mcpService.servers.get().length;
this._renderTotalCount(this._countContainer, total);
@@ -231,8 +257,17 @@ export class CustomizationsToolbarContribution extends Disposable implements IWo
}
async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService);
const harnessService = accessor.get(ICustomizationHarnessService);
const sessionsManagementService = accessor.get(ISessionsManagementService);
const activeSessionType = sessionsManagementService.activeSession.get()?.sessionType;
if (activeSessionType && harnessService.findHarnessById(activeSessionType)) {
harnessService.setActiveHarness(activeSessionType);
}
const input = AICustomizationManagementEditorInput.getOrCreate();
await editorService.openEditor(input, { pinned: true });
const pane = await editorService.openEditor(input, { pinned: true });
if (pane instanceof AICustomizationManagementEditor) {
pane.selectSectionById(config.section);
}
}
}));
}

View File

@@ -21,6 +21,7 @@ import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/co
import { IMcpServer, IMcpService } from '../../../../../workbench/contrib/mcp/common/mcpTypes.js';
import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IAgentPluginService } from '../../../../../workbench/contrib/chat/common/plugins/agentPluginService.js';
import { ICustomizationHarnessService } from '../../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../../../../../workbench/test/browser/componentFixtures/fixtureUtils.js';
import { AICustomizationShortcutsWidget } from '../../browser/aiCustomizationShortcutsWidget.js';
import { CUSTOMIZATION_ITEMS, CustomizationLinkViewItem } from '../../browser/customizationsToolbar.contribution.js';
@@ -204,6 +205,10 @@ function renderWidget(ctx: ComponentFixtureContext, options?: { mcpServerCount?:
reg.defineInstance(ISessionsManagementService, new class extends mock<ISessionsManagementService>() {
override readonly activeSession = observableValue<IActiveSession | undefined>('activeSession', undefined);
}());
reg.defineInstance(ICustomizationHarnessService, new class extends mock<ICustomizationHarnessService>() {
override readonly availableHarnesses = observableValue<readonly never[]>('availableHarnesses', []);
override findHarnessById() { return undefined; }
}());
reg.defineInstance(IFileService, new class extends mock<IFileService>() {
override readonly onDidFilesChange = Event.None;
}());

View File

@@ -10,10 +10,14 @@ import { PromptsType } from '../../../../../workbench/contrib/chat/common/prompt
import { IPromptsService, PromptsStorage, IPromptPath, ILocalPromptPath, IUserPromptPath, IExtensionPromptPath, IAgentInstructionFile, AgentInstructionFileType } from '../../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
import { IAICustomizationWorkspaceService, IStorageSourceFilter } from '../../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
import { IWorkspaceContextService, IWorkspace, IWorkspaceFolder, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js';
import { getSourceCounts, getSourceCountsTotal, getCustomizationTotalCount } from '../../browser/customizationCounts.js';
import { getSourceCounts, getSourceCountsTotal, getCustomizationTotalCount, getActiveItemProvider } from '../../browser/customizationCounts.js';
import { IMcpService } from '../../../../../workbench/contrib/mcp/common/mcpTypes.js';
import { Event } from '../../../../../base/common/event.js';
import { observableValue } from '../../../../../base/common/observable.js';
import { ICustomizationHarnessService, ICustomizationItem, ICustomizationItemProvider, IHarnessDescriptor } from '../../../../../workbench/contrib/chat/common/customizationHarnessService.js';
import { IActiveSession, ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { IAgentPluginService } from '../../../../../workbench/contrib/chat/common/plugins/agentPluginService.js';
function localFile(path: string): ILocalPromptPath {
return { uri: URI.file(path), storage: PromptsStorage.local, type: PromptsType.instructions };
@@ -691,6 +695,148 @@ suite('customizationCounts', () => {
});
});
suite('getActiveItemProvider', () => {
function createMockSessionsService(sessionType?: string): ISessionsManagementService {
const activeSession = observableValue<IActiveSession | undefined>(
'test',
sessionType ? { sessionType } as IActiveSession : undefined,
);
return { activeSession } as unknown as ISessionsManagementService;
}
function createMockHarnessService(harnesses: { id: string; itemProvider?: ICustomizationItemProvider }[]): ICustomizationHarnessService {
return {
findHarnessById: (sessionType: string) => {
const h = harnesses.find(h => h.id === sessionType);
return h ? { id: h.id, itemProvider: h.itemProvider } as IHarnessDescriptor : undefined;
},
} as unknown as ICustomizationHarnessService;
}
test('returns undefined when no active session', () => {
const sessionsService = createMockSessionsService(undefined);
const harnessService = createMockHarnessService([]);
assert.strictEqual(getActiveItemProvider(sessionsService, harnessService), undefined);
});
test('returns undefined when session type has no matching harness', () => {
const sessionsService = createMockSessionsService('unknown-type');
const harnessService = createMockHarnessService([{ id: 'copilotcli' }]);
assert.strictEqual(getActiveItemProvider(sessionsService, harnessService), undefined);
});
test('returns undefined when harness has no itemProvider', () => {
const sessionsService = createMockSessionsService('copilotcli');
const harnessService = createMockHarnessService([{ id: 'copilotcli', itemProvider: undefined }]);
assert.strictEqual(getActiveItemProvider(sessionsService, harnessService), undefined);
});
test('returns the itemProvider when harness exists with one', () => {
const mockProvider: ICustomizationItemProvider = {
onDidChange: Event.None,
provideChatSessionCustomizations: async () => [],
};
const sessionsService = createMockSessionsService('claude-code');
const harnessService = createMockHarnessService([{ id: 'claude-code', itemProvider: mockProvider }]);
assert.strictEqual(getActiveItemProvider(sessionsService, harnessService), mockProvider);
});
});
suite('getCustomizationTotalCount with itemProvider', () => {
function createItemProvider(items: ICustomizationItem[]): ICustomizationItemProvider {
return {
onDidChange: Event.None,
provideChatSessionCustomizations: async (_token: CancellationToken) => items,
};
}
function makeItem(type: string, name: string): ICustomizationItem {
return { uri: URI.file(`/mock/${name}`), type, name, extensionId: undefined, pluginUri: undefined };
}
test('uses itemProvider counts when provided', async () => {
const promptsService = createMockPromptsService({});
const mcpService = {
servers: observableValue('test', [{ id: 's1' }]),
} as unknown as IMcpService;
const workspaceService = createMockWorkspaceService({ filter: { sources: [PromptsStorage.local] } });
const contextService = createMockWorkspaceContextService([]);
const provider = createItemProvider([
makeItem('agent', 'my-agent'),
makeItem('skill', 'my-skill'),
makeItem('instructions', 'my-instruction'),
makeItem('hook', 'my-hook'),
]);
const total = await getCustomizationTotalCount(promptsService, mcpService, workspaceService, contextService, undefined, provider);
// 4 from provider + 1 mcp = 5
assert.strictEqual(total, 5);
});
test('ignores non-prompt types from itemProvider', async () => {
const promptsService = createMockPromptsService({});
const mcpService = {
servers: observableValue('test', []),
} as unknown as IMcpService;
const workspaceService = createMockWorkspaceService({ filter: { sources: [PromptsStorage.local] } });
const contextService = createMockWorkspaceContextService([]);
const provider = createItemProvider([
makeItem('agent', 'a'),
makeItem('unknown-type', 'x'),
makeItem('prompt', 'p'),
]);
const total = await getCustomizationTotalCount(promptsService, mcpService, workspaceService, contextService, undefined, provider);
// Only 'agent' matches the prompt types (agent, skill, instructions, hook)
assert.strictEqual(total, 1);
});
test('itemProvider returning undefined counts as zero', async () => {
const promptsService = createMockPromptsService({});
const mcpService = {
servers: observableValue('test', [{ id: 's1' }, { id: 's2' }]),
} as unknown as IMcpService;
const workspaceService = createMockWorkspaceService({ filter: { sources: [PromptsStorage.local] } });
const contextService = createMockWorkspaceContextService([]);
const provider: ICustomizationItemProvider = {
onDidChange: Event.None,
provideChatSessionCustomizations: async () => undefined,
};
const total = await getCustomizationTotalCount(promptsService, mcpService, workspaceService, contextService, undefined, provider);
// 0 from provider + 2 mcp = 2
assert.strictEqual(total, 2);
});
test('sums itemProvider counts with plugins and mcp', async () => {
const promptsService = createMockPromptsService({});
const mcpService = {
servers: observableValue('test', [{ id: 's1' }]),
} as unknown as IMcpService;
const workspaceService = createMockWorkspaceService({ filter: { sources: [PromptsStorage.local] } });
const contextService = createMockWorkspaceContextService([]);
const provider = createItemProvider([
makeItem('agent', 'a'),
makeItem('skill', 's'),
]);
const agentPluginService = {
plugins: observableValue('test', [{ id: 'p1' }, { id: 'p2' }, { id: 'p3' }]),
} as unknown as IAgentPluginService;
const total = await getCustomizationTotalCount(promptsService, mcpService, workspaceService, contextService, agentPluginService, provider);
// 2 from provider + 1 mcp + 3 plugins = 6
assert.strictEqual(total, 6);
});
});
suite('data source consistency', () => {
// These tests verify that getSourceCounts uses the same data sources
// as the list widget's loadItems() — the root cause of the count mismatch bug.

View File

@@ -714,10 +714,10 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
}
async $registerChatSessionCustomizationProvider(handle: number, chatSessionType: string, metadata: IChatSessionCustomizationProviderMetadataDto, extensionId: ExtensionIdentifier): Promise<void> {
// In the sessions window, only the Copilot CLI harness is accepted via the
// extension API. Other harnesses (e.g. Claude) are not shown in sessions.
// In the sessions window, only accept harnesses for session types that
// have a registered content provider (i.e., can actually run sessions).
// AHP remote servers register directly via registerExternalHarness.
if (this._environmentService.isSessionsWindow && chatSessionType !== 'copilotcli') {
if (this._environmentService.isSessionsWindow && !this._chatSessionService.getContentProviderSchemes().includes(chatSessionType)) {
return;
}