mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-31 00:10:04 +08:00
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:
committed by
GitHub
parent
4651bee042
commit
439c85ab2d
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}());
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user