mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
chat: replace chatSessionCustomizations with chatSessionCustomizationProvider API (#303017)
* feat: add chat.registerCustomizationProvider extension API Introduces a new proposed extension API (chatCustomizationProvider) that enables extensions to register as customization providers for the AI Customization UI. This replaces core-based harness filtering with extension-driven discovery. Key changes: - New proposed API: vscode.proposed.chatCustomizationProvider.d.ts - ChatCustomizationProvider, ChatCustomizationItem, ChatCustomizationType - chat.registerCustomizationProvider(id, metadata, provider) - ExtHost/MainThread RPC bridge for provider registration - ICustomizationHarnessService extended with registerExternalHarness() for dynamic harness registration from extensions - IHarnessDescriptor.itemProvider for extension-driven item discovery - AICustomizationListWidget falls through to provider when active harness has an itemProvider - Unit tests for dynamic harness registration and lifecycle The static CLI/Claude harness descriptors remain as fallback until extensions adopt the new API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address code review issues - Register chatCustomizationProvider in extensionsApiProposals.ts - Fix duplicate 'descriptor' variable in fetchItemsForSection - Add missing IExternalCustomizationItemProvider import Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make management editor reactive to dynamic harness registration - Track availableHarnesses in autorun (not just activeHarness) - Add ensureHarnessDropdown() to lazily create/remove the dropdown when harnesses are dynamically registered/unregistered - Store sidebarContent and harnessDropdownContainer refs for dynamic dropdown management Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: address API review feedback for ChatCustomizationProvider - Replace ChatCustomizationType enum with TaskGroup-style class pattern (static instances with string-backed ids, extensible via constructor) - Rename provideCustomizations → provideChatCustomizations to match VS Code provider naming conventions - Add comprehensive JSDoc explaining customization lifecycle and caching semantics (cached until onDidChange fires) - Simplify type converter to use class id directly - Bump proposal version to 2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: quality improvements for ChatCustomizationProvider plumbing - Skip storage-based grouping for provider-backed items in the customization list widget. External providers manage their own items, so Workspace/User/Extension categories don't apply — render a flat sorted list instead. - Use AICustomizationManagementSection constants instead of hardcoded string literals in hiddenSections mapping. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: make storage optional on IAICustomizationListItem External provider items don't have a storage origin — the provider manages discovery, so Workspace/User/Extension categories don't apply. Make the storage field optional: - Provider items omit storage entirely (no fake PromptsStorage.local) - Context key overlay only sets storage key when present - Management editor falls back gracefully for provider items - Debug panel accepts optional storage - Built-in path (promptsService) is unchanged — items always have storage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: include external provider data in customization debug report When the active harness has an external provider, the debug report now shows the provider's raw items grouped by type, with name, URI, and description for each item, plus a count of items matching the current section. The promptsService stages are skipped since they don't apply to provider-backed harnesses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: rename ChatCustomization → ChatSessionCustomization per API review Renames all types, methods, events, DTOs, and the proposal file to use the ChatSession prefix as requested in API review feedback. - ChatCustomizationType → ChatSessionCustomizationType - ChatCustomizationItem → ChatSessionCustomizationItem - ChatCustomizationProvider → ChatSessionCustomizationProvider - provideChatCustomizations → provideChatSessionCustomizations - onDidChangeChatCustomizations → onDidChangeChatSessionCustomizations - registerCustomizationProvider → registerChatSessionCustomizationProvider - Proposal: chatCustomizationProvider → chatSessionCustomizationProvider Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: resolve rebase conflicts and remove old ChatSessionCustomizations API Remove the old group-based ChatSessionCustomizations API from the merged PR #304532, which is superseded by our new ChatSessionCustomizationProvider API. The old API used groups, storageLocation, and commands on the chatSessionsService path; the new API uses a flat item model on the customizationHarnessService path. Removed: - IChatSessionCustomizationItem/Group DTOs from extHost.protocol.ts - registerChatSessionCustomizationsProvider from extHostChatSessions.ts, mainThreadChatSessions.ts, extHost.api.impl.ts, chatSessionsProvider.d.ts - ChatSessionCustomizations converter namespace from extHostTypeConverters.ts - mapProviderItemToListItem and old group command fields from list widget Fixed: - registerContributedHarness → registerExternalHarness in fixtures and mainThreadChatSessions.ts - Missing AGENT_MD_FILENAME import in customizationHarnessService.ts - Constructor arg mismatch in browser customizationHarnessService.ts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove version number from new chatSessionCustomizationProvider proposal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: rename id → chatSessionType in registerChatSessionCustomizationProvider Aligns the parameter name with the chatSessions API convention. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add AICustomizationManagementCreateMenuId for extension create actions Extensions can now contribute create/add button actions to the customizations management editor via contributes.menus targeting 'AICustomizationManagementCreate'. Use the aiCustomizationManagementSection context key to scope commands to specific sections (agents, skills, etc.). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: register chat/customizations/create as extension menu contribution point Extensions contribute to 'chat/customizations/create' in package.json contributes.menus, gated by chatSessionCustomizationProvider proposal. Uses MenuId.for() to avoid cross-layer import. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: scope create menu to active harness, replace built-in actions Add aiCustomizationManagementHarness context key set to the active harness ID. Extensions scope create menu contributions using 'when: aiCustomizationManagementHarness == myHarness'. When a harness has menu-contributed create actions, they fully replace the built-in create buttons for that section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address code review feedback - Provider items without storage are now read-only (not editable/deletable) - Wrap provideChatSessionCustomizations in try/catch to handle extension errors - Use menuItem.run() instead of commandService.executeCommand for menu actions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: gate customization provider registration on kill-switch setting Registration is now blocked when chat.customizations.providerApi.enabled is false (default), preventing providers from affecting the UI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -79,8 +79,8 @@ const _allApiProposals = {
|
||||
chatReferenceDiagnostic: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts',
|
||||
},
|
||||
chatSessionCustomizations: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSessionCustomizations.d.ts',
|
||||
chatSessionCustomizationProvider: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSessionCustomizationProvider.d.ts',
|
||||
},
|
||||
chatSessionsProvider: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Schemas } from '../../../base/common/network.js';
|
||||
import { escapeRegExpCharacters } from '../../../base/common/strings.js';
|
||||
import { ThemeIcon } from '../../../base/common/themables.js';
|
||||
import { URI, UriComponents } from '../../../base/common/uri.js';
|
||||
import { Codicon } from '../../../base/common/codicons.js';
|
||||
import { Position } from '../../../editor/common/core/position.js';
|
||||
import { Range } from '../../../editor/common/core/range.js';
|
||||
import { getWordAtText } from '../../../editor/common/core/wordHelper.js';
|
||||
@@ -27,7 +28,7 @@ import { IChatWidget, IChatWidgetService } from '../../contrib/chat/browser/chat
|
||||
import { AgentSessionProviders, getAgentSessionProvider } from '../../contrib/chat/browser/agentSessions/agentSessions.js';
|
||||
import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/attachments/chatDynamicVariables.js';
|
||||
import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/participants/chatAgents.js';
|
||||
import { IPromptFileContext, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { IPromptFileContext, IPromptsService, PromptsStorage } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { isValidPromptType } from '../../contrib/chat/common/promptSyntax/promptTypes.js';
|
||||
import { IChatModel } from '../../contrib/chat/common/model/chatModel.js';
|
||||
import { ChatRequestAgentPart } from '../../contrib/chat/common/requestParser/chatParserTypes.js';
|
||||
@@ -40,9 +41,12 @@ import { ILanguageModelToolsService } from '../../contrib/chat/common/tools/lang
|
||||
import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js';
|
||||
import { IExtensionService } from '../../services/extensions/common/extensions.js';
|
||||
import { Dto } from '../../services/extensions/common/proxyIdentifier.js';
|
||||
import { ExtHostChatAgentsShape2, ExtHostContext, IChatNotebookEditDto, IChatParticipantMetadata, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, IInstructionDto, ISkillDto, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js';
|
||||
import { ExtHostChatAgentsShape2, ExtHostContext, IChatSessionCustomizationItemDto, IChatSessionCustomizationProviderMetadataDto, IChatNotebookEditDto, IChatParticipantMetadata, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, IInstructionDto, ISkillDto, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js';
|
||||
import { NotebookDto } from './mainThreadNotebookDto.js';
|
||||
import { isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.js';
|
||||
import { ICustomizationHarnessService, IExternalCustomizationItem, IExternalCustomizationItemProvider, IHarnessDescriptor } from '../../contrib/chat/common/customizationHarnessService.js';
|
||||
import { AICustomizationManagementSection } from '../../contrib/chat/common/aiCustomizationWorkspaceService.js';
|
||||
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
|
||||
|
||||
interface AgentData {
|
||||
dispose: () => void;
|
||||
@@ -102,6 +106,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
|
||||
private readonly _promptFileProviderEmitters = this._register(new DisposableMap<number, Emitter<void>>());
|
||||
private readonly _promptFileContentRegistrations = this._register(new DisposableMap<number, DisposableMap<string, IDisposable>>());
|
||||
|
||||
private readonly _customizationProviders = this._register(new DisposableMap<number, IDisposable>());
|
||||
private readonly _customizationProviderEmitters = this._register(new DisposableMap<number, Emitter<void>>());
|
||||
|
||||
private readonly _pendingProgress = new Map<string, { progress: (parts: IChatProgress[]) => void; chatSession: IChatModel | undefined }>();
|
||||
private readonly _proxy: ExtHostChatAgentsShape2;
|
||||
|
||||
@@ -122,10 +129,22 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
|
||||
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
|
||||
@IPromptsService private readonly _promptsService: IPromptsService,
|
||||
@ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService,
|
||||
@ICustomizationHarnessService private readonly _customizationHarnessService: ICustomizationHarnessService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2);
|
||||
|
||||
// When the provider API kill-switch is toggled off, dispose all registered providers
|
||||
this._register(this._configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('chat.customizations.providerApi.enabled')) {
|
||||
if (!this._configurationService.getValue<boolean>('chat.customizations.providerApi.enabled')) {
|
||||
this._customizationProviders.clearAndDisposeAll();
|
||||
this._customizationProviderEmitters.clearAndDisposeAll();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._chatService.onDidDisposeSession(e => {
|
||||
for (const resource of e.sessionResource) {
|
||||
this._proxy.$releaseSession(resource);
|
||||
@@ -584,6 +603,80 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
|
||||
emitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
async $registerChatSessionCustomizationProvider(handle: number, chatSessionType: string, metadata: IChatSessionCustomizationProviderMetadataDto, extensionId: ExtensionIdentifier): Promise<void> {
|
||||
if (!this._configurationService.getValue<boolean>('chat.customizations.providerApi.enabled')) {
|
||||
this._logService.trace(`[MainThreadChatAgents2] Customization provider API is disabled, ignoring registration from ${extensionId.value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = await this._extensionService.getExtension(extensionId.value);
|
||||
if (!extension) {
|
||||
this._logService.error(`[MainThreadChatAgents2] Could not find extension for customization provider: ${extensionId.value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const emitter = new Emitter<void>();
|
||||
this._customizationProviderEmitters.set(handle, emitter);
|
||||
|
||||
// Build the item provider that calls back to the ExtHost
|
||||
const itemProvider: IExternalCustomizationItemProvider = {
|
||||
onDidChange: emitter.event,
|
||||
provideChatSessionCustomizations: async (token) => {
|
||||
const items = await this._proxy.$provideChatSessionCustomizations(handle, token);
|
||||
if (!items) {
|
||||
return undefined;
|
||||
}
|
||||
return items.map((item: IChatSessionCustomizationItemDto): IExternalCustomizationItem => ({
|
||||
uri: URI.revive(item.uri),
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
// Convert metadata to a harness descriptor
|
||||
const hiddenSections = metadata.unsupportedTypes?.map(type => {
|
||||
switch (type) {
|
||||
case 'agent': return AICustomizationManagementSection.Agents;
|
||||
case 'skill': return AICustomizationManagementSection.Skills;
|
||||
case 'instructions': return AICustomizationManagementSection.Instructions;
|
||||
case 'prompt': return AICustomizationManagementSection.Prompts;
|
||||
case 'hook': return AICustomizationManagementSection.Hooks;
|
||||
default: return type;
|
||||
}
|
||||
});
|
||||
|
||||
const descriptor: IHarnessDescriptor = {
|
||||
id: chatSessionType,
|
||||
label: metadata.label,
|
||||
icon: metadata.iconId ? ThemeIcon.fromId(metadata.iconId) : ThemeIcon.fromId(Codicon.extensions.id),
|
||||
hiddenSections,
|
||||
workspaceSubpaths: metadata.workspaceSubpaths ? [...metadata.workspaceSubpaths] : undefined,
|
||||
getStorageSourceFilter: () => ({
|
||||
// Extension-provided harnesses manage their own items via the provider,
|
||||
// so we show all sources for storage-filter-based flows.
|
||||
sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin, PromptsStorage.extension],
|
||||
}),
|
||||
itemProvider,
|
||||
};
|
||||
|
||||
const registration = this._customizationHarnessService.registerExternalHarness(descriptor);
|
||||
this._customizationProviders.set(handle, registration);
|
||||
}
|
||||
|
||||
$unregisterChatSessionCustomizationProvider(handle: number): void {
|
||||
this._customizationProviders.deleteAndDispose(handle);
|
||||
this._customizationProviderEmitters.deleteAndDispose(handle);
|
||||
}
|
||||
|
||||
$onDidChangeCustomizations(handle: number): void {
|
||||
const emitter = this._customizationProviderEmitters.get(handle);
|
||||
if (emitter) {
|
||||
emitter.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { raceCancellationError } from '../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { Codicon } from '../../../base/common/codicons.js';
|
||||
import { Emitter } from '../../../base/common/event.js';
|
||||
import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js';
|
||||
import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
|
||||
@@ -13,11 +12,9 @@ import { ResourceMap } from '../../../base/common/map.js';
|
||||
import { revive } from '../../../base/common/marshalling.js';
|
||||
import { autorun, IObservable, observableValue } from '../../../base/common/observable.js';
|
||||
import { isEqual } from '../../../base/common/resources.js';
|
||||
import { ThemeIcon } from '../../../base/common/themables.js';
|
||||
import { URI, UriComponents } from '../../../base/common/uri.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';
|
||||
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
|
||||
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
|
||||
import { ILogService } from '../../../platform/log/common/log.js';
|
||||
import { hasValidDiff, IAgentSession } from '../../contrib/chat/browser/agentSessions/agentSessionsModel.js';
|
||||
@@ -29,14 +26,12 @@ import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments
|
||||
import { awaitStatsForSession } from '../../contrib/chat/common/chat.js';
|
||||
import { getInProgressSessionDescription } from '../../contrib/chat/browser/chatSessions/chatSessionDescription.js';
|
||||
import { IChatContentInlineReference, IChatProgress, IChatService, ResponseModelState } from '../../contrib/chat/common/chatService/chatService.js';
|
||||
import { ChatSessionOptionsMap, ChatSessionStatus, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionCustomizationsProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsService, ReadonlyChatSessionOptionsMap } from '../../contrib/chat/common/chatSessionsService.js';
|
||||
import { ChatAgentLocation, ChatConfiguration } from '../../contrib/chat/common/constants.js';
|
||||
import { ChatSessionOptionsMap, ChatSessionStatus, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsService, ReadonlyChatSessionOptionsMap } from '../../contrib/chat/common/chatSessionsService.js';
|
||||
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
|
||||
import { IChatModel } from '../../contrib/chat/common/model/chatModel.js';
|
||||
import { isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.js';
|
||||
import { IChatAgentRequest } from '../../contrib/chat/common/participants/chatAgents.js';
|
||||
import { IChatDebugService } from '../../contrib/chat/common/chatDebugService.js';
|
||||
import { ICustomizationHarnessService, IHarnessDescriptor } from '../../contrib/chat/common/customizationHarnessService.js';
|
||||
import { PromptsStorage } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';
|
||||
import { IChatArtifactsService } from '../../contrib/chat/common/tools/chatArtifactsService.js';
|
||||
import { IChatTodoListService } from '../../contrib/chat/common/tools/chatTodoListService.js';
|
||||
import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js';
|
||||
@@ -443,7 +438,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
|
||||
readonly controller: MainThreadChatSessionItemController;
|
||||
}>());
|
||||
private readonly _contentProvidersRegistrations = this._register(new DisposableMap<number>());
|
||||
private readonly _customizationsProviderRegistrations = new Map<number, { chatSessionType: string; emitter: Emitter<void>; dispose: () => void }>();
|
||||
private readonly _sessionTypeToHandle = new Map<string, number>();
|
||||
|
||||
private readonly _activeSessions = new ResourceMap<ObservableChatSession>();
|
||||
@@ -465,8 +459,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
|
||||
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@ICustomizationHarnessService private readonly _harnessService: ICustomizationHarnessService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -847,68 +839,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
|
||||
}).catch(err => this._logService.error('Error fetching chat session options', err));
|
||||
}
|
||||
|
||||
$registerChatSessionCustomizationsProvider(handle: number, chatSessionType: string): void {
|
||||
// Kill switch: when disabled, ignore all extension customizations providers
|
||||
if (!this._configurationService.getValue<boolean>(ChatConfiguration.CustomizationsProviderApi)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
const emitter = disposables.add(new Emitter<void>());
|
||||
|
||||
const provider: IChatSessionCustomizationsProvider = {
|
||||
onDidChangeCustomizations: emitter.event,
|
||||
provideCustomizations: (token) => {
|
||||
return this._proxy.$provideChatSessionCustomizations(handle, token).then(groups => {
|
||||
if (!groups) { return undefined; }
|
||||
return groups.map(g => ({
|
||||
...g,
|
||||
items: g.items.map(item => ({
|
||||
...item,
|
||||
uri: URI.revive(item.uri),
|
||||
})),
|
||||
}));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
disposables.add(this._chatSessionsService.registerCustomizationsProvider(chatSessionType, provider));
|
||||
|
||||
// Register as a harness so it appears in the harness picker
|
||||
const contribution = this._chatSessionsService.getChatSessionContribution(chatSessionType);
|
||||
if (contribution) {
|
||||
const icon = ThemeIcon.isThemeIcon(contribution.icon)
|
||||
? contribution.icon
|
||||
: ThemeIcon.fromId(Codicon.copilot.id);
|
||||
const filter = { sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.extension, PromptsStorage.plugin] };
|
||||
const harnessDescriptor: IHarnessDescriptor = {
|
||||
id: chatSessionType,
|
||||
label: contribution.displayName ?? chatSessionType,
|
||||
icon,
|
||||
hideGenerateButton: true,
|
||||
getStorageSourceFilter: () => filter,
|
||||
};
|
||||
disposables.add(this._harnessService.registerContributedHarness(harnessDescriptor));
|
||||
}
|
||||
|
||||
this._customizationsProviderRegistrations.set(handle, { chatSessionType, emitter, dispose: () => disposables.dispose() });
|
||||
}
|
||||
|
||||
$unregisterChatSessionCustomizationsProvider(handle: number): void {
|
||||
const reg = this._customizationsProviderRegistrations.get(handle);
|
||||
if (reg) {
|
||||
reg.dispose();
|
||||
this._customizationsProviderRegistrations.delete(handle);
|
||||
}
|
||||
}
|
||||
|
||||
$onDidChangeChatSessionCustomizations(handle: number): void {
|
||||
const reg = this._customizationsProviderRegistrations.get(handle);
|
||||
if (reg) {
|
||||
reg.emitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
for (const session of this._activeSessions.values()) {
|
||||
session.dispose();
|
||||
@@ -920,11 +850,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
|
||||
}
|
||||
this._sessionDisposables.clear();
|
||||
|
||||
for (const reg of this._customizationsProviderRegistrations.values()) {
|
||||
reg.dispose();
|
||||
}
|
||||
this._customizationsProviderRegistrations.clear();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -1664,10 +1664,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
checkProposedApiEnabled(extension, 'chatSessionsProvider');
|
||||
return extHostChatSessions.createChatSessionItemController(extension, chatSessionType, refreshHandler);
|
||||
},
|
||||
registerChatSessionCustomizationsProvider(chatSessionType: string, provider: vscode.ChatSessionCustomizationsProvider): vscode.Disposable {
|
||||
checkProposedApiEnabled(extension, 'chatSessionsProvider');
|
||||
return extHostChatSessions.registerChatSessionCustomizationsProvider(extension, chatSessionType, provider);
|
||||
},
|
||||
registerChatSessionContentProvider(scheme: string, provider: vscode.ChatSessionContentProvider, chatParticipant: vscode.ChatParticipant, capabilities?: vscode.ChatSessionCapabilities) {
|
||||
checkProposedApiEnabled(extension, 'chatSessionsProvider');
|
||||
return extHostChatSessions.registerChatSessionContentProvider(extension, scheme, chatParticipant, provider, capabilities);
|
||||
@@ -1741,6 +1737,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
checkProposedApiEnabled(extension, 'chatPromptFiles');
|
||||
return extHostChatAgents2.onDidChangeSkills(listener, thisArgs, disposables);
|
||||
},
|
||||
registerChatSessionCustomizationProvider(chatSessionType: string, metadata: vscode.ChatSessionCustomizationProviderMetadata, provider: vscode.ChatSessionCustomizationProvider): vscode.Disposable {
|
||||
checkProposedApiEnabled(extension, 'chatSessionCustomizationProvider');
|
||||
return extHostChatAgents2.registerChatSessionCustomizationProvider(extension, chatSessionType, metadata, provider);
|
||||
},
|
||||
};
|
||||
|
||||
// namespace: lm
|
||||
@@ -2130,7 +2130,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
ChatLocation: extHostTypes.ChatLocation,
|
||||
ChatSessionStatus: extHostTypes.ChatSessionStatus,
|
||||
ChatSessionCustomizationType: extHostTypes.ChatSessionCustomizationType,
|
||||
ChatSessionCustomizationStorageLocation: extHostTypes.ChatSessionCustomizationStorageLocation,
|
||||
ChatDebugLogLevel: extHostTypes.ChatDebugLogLevel,
|
||||
ChatDebugToolCallResult: extHostTypes.ChatDebugToolCallResult,
|
||||
ChatDebugHookResult: extHostTypes.ChatDebugHookResult,
|
||||
|
||||
@@ -1587,6 +1587,9 @@ export interface MainThreadChatAgentsShape2 extends IChatAgentProgressShape, IDi
|
||||
$registerPromptFileProvider(handle: number, type: string, extension: ExtensionIdentifier): void;
|
||||
$unregisterPromptFileProvider(handle: number): void;
|
||||
$onDidChangePromptFiles(handle: number): void;
|
||||
$registerChatSessionCustomizationProvider(handle: number, chatSessionType: string, metadata: IChatSessionCustomizationProviderMetadataDto, extension: ExtensionIdentifier): void;
|
||||
$unregisterChatSessionCustomizationProvider(handle: number): void;
|
||||
$onDidChangeCustomizations(handle: number): void;
|
||||
$registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void;
|
||||
$unregisterAgentCompletionsProvider(handle: number, id: string): void;
|
||||
$updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void;
|
||||
@@ -1654,6 +1657,7 @@ export interface ExtHostChatAgentsShape2 {
|
||||
$releaseSession(sessionResource: UriComponents): void;
|
||||
$detectChatParticipant(handle: number, request: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise<IChatParticipantDetectionResult | null | undefined>;
|
||||
$providePromptFiles(handle: number, type: PromptsType, context: IPromptFileContext, token: CancellationToken): Promise<Dto<IPromptFileResource>[] | undefined>;
|
||||
$provideChatSessionCustomizations(handle: number, token: CancellationToken): Promise<IChatSessionCustomizationItemDto[] | undefined>;
|
||||
$setRequestTools(requestId: string, tools: UserSelectedTools): void;
|
||||
$setYieldRequested(requestId: string, value: boolean): void;
|
||||
$acceptActiveChatSession(sessionResource: UriComponents | undefined): void;
|
||||
@@ -1673,6 +1677,20 @@ export interface IInstructionDto {
|
||||
export interface ISkillDto {
|
||||
uri: UriComponents;
|
||||
}
|
||||
|
||||
export interface IChatSessionCustomizationProviderMetadataDto {
|
||||
readonly label: string;
|
||||
readonly iconId?: string;
|
||||
readonly unsupportedTypes?: readonly string[];
|
||||
readonly workspaceSubpaths?: readonly string[];
|
||||
}
|
||||
|
||||
export interface IChatSessionCustomizationItemDto {
|
||||
readonly uri: UriComponents;
|
||||
readonly type: string;
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
}
|
||||
export interface IChatParticipantMetadata {
|
||||
participant: string;
|
||||
command?: string;
|
||||
@@ -3628,21 +3646,6 @@ export interface IChatSessionItemsChange {
|
||||
readonly removed: readonly UriComponents[];
|
||||
}
|
||||
|
||||
export interface IChatSessionCustomizationItemDto {
|
||||
readonly label: string;
|
||||
readonly description?: string;
|
||||
readonly uri: UriComponents;
|
||||
readonly storageLocation: number;
|
||||
readonly icon?: ThemeIcon;
|
||||
}
|
||||
|
||||
export interface IChatSessionCustomizationItemGroupDto {
|
||||
readonly id: string;
|
||||
readonly items: IChatSessionCustomizationItemDto[];
|
||||
readonly commands?: ICommandDto[];
|
||||
readonly itemCommands?: ICommandDto[];
|
||||
}
|
||||
|
||||
export interface MainThreadChatSessionsShape extends IDisposable {
|
||||
$registerChatSessionItemController(controllerHandle: number, chatSessionType: string): void;
|
||||
$unregisterChatSessionItemController(controllerHandle: number): void;
|
||||
@@ -3654,10 +3657,6 @@ export interface MainThreadChatSessionsShape extends IDisposable {
|
||||
$onDidChangeChatSessionOptions(handle: number, sessionResource: UriComponents, updates: Record<string, string | IChatSessionProviderOptionItem>): void;
|
||||
$onDidChangeChatSessionProviderOptions(handle: number): void;
|
||||
|
||||
$registerChatSessionCustomizationsProvider(handle: number, chatSessionType: string): void;
|
||||
$unregisterChatSessionCustomizationsProvider(handle: number): void;
|
||||
$onDidChangeChatSessionCustomizations(handle: number): void;
|
||||
|
||||
$handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise<void>;
|
||||
$handleAnchorResolve(handle: number, sessionResource: UriComponents, requestId: string, requestHandle: string, anchor: Dto<IChatContentInlineReference>): void;
|
||||
$handleProgressComplete(handle: number, sessionResource: UriComponents, requestId: string): void;
|
||||
@@ -3676,7 +3675,6 @@ export interface ExtHostChatSessionsShape {
|
||||
$invokeOptionGroupSearch(providerHandle: number, optionGroupId: string, query: string, token: CancellationToken): Promise<IChatSessionProviderOptionItem[]>;
|
||||
$provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: Record<string, string | IChatSessionProviderOptionItem | undefined>, token: CancellationToken): Promise<void>;
|
||||
$forkChatSession(providerHandle: number, sessionResource: UriComponents, request: IChatSessionRequestHistoryItemDto | undefined, token: CancellationToken): Promise<Dto<IChatSessionItem>>;
|
||||
$provideChatSessionCustomizations(handle: number, token: CancellationToken): Promise<IChatSessionCustomizationItemGroupDto[] | undefined>;
|
||||
}
|
||||
|
||||
export interface GitRefQueryDto {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js'
|
||||
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
|
||||
import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js';
|
||||
import { Dto } from '../../services/extensions/common/proxyIdentifier.js';
|
||||
import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatAgentProgressShape, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IExtensionChatAgentMetadata, IInstructionDto, IMainContext, ISkillDto, MainContext, MainThreadChatAgentsShape2 } from './extHost.protocol.js';
|
||||
import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatAgentProgressShape, IChatSessionCustomizationItemDto, IChatSessionCustomizationProviderMetadataDto, IChatProgressDto, IChatSessionContextDto, ICustomAgentDto, IExtensionChatAgentMetadata, IInstructionDto, IMainContext, ISkillDto, MainContext, MainThreadChatAgentsShape2 } from './extHost.protocol.js';
|
||||
import { CommandsConverter, ExtHostCommands } from './extHostCommands.js';
|
||||
import { ExtHostDiagnostics } from './extHostDiagnostics.js';
|
||||
import { ExtHostDocuments } from './extHostDocuments.js';
|
||||
@@ -474,6 +474,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
private static _contributionsProviderIdPool = 0;
|
||||
private readonly _promptFileProviders = new Map<number, { extension: IExtensionDescription; provider: vscode.ChatCustomAgentProvider | vscode.ChatInstructionsProvider | vscode.ChatPromptFileProvider | vscode.ChatSkillProvider }>();
|
||||
|
||||
private static _customizationProviderIdPool = 0;
|
||||
private readonly _customizationProviders = new Map<number, { extension: IExtensionDescription; provider: vscode.ChatSessionCustomizationProvider }>();
|
||||
|
||||
private readonly _sessionDisposables: DisposableResourceMap<DisposableStore> = this._register(new DisposableResourceMap());
|
||||
private readonly _completionDisposables: DisposableMap<number, DisposableStore> = this._register(new DisposableMap());
|
||||
|
||||
@@ -661,6 +664,58 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
|
||||
return resources;
|
||||
}
|
||||
|
||||
registerChatSessionCustomizationProvider(extension: IExtensionDescription, chatSessionType: string, metadata: vscode.ChatSessionCustomizationProviderMetadata, provider: vscode.ChatSessionCustomizationProvider): vscode.Disposable {
|
||||
const handle = ExtHostChatAgents2._customizationProviderIdPool++;
|
||||
this._customizationProviders.set(handle, { extension, provider });
|
||||
|
||||
const metadataDto: IChatSessionCustomizationProviderMetadataDto = {
|
||||
label: metadata.label,
|
||||
iconId: metadata.iconId,
|
||||
unsupportedTypes: metadata.unsupportedTypes?.map(t => typeConvert.ChatSessionCustomizationType.from(t)),
|
||||
workspaceSubpaths: metadata.workspaceSubpaths ? [...metadata.workspaceSubpaths] : undefined,
|
||||
};
|
||||
|
||||
this._proxy.$registerChatSessionCustomizationProvider(handle, chatSessionType, metadataDto, extension.identifier);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
if (provider.onDidChange) {
|
||||
disposables.add(provider.onDidChange(() => {
|
||||
this._proxy.$onDidChangeCustomizations(handle);
|
||||
}));
|
||||
}
|
||||
|
||||
disposables.add(toDisposable(() => {
|
||||
this._customizationProviders.delete(handle);
|
||||
this._proxy.$unregisterChatSessionCustomizationProvider(handle);
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
async $provideChatSessionCustomizations(handle: number, token: CancellationToken): Promise<IChatSessionCustomizationItemDto[] | undefined> {
|
||||
const providerData = this._customizationProviders.get(handle);
|
||||
if (!providerData) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const items = await providerData.provider.provideChatSessionCustomizations(token);
|
||||
if (!items) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return items.map(item => ({
|
||||
uri: item.uri,
|
||||
type: typeConvert.ChatSessionCustomizationType.from(item.type),
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
}));
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async $detectChatParticipant(handle: number, requestDto: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise<vscode.ChatParticipantDetectionResult | null | undefined> {
|
||||
const detector = this._participantDetectionProviders.get(handle);
|
||||
if (!detector) {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSe
|
||||
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
|
||||
import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/participants/chatAgents.js';
|
||||
import { Proxied } from '../../services/extensions/common/proxyIdentifier.js';
|
||||
import { ChatSessionContentContextDto, IChatSessionDto, ExtHostChatSessionsShape, IChatAgentProgressShape, IChatSessionRequestHistoryItemDto, IChatSessionProviderOptions, MainContext, MainThreadChatSessionsShape, IChatNewSessionRequestDto, IChatSessionCustomizationItemGroupDto } from './extHost.protocol.js';
|
||||
import { ChatSessionContentContextDto, IChatSessionDto, ExtHostChatSessionsShape, IChatAgentProgressShape, IChatSessionRequestHistoryItemDto, IChatSessionProviderOptions, MainContext, MainThreadChatSessionsShape, IChatNewSessionRequestDto } from './extHost.protocol.js';
|
||||
import { ChatAgentResponseStream } from './extHostChatAgents2.js';
|
||||
import { CommandsConverter, ExtHostCommands } from './extHostCommands.js';
|
||||
import { ExtHostLanguageModels } from './extHostLanguageModels.js';
|
||||
@@ -331,9 +331,6 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
|
||||
*/
|
||||
private readonly _providerOptionGroups = new Map<number, readonly vscode.ChatSessionProviderOptionGroup[]>();
|
||||
|
||||
private _customizationsHandlePool = 0;
|
||||
private readonly _customizationsProviders = new Map<number, { provider: vscode.ChatSessionCustomizationsProvider; chatSessionType: string; disposable: DisposableStore }>();
|
||||
|
||||
constructor(
|
||||
private readonly commands: ExtHostCommands,
|
||||
private readonly _languageModels: ExtHostLanguageModels,
|
||||
@@ -880,43 +877,4 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
|
||||
controllerData.onDidChangeChatSessionItemStateEmitter.fire(item);
|
||||
}
|
||||
|
||||
registerChatSessionCustomizationsProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionCustomizationsProvider): vscode.Disposable {
|
||||
const handle = this._customizationsHandlePool++;
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
this._customizationsProviders.set(handle, { provider, chatSessionType, disposable: disposables });
|
||||
|
||||
if (provider.onDidChangeCustomizations) {
|
||||
disposables.add(provider.onDidChangeCustomizations(() => {
|
||||
this._proxy.$onDidChangeChatSessionCustomizations(handle);
|
||||
}));
|
||||
}
|
||||
|
||||
this._proxy.$registerChatSessionCustomizationsProvider(handle, chatSessionType);
|
||||
|
||||
disposables.add(toDisposable(() => {
|
||||
this._customizationsProviders.delete(handle);
|
||||
this._proxy.$unregisterChatSessionCustomizationsProvider(handle);
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
async $provideChatSessionCustomizations(handle: number, token: CancellationToken): Promise<IChatSessionCustomizationItemGroupDto[] | undefined> {
|
||||
const entry = this._customizationsProviders.get(handle);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const result = await entry.provider.provideCustomizations(token);
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
return result.map(g => typeConvert.ChatSessionCustomizations.fromGroup(g));
|
||||
} catch (err) {
|
||||
this._logService.error(`[ExtHostChatSessions] provideCustomizations failed:`, err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3512,6 +3512,12 @@ export namespace ChatLocation {
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ChatSessionCustomizationType {
|
||||
export function from(type: types.ChatSessionCustomizationType): string {
|
||||
return type.id;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ChatPromptReference {
|
||||
export function to(variable: IChatRequestVariableEntry, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], logService: ILogService): vscode.ChatPromptReference | undefined {
|
||||
let value: vscode.ChatPromptReference['value'] = variable.value;
|
||||
@@ -4208,33 +4214,3 @@ export namespace ChatSessionItem {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ChatSessionCustomizations {
|
||||
function commandFrom(cmd: vscode.Command): extHostProtocol.ICommandDto {
|
||||
return {
|
||||
id: cmd.command,
|
||||
title: cmd.title,
|
||||
tooltip: cmd.tooltip,
|
||||
arguments: cmd.arguments,
|
||||
};
|
||||
}
|
||||
|
||||
export function fromItem(item: vscode.ChatSessionCustomizationItem): extHostProtocol.IChatSessionCustomizationItemDto {
|
||||
return {
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
uri: item.uri,
|
||||
storageLocation: item.storageLocation,
|
||||
icon: item.icon ? { id: item.icon.id, color: item.icon.color } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function fromGroup(group: vscode.ChatSessionCustomizationItemGroup): extHostProtocol.IChatSessionCustomizationItemGroupDto {
|
||||
return {
|
||||
id: group.id,
|
||||
items: group.items.map(fromItem),
|
||||
commands: group.commands?.map(commandFrom),
|
||||
itemCommands: group.itemCommands?.map(commandFrom),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3568,21 +3568,14 @@ export enum ChatSessionStatus {
|
||||
NeedsInput = 3
|
||||
}
|
||||
|
||||
export enum ChatSessionCustomizationType {
|
||||
Agents = 'agents',
|
||||
Skills = 'skills',
|
||||
AgentInstructions = 'agentInstructions',
|
||||
ContextInstructions = 'contextInstructions',
|
||||
OnDemandInstructions = 'onDemandInstructions',
|
||||
Prompts = 'prompts',
|
||||
}
|
||||
export class ChatSessionCustomizationType {
|
||||
static readonly Agent = new ChatSessionCustomizationType('agent');
|
||||
static readonly Skill = new ChatSessionCustomizationType('skill');
|
||||
static readonly Instructions = new ChatSessionCustomizationType('instructions');
|
||||
static readonly Prompt = new ChatSessionCustomizationType('prompt');
|
||||
static readonly Hook = new ChatSessionCustomizationType('hook');
|
||||
|
||||
export enum ChatSessionCustomizationStorageLocation {
|
||||
Workspace = 1,
|
||||
User = 2,
|
||||
Extension = 3,
|
||||
Plugin = 4,
|
||||
BuiltIn = 5,
|
||||
constructor(public readonly id: string) { }
|
||||
}
|
||||
|
||||
export enum ChatDebugLogLevel {
|
||||
|
||||
@@ -74,7 +74,6 @@ suite('ObservableChatSession', function () {
|
||||
$onDidChangeChatSessionItemState: sinon.stub(),
|
||||
$newChatSessionItem: sinon.stub().resolves(undefined),
|
||||
$forkChatSession: sinon.stub().resolves(undefined),
|
||||
$provideChatSessionCustomizations: sinon.stub().resolves(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -525,7 +524,6 @@ suite('MainThreadChatSessions', function () {
|
||||
$onDidChangeChatSessionItemState: sinon.stub(),
|
||||
$newChatSessionItem: sinon.stub().resolves(undefined),
|
||||
$forkChatSession: sinon.stub().resolves(undefined),
|
||||
$provideChatSessionCustomizations: sinon.stub().resolves(undefined),
|
||||
};
|
||||
|
||||
const extHostContext = new class implements IExtHostContext {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { IPromptsService, PromptsStorage, IPromptPath } from '../../common/promp
|
||||
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
|
||||
import { IAICustomizationWorkspaceService, applyStorageSourceFilter, IStorageSourceFilter } from '../../common/aiCustomizationWorkspaceService.js';
|
||||
import { AICustomizationManagementSection } from './aiCustomizationManagement.js';
|
||||
import { IExternalCustomizationItemProvider } from '../../common/customizationHarnessService.js';
|
||||
|
||||
/**
|
||||
* Maps section ID to prompt type. Duplicated from aiCustomizationListWidget
|
||||
@@ -34,7 +35,7 @@ function sectionToPromptType(section: AICustomizationManagementSection): Prompts
|
||||
* Snapshot of the list widget's internal state, passed in to avoid coupling.
|
||||
*/
|
||||
export interface IDebugWidgetState {
|
||||
readonly allItems: readonly { readonly storage: PromptsStorage }[];
|
||||
readonly allItems: readonly { readonly storage?: PromptsStorage }[];
|
||||
readonly displayEntries: readonly { type: string; label?: string; count?: number; collapsed?: boolean }[];
|
||||
}
|
||||
|
||||
@@ -47,6 +48,7 @@ export async function generateCustomizationDebugReport(
|
||||
promptsService: IPromptsService,
|
||||
workspaceService: IAICustomizationWorkspaceService,
|
||||
widgetState: IDebugWidgetState,
|
||||
externalProvider?: IExternalCustomizationItemProvider,
|
||||
): Promise<string> {
|
||||
const promptType = sectionToPromptType(section);
|
||||
const filter = workspaceService.getStorageSourceFilter(promptType);
|
||||
@@ -67,14 +69,55 @@ export async function generateCustomizationDebugReport(
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
await appendRawServiceData(lines, promptsService, promptType);
|
||||
await appendFilteredData(lines, promptsService, promptType, filter);
|
||||
if (externalProvider) {
|
||||
await appendExternalProviderData(lines, externalProvider, promptType);
|
||||
} else {
|
||||
await appendRawServiceData(lines, promptsService, promptType);
|
||||
await appendFilteredData(lines, promptsService, promptType, filter);
|
||||
}
|
||||
appendWidgetState(lines, widgetState);
|
||||
await appendSourceFolders(lines, promptsService, promptType);
|
||||
if (!externalProvider) {
|
||||
await appendSourceFolders(lines, promptsService, promptType);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function appendExternalProviderData(lines: string[], provider: IExternalCustomizationItemProvider, promptType: PromptsType): Promise<void> {
|
||||
lines.push('--- External Provider Data ---');
|
||||
|
||||
const allItems = await provider.provideChatSessionCustomizations(CancellationToken.None);
|
||||
if (!allItems) {
|
||||
lines.push(' Provider returned undefined');
|
||||
lines.push('');
|
||||
return;
|
||||
}
|
||||
|
||||
lines.push(` Total items from provider: ${allItems.length}`);
|
||||
|
||||
// Group by type for summary
|
||||
const byType = new Map<string, typeof allItems>();
|
||||
for (const item of allItems) {
|
||||
const existing = byType.get(item.type) ?? [];
|
||||
existing.push(item);
|
||||
byType.set(item.type, existing);
|
||||
}
|
||||
for (const [type, items] of byType) {
|
||||
lines.push(` ${type}: ${items.length} items`);
|
||||
for (const item of items) {
|
||||
lines.push(` ${item.name} — ${item.uri.fsPath ?? item.uri.toString()}`);
|
||||
if (item.description) {
|
||||
lines.push(` desc: ${item.description}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show items matching the current section
|
||||
const sectionItems = allItems.filter(i => i.type === promptType);
|
||||
lines.push(` Items matching current section (${promptType}): ${sectionItems.length}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
async function appendRawServiceData(lines: string[], promptsService: IPromptsService, promptType: PromptsType): Promise<void> {
|
||||
lines.push('--- Stage 1: Raw PromptsService Data ---');
|
||||
|
||||
|
||||
+106
-179
@@ -6,7 +6,7 @@
|
||||
import './media/aiCustomizationManagement.css';
|
||||
import * as DOM from '../../../../../base/browser/dom.js';
|
||||
import { ActionBar } from '../../../../../base/browser/ui/actionbar/actionbar.js';
|
||||
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
|
||||
import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { Emitter, Event } from '../../../../../base/common/event.js';
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { autorun } from '../../../../../base/common/observable.js';
|
||||
@@ -22,7 +22,7 @@ import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../.
|
||||
import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
|
||||
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
|
||||
import { agentIcon, instructionsIcon, promptIcon, skillIcon, hookIcon, userIcon, workspaceIcon, extensionIcon, pluginIcon, builtinIcon } from './aiCustomizationIcons.js';
|
||||
import { AI_CUSTOMIZATION_ITEM_STORAGE_KEY, AI_CUSTOMIZATION_ITEM_TYPE_KEY, AI_CUSTOMIZATION_ITEM_URI_KEY, AI_CUSTOMIZATION_ITEM_PLUGIN_URI_KEY, AICustomizationManagementItemMenuId, AICustomizationManagementSection, BUILTIN_STORAGE, AI_CUSTOMIZATION_ITEM_DISABLED_KEY } from './aiCustomizationManagement.js';
|
||||
import { AI_CUSTOMIZATION_ITEM_STORAGE_KEY, AI_CUSTOMIZATION_ITEM_TYPE_KEY, AI_CUSTOMIZATION_ITEM_URI_KEY, AI_CUSTOMIZATION_ITEM_PLUGIN_URI_KEY, AICustomizationManagementItemMenuId, AICustomizationManagementCreateMenuId, AICustomizationManagementSection, BUILTIN_STORAGE, AI_CUSTOMIZATION_ITEM_DISABLED_KEY } from './aiCustomizationManagement.js';
|
||||
import { IAgentPluginService } from '../../common/plugins/agentPluginService.js';
|
||||
import { InputBox } from '../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { defaultButtonStyles, defaultInputBoxStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
|
||||
@@ -32,7 +32,7 @@ import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabe
|
||||
import { matchesContiguousSubString, IMatch } from '../../../../../base/common/filters.js';
|
||||
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
|
||||
import { Button, ButtonWithDropdown } from '../../../../../base/browser/ui/button/button.js';
|
||||
import { IMenuService } from '../../../../../platform/actions/common/actions.js';
|
||||
import { IMenuService, MenuItemAction } from '../../../../../platform/actions/common/actions.js';
|
||||
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { createActionViewItem, getContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
|
||||
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
|
||||
@@ -53,13 +53,12 @@ import { parse as parseJSONC } from '../../../../../base/common/json.js';
|
||||
import { Schemas } from '../../../../../base/common/network.js';
|
||||
import { OS } from '../../../../../base/common/platform.js';
|
||||
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
||||
import { ICustomizationHarnessService, matchesWorkspaceSubpath, matchesInstructionFileFilter } from '../../common/customizationHarnessService.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { getCleanPromptName, isInClaudeRulesFolder } from '../../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { ICustomizationHarnessService, IExternalCustomizationItem, IExternalCustomizationItemProvider, matchesWorkspaceSubpath, matchesInstructionFileFilter } from '../../common/customizationHarnessService.js';
|
||||
import { evaluateApplyToPattern } from '../../common/promptSyntax/computeAutomaticInstructions.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { isInClaudeRulesFolder, getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
|
||||
import { IChatSessionsService, IChatSessionCustomizationItem, IChatSessionCustomizationItemGroup } from '../../common/chatSessionsService.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
|
||||
export { truncateToFirstLine } from './aiCustomizationListWidgetUtils.js';
|
||||
|
||||
@@ -94,7 +93,8 @@ export interface IAICustomizationListItem {
|
||||
readonly name: string;
|
||||
readonly filename: string;
|
||||
readonly description?: string;
|
||||
readonly storage: PromptsStorage;
|
||||
/** Storage origin. Set by core when items come from promptsService; omitted for external provider items. */
|
||||
readonly storage?: PromptsStorage;
|
||||
readonly promptType: PromptsType;
|
||||
readonly disabled: boolean;
|
||||
/** When set, overrides `storage` for display grouping purposes. */
|
||||
@@ -419,7 +419,7 @@ class AICustomizationItemRenderer implements IListRenderer<IFileItemEntry, IAICu
|
||||
}
|
||||
|
||||
// Inline action bar from menu
|
||||
const context = {
|
||||
const context: Record<string, unknown> = {
|
||||
uri: element.uri.toString(),
|
||||
name: element.name,
|
||||
promptType: element.promptType,
|
||||
@@ -430,10 +430,12 @@ class AICustomizationItemRenderer implements IListRenderer<IFileItemEntry, IAICu
|
||||
// Create scoped context key service with item-specific keys for when-clause filtering
|
||||
const overlayPairs: [string, string | boolean][] = [
|
||||
[AI_CUSTOMIZATION_ITEM_TYPE_KEY, element.promptType],
|
||||
[AI_CUSTOMIZATION_ITEM_STORAGE_KEY, element.storage],
|
||||
[AI_CUSTOMIZATION_ITEM_URI_KEY, element.uri.toString()],
|
||||
[AI_CUSTOMIZATION_ITEM_DISABLED_KEY, element.disabled],
|
||||
];
|
||||
if (element.storage) {
|
||||
overlayPairs.push([AI_CUSTOMIZATION_ITEM_STORAGE_KEY, element.storage]);
|
||||
}
|
||||
if (element.pluginUri) {
|
||||
overlayPairs.push([AI_CUSTOMIZATION_ITEM_PLUGIN_URI_KEY, element.pluginUri.toString()]);
|
||||
}
|
||||
@@ -480,92 +482,6 @@ export function sectionToPromptType(section: AICustomizationManagementSection):
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a management section to the well-known customization group IDs
|
||||
* used by {@link IChatSessionCustomizationsProvider}.
|
||||
*/
|
||||
function sectionToCustomizationGroupIds(section: AICustomizationManagementSection): string[] {
|
||||
switch (section) {
|
||||
case AICustomizationManagementSection.Agents:
|
||||
return ['agents'];
|
||||
case AICustomizationManagementSection.Skills:
|
||||
return ['skills'];
|
||||
case AICustomizationManagementSection.Instructions:
|
||||
return ['agentInstructions', 'contextInstructions', 'onDemandInstructions'];
|
||||
case AICustomizationManagementSection.Prompts:
|
||||
return ['prompts'];
|
||||
case AICustomizationManagementSection.Hooks:
|
||||
return ['hooks'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the numeric {@link IChatSessionCustomizationItem.storageLocation}
|
||||
* to its corresponding {@link PromptsStorage}.
|
||||
*/
|
||||
function storageLocationToPromptsStorage(storageLocation: number): PromptsStorage {
|
||||
switch (storageLocation) {
|
||||
case 1: // Workspace
|
||||
return PromptsStorage.local;
|
||||
case 2: // User
|
||||
return PromptsStorage.user;
|
||||
case 3: // Extension
|
||||
return PromptsStorage.extension;
|
||||
case 4: // Plugin
|
||||
return PromptsStorage.plugin;
|
||||
case 5: // BuiltIn
|
||||
return PromptsStorage.extension;
|
||||
default:
|
||||
return PromptsStorage.local;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the numeric {@link IChatSessionCustomizationItem.storageLocation}
|
||||
* to an optional groupKey override for the list display.
|
||||
*/
|
||||
function storageLocationToGroupKey(storageLocation: number): string | undefined {
|
||||
// BuiltIn items are grouped under the special "builtin" key
|
||||
if (storageLocation === 5) {
|
||||
return BUILTIN_STORAGE;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link IChatSessionCustomizationItem} from the provider
|
||||
* into an {@link IAICustomizationListItem} for the list widget.
|
||||
*/
|
||||
function mapProviderItemToListItem(
|
||||
item: IChatSessionCustomizationItem,
|
||||
groupId: string,
|
||||
promptType: PromptsType,
|
||||
): IAICustomizationListItem {
|
||||
const storage = storageLocationToPromptsStorage(item.storageLocation);
|
||||
const filename = basename(item.uri);
|
||||
// For instructions, use the group id as the groupKey so items
|
||||
// are placed in the correct sub-group (agent-instructions, etc.)
|
||||
const instructionGroupIds = new Set(['agentInstructions', 'contextInstructions', 'onDemandInstructions']);
|
||||
const groupKey = instructionGroupIds.has(groupId)
|
||||
? groupId.replace('Instructions', '-instructions')
|
||||
: storageLocationToGroupKey(item.storageLocation);
|
||||
|
||||
return {
|
||||
id: item.uri.toString(),
|
||||
uri: item.uri,
|
||||
name: item.label,
|
||||
filename,
|
||||
description: item.description,
|
||||
storage,
|
||||
promptType,
|
||||
disabled: false,
|
||||
groupKey,
|
||||
typeIcon: item.icon,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An ordered create action for the add button.
|
||||
*/
|
||||
@@ -604,8 +520,6 @@ export class AICustomizationListWidget extends Disposable {
|
||||
private displayEntries: IListEntry[] = [];
|
||||
private searchQuery: string = '';
|
||||
private readonly collapsedGroups = new Set<string>();
|
||||
private _currentGroupCommands: IChatSessionCustomizationItemGroup['commands'];
|
||||
private _currentGroupItemCommands: IChatSessionCustomizationItemGroup['itemCommands'];
|
||||
private readonly dropdownActionDisposables = this._register(new DisposableStore());
|
||||
|
||||
private readonly delayedFilter = new Delayer<void>(200);
|
||||
@@ -642,7 +556,6 @@ export class AICustomizationListWidget extends Disposable {
|
||||
@IAgentPluginService private readonly agentPluginService: IAgentPluginService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
|
||||
) {
|
||||
super();
|
||||
this.element = $('.ai-customization-list-widget');
|
||||
@@ -662,6 +575,24 @@ export class AICustomizationListWidget extends Disposable {
|
||||
this.refresh();
|
||||
}));
|
||||
|
||||
// Refresh when available harnesses change (external provider registered/unregistered)
|
||||
this._register(autorun(reader => {
|
||||
this.harnessService.availableHarnesses.read(reader);
|
||||
this.refresh();
|
||||
}));
|
||||
|
||||
// Subscribe to the active provider's onDidChange event
|
||||
const providerChangeDisposable = this._register(new MutableDisposable());
|
||||
this._register(autorun(reader => {
|
||||
this.harnessService.activeHarness.read(reader);
|
||||
const activeDescriptor = this.harnessService.getActiveDescriptor();
|
||||
if (activeDescriptor.itemProvider) {
|
||||
providerChangeDisposable.value = activeDescriptor.itemProvider.onDidChange(() => this.refresh());
|
||||
} else {
|
||||
providerChangeDisposable.clear();
|
||||
}
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
private create(): void {
|
||||
@@ -776,7 +707,6 @@ 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.chatSessionsService.onDidChangeCustomizations(() => this.refresh()));
|
||||
|
||||
// Refresh on file deletions so the list updates after inline delete actions
|
||||
this._register(this.fileService.onDidFilesChange(e => {
|
||||
@@ -810,7 +740,7 @@ export class AICustomizationListWidget extends Disposable {
|
||||
const item = e.element.item;
|
||||
|
||||
// Create context for the menu actions
|
||||
const context = {
|
||||
const context: Record<string, unknown> = {
|
||||
uri: item.uri.toString(),
|
||||
name: item.name,
|
||||
promptType: item.promptType,
|
||||
@@ -821,10 +751,12 @@ export class AICustomizationListWidget extends Disposable {
|
||||
// Create scoped context key service with item-specific keys for when-clause filtering
|
||||
const overlayPairs: [string, string | boolean][] = [
|
||||
[AI_CUSTOMIZATION_ITEM_TYPE_KEY, item.promptType],
|
||||
[AI_CUSTOMIZATION_ITEM_STORAGE_KEY, item.storage],
|
||||
[AI_CUSTOMIZATION_ITEM_URI_KEY, item.uri.toString()],
|
||||
[AI_CUSTOMIZATION_ITEM_DISABLED_KEY, item.disabled],
|
||||
];
|
||||
if (item.storage) {
|
||||
overlayPairs.push([AI_CUSTOMIZATION_ITEM_STORAGE_KEY, item.storage]);
|
||||
}
|
||||
if (item.pluginUri) {
|
||||
overlayPairs.push([AI_CUSTOMIZATION_ITEM_PLUGIN_URI_KEY, item.pluginUri.toString()]);
|
||||
}
|
||||
@@ -857,25 +789,9 @@ export class AICustomizationListWidget extends Disposable {
|
||||
}),
|
||||
];
|
||||
|
||||
// Add extension-provided item commands for non-built-in harnesses
|
||||
const activeHarness = this.harnessService.activeHarness.get();
|
||||
const hasProvider = this.chatSessionsService.hasCustomizationsProvider(activeHarness);
|
||||
const extensionItemActions = (hasProvider && this._currentGroupItemCommands?.length)
|
||||
? [
|
||||
new Separator(),
|
||||
...this._currentGroupItemCommands.map(cmd => new Action(
|
||||
cmd.id,
|
||||
cmd.title,
|
||||
undefined,
|
||||
true,
|
||||
async () => { this.commandService.executeCommand(cmd.id, item.uri.toString()); },
|
||||
)),
|
||||
]
|
||||
: [];
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => [...secondary, ...extensionItemActions, ...copyActions],
|
||||
getActions: () => [...secondary, ...copyActions],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -971,17 +887,6 @@ export class AICustomizationListWidget extends Disposable {
|
||||
const override = descriptor.sectionOverrides?.get(this.currentSection);
|
||||
const hasWorkspace = this.hasActiveWorkspace();
|
||||
|
||||
// Extension-contributed harness: use commands from the provider group
|
||||
const activeHarness = this.harnessService.activeHarness.get();
|
||||
const hasProvider = this.chatSessionsService.hasCustomizationsProvider(activeHarness);
|
||||
if (hasProvider && this._currentGroupCommands?.length) {
|
||||
return this._currentGroupCommands.map(cmd => ({
|
||||
label: `$(${Codicon.add.id}) ${cmd.title}`,
|
||||
enabled: true,
|
||||
run: () => { this.commandService.executeCommand(cmd.id, ...(cmd.arguments ?? [])); },
|
||||
}));
|
||||
}
|
||||
|
||||
// Full command override (e.g. Claude hooks) — single action, no dropdown
|
||||
if (override?.commandId) {
|
||||
return [{
|
||||
@@ -1092,6 +997,34 @@ export class AICustomizationListWidget extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for menu-contributed create actions from extensions.
|
||||
// Extensions contribute to AICustomizationManagementCreateMenuId with
|
||||
// when-clauses targeting aiCustomizationManagementHarness and
|
||||
// aiCustomizationManagementSection context keys.
|
||||
// When a harness contributes create actions, they REPLACE the built-in ones.
|
||||
const menuActions = this.menuService.getMenuActions(
|
||||
AICustomizationManagementCreateMenuId,
|
||||
this.contextKeyService,
|
||||
{ shouldForwardArgs: true },
|
||||
);
|
||||
const extensionCreateActions: ICreateAction[] = [];
|
||||
for (const [, group] of menuActions) {
|
||||
for (const menuItem of group) {
|
||||
if (menuItem instanceof MenuItemAction) {
|
||||
const icon = ThemeIcon.isThemeIcon(menuItem.item.icon) ? menuItem.item.icon.id : Codicon.add.id;
|
||||
extensionCreateActions.push({
|
||||
label: `$(${icon}) ${typeof menuItem.item.title === 'string' ? menuItem.item.title : menuItem.item.title.value}`,
|
||||
enabled: menuItem.enabled,
|
||||
run: () => { menuItem.run(); },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionCreateActions.length > 0) {
|
||||
return extensionCreateActions;
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
@@ -1157,28 +1090,12 @@ export class AICustomizationListWidget extends Disposable {
|
||||
*/
|
||||
private async loadItems(): Promise<void> {
|
||||
const section = this.currentSection;
|
||||
this._currentGroupCommands = undefined;
|
||||
this._currentGroupItemCommands = undefined;
|
||||
const items = await this.fetchItemsForSection(section);
|
||||
|
||||
if (this.currentSection !== section) {
|
||||
return; // section changed while loading
|
||||
}
|
||||
|
||||
// Fetch commands for extension-contributed harnesses separately to avoid
|
||||
// race conditions when _fetchItemsFromProvider is also called for count
|
||||
// computation on other sections.
|
||||
const activeHarness = this.harnessService.activeHarness.get();
|
||||
const hasProvider = this.chatSessionsService.hasCustomizationsProvider(activeHarness);
|
||||
if (hasProvider) {
|
||||
const groups = await this.chatSessionsService.getCustomizations(activeHarness, CancellationToken.None);
|
||||
if (groups) {
|
||||
const matchingIds = sectionToCustomizationGroupIds(section);
|
||||
this._currentGroupCommands = groups.filter(g => matchingIds.includes(g.id)).flatMap(g => g.commands ?? []);
|
||||
this._currentGroupItemCommands = groups.filter(g => matchingIds.includes(g.id)).flatMap(g => g.itemCommands ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
this.allItems = items;
|
||||
this.filterItems();
|
||||
this._onDidChangeItemCount.fire(items.length);
|
||||
@@ -1241,15 +1158,15 @@ export class AICustomizationListWidget extends Disposable {
|
||||
* Shared between `loadItems` (active section) and `computeItemCountForSection` (any section).
|
||||
*/
|
||||
private async fetchItemsForSection(section: AICustomizationManagementSection): Promise<IAICustomizationListItem[]> {
|
||||
// Extension-contributed harnesses always use the provider path
|
||||
const activeHarness = this.harnessService.activeHarness.get();
|
||||
const hasProvider = this.chatSessionsService.hasCustomizationsProvider(activeHarness);
|
||||
const promptType = sectionToPromptType(section);
|
||||
|
||||
if (hasProvider) {
|
||||
return this._fetchItemsFromProvider(section);
|
||||
// When the active harness has an external item provider, delegate to it
|
||||
// instead of querying promptsService and applying filters.
|
||||
const activeDescriptor = this.harnessService.getActiveDescriptor();
|
||||
if (activeDescriptor.itemProvider && promptType) {
|
||||
return this.fetchItemsFromProvider(activeDescriptor.itemProvider, promptType);
|
||||
}
|
||||
|
||||
const promptType = sectionToPromptType(section);
|
||||
const items: IAICustomizationListItem[] = [];
|
||||
const disabledUris = this.promptsService.getDisabledPromptFiles(promptType);
|
||||
const extensionInfoByUri = new Map<string, { id: ExtensionIdentifier; displayName?: string }>();
|
||||
@@ -1569,7 +1486,9 @@ export class AICustomizationListWidget extends Disposable {
|
||||
|
||||
// Apply storage source filter (removes items not in visible sources or excluded user roots)
|
||||
const filter = this.workspaceService.getStorageSourceFilter(promptType);
|
||||
const filteredItems = applyStorageSourceFilter(groupedItems, filter);
|
||||
const withStorage = groupedItems.filter((item): item is IAICustomizationListItem & { readonly storage: PromptsStorage } => item.storage !== undefined);
|
||||
const withoutStorage = groupedItems.filter(item => item.storage === undefined);
|
||||
const filteredItems = [...applyStorageSourceFilter(withStorage, filter), ...withoutStorage];
|
||||
items.length = 0;
|
||||
items.push(...filteredItems);
|
||||
|
||||
@@ -1616,33 +1535,27 @@ export class AICustomizationListWidget extends Disposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches items from the registered {@link IChatSessionCustomizationsProvider}
|
||||
* instead of the built-in {@link IPromptsService} discovery.
|
||||
* Fetches items from an external customization provider, converting
|
||||
* the provider's items into the list widget format.
|
||||
*/
|
||||
private async _fetchItemsFromProvider(section: AICustomizationManagementSection): Promise<IAICustomizationListItem[]> {
|
||||
const promptType = sectionToPromptType(section);
|
||||
|
||||
// Use the active harness ID as the session type
|
||||
const activeHarness = this.harnessService.activeHarness.get();
|
||||
const groups = await this.chatSessionsService.getCustomizations(activeHarness, CancellationToken.None);
|
||||
|
||||
if (!groups) {
|
||||
private async fetchItemsFromProvider(provider: IExternalCustomizationItemProvider, promptType: PromptsType): Promise<IAICustomizationListItem[]> {
|
||||
const allItems = await provider.provideChatSessionCustomizations(CancellationToken.None);
|
||||
if (!allItems) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter groups to only those matching the current section
|
||||
const matchingGroupIds = sectionToCustomizationGroupIds(section);
|
||||
const filteredGroups = groups.filter(g => matchingGroupIds.includes(g.id));
|
||||
|
||||
const items: IAICustomizationListItem[] = [];
|
||||
for (const group of filteredGroups) {
|
||||
for (const item of group.items) {
|
||||
items.push(mapProviderItemToListItem(item, group.id, promptType));
|
||||
}
|
||||
}
|
||||
|
||||
items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return items;
|
||||
return allItems
|
||||
.filter(item => item.type === promptType)
|
||||
.map((item: IExternalCustomizationItem) => ({
|
||||
id: item.uri.toString(),
|
||||
uri: item.uri,
|
||||
name: item.name,
|
||||
filename: basename(item.uri),
|
||||
description: item.description,
|
||||
promptType,
|
||||
disabled: false,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1695,7 +1608,19 @@ export class AICustomizationListWidget extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
// Group items — instructions use category-based grouping; other sections use storage-based
|
||||
// When items come from an external provider, skip storage-based grouping
|
||||
// and render a flat list. The provider controls the full item set, so
|
||||
// Workspace/User/Extension categories don't apply.
|
||||
const activeDescriptor = this.harnessService.getActiveDescriptor();
|
||||
if (activeDescriptor.itemProvider) {
|
||||
matchedItems.sort((a, b) => a.name.localeCompare(b.name));
|
||||
this.displayEntries = matchedItems.map(item => ({ type: 'file-item' as const, item }));
|
||||
this.list.splice(0, this.list.length, this.displayEntries);
|
||||
this.updateEmptyState();
|
||||
return matchedItems.length;
|
||||
}
|
||||
|
||||
// Group items by storage
|
||||
const promptType = sectionToPromptType(this.currentSection);
|
||||
const visibleSources = new Set(this.workspaceService.getStorageSourceFilter(promptType).sources);
|
||||
const groups: { groupKey: string; label: string; icon: ThemeIcon; description: string; items: IAICustomizationListItem[] }[] =
|
||||
@@ -1715,7 +1640,7 @@ export class AICustomizationListWidget extends Disposable {
|
||||
].filter(g => visibleSources.has(g.groupKey as PromptsStorage) || g.groupKey === 'agents');
|
||||
|
||||
for (const item of matchedItems) {
|
||||
const key = item.groupKey ?? item.storage;
|
||||
const key = item.groupKey ?? item.storage ?? PromptsStorage.local;
|
||||
const group = groups.find(g => g.groupKey === key);
|
||||
if (group) {
|
||||
group.items.push(item);
|
||||
@@ -1934,11 +1859,13 @@ export class AICustomizationListWidget extends Disposable {
|
||||
* Generates a debug report for the current section.
|
||||
*/
|
||||
async generateDebugReport(): Promise<string> {
|
||||
const activeDescriptor = this.harnessService.getActiveDescriptor();
|
||||
return generateCustomizationDebugReport(
|
||||
this.currentSection,
|
||||
this.promptsService,
|
||||
this.workspaceService,
|
||||
{ allItems: this.allItems, displayEntries: this.displayEntries },
|
||||
activeDescriptor.itemProvider,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,16 @@ export const CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_SECTION = new RawContextKey<str
|
||||
localize('aiCustomizationManagementSection', "The currently selected section in the Chat Customizations editor")
|
||||
);
|
||||
|
||||
/**
|
||||
* Context key for the active harness (session type) in the customizations editor.
|
||||
* Extensions use this in when-clauses to scope create actions to their harness.
|
||||
*/
|
||||
export const CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_HARNESS = new RawContextKey<string>(
|
||||
'aiCustomizationManagementHarness',
|
||||
'',
|
||||
localize('aiCustomizationManagementHarness', "The active harness (session type) in the Chat Customizations editor")
|
||||
);
|
||||
|
||||
/**
|
||||
* Menu ID for the AI Customization Management Editor title bar actions.
|
||||
*/
|
||||
@@ -62,6 +72,13 @@ export const AICustomizationManagementTitleMenuId = MenuId.for('AICustomizationM
|
||||
*/
|
||||
export const AICustomizationManagementItemMenuId = MenuId.for('AICustomizationManagementEditorItem');
|
||||
|
||||
/**
|
||||
* Menu ID for the AI Customization Management Editor create/add button.
|
||||
* Extensions can contribute commands here to add create actions to the section's add button dropdown.
|
||||
* Use the `aiCustomizationManagementSection` context key to target a specific section.
|
||||
*/
|
||||
export const AICustomizationManagementCreateMenuId = MenuId.for('AICustomizationManagementCreate');
|
||||
|
||||
/**
|
||||
* Context key for the item prompt type (e.g. 'prompt', 'agent') used in when-clause filtering.
|
||||
*/
|
||||
|
||||
+33
-5
@@ -46,6 +46,7 @@ import {
|
||||
BUILTIN_STORAGE,
|
||||
CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_EDITOR,
|
||||
CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_SECTION,
|
||||
CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_HARNESS,
|
||||
SIDEBAR_DEFAULT_WIDTH,
|
||||
SIDEBAR_MIN_WIDTH,
|
||||
SIDEBAR_MAX_WIDTH,
|
||||
@@ -311,9 +312,11 @@ export class AICustomizationManagementEditor extends EditorPane {
|
||||
private harnessDropdownButton: HTMLElement | undefined;
|
||||
private harnessDropdownIcon: HTMLElement | undefined;
|
||||
private harnessDropdownLabel: HTMLElement | undefined;
|
||||
private sidebarContent: HTMLElement | undefined;
|
||||
|
||||
private readonly inEditorContextKey: IContextKey<boolean>;
|
||||
private readonly sectionContextKey: IContextKey<string>;
|
||||
private readonly harnessContextKey: IContextKey<string>;
|
||||
|
||||
constructor(
|
||||
group: IEditorGroup,
|
||||
@@ -342,6 +345,7 @@ export class AICustomizationManagementEditor extends EditorPane {
|
||||
|
||||
this.inEditorContextKey = CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_EDITOR.bindTo(contextKeyService);
|
||||
this.sectionContextKey = CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_SECTION.bindTo(contextKeyService);
|
||||
this.harnessContextKey = CONTEXT_AI_CUSTOMIZATION_MANAGEMENT_HARNESS.bindTo(contextKeyService);
|
||||
|
||||
// Track workspace changes for embedded editor
|
||||
this._register(autorun(reader => {
|
||||
@@ -513,7 +517,7 @@ export class AICustomizationManagementEditor extends EditorPane {
|
||||
}
|
||||
|
||||
private createSidebar(): void {
|
||||
const sidebarContent = DOM.append(this.sidebarContainer, $('.sidebar-content'));
|
||||
const sidebarContent = this.sidebarContent = DOM.append(this.sidebarContainer, $('.sidebar-content'));
|
||||
|
||||
// Harness dropdown (shown when multiple harnesses available)
|
||||
this.createHarnessDropdown(sidebarContent);
|
||||
@@ -565,7 +569,9 @@ export class AICustomizationManagementEditor extends EditorPane {
|
||||
return; // setActiveHarness will trigger another autorun cycle
|
||||
}
|
||||
|
||||
this.harnessContextKey.set(activeId);
|
||||
this.rebuildVisibleSections();
|
||||
this.ensureHarnessDropdown();
|
||||
this.updateHarnessDropdown();
|
||||
this.refreshAllPromptsSectionCounts();
|
||||
}));
|
||||
@@ -613,6 +619,27 @@ export class AICustomizationManagementEditor extends EditorPane {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily creates the harness dropdown if it doesn't exist but
|
||||
* multiple harnesses are now available, or hides it if only one
|
||||
* harness remains (e.g. after an extension-contributed harness is
|
||||
* unregistered).
|
||||
*/
|
||||
private ensureHarnessDropdown(): void {
|
||||
const harnesses = this.harnessService.availableHarnesses.get();
|
||||
const shouldShow = this.isHarnessSelectorEnabled && harnesses.length > 1;
|
||||
|
||||
if (shouldShow && !this.harnessDropdownContainer && this.sidebarContent) {
|
||||
this.createHarnessDropdown(this.sidebarContent);
|
||||
} else if (!shouldShow && this.harnessDropdownContainer) {
|
||||
this.harnessDropdownContainer.remove();
|
||||
this.harnessDropdownContainer = undefined;
|
||||
this.harnessDropdownButton = undefined;
|
||||
this.harnessDropdownIcon = undefined;
|
||||
this.harnessDropdownLabel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private updateHarnessDropdown(): void {
|
||||
if (!this.harnessDropdownContainer || !this.harnessDropdownIcon || !this.harnessDropdownLabel) {
|
||||
return;
|
||||
@@ -726,11 +753,12 @@ export class AICustomizationManagementEditor extends EditorPane {
|
||||
this.telemetryService.publicLog2<CustomizationEditorItemSelectedEvent, CustomizationEditorItemSelectedClassification>('chatCustomizationEditor.itemSelected', {
|
||||
section: this.selectedSection,
|
||||
promptType: item.promptType,
|
||||
storage: item.storage,
|
||||
storage: item.storage ?? 'external',
|
||||
});
|
||||
const isWorkspaceFile = item.storage === PromptsStorage.local;
|
||||
const isReadOnly = item.storage === PromptsStorage.extension || item.storage === PromptsStorage.plugin || item.storage === BUILTIN_STORAGE;
|
||||
this.showEmbeddedEditor(item.uri, item.name, item.promptType, item.storage, isWorkspaceFile, isReadOnly);
|
||||
const storage = item.storage;
|
||||
const isWorkspaceFile = storage === PromptsStorage.local;
|
||||
const isReadOnly = !storage || storage === PromptsStorage.extension || storage === PromptsStorage.plugin || storage === BUILTIN_STORAGE;
|
||||
this.showEmbeddedEditor(item.uri, item.name, item.promptType, storage ?? BUILTIN_STORAGE, isWorkspaceFile, isReadOnly);
|
||||
}));
|
||||
|
||||
// Handle create actions - AI-guided creation
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { derived, observableFromEvent } from '../../../../../base/common/observable.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
|
||||
import {
|
||||
CustomizationHarness,
|
||||
@@ -19,17 +18,14 @@ import {
|
||||
import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
|
||||
import { BUILTIN_STORAGE } from '../../common/aiCustomizationWorkspaceService.js';
|
||||
import { IPathService } from '../../../../services/path/common/pathService.js';
|
||||
import { IChatAgentService } from '../../common/participants/chatAgents.js';
|
||||
|
||||
/**
|
||||
* Core implementation of the customization harness service.
|
||||
* Exposes VS Code, CLI, and Claude harnesses for filtering customizations.
|
||||
* CLI and Claude harnesses are only shown when their respective agents are registered.
|
||||
*/
|
||||
class CustomizationHarnessService extends CustomizationHarnessServiceBase {
|
||||
constructor(
|
||||
@IPathService pathService: IPathService,
|
||||
@IChatAgentService chatAgentService: IChatAgentService,
|
||||
) {
|
||||
const userHome = pathService.userHome({ preferLocal: true });
|
||||
// The Local harness includes extension-contributed and built-in customizations.
|
||||
@@ -43,26 +39,9 @@ class CustomizationHarnessService extends CustomizationHarnessServiceBase {
|
||||
createClaudeHarnessDescriptor(getClaudeUserRoots(userHome), restrictedExtras),
|
||||
];
|
||||
|
||||
// Track agent registration changes as an observable.
|
||||
// Return the agent count so the value changes on each event
|
||||
// (observableFromEvent uses strictEquals to decide whether to notify).
|
||||
const agentCount = observableFromEvent(chatAgentService.onDidChangeAgents, () => chatAgentService.getAgents().length);
|
||||
|
||||
// Derive available harnesses from agent registration state
|
||||
const available = derived(reader => {
|
||||
agentCount.read(reader);
|
||||
return allHarnesses.filter(h => {
|
||||
if (!h.requiredAgentId) {
|
||||
return true;
|
||||
}
|
||||
return !!chatAgentService.getAgent(h.requiredAgentId);
|
||||
});
|
||||
});
|
||||
|
||||
super(
|
||||
allHarnesses,
|
||||
CustomizationHarness.VSCode,
|
||||
available,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { constObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js';
|
||||
import { IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js';
|
||||
import { IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { joinPath } from '../../../../base/common/resources.js';
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
@@ -13,8 +14,9 @@ import { localize } from '../../../../nls.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { AICustomizationManagementSection, IStorageSourceFilter } from './aiCustomizationWorkspaceService.js';
|
||||
import { PromptsType } from './promptSyntax/promptTypes.js';
|
||||
import { PromptsStorage } from './promptSyntax/service/promptsService.js';
|
||||
import { AGENT_MD_FILENAME } from './promptSyntax/config/promptFileLocations.js';
|
||||
import { PromptsStorage } from './promptSyntax/service/promptsService.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
|
||||
export const ICustomizationHarnessService = createDecorator<ICustomizationHarnessService>('customizationHarnessService');
|
||||
|
||||
@@ -119,6 +121,37 @@ export interface IHarnessDescriptor {
|
||||
* items of the given type when this harness is active.
|
||||
*/
|
||||
getStorageSourceFilter(type: PromptsType): IStorageSourceFilter;
|
||||
/**
|
||||
* When set, this harness is backed by an extension-contributed provider
|
||||
* that can supply customization items directly (bypassing promptsService
|
||||
* discovery and filtering).
|
||||
*/
|
||||
readonly itemProvider?: IExternalCustomizationItemProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a customization item provided by an external extension.
|
||||
*/
|
||||
export interface IExternalCustomizationItem {
|
||||
readonly uri: URI;
|
||||
readonly type: string;
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider interface for extension-contributed harnesses that supply
|
||||
* customization items directly from their SDK.
|
||||
*/
|
||||
export interface IExternalCustomizationItemProvider {
|
||||
/**
|
||||
* Event that fires when the provider's customizations change.
|
||||
*/
|
||||
readonly onDidChange: Event<void>;
|
||||
/**
|
||||
* Provide the customization items this harness supports.
|
||||
*/
|
||||
provideChatSessionCustomizations(token: CancellationToken): Promise<IExternalCustomizationItem[] | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,12 +182,6 @@ export interface ICustomizationHarnessService {
|
||||
*/
|
||||
setActiveHarness(id: string): void;
|
||||
|
||||
/**
|
||||
* Registers a harness descriptor contributed by an extension's
|
||||
* customizations provider. Returns a disposable that removes the harness.
|
||||
*/
|
||||
registerContributedHarness(descriptor: IHarnessDescriptor): IDisposable;
|
||||
|
||||
/**
|
||||
* Convenience: returns the storage source filter for the active harness
|
||||
* and the given customization type.
|
||||
@@ -165,6 +192,13 @@ export interface ICustomizationHarnessService {
|
||||
* Returns the descriptor of the currently active harness.
|
||||
*/
|
||||
getActiveDescriptor(): IHarnessDescriptor;
|
||||
|
||||
/**
|
||||
* Registers an external harness contributed by an extension.
|
||||
* The harness appears in the UI toggle alongside static harnesses.
|
||||
* Returns a disposable that removes the harness when disposed.
|
||||
*/
|
||||
registerExternalHarness(descriptor: IHarnessDescriptor): IDisposable;
|
||||
}
|
||||
|
||||
// #region Shared filter constants
|
||||
@@ -381,53 +415,69 @@ export class CustomizationHarnessServiceBase implements ICustomizationHarnessSer
|
||||
|
||||
private readonly _activeHarness: ISettableObservable<string>;
|
||||
readonly activeHarness: IObservable<string>;
|
||||
|
||||
private readonly _staticHarnesses: readonly IHarnessDescriptor[];
|
||||
private readonly _externalHarnesses: IHarnessDescriptor[] = [];
|
||||
private readonly _availableHarnesses: ISettableObservable<readonly IHarnessDescriptor[]>;
|
||||
readonly availableHarnesses: IObservable<readonly IHarnessDescriptor[]>;
|
||||
|
||||
private readonly _allHarnesses: readonly IHarnessDescriptor[];
|
||||
private readonly _contributedHarnesses = observableValue<readonly IHarnessDescriptor[]>(this, []);
|
||||
|
||||
constructor(
|
||||
harnesses: readonly IHarnessDescriptor[],
|
||||
staticHarnesses: readonly IHarnessDescriptor[],
|
||||
defaultHarness: string,
|
||||
builtInAvailable?: IObservable<readonly IHarnessDescriptor[]>,
|
||||
) {
|
||||
this._allHarnesses = harnesses;
|
||||
this._staticHarnesses = staticHarnesses;
|
||||
this._activeHarness = observableValue<string>(this, defaultHarness);
|
||||
this.activeHarness = this._activeHarness;
|
||||
this._availableHarnesses = observableValue<readonly IHarnessDescriptor[]>(this, [...this._staticHarnesses]);
|
||||
this.availableHarnesses = this._availableHarnesses;
|
||||
}
|
||||
|
||||
const builtIn = builtInAvailable ?? constObservable(harnesses);
|
||||
this.availableHarnesses = derived(this, reader => {
|
||||
const built = builtIn.read(reader);
|
||||
const contributed = this._contributedHarnesses.read(reader);
|
||||
return [...built, ...contributed];
|
||||
});
|
||||
private _getAllHarnesses(): readonly IHarnessDescriptor[] {
|
||||
return [...this._staticHarnesses, ...this._externalHarnesses];
|
||||
}
|
||||
|
||||
private _refreshAvailableHarnesses(): void {
|
||||
this._availableHarnesses.set(this._getAllHarnesses(), undefined);
|
||||
}
|
||||
|
||||
registerExternalHarness(descriptor: IHarnessDescriptor): IDisposable {
|
||||
this._externalHarnesses.push(descriptor);
|
||||
this._refreshAvailableHarnesses();
|
||||
return {
|
||||
dispose: () => {
|
||||
const idx = this._externalHarnesses.indexOf(descriptor);
|
||||
if (idx >= 0) {
|
||||
this._externalHarnesses.splice(idx, 1);
|
||||
this._refreshAvailableHarnesses();
|
||||
// If the removed harness was active, fall back to the first available
|
||||
if (this._activeHarness.get() === descriptor.id) {
|
||||
const all = this._getAllHarnesses();
|
||||
if (all.length > 0) {
|
||||
this._activeHarness.set(all[0].id, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setActiveHarness(id: string): void {
|
||||
const available = this.availableHarnesses.get();
|
||||
if (available.some(h => h.id === id)) {
|
||||
if (this._getAllHarnesses().some(h => h.id === id)) {
|
||||
this._activeHarness.set(id, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
registerContributedHarness(descriptor: IHarnessDescriptor): IDisposable {
|
||||
const current = this._contributedHarnesses.get();
|
||||
this._contributedHarnesses.set([...current, descriptor], undefined);
|
||||
return toDisposable(() => {
|
||||
const updated = this._contributedHarnesses.get().filter(h => h !== descriptor);
|
||||
this._contributedHarnesses.set(updated, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
getStorageSourceFilter(type: PromptsType): IStorageSourceFilter {
|
||||
const descriptor = this.getActiveDescriptor();
|
||||
return descriptor.getStorageSourceFilter(type);
|
||||
const activeId = this._activeHarness.get();
|
||||
const all = this._getAllHarnesses();
|
||||
const descriptor = all.find(h => h.id === activeId);
|
||||
return descriptor?.getStorageSourceFilter(type) ?? all[0].getStorageSourceFilter(type);
|
||||
}
|
||||
|
||||
getActiveDescriptor(): IHarnessDescriptor {
|
||||
const activeId = this._activeHarness.get();
|
||||
const available = this.availableHarnesses.get();
|
||||
return available.find(h => h.id === activeId) ?? available[0] ?? this._allHarnesses[0];
|
||||
const all = this._getAllHarnesses();
|
||||
return all.find(h => h.id === activeId) ?? all[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { Emitter } from '../../../../../base/common/event.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { CustomizationHarness, CustomizationHarnessServiceBase, createVSCodeHarnessDescriptor, IExternalCustomizationItemProvider, IHarnessDescriptor, matchesWorkspaceSubpath } from '../../common/customizationHarnessService.js';
|
||||
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
|
||||
import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
|
||||
suite('CustomizationHarnessService', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
function createService(...harnesses: IHarnessDescriptor[]): CustomizationHarnessServiceBase {
|
||||
if (harnesses.length === 0) {
|
||||
harnesses = [createVSCodeHarnessDescriptor([PromptsStorage.extension])];
|
||||
}
|
||||
return new CustomizationHarnessServiceBase(harnesses, harnesses[0].id);
|
||||
}
|
||||
|
||||
suite('registerExternalHarness', () => {
|
||||
test('adds harness to available list', () => {
|
||||
const service = createService();
|
||||
assert.strictEqual(service.availableHarnesses.get().length, 1);
|
||||
|
||||
const emitter = new Emitter<void>();
|
||||
store.add(emitter);
|
||||
const externalDescriptor: IHarnessDescriptor = {
|
||||
id: 'test-ext',
|
||||
label: 'Test Extension',
|
||||
icon: ThemeIcon.fromId('extensions'),
|
||||
getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }),
|
||||
itemProvider: {
|
||||
onDidChange: emitter.event,
|
||||
provideChatSessionCustomizations: async () => [],
|
||||
},
|
||||
};
|
||||
|
||||
const reg = service.registerExternalHarness(externalDescriptor);
|
||||
store.add(reg);
|
||||
|
||||
assert.strictEqual(service.availableHarnesses.get().length, 2);
|
||||
assert.strictEqual(service.availableHarnesses.get()[1].id, 'test-ext');
|
||||
});
|
||||
|
||||
test('removes harness on dispose', () => {
|
||||
const service = createService();
|
||||
const emitter = new Emitter<void>();
|
||||
store.add(emitter);
|
||||
const externalDescriptor: IHarnessDescriptor = {
|
||||
id: 'test-ext',
|
||||
label: 'Test Extension',
|
||||
icon: ThemeIcon.fromId('extensions'),
|
||||
getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }),
|
||||
itemProvider: {
|
||||
onDidChange: emitter.event,
|
||||
provideChatSessionCustomizations: async () => [],
|
||||
},
|
||||
};
|
||||
|
||||
const reg = service.registerExternalHarness(externalDescriptor);
|
||||
assert.strictEqual(service.availableHarnesses.get().length, 2);
|
||||
|
||||
reg.dispose();
|
||||
assert.strictEqual(service.availableHarnesses.get().length, 1);
|
||||
});
|
||||
|
||||
test('falls back to first harness when active external harness is removed', () => {
|
||||
const service = createService();
|
||||
const emitter = new Emitter<void>();
|
||||
store.add(emitter);
|
||||
const externalDescriptor: IHarnessDescriptor = {
|
||||
id: 'test-ext',
|
||||
label: 'Test Extension',
|
||||
icon: ThemeIcon.fromId('extensions'),
|
||||
getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }),
|
||||
itemProvider: {
|
||||
onDidChange: emitter.event,
|
||||
provideChatSessionCustomizations: async () => [],
|
||||
},
|
||||
};
|
||||
|
||||
const reg = service.registerExternalHarness(externalDescriptor);
|
||||
service.setActiveHarness('test-ext');
|
||||
assert.strictEqual(service.activeHarness.get(), 'test-ext');
|
||||
|
||||
reg.dispose();
|
||||
assert.strictEqual(service.activeHarness.get(), CustomizationHarness.VSCode);
|
||||
});
|
||||
|
||||
test('allows switching to external harness', () => {
|
||||
const service = createService();
|
||||
const emitter = new Emitter<void>();
|
||||
store.add(emitter);
|
||||
const externalDescriptor: IHarnessDescriptor = {
|
||||
id: 'test-ext',
|
||||
label: 'Test Extension',
|
||||
icon: ThemeIcon.fromId('extensions'),
|
||||
getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }),
|
||||
itemProvider: {
|
||||
onDidChange: emitter.event,
|
||||
provideChatSessionCustomizations: async () => [],
|
||||
},
|
||||
};
|
||||
|
||||
store.add(service.registerExternalHarness(externalDescriptor));
|
||||
service.setActiveHarness('test-ext');
|
||||
assert.strictEqual(service.activeHarness.get(), 'test-ext');
|
||||
|
||||
const activeDescriptor = service.getActiveDescriptor();
|
||||
assert.strictEqual(activeDescriptor.id, 'test-ext');
|
||||
assert.strictEqual(activeDescriptor.label, 'Test Extension');
|
||||
assert.ok(activeDescriptor.itemProvider);
|
||||
});
|
||||
|
||||
test('external harness provides storage filter', () => {
|
||||
const service = createService();
|
||||
const emitter = new Emitter<void>();
|
||||
store.add(emitter);
|
||||
const customFilter = { sources: [PromptsStorage.local, PromptsStorage.user] };
|
||||
const externalDescriptor: IHarnessDescriptor = {
|
||||
id: 'test-ext',
|
||||
label: 'Test Extension',
|
||||
icon: ThemeIcon.fromId('extensions'),
|
||||
getStorageSourceFilter: () => customFilter,
|
||||
itemProvider: {
|
||||
onDidChange: emitter.event,
|
||||
provideChatSessionCustomizations: async () => [],
|
||||
},
|
||||
};
|
||||
|
||||
store.add(service.registerExternalHarness(externalDescriptor));
|
||||
service.setActiveHarness('test-ext');
|
||||
assert.deepStrictEqual(service.getStorageSourceFilter(PromptsType.agent), customFilter);
|
||||
});
|
||||
|
||||
test('external harness item provider returns items', async () => {
|
||||
const service = createService();
|
||||
const emitter = new Emitter<void>();
|
||||
store.add(emitter);
|
||||
const testItems = [
|
||||
{ uri: URI.parse('file:///workspace/.claude/SKILL.md'), type: 'skill', name: 'Test Skill', description: 'A test skill' },
|
||||
];
|
||||
|
||||
const itemProvider: IExternalCustomizationItemProvider = {
|
||||
onDidChange: emitter.event,
|
||||
provideChatSessionCustomizations: async () => testItems,
|
||||
};
|
||||
|
||||
const externalDescriptor: IHarnessDescriptor = {
|
||||
id: 'test-ext',
|
||||
label: 'Test Extension',
|
||||
icon: ThemeIcon.fromId('extensions'),
|
||||
getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }),
|
||||
itemProvider,
|
||||
};
|
||||
|
||||
store.add(service.registerExternalHarness(externalDescriptor));
|
||||
service.setActiveHarness('test-ext');
|
||||
|
||||
const items = await service.getActiveDescriptor().itemProvider!.provideChatSessionCustomizations(CancellationToken.None);
|
||||
assert.strictEqual(items?.length, 1);
|
||||
assert.strictEqual(items![0].name, 'Test Skill');
|
||||
assert.strictEqual(items![0].type, 'skill');
|
||||
});
|
||||
|
||||
test('external harness with hidden sections and workspace subpaths', () => {
|
||||
const service = createService();
|
||||
const emitter = new Emitter<void>();
|
||||
store.add(emitter);
|
||||
const externalDescriptor: IHarnessDescriptor = {
|
||||
id: 'test-ext',
|
||||
label: 'Test Extension',
|
||||
icon: ThemeIcon.fromId('extensions'),
|
||||
hiddenSections: ['agents', 'prompts'],
|
||||
workspaceSubpaths: ['.test-ext'],
|
||||
getStorageSourceFilter: () => ({ sources: [PromptsStorage.local] }),
|
||||
itemProvider: {
|
||||
onDidChange: emitter.event,
|
||||
provideChatSessionCustomizations: async () => [],
|
||||
},
|
||||
};
|
||||
|
||||
store.add(service.registerExternalHarness(externalDescriptor));
|
||||
service.setActiveHarness('test-ext');
|
||||
|
||||
const descriptor = service.getActiveDescriptor();
|
||||
assert.deepStrictEqual(descriptor.hiddenSections, ['agents', 'prompts']);
|
||||
assert.deepStrictEqual(descriptor.workspaceSubpaths, ['.test-ext']);
|
||||
});
|
||||
});
|
||||
|
||||
suite('matchesWorkspaceSubpath', () => {
|
||||
test('matches segment boundary', () => {
|
||||
assert.ok(matchesWorkspaceSubpath('/workspace/.claude/skills/SKILL.md', ['.claude']));
|
||||
assert.ok(matchesWorkspaceSubpath('/workspace/.github/instructions.md', ['.github']));
|
||||
});
|
||||
|
||||
test('does not match partial segment', () => {
|
||||
assert.ok(!matchesWorkspaceSubpath('/workspace/not.claude/file.md', ['.claude']));
|
||||
});
|
||||
|
||||
test('matches path ending with subpath', () => {
|
||||
assert.ok(matchesWorkspaceSubpath('/workspace/.claude', ['.claude']));
|
||||
});
|
||||
|
||||
test('matches any of multiple subpaths', () => {
|
||||
assert.ok(matchesWorkspaceSubpath('/workspace/.copilot/file.md', ['.github', '.copilot']));
|
||||
assert.ok(matchesWorkspaceSubpath('/workspace/.github/file.md', ['.github', '.copilot']));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -524,6 +524,13 @@ const apiMenus: IAPIMenu[] = [
|
||||
supportsSubmenus: false,
|
||||
proposed: 'chatSessionsProvider',
|
||||
},
|
||||
{
|
||||
key: 'chat/customizations/create',
|
||||
id: MenuId.for('AICustomizationManagementCreate'),
|
||||
description: localize('menus.chatCustomizationsCreate', "The create button in the Chat Customizations management editor."),
|
||||
supportsSubmenus: false,
|
||||
proposed: 'chatSessionCustomizationProvider',
|
||||
},
|
||||
{
|
||||
key: 'chat/editor/inlineGutter',
|
||||
id: MenuId.ChatEditorInlineMenu,
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ function createMockHarnessService(): ICustomizationHarnessService {
|
||||
override readonly availableHarnesses = observableValue<readonly IHarnessDescriptor[]>('harnesses', [descriptor]);
|
||||
override getStorageSourceFilter() { return defaultFilter; }
|
||||
override getActiveDescriptor() { return descriptor; }
|
||||
override registerContributedHarness() { return { dispose() { } }; }
|
||||
override registerExternalHarness() { return { dispose() { } }; }
|
||||
}();
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -176,7 +176,7 @@ function createMockHarnessService(activeHarness: CustomizationHarness, descripto
|
||||
return descriptors.find(h => h.id === active.get()) ?? descriptors[0];
|
||||
}
|
||||
override setActiveHarness(id: string) { active.set(id, undefined); }
|
||||
override registerContributedHarness() { return { dispose() { } }; }
|
||||
override registerExternalHarness() { return { dispose() { } }; }
|
||||
}();
|
||||
}
|
||||
|
||||
@@ -653,7 +653,7 @@ async function renderMcpBrowseMode(ctx: ComponentFixtureContext): Promise<void>
|
||||
reg.defineInstance(ICustomizationHarnessService, new class extends mock<ICustomizationHarnessService>() {
|
||||
override readonly activeHarness = observableValue<string>('activeHarness', CustomizationHarness.VSCode);
|
||||
override getActiveDescriptor() { return createVSCodeHarnessDescriptor([PromptsStorage.extension, BUILTIN_STORAGE]); }
|
||||
override registerContributedHarness() { return { dispose() { } }; }
|
||||
override registerExternalHarness() { return { dispose() { } }; }
|
||||
}());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'vscode' {
|
||||
|
||||
// #region Customization Provider Types
|
||||
|
||||
/**
|
||||
* Identifies the kind of customization an item represents.
|
||||
*
|
||||
* Use the built-in static instances (e.g. {@link ChatSessionCustomizationType.Agent})
|
||||
* for well-known customization types, or create a new instance with a custom
|
||||
* string id for extension-defined types.
|
||||
*/
|
||||
export class ChatSessionCustomizationType {
|
||||
/** Agent customization (`.agent.md` files). */
|
||||
static readonly Agent: ChatSessionCustomizationType;
|
||||
/** Skill customization (`SKILL.md` files). */
|
||||
static readonly Skill: ChatSessionCustomizationType;
|
||||
/** Instruction customization (`.instructions.md` files). */
|
||||
static readonly Instructions: ChatSessionCustomizationType;
|
||||
/** Prompt customization (`.prompt.md` files). */
|
||||
static readonly Prompt: ChatSessionCustomizationType;
|
||||
/** Hook customization (event-driven automation). */
|
||||
static readonly Hook: ChatSessionCustomizationType;
|
||||
|
||||
/**
|
||||
* The string identifier for this customization type.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Create a new customization type.
|
||||
*
|
||||
* @param id A unique string identifier for this type (e.g. `'agent'`, `'skill'`).
|
||||
*/
|
||||
constructor(id: string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata describing a customization provider and its capabilities.
|
||||
* This drives UI presentation (label, icon) and filtering (unsupported types,
|
||||
* workspace sub-paths).
|
||||
*/
|
||||
export interface ChatSessionCustomizationProviderMetadata {
|
||||
/**
|
||||
* Display label for this provider (e.g. "Copilot CLI", "Claude Code").
|
||||
*/
|
||||
readonly label: string;
|
||||
|
||||
/**
|
||||
* Optional codicon ID for this provider's icon in the UI.
|
||||
*/
|
||||
readonly iconId?: string;
|
||||
|
||||
/**
|
||||
* Customization types that this provider does **not** support.
|
||||
* The corresponding sections will be hidden in the management UI
|
||||
* when this provider is active.
|
||||
*/
|
||||
readonly unsupportedTypes?: readonly ChatSessionCustomizationType[];
|
||||
|
||||
/**
|
||||
* Workspace sub-paths that this provider recognizes for customization files.
|
||||
* When set, only workspace files under these paths are shown in the UI.
|
||||
* For example, `['.claude']` for Claude or `['.github', '.copilot']` for CLI.
|
||||
* When `undefined`, all workspace paths are shown.
|
||||
*/
|
||||
readonly workspaceSubpaths?: readonly string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single customization item reported by a provider.
|
||||
*/
|
||||
export interface ChatSessionCustomizationItem {
|
||||
/**
|
||||
* URI to the customization file (e.g. an `.agent.md`, `SKILL.md`, or `.instructions.md` file).
|
||||
*/
|
||||
readonly uri: Uri;
|
||||
|
||||
/**
|
||||
* The type of customization this item represents.
|
||||
*/
|
||||
readonly type: ChatSessionCustomizationType;
|
||||
|
||||
/**
|
||||
* Display name for this customization.
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Optional description of this customization.
|
||||
*/
|
||||
readonly description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider that reports which chat customizations are available.
|
||||
*
|
||||
* Chat customizations are configuration artifacts — agents, skills,
|
||||
* instructions, prompts, and hooks — that augment LLM behavior during
|
||||
* a chat session. Extensions that manage their own customization files
|
||||
* (e.g. from an SDK's config directory) register a provider so the
|
||||
* management UI can discover and display them.
|
||||
*
|
||||
* ### Lifecycle
|
||||
*
|
||||
* 1. Register via {@link chat.registerChatSessionCustomizationProvider}.
|
||||
* 2. The UI calls {@link provideChatSessionCustomizations} once and caches
|
||||
* the result.
|
||||
* 3. When the underlying files change, fire {@link onDidChange} to
|
||||
* trigger a fresh call to {@link provideChatSessionCustomizations}.
|
||||
*/
|
||||
export interface ChatSessionCustomizationProvider {
|
||||
/**
|
||||
* An optional event that fires when the provider's customizations change.
|
||||
* The UI caches the result of {@link provideChatSessionCustomizations} and will
|
||||
* only re-query the provider when this event fires.
|
||||
*/
|
||||
readonly onDidChange?: Event<void>;
|
||||
|
||||
/**
|
||||
* Provide the customization items this provider supports.
|
||||
*
|
||||
* The result is cached by the UI until {@link onDidChange} fires.
|
||||
*
|
||||
* @param token A cancellation token.
|
||||
* @returns The list of customization items, or `undefined` if unavailable.
|
||||
*/
|
||||
provideChatSessionCustomizations(token: CancellationToken): ProviderResult<ChatSessionCustomizationItem[]>;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Registration
|
||||
|
||||
export namespace chat {
|
||||
/**
|
||||
* Register a customization provider that reports what customizations
|
||||
* a harness or runtime supports. The provider's metadata drives UI
|
||||
* presentation and filtering, while {@link ChatSessionCustomizationProvider.provideChatSessionCustomizations}
|
||||
* supplies the actual items.
|
||||
*
|
||||
* @param chatSessionType The session type this provider is for (e.g. `'cli'`, `'claude'`).
|
||||
* @param metadata Metadata describing the provider's capabilities and UI presentation.
|
||||
* @param provider The customization provider implementation.
|
||||
* @returns A disposable that unregisters the provider when disposed.
|
||||
*/
|
||||
export function registerChatSessionCustomizationProvider(chatSessionType: string, metadata: ChatSessionCustomizationProviderMetadata, provider: ChatSessionCustomizationProvider): Disposable;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Types used by the chatSessionsProvider proposal for chat customizations.
|
||||
|
||||
declare module 'vscode' {
|
||||
|
||||
/**
|
||||
* Well-known customization type identifiers.
|
||||
*
|
||||
* Extensions may use these as the {@link ChatSessionCustomizationItemGroup.id id}
|
||||
* of a {@link ChatSessionCustomizationItemGroup} to place items into standard
|
||||
* sections in the management UI.
|
||||
*
|
||||
* TODO: How granular should we be? Consider removing the sub-instruction
|
||||
* types (ContextInstructions, OnDemandInstructions) and collapsing to a
|
||||
* single 'instructions' type.
|
||||
*/
|
||||
export enum ChatSessionCustomizationType {
|
||||
Agents = 'agents',
|
||||
Skills = 'skills',
|
||||
AgentInstructions = 'agentInstructions',
|
||||
ContextInstructions = 'contextInstructions',
|
||||
OnDemandInstructions = 'onDemandInstructions',
|
||||
Prompts = 'prompts',
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a customization item originates from.
|
||||
*
|
||||
* Controls default behaviour in the management UI (grouping, delete-ability).
|
||||
*
|
||||
* TODO: Should this be inferred by core itself depending on the URI
|
||||
* scheme/path rather than declared by the extension?
|
||||
*/
|
||||
export enum ChatSessionCustomizationStorageLocation {
|
||||
/** From the current workspace (`.github/` folder, workspace root, etc.) */
|
||||
Workspace = 1,
|
||||
/** From user-level configuration (`~/.copilot/`, `~/.config/`, etc.) */
|
||||
User = 2,
|
||||
/** From an extension's contribution */
|
||||
Extension = 3,
|
||||
/** From an installed plugin */
|
||||
Plugin = 4,
|
||||
/** Built into the session provider itself */
|
||||
BuiltIn = 5,
|
||||
}
|
||||
|
||||
/**
|
||||
* A single customization item such as an agent, skill, instruction, or prompt.
|
||||
*/
|
||||
export interface ChatSessionCustomizationItem {
|
||||
/**
|
||||
* Display label for the item.
|
||||
*/
|
||||
readonly label: string;
|
||||
|
||||
/**
|
||||
* Optional description shown as secondary text or tooltip.
|
||||
*/
|
||||
readonly description?: string;
|
||||
|
||||
/**
|
||||
* URI pointing to the underlying resource
|
||||
* (`.agent.md`, `.instructions.md`, `SKILL.md`, etc.).
|
||||
* Also serves as the unique identity for this item.
|
||||
*/
|
||||
readonly uri: Uri;
|
||||
|
||||
/**
|
||||
* Where this item comes from. The management UI uses this to
|
||||
* group items under "Workspace", "User", "Extensions", etc.
|
||||
*/
|
||||
readonly storageLocation: ChatSessionCustomizationStorageLocation;
|
||||
|
||||
/**
|
||||
* Optional icon for the item. Overrides the default icon derived
|
||||
* from the customization type.
|
||||
*/
|
||||
readonly icon?: ThemeIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
* A named group of customization items of a single type.
|
||||
*
|
||||
* Use a well-known {@link ChatSessionCustomizationType} as the
|
||||
* {@link id} to place items into a standard management UI section.
|
||||
*/
|
||||
export interface ChatSessionCustomizationItemGroup {
|
||||
/**
|
||||
* Identifier for this group. Use a value from
|
||||
* {@link ChatSessionCustomizationType} to map to a built-in section,
|
||||
* or a custom string for extension-defined sections.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* The items in this group.
|
||||
*/
|
||||
readonly items: ChatSessionCustomizationItem[];
|
||||
|
||||
/**
|
||||
* Commands shown in the toolbar / "New" dropdown for this group.
|
||||
*
|
||||
* @example A "New Agent" command that opens a scaffold wizard.
|
||||
*/
|
||||
readonly commands?: Command[];
|
||||
|
||||
/**
|
||||
* Commands shown in the context menu for individual items.
|
||||
* Each command receives the item's {@link ChatSessionCustomizationItem.uri uri}
|
||||
* as its first argument.
|
||||
*
|
||||
* @example A "Run Prompt" command, a "Disable Skill" command.
|
||||
*/
|
||||
readonly itemCommands?: Command[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides customization items for a chat session type.
|
||||
*
|
||||
* Registered via {@link chat.registerChatSessionCustomizationsProvider}.
|
||||
* The provider is called when the management UI needs to display
|
||||
* customizations, and re-called whenever
|
||||
* {@link onDidChangeCustomizations} fires.
|
||||
*/
|
||||
export interface ChatSessionCustomizationsProvider {
|
||||
/**
|
||||
* Fired when customization items have changed and the UI should
|
||||
* re-fetch them.
|
||||
*/
|
||||
readonly onDidChangeCustomizations: Event<void>;
|
||||
|
||||
/**
|
||||
* Provide the current customization groups.
|
||||
*
|
||||
* @param token A cancellation token.
|
||||
* @returns An array of customization groups, or a thenable that resolves to one.
|
||||
*/
|
||||
provideCustomizations(token: CancellationToken): ProviderResult<ChatSessionCustomizationItemGroup[]>;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -57,18 +57,6 @@ declare module 'vscode' {
|
||||
* @returns A new controller instance that can be used to manage chat session items for the given chat session type.
|
||||
*/
|
||||
export function createChatSessionItemController(chatSessionType: string, refreshHandler: ChatSessionItemControllerRefreshHandler): ChatSessionItemController;
|
||||
|
||||
/**
|
||||
* Registers a {@link ChatSessionCustomizationsProvider customizations provider} for a chat session type.
|
||||
*
|
||||
* The provider supplies customization items (agents, skills, instructions, prompts)
|
||||
* that appear in the Customizations management UI for the given session type.
|
||||
*
|
||||
* @param chatSessionType The chat session type to provide customizations for.
|
||||
* @param provider The customizations provider.
|
||||
* @returns A disposable that unregisters the provider when disposed.
|
||||
*/
|
||||
export function registerChatSessionCustomizationsProvider(chatSessionType: string, provider: ChatSessionCustomizationsProvider): Disposable;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user