Path: blob/main/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts
5251 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, IChatExplicitContextProvider, IChatResourceContextProvider, IChatWorkspaceContextProvider } 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';16import { basename } from '../../../../../base/common/resources.js';1718export const IChatContextService = createDecorator<IChatContextService>('chatContextService');1920export interface IChatContextService extends ChatContextService { }2122interface IChatContextProviderEntry {23picker?: { title: string; icon: ThemeIcon };24workspaceProvider?: IChatWorkspaceContextProvider;25explicitProvider?: IChatExplicitContextProvider;26resourceProvider?: {27selector: LanguageSelector;28provider: IChatResourceContextProvider;29};30}3132export class ChatContextService extends Disposable {33_serviceBrand: undefined;3435private readonly _providers = new Map<string, IChatContextProviderEntry>();36private readonly _workspaceContext = new Map<string, IChatContextItem[]>();37private readonly _registeredPickers = this._register(new DisposableMap<string, IDisposable>());38private _lastResourceContext: Map<StringChatContextValue, { originalItem: IChatContextItem; provider: IChatResourceContextProvider }> = new Map();39private _executeCommandCallback: ((itemHandle: number) => Promise<void>) | undefined;4041constructor(42@IChatContextPickService private readonly _contextPickService: IChatContextPickService,43@IExtensionService private readonly _extensionService: IExtensionService44) {45super();46}4748setExecuteCommandCallback(callback: (itemHandle: number) => Promise<void>): void {49this._executeCommandCallback = callback;50}5152async executeChatContextItemCommand(handle: number): Promise<void> {53if (!this._executeCommandCallback) {54return;55}56await this._executeCommandCallback(handle);57}5859setChatContextProvider(id: string, picker: { title: string; icon: ThemeIcon }): void {60const providerEntry = this._providers.get(id) ?? {};61providerEntry.picker = picker;62this._providers.set(id, providerEntry);63this._registerWithPickService(id);64}6566private _registerWithPickService(id: string): void {67const providerEntry = this._providers.get(id);68if (!providerEntry || !providerEntry.picker || !providerEntry.explicitProvider) {69return;70}71const title = `${providerEntry.picker.title.replace(/\.+$/, '')}...`;72this._registeredPickers.set(id, this._contextPickService.registerChatContextItem(this._asPicker(title, providerEntry.picker.icon, id)));73}7475registerChatWorkspaceContextProvider(id: string, provider: IChatWorkspaceContextProvider): void {76const providerEntry = this._providers.get(id) ?? {};77providerEntry.workspaceProvider = provider;78this._providers.set(id, providerEntry);79}8081registerChatExplicitContextProvider(id: string, provider: IChatExplicitContextProvider): void {82const providerEntry = this._providers.get(id) ?? {};83providerEntry.explicitProvider = provider;84this._providers.set(id, providerEntry);85this._registerWithPickService(id);86}8788registerChatResourceContextProvider(id: string, selector: LanguageSelector, provider: IChatResourceContextProvider): void {89const providerEntry = this._providers.get(id) ?? {};90providerEntry.resourceProvider = { selector, provider };91this._providers.set(id, providerEntry);92}9394unregisterChatContextProvider(id: string): void {95this._providers.delete(id);96this._registeredPickers.deleteAndDispose(id);97}9899updateWorkspaceContextItems(id: string, items: IChatContextItem[]): void {100this._workspaceContext.set(id, items);101}102103getWorkspaceContextItems(): IChatRequestWorkspaceVariableEntry[] {104const items: IChatRequestWorkspaceVariableEntry[] = [];105for (const workspaceContexts of this._workspaceContext.values()) {106for (const item of workspaceContexts) {107if (!item.value) {108continue;109}110// Derive label from resourceUri if label is not set111const derivedLabel = item.label ?? (item.resourceUri ? basename(item.resourceUri) : 'Unknown');112items.push({113value: item.value,114name: derivedLabel,115modelDescription: item.modelDescription,116id: derivedLabel,117kind: 'workspace'118});119}120}121return items;122}123124async contextForResource(uri: URI, language?: string): Promise<StringChatContextValue | undefined> {125return this._contextForResource(uri, false, language);126}127128private async _contextForResource(uri: URI, withValue: boolean, language?: string): Promise<StringChatContextValue | undefined> {129const scoredProviders: Array<{ score: number; provider: IChatResourceContextProvider }> = [];130for (const providerEntry of this._providers.values()) {131if (!providerEntry.resourceProvider) {132continue;133}134const matchScore = score(providerEntry.resourceProvider.selector, uri, language ?? '', true, undefined, undefined);135scoredProviders.push({ score: matchScore, provider: providerEntry.resourceProvider.provider });136}137scoredProviders.sort((a, b) => b.score - a.score);138if (scoredProviders.length === 0 || scoredProviders[0].score <= 0) {139return;140}141const provider = scoredProviders[0].provider;142const context = (await provider.provideChatContext(uri, withValue, CancellationToken.None));143if (!context) {144return;145}146// Derive label from resourceUri if label is not set147const effectiveResourceUri = context.resourceUri ?? uri;148const derivedLabel = context.label ?? basename(effectiveResourceUri);149const contextValue: StringChatContextValue = {150value: undefined,151name: derivedLabel,152icon: context.icon,153uri: uri,154resourceUri: context.resourceUri,155modelDescription: context.modelDescription,156tooltip: context.tooltip,157commandId: context.command?.id,158handle: context.handle159};160this._lastResourceContext.clear();161this._lastResourceContext.set(contextValue, { originalItem: context, provider });162return contextValue;163}164165async resolveChatContext(context: StringChatContextValue, language?: string): Promise<StringChatContextValue> {166if (context.value !== undefined) {167return context;168}169170const item = this._lastResourceContext.get(context);171if (!item) {172const resolved = await this._contextForResource(context.uri, true, language);173context.value = resolved?.value;174context.modelDescription = resolved?.modelDescription;175context.tooltip = resolved?.tooltip;176return context;177} else {178const resolved = await item.provider.resolveChatContext(item.originalItem, CancellationToken.None);179if (resolved) {180context.value = resolved.value;181context.modelDescription = resolved.modelDescription;182context.tooltip = resolved.tooltip;183return context;184}185}186return context;187}188189private _asPicker(title: string, icon: ThemeIcon, id: string): IChatContextPickerItem {190const asPicker = (): IChatContextPicker => {191let providerEntry = this._providers.get(id);192if (!providerEntry) {193throw new Error('No chat context provider registered');194}195196const picks = async (): Promise<IChatContextItem[]> => {197if (providerEntry && !providerEntry.explicitProvider) {198// Activate the extension providing the chat context provider199await this._extensionService.activateByEvent(`onChatContextProvider:${id}`);200providerEntry = this._providers.get(id);201if (!providerEntry?.explicitProvider) {202return [];203}204}205const results = await providerEntry?.explicitProvider!.provideChatContext(CancellationToken.None);206return results || [];207};208209return {210picks: picks().then(items => {211return items.map(item => {212// Derive label from resourceUri if label is not set213const derivedLabel = item.label ?? (item.resourceUri ? basename(item.resourceUri) : 'Unknown');214return {215label: derivedLabel,216iconClass: item.icon ? ThemeIcon.asClassName(item.icon) : undefined,217asAttachment: async (): Promise<IGenericChatRequestVariableEntry> => {218let contextValue = item;219if ((contextValue.value === undefined) && providerEntry?.explicitProvider) {220contextValue = await providerEntry.explicitProvider.resolveChatContext(item, CancellationToken.None);221}222// Derive label from resourceUri if label is not set223const resolvedLabel = contextValue.label ?? (contextValue.resourceUri ? basename(contextValue.resourceUri) : 'Unknown');224return {225kind: 'generic',226id: resolvedLabel,227name: resolvedLabel,228icon: contextValue.icon,229value: contextValue.value,230};231}232};233});234}),235placeholder: title236};237};238239const picker: IChatContextPickerItem = {240asPicker,241type: 'pickerPick',242label: title,243icon244};245246return picker;247}248}249250registerSingleton(IChatContextService, ChatContextService, InstantiationType.Delayed);251252253