Path: blob/main/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts
5245 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/456import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js';7import { joinPath, isEqualOrParent } from '../../../../../base/common/resources.js';8import { localize } from '../../../../../nls.js';9import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js';10import { IWorkbenchContribution } from '../../../../common/contributions.js';11import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js';12import { IPromptsService, PromptsStorage } from './service/promptsService.js';13import { PromptsType } from './promptTypes.js';14import { UriComponents } from '../../../../../base/common/uri.js';15import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';16import { CancellationToken } from '../../../../../base/common/cancellation.js';17import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';18import { Registry } from '../../../../../platform/registry/common/platform.js';19import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js';2021interface IRawChatFileContribution {22readonly path: string;23readonly name?: string;24readonly description?: string;25}2627enum ChatContributionPoint {28chatInstructions = 'chatInstructions',29chatAgents = 'chatAgents',30chatPromptFiles = 'chatPromptFiles',31chatSkills = 'chatSkills',32}3334function registerChatFilesExtensionPoint(point: ChatContributionPoint) {35return extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatFileContribution[]>({36extensionPoint: point,37jsonSchema: {38description: localize('chatContribution.schema.description', 'Contributes {0} for chat prompts.', point),39type: 'array',40items: {41additionalProperties: false,42type: 'object',43defaultSnippets: [{44body: {45path: './relative/path/to/file.md',46}47}],48required: ['path'],49properties: {50path: {51description: localize('chatContribution.property.path', 'Path to the file relative to the extension root.'),52type: 'string'53},54name: {55description: localize('chatContribution.property.name', '(Optional) Name for this entry.'),56deprecationMessage: localize('chatContribution.property.name.deprecated', 'Specify "name" in the prompt file itself instead.'),57type: 'string'58},59description: {60description: localize('chatContribution.property.description', '(Optional) Description of the entry.'),61deprecationMessage: localize('chatContribution.property.description.deprecated', 'Specify "description" in the prompt file itself instead.'),62type: 'string'63}64}65}66}67});68}6970const epPrompt = registerChatFilesExtensionPoint(ChatContributionPoint.chatPromptFiles);71const epInstructions = registerChatFilesExtensionPoint(ChatContributionPoint.chatInstructions);72const epAgents = registerChatFilesExtensionPoint(ChatContributionPoint.chatAgents);73const epSkills = registerChatFilesExtensionPoint(ChatContributionPoint.chatSkills);7475function pointToType(contributionPoint: ChatContributionPoint): PromptsType {76switch (contributionPoint) {77case ChatContributionPoint.chatPromptFiles: return PromptsType.prompt;78case ChatContributionPoint.chatInstructions: return PromptsType.instructions;79case ChatContributionPoint.chatAgents: return PromptsType.agent;80case ChatContributionPoint.chatSkills: return PromptsType.skill;81default: {82const exhaustiveCheck: never = contributionPoint;83throw new Error(`Unknown contribution point: ${exhaustiveCheck}`);84}85}86}8788function key(extensionId: ExtensionIdentifier, type: PromptsType, path: string) {89return `${extensionId.value}/${type}/${path}`;90}9192export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribution {93public static readonly ID = 'workbench.contrib.chatPromptFilesExtensionPointHandler';9495private readonly registrations = new DisposableMap<string>();9697constructor(98@IPromptsService private readonly promptsService: IPromptsService,99) {100this.handle(epPrompt, ChatContributionPoint.chatPromptFiles);101this.handle(epInstructions, ChatContributionPoint.chatInstructions);102this.handle(epAgents, ChatContributionPoint.chatAgents);103this.handle(epSkills, ChatContributionPoint.chatSkills);104}105106private handle(extensionPoint: extensionsRegistry.IExtensionPoint<IRawChatFileContribution[]>, contributionPoint: ChatContributionPoint) {107extensionPoint.setHandler((_extensions, delta) => {108for (const ext of delta.added) {109const type = pointToType(contributionPoint);110for (const raw of ext.value) {111if (!raw.path) {112ext.collector.error(localize('extension.missing.path', "Extension '{0}' cannot register {1} entry without path.", ext.description.identifier.value, contributionPoint));113continue;114}115const fileUri = joinPath(ext.description.extensionLocation, raw.path);116if (!isEqualOrParent(fileUri, ext.description.extensionLocation)) {117ext.collector.error(localize('extension.invalid.path', "Extension '{0}' {1} entry '{2}' resolves outside the extension.", ext.description.identifier.value, contributionPoint, raw.path));118continue;119}120try {121const d = this.promptsService.registerContributedFile(type, fileUri, ext.description, raw.name, raw.description);122this.registrations.set(key(ext.description.identifier, type, raw.path), d);123} catch (e) {124const msg = e instanceof Error ? e.message : String(e);125ext.collector.error(localize('extension.registration.failed', "Extension '{0}' {1}. Failed to register {2}: {3}", ext.description.identifier.value, contributionPoint, raw.path, msg));126}127}128}129for (const ext of delta.removed) {130const type = pointToType(contributionPoint);131for (const raw of ext.value) {132this.registrations.deleteAndDispose(key(ext.description.identifier, type, raw.path));133}134}135});136}137}138139/**140* Result type for the extension prompt file provider command.141*/142export interface IExtensionPromptFileResult {143readonly uri: UriComponents;144readonly type: PromptsType;145}146147/**148* Register the command to list all extension-contributed prompt files.149*/150CommandsRegistry.registerCommand('_listExtensionPromptFiles', async (accessor): Promise<IExtensionPromptFileResult[]> => {151const promptsService = accessor.get(IPromptsService);152153// Get extension prompt files for all prompt types in parallel154const [agents, instructions, prompts, skills, hooks] = await Promise.all([155promptsService.listPromptFiles(PromptsType.agent, CancellationToken.None),156promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None),157promptsService.listPromptFiles(PromptsType.prompt, CancellationToken.None),158promptsService.listPromptFiles(PromptsType.skill, CancellationToken.None),159promptsService.listPromptFiles(PromptsType.hook, CancellationToken.None),160]);161162// Combine all files and collect extension-contributed ones163const result: IExtensionPromptFileResult[] = [];164for (const file of [...agents, ...instructions, ...prompts, ...skills, ...hooks]) {165if (file.storage === PromptsStorage.extension) {166result.push({ uri: file.uri.toJSON(), type: file.type });167}168}169170return result;171});172173class ChatPromptFilesDataRenderer extends Disposable implements IExtensionFeatureTableRenderer {174readonly type = 'table';175176constructor(private readonly contributionPoint: ChatContributionPoint) {177super();178}179180shouldRender(manifest: IExtensionManifest): boolean {181return !!manifest.contributes?.[this.contributionPoint];182}183184render(manifest: IExtensionManifest): IRenderedData<ITableData> {185const contributions = manifest.contributes?.[this.contributionPoint] ?? [];186if (!contributions.length) {187return { data: { headers: [], rows: [] }, dispose: () => { } };188}189190const headers = [191localize('chatFilesName', "Name"),192localize('chatFilesDescription', "Description"),193localize('chatFilesPath', "Path"),194];195196const rows: IRowData[][] = contributions.map(d => {197return [198d.name ?? '-',199d.description ?? '-',200d.path,201];202});203204return {205data: {206headers,207rows208},209dispose: () => { }210};211}212}213214Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({215id: ChatContributionPoint.chatPromptFiles,216label: localize('chatPromptFiles', "Chat Prompt Files"),217access: {218canToggle: false219},220renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatPromptFiles]),221});222223Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({224id: ChatContributionPoint.chatInstructions,225label: localize('chatInstructions', "Chat Instructions"),226access: {227canToggle: false228},229renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatInstructions]),230});231232Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({233id: ChatContributionPoint.chatAgents,234label: localize('chatAgents', "Chat Agents"),235access: {236canToggle: false237},238renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatAgents]),239});240241Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({242id: ChatContributionPoint.chatSkills,243label: localize('chatSkills', "Chat Skills"),244access: {245canToggle: false246},247renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatSkills]),248});249250251