Path: blob/main/src/vs/workbench/api/common/extHostChatContext.ts
5240 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 type * as vscode from 'vscode';6import { CancellationToken } from '../../../base/common/cancellation.js';7import { URI, UriComponents } from '../../../base/common/uri.js';8import { ExtHostChatContextShape, MainContext, MainThreadChatContextShape } from './extHost.protocol.js';9import { DocumentSelector, MarkdownString } from './extHostTypeConverters.js';10import { IExtHostRpcService } from './extHostRpcService.js';11import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatContext.js';12import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';13import { IExtHostCommands } from './extHostCommands.js';1415type ProviderType = 'workspace' | 'explicit' | 'resource';1617interface ProviderEntry {18type: ProviderType;19provider: vscode.ChatWorkspaceContextProvider | vscode.ChatExplicitContextProvider | vscode.ChatResourceContextProvider;20disposables: DisposableStore;21}2223export class ExtHostChatContext extends Disposable implements ExtHostChatContextShape {24declare _serviceBrand: undefined;2526private _proxy: MainThreadChatContextShape;27private _handlePool: number = 0;28private _providers: Map<number, ProviderEntry> = new Map();29private _itemPool: number = 0;30/** Global map of itemHandle -> original item for command execution with reference equality */31private _globalItems: Map<number, vscode.ChatContextItem> = new Map();32/** Track which items belong to which provider for cleanup */33private _providerItems: Map<number, Set<number>> = new Map(); // providerHandle -> Set<itemHandle>3435constructor(36@IExtHostRpcService extHostRpc: IExtHostRpcService,37@IExtHostCommands private readonly _commands: IExtHostCommands,38) {39super();40this._proxy = extHostRpc.getProxy(MainContext.MainThreadChatContext);41}4243// Workspace context provider methods4445async $provideWorkspaceChatContext(handle: number, token: CancellationToken): Promise<IChatContextItem[]> {46this._clearProviderItems(handle);47const entry = this._providers.get(handle);48if (!entry || entry.type !== 'workspace') {49throw new Error('Workspace context provider not found');50}51const provider = entry.provider as vscode.ChatWorkspaceContextProvider;52const result = (await provider.provideChatContext(token)) ?? [];53return this._convertItems(handle, result);54}5556// Explicit context provider methods5758async $provideExplicitChatContext(handle: number, token: CancellationToken): Promise<IChatContextItem[]> {59this._clearProviderItems(handle);60const entry = this._providers.get(handle);61if (!entry || entry.type !== 'explicit') {62throw new Error('Explicit context provider not found');63}64const provider = entry.provider as vscode.ChatExplicitContextProvider;65const result = (await provider.provideChatContext(token)) ?? [];66return this._convertItems(handle, result);67}6869async $resolveExplicitChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise<IChatContextItem> {70const entry = this._providers.get(handle);71if (!entry || entry.type !== 'explicit') {72throw new Error('Explicit context provider not found');73}74const provider = entry.provider as vscode.ChatExplicitContextProvider;75const extItem = this._globalItems.get(context.handle);76if (!extItem) {77throw new Error('Chat context item not found');78}79return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token);80}8182// Resource context provider methods8384async $provideResourceChatContext(handle: number, options: { resource: UriComponents; withValue: boolean }, token: CancellationToken): Promise<IChatContextItem | undefined> {85const entry = this._providers.get(handle);86if (!entry || entry.type !== 'resource') {87throw new Error('Resource context provider not found');88}89const provider = entry.provider as vscode.ChatResourceContextProvider;9091const result = await provider.provideChatContext({ resource: URI.revive(options.resource) }, token);92if (!result) {93return undefined;94}95if (result.label === undefined && result.resourceUri === undefined) {96throw new Error('ChatContextItem must have either a label or a resourceUri');97}98const itemHandle = this._addTrackedItem(handle, result);99100const item: IChatContextItem = {101handle: itemHandle,102icon: result.icon,103label: result.label,104resourceUri: result.resourceUri,105modelDescription: result.modelDescription,106tooltip: result.tooltip ? MarkdownString.from(result.tooltip) : undefined,107value: options.withValue ? result.value : undefined,108command: result.command ? { id: result.command.command } : undefined109};110if (options.withValue && !item.value) {111const resolved = await provider.resolveChatContext(result, token);112item.value = resolved?.value;113item.tooltip = resolved?.tooltip ? MarkdownString.from(resolved.tooltip) : item.tooltip;114}115116return item;117}118119async $resolveResourceChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise<IChatContextItem> {120const entry = this._providers.get(handle);121if (!entry || entry.type !== 'resource') {122throw new Error('Resource context provider not found');123}124const provider = entry.provider as vscode.ChatResourceContextProvider;125const extItem = this._globalItems.get(context.handle);126if (!extItem) {127throw new Error('Chat context item not found');128}129return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token);130}131132// Command execution133134async $executeChatContextItemCommand(itemHandle: number): Promise<void> {135const extItem = this._globalItems.get(itemHandle);136if (!extItem) {137throw new Error('Chat context item not found');138}139if (!extItem.command) {140throw new Error('Chat context item has no command');141}142// Execute the command with the original extension item as an argument (reference equality)143const args = extItem.command.arguments ? [extItem, ...extItem.command.arguments] : [extItem];144await this._commands.executeCommand(extItem.command.command, ...args);145}146147// Registration methods148149registerChatWorkspaceContextProvider(id: string, provider: vscode.ChatWorkspaceContextProvider): vscode.Disposable {150const handle = this._handlePool++;151const disposables = new DisposableStore();152this._providers.set(handle, { type: 'workspace', provider, disposables });153this._listenForWorkspaceContextChanges(handle, provider, disposables);154this._proxy.$registerChatWorkspaceContextProvider(handle, id);155156return {157dispose: () => {158this._providers.delete(handle);159this._clearProviderItems(handle);160this._providerItems.delete(handle);161this._proxy.$unregisterChatContextProvider(handle);162disposables.dispose();163}164};165}166167registerChatExplicitContextProvider(id: string, provider: vscode.ChatExplicitContextProvider): vscode.Disposable {168const handle = this._handlePool++;169const disposables = new DisposableStore();170this._providers.set(handle, { type: 'explicit', provider, disposables });171this._proxy.$registerChatExplicitContextProvider(handle, id);172173return {174dispose: () => {175this._providers.delete(handle);176this._clearProviderItems(handle);177this._providerItems.delete(handle);178this._proxy.$unregisterChatContextProvider(handle);179disposables.dispose();180}181};182}183184registerChatResourceContextProvider(selector: vscode.DocumentSelector, id: string, provider: vscode.ChatResourceContextProvider): vscode.Disposable {185const handle = this._handlePool++;186const disposables = new DisposableStore();187this._providers.set(handle, { type: 'resource', provider, disposables });188this._proxy.$registerChatResourceContextProvider(handle, id, DocumentSelector.from(selector));189190return {191dispose: () => {192this._providers.delete(handle);193this._clearProviderItems(handle);194this._providerItems.delete(handle);195this._proxy.$unregisterChatContextProvider(handle);196disposables.dispose();197}198};199}200201/**202* @deprecated Use registerChatWorkspaceContextProvider, registerChatExplicitContextProvider, or registerChatResourceContextProvider instead.203*/204registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable {205const disposables: vscode.Disposable[] = [];206207// Register workspace context provider if the provider supports it208if (provider.provideWorkspaceChatContext) {209const workspaceProvider: vscode.ChatWorkspaceContextProvider = {210onDidChangeWorkspaceChatContext: provider.onDidChangeWorkspaceChatContext,211provideChatContext: (token) => provider.provideWorkspaceChatContext!(token)212};213disposables.push(this.registerChatWorkspaceContextProvider(id, workspaceProvider));214}215216// Register explicit context provider if the provider supports it217if (provider.provideChatContextExplicit) {218const explicitProvider: vscode.ChatExplicitContextProvider = {219provideChatContext: (token) => provider.provideChatContextExplicit!(token),220resolveChatContext: provider.resolveChatContext221? (context, token) => provider.resolveChatContext!(context, token)222: (context) => context223};224disposables.push(this.registerChatExplicitContextProvider(id, explicitProvider));225}226227// Register resource context provider if the provider supports it and has a selector228if (provider.provideChatContextForResource && selector) {229const resourceProvider: vscode.ChatResourceContextProvider = {230provideChatContext: (options, token) => provider.provideChatContextForResource!(options, token),231resolveChatContext: provider.resolveChatContext232? (context, token) => provider.resolveChatContext!(context, token)233: (context) => context234};235disposables.push(this.registerChatResourceContextProvider(selector, id, resourceProvider));236}237238return {239dispose: () => {240for (const disposable of disposables) {241disposable.dispose();242}243}244};245}246247// Helper methods248249private _clearProviderItems(handle: number): void {250const itemHandles = this._providerItems.get(handle);251if (itemHandles) {252for (const itemHandle of itemHandles) {253this._globalItems.delete(itemHandle);254}255itemHandles.clear();256}257}258259private _addTrackedItem(providerHandle: number, item: vscode.ChatContextItem): number {260const itemHandle = this._itemPool++;261this._globalItems.set(itemHandle, item);262if (!this._providerItems.has(providerHandle)) {263this._providerItems.set(providerHandle, new Set());264}265this._providerItems.get(providerHandle)!.add(itemHandle);266return itemHandle;267}268269private _convertItems(handle: number, items: vscode.ChatContextItem[]): IChatContextItem[] {270const result: IChatContextItem[] = [];271for (const item of items) {272if (item.label === undefined && item.resourceUri === undefined) {273throw new Error('ChatContextItem must have either a label or a resourceUri');274}275const itemHandle = this._addTrackedItem(handle, item);276result.push({277handle: itemHandle,278icon: item.icon,279label: item.label,280resourceUri: item.resourceUri,281modelDescription: item.modelDescription,282tooltip: item.tooltip ? MarkdownString.from(item.tooltip) : undefined,283value: item.value,284command: item.command ? { id: item.command.command } : undefined285});286}287return result;288}289290private async _doResolve(291resolveFn: (item: vscode.ChatContextItem, token: CancellationToken) => vscode.ProviderResult<vscode.ChatContextItem>,292context: IChatContextItem,293extItem: vscode.ChatContextItem,294token: CancellationToken295): Promise<IChatContextItem> {296const extResult = await resolveFn(extItem, token);297if (extResult) {298return {299handle: context.handle,300icon: extResult.icon,301label: extResult.label,302resourceUri: extResult.resourceUri,303modelDescription: extResult.modelDescription,304tooltip: extResult.tooltip ? MarkdownString.from(extResult.tooltip) : undefined,305value: extResult.value,306command: extResult.command ? { id: extResult.command.command } : undefined307};308}309return context;310}311312private _listenForWorkspaceContextChanges(handle: number, provider: vscode.ChatWorkspaceContextProvider, disposables: DisposableStore): void {313if (!provider.onDidChangeWorkspaceChatContext) {314return;315}316const provideWorkspaceContext = async () => {317const workspaceContexts = await provider.provideChatContext(CancellationToken.None);318const resolvedContexts = this._convertItems(handle, workspaceContexts ?? []);319return this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts);320};321322disposables.add(provider.onDidChangeWorkspaceChatContext(async () => provideWorkspaceContext()));323// kick off initial workspace context fetch324provideWorkspaceContext();325}326327public override dispose(): void {328super.dispose();329for (const { disposables } of this._providers.values()) {330disposables.dispose();331}332}333}334335336