Path: blob/main/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts
4780 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*--------------------------------------------------------------------------------------------*/45import { ThemeIcon } from '../../../../../base/common/themables.js';6import { LanguageSelector, score } from '../../../../../editor/common/languageSelector.js';7import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';8import { IChatContextPicker, IChatContextPickerItem, IChatContextPickService } from '../attachments/chatContextPickService.js';9import { IChatContextItem, IChatContextProvider } from '../../common/contextContrib/chatContext.js';10import { CancellationToken } from '../../../../../base/common/cancellation.js';11import { IChatRequestWorkspaceVariableEntry, IGenericChatRequestVariableEntry, StringChatContextValue } from '../../common/attachments/chatVariableEntries.js';12import { IExtensionService } from '../../../../services/extensions/common/extensions.js';13import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';14import { Disposable, DisposableMap, IDisposable } from '../../../../../base/common/lifecycle.js';15import { URI } from '../../../../../base/common/uri.js';1617export const IChatContextService = createDecorator<IChatContextService>('chatContextService');1819export interface IChatContextService extends ChatContextService { }2021interface IChatContextProviderEntry {22picker?: { title: string; icon: ThemeIcon };23chatContextProvider?: {24selector: LanguageSelector | undefined;25provider: IChatContextProvider;26};27}2829export class ChatContextService extends Disposable {30_serviceBrand: undefined;3132private readonly _providers = new Map<string, IChatContextProviderEntry>();33private readonly _workspaceContext = new Map<string, IChatContextItem[]>();34private readonly _registeredPickers = this._register(new DisposableMap<string, IDisposable>());35private _lastResourceContext: Map<StringChatContextValue, { originalItem: IChatContextItem; provider: IChatContextProvider }> = new Map();3637constructor(38@IChatContextPickService private readonly _contextPickService: IChatContextPickService,39@IExtensionService private readonly _extensionService: IExtensionService40) {41super();42}4344setChatContextProvider(id: string, picker: { title: string; icon: ThemeIcon }): void {45const providerEntry = this._providers.get(id) ?? { picker: undefined };46providerEntry.picker = picker;47this._providers.set(id, providerEntry);48this._registerWithPickService(id);49}5051private _registerWithPickService(id: string): void {52const providerEntry = this._providers.get(id);53if (!providerEntry || !providerEntry.picker || !providerEntry.chatContextProvider) {54return;55}56const title = `${providerEntry.picker.title.replace(/\.+$/, '')}...`;57this._registeredPickers.set(id, this._contextPickService.registerChatContextItem(this._asPicker(title, providerEntry.picker.icon, id)));58}5960registerChatContextProvider(id: string, selector: LanguageSelector | undefined, provider: IChatContextProvider): void {61const providerEntry = this._providers.get(id) ?? { picker: undefined };62providerEntry.chatContextProvider = { selector, provider };63this._providers.set(id, providerEntry);64this._registerWithPickService(id);65}6667unregisterChatContextProvider(id: string): void {68this._providers.delete(id);69this._registeredPickers.deleteAndDispose(id);70}7172updateWorkspaceContextItems(id: string, items: IChatContextItem[]): void {73this._workspaceContext.set(id, items);74}7576getWorkspaceContextItems(): IChatRequestWorkspaceVariableEntry[] {77const items: IChatRequestWorkspaceVariableEntry[] = [];78for (const workspaceContexts of this._workspaceContext.values()) {79for (const item of workspaceContexts) {80if (!item.value) {81continue;82}83items.push({84value: item.value,85name: item.label,86modelDescription: item.modelDescription,87id: item.label,88kind: 'workspace'89});90}91}92return items;93}9495async contextForResource(uri: URI): Promise<StringChatContextValue | undefined> {96return this._contextForResource(uri, false);97}9899private async _contextForResource(uri: URI, withValue: boolean): Promise<StringChatContextValue | undefined> {100const scoredProviders: Array<{ score: number; provider: IChatContextProvider }> = [];101for (const providerEntry of this._providers.values()) {102if (!providerEntry.chatContextProvider?.provider.provideChatContextForResource || (providerEntry.chatContextProvider.selector === undefined)) {103continue;104}105const matchScore = score(providerEntry.chatContextProvider.selector, uri, '', true, undefined, undefined);106scoredProviders.push({ score: matchScore, provider: providerEntry.chatContextProvider.provider });107}108scoredProviders.sort((a, b) => b.score - a.score);109if (scoredProviders.length === 0 || scoredProviders[0].score <= 0) {110return;111}112const context = (await scoredProviders[0].provider.provideChatContextForResource!(uri, withValue, CancellationToken.None));113if (!context) {114return;115}116const contextValue: StringChatContextValue = {117value: undefined,118name: context.label,119icon: context.icon,120uri: uri,121modelDescription: context.modelDescription122};123this._lastResourceContext.clear();124this._lastResourceContext.set(contextValue, { originalItem: context, provider: scoredProviders[0].provider });125return contextValue;126}127128async resolveChatContext(context: StringChatContextValue): Promise<StringChatContextValue> {129if (context.value !== undefined) {130return context;131}132133const item = this._lastResourceContext.get(context);134if (!item) {135const resolved = await this._contextForResource(context.uri, true);136context.value = resolved?.value;137context.modelDescription = resolved?.modelDescription;138return context;139} else if (item.provider.resolveChatContext) {140const resolved = await item.provider.resolveChatContext(item.originalItem, CancellationToken.None);141if (resolved) {142context.value = resolved.value;143context.modelDescription = resolved.modelDescription;144return context;145}146}147return context;148}149150private _asPicker(title: string, icon: ThemeIcon, id: string): IChatContextPickerItem {151const asPicker = (): IChatContextPicker => {152let providerEntry = this._providers.get(id);153if (!providerEntry) {154throw new Error('No chat context provider registered');155}156157const picks = async (): Promise<IChatContextItem[]> => {158if (providerEntry && !providerEntry.chatContextProvider) {159// Activate the extension providing the chat context provider160await this._extensionService.activateByEvent(`onChatContextProvider:${id}`);161providerEntry = this._providers.get(id);162if (!providerEntry?.chatContextProvider) {163return [];164}165}166const results = await providerEntry?.chatContextProvider!.provider.provideChatContext({}, CancellationToken.None);167return results || [];168};169170return {171picks: picks().then(items => {172return items.map(item => ({173label: item.label,174iconClass: ThemeIcon.asClassName(item.icon),175asAttachment: async (): Promise<IGenericChatRequestVariableEntry> => {176let contextValue = item;177if ((contextValue.value === undefined) && providerEntry?.chatContextProvider?.provider!.resolveChatContext) {178contextValue = await providerEntry.chatContextProvider.provider.resolveChatContext(item, CancellationToken.None);179}180return {181kind: 'generic',182id: contextValue.label,183name: contextValue.label,184icon: contextValue.icon,185value: contextValue.value186};187}188}));189}),190placeholder: title191};192};193194const picker: IChatContextPickerItem = {195asPicker,196type: 'pickerPick',197label: title,198icon199};200201return picker;202}203}204205registerSingleton(IChatContextService, ChatContextService, InstantiationType.Delayed);206207208