From b5a77387eeedcdc8e56f2c5f2565e16d1da97569 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:01:35 +0100 Subject: [PATCH] Break chat context provider into 3 different providers (#289951) * Break chat context provider into 3 different providers * Keep backwards compatibility --- .../api/browser/mainThreadChatContext.ts | 45 ++- .../workbench/api/common/extHost.api.impl.ts | 13 + .../workbench/api/common/extHost.protocol.ts | 14 +- .../api/common/extHostChatContext.ts | 353 ++++++++++++------ .../contextContrib/chatContextService.ts | 56 +-- .../chat/common/contextContrib/chatContext.ts | 17 +- .../vscode.proposed.chatContextProvider.d.ts | 99 ++++- 7 files changed, 419 insertions(+), 178 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatContext.ts b/src/vs/workbench/api/browser/mainThreadChatContext.ts index 917aeb8c02a..d819fb0ab68 100644 --- a/src/vs/workbench/api/browser/mainThreadChatContext.ts +++ b/src/vs/workbench/api/browser/mainThreadChatContext.ts @@ -5,8 +5,7 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Disposable } from '../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../base/common/themables.js'; -import { IChatContextItem, IChatContextSupport } from '../../contrib/chat/common/contextContrib/chatContext.js'; +import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatContext.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostChatContextShape, ExtHostContext, IDocumentFilterDto, MainContext, MainThreadChatContextShape } from '../common/extHost.protocol.js'; import { IChatContextService } from '../../contrib/chat/browser/contextContrib/chatContextService.js'; @@ -15,7 +14,7 @@ import { URI } from '../../../base/common/uri.js'; @extHostNamedCustomer(MainContext.MainThreadChatContext) export class MainThreadChatContext extends Disposable implements MainThreadChatContextShape { private readonly _proxy: ExtHostChatContextShape; - private readonly _providers = new Map(); + private readonly _providers = new Map(); constructor( extHostContext: IExtHostContext, @@ -26,18 +25,36 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC this._chatContextService.setExecuteCommandCallback((itemHandle) => this._proxy.$executeChatContextItemCommand(itemHandle)); } - $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[] | undefined, _options: { icon: ThemeIcon }, support: IChatContextSupport): void { - this._providers.set(handle, { selector, support, id }); - this._chatContextService.registerChatContextProvider(id, selector, { - provideChatContext: (_options: {}, token: CancellationToken) => { - return this._proxy.$provideChatContext(handle, token); + $registerChatWorkspaceContextProvider(handle: number, id: string): void { + this._providers.set(handle, { id }); + this._chatContextService.registerChatWorkspaceContextProvider(id, { + provideWorkspaceChatContext: (token: CancellationToken) => { + return this._proxy.$provideWorkspaceChatContext(handle, token); + } + }); + } + + $registerChatExplicitContextProvider(handle: number, id: string): void { + this._providers.set(handle, { id }); + this._chatContextService.registerChatExplicitContextProvider(id, { + provideChatContext: (token: CancellationToken) => { + return this._proxy.$provideExplicitChatContext(handle, token); }, - resolveChatContext: support.supportsResolve ? (context: IChatContextItem, token: CancellationToken) => { - return this._proxy.$resolveChatContext(handle, context, token); - } : undefined, - provideChatContextForResource: support.supportsResource ? (resource: URI, withValue: boolean, token: CancellationToken) => { - return this._proxy.$provideChatContextForResource(handle, { resource, withValue }, token); - } : undefined, + resolveChatContext: (context: IChatContextItem, token: CancellationToken) => { + return this._proxy.$resolveExplicitChatContext(handle, context, token); + } + }); + } + + $registerChatResourceContextProvider(handle: number, id: string, selector: IDocumentFilterDto[]): void { + this._providers.set(handle, { id, selector }); + this._chatContextService.registerChatResourceContextProvider(id, selector, { + provideChatContext: (resource: URI, withValue: boolean, token: CancellationToken) => { + return this._proxy.$provideResourceChatContext(handle, { resource, withValue }, token); + }, + resolveChatContext: (context: IChatContextItem, token: CancellationToken) => { + return this._proxy.$resolveResourceChatContext(handle, context, token); + } }); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 779c6c822ea..b99cba602b7 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1546,6 +1546,19 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatOutputRenderer'); return extHostChatOutputRenderer.registerChatOutputRenderer(extension, viewType, renderer); }, + registerChatWorkspaceContextProvider(id: string, provider: vscode.ChatWorkspaceContextProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'chatContextProvider'); + return extHostChatContext.registerChatWorkspaceContextProvider(`${extension.id}-${id}`, provider); + }, + registerChatExplicitContextProvider(id: string, provider: vscode.ChatExplicitContextProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'chatContextProvider'); + return extHostChatContext.registerChatExplicitContextProvider(`${extension.id}-${id}`, provider); + }, + registerChatResourceContextProvider(selector: vscode.DocumentSelector, id: string, provider: vscode.ChatResourceContextProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'chatContextProvider'); + return extHostChatContext.registerChatResourceContextProvider(checkSelector(selector), `${extension.id}-${id}`, provider); + }, + /** @deprecated Use registerChatWorkspaceContextProvider, registerChatExplicitContextProvider, or registerChatResourceContextProvider instead. */ registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatContextProvider'); return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a61232dde5e..e0a094eac0e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -56,7 +56,7 @@ import { IRevealOptions, ITreeItem, IViewBadge } from '../../common/views.js'; import { CallHierarchyItem } from '../../contrib/callHierarchy/common/callHierarchy.js'; import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, UserSelectedTools } from '../../contrib/chat/common/participants/chatAgents.js'; import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/editing/chatCodeMapperService.js'; -import { IChatContextItem, IChatContextSupport } from '../../contrib/chat/common/contextContrib/chatContext.js'; +import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatContext.js'; import { IChatRelatedFile, IChatRelatedFileProviderMetadata as IChatRelatedFilesProviderMetadata, IChatRequestDraft } from '../../contrib/chat/common/editing/chatEditingService.js'; import { IChatProgressHistoryResponseContent, IChatRequestVariableData } from '../../contrib/chat/common/model/chatModel.js'; import { ChatResponseClearToPreviousToolInvocationReason, IChatContentInlineReference, IChatExternalEditsDto, IChatFollowup, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService/chatService.js'; @@ -1355,14 +1355,18 @@ export interface ExtHostLanguageModelsShape { } export interface ExtHostChatContextShape { - $provideChatContext(handle: number, token: CancellationToken): Promise; - $provideChatContextForResource(handle: number, options: { resource: UriComponents; withValue: boolean }, token: CancellationToken): Promise; - $resolveChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise; + $provideWorkspaceChatContext(handle: number, token: CancellationToken): Promise; + $provideExplicitChatContext(handle: number, token: CancellationToken): Promise; + $resolveExplicitChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise; + $provideResourceChatContext(handle: number, options: { resource: UriComponents; withValue: boolean }, token: CancellationToken): Promise; + $resolveResourceChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise; $executeChatContextItemCommand(itemHandle: number): Promise; } export interface MainThreadChatContextShape extends IDisposable { - $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[] | undefined, options: {}, support: IChatContextSupport): void; + $registerChatWorkspaceContextProvider(handle: number, id: string): void; + $registerChatExplicitContextProvider(handle: number, id: string): void; + $registerChatResourceContextProvider(handle: number, id: string, selector: IDocumentFilterDto[]): void; $unregisterChatContextProvider(handle: number): void; $updateWorkspaceContextItems(handle: number, items: IChatContextItem[]): void; $executeChatContextItemCommand(itemHandle: number): Promise; diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 83f8513a0b9..43264256157 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -13,12 +13,20 @@ import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatC import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; import { IExtHostCommands } from './extHostCommands.js'; +type ProviderType = 'workspace' | 'explicit' | 'resource'; + +interface ProviderEntry { + type: ProviderType; + provider: vscode.ChatWorkspaceContextProvider | vscode.ChatExplicitContextProvider | vscode.ChatResourceContextProvider; + disposables: DisposableStore; +} + export class ExtHostChatContext extends Disposable implements ExtHostChatContextShape { declare _serviceBrand: undefined; private _proxy: MainThreadChatContextShape; private _handlePool: number = 0; - private _providers: Map = new Map(); + private _providers: Map = new Map(); private _itemPool: number = 0; /** Global map of itemHandle -> original item for command execution with reference equality */ private _globalItems: Map = new Map(); @@ -33,29 +41,208 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext this._proxy = extHostRpc.getProxy(MainContext.MainThreadChatContext); } - async $provideChatContext(handle: number, token: CancellationToken): Promise { - this._clearProviderItems(handle); // clear previous items for this provider - const provider = this._getProvider(handle); - if (!provider.provideChatContextExplicit) { - throw new Error('provideChatContext not implemented'); + // Workspace context provider methods + + async $provideWorkspaceChatContext(handle: number, token: CancellationToken): Promise { + this._clearProviderItems(handle); + const entry = this._providers.get(handle); + if (!entry || entry.type !== 'workspace') { + throw new Error('Workspace context provider not found'); } - const result = (await provider.provideChatContextExplicit!(token)) ?? []; - const items: IChatContextItem[] = []; - for (const item of result) { - const itemHandle = this._addTrackedItem(handle, item); - items.push({ - handle: itemHandle, - icon: item.icon, - label: item.label, - modelDescription: item.modelDescription, - tooltip: item.tooltip ? MarkdownString.from(item.tooltip) : undefined, - value: item.value, - command: item.command ? { id: item.command.command } : undefined - }); - } - return items; + const provider = entry.provider as vscode.ChatWorkspaceContextProvider; + const result = (await provider.provideChatContext(token)) ?? []; + return this._convertItems(handle, result); } + // Explicit context provider methods + + async $provideExplicitChatContext(handle: number, token: CancellationToken): Promise { + this._clearProviderItems(handle); + const entry = this._providers.get(handle); + if (!entry || entry.type !== 'explicit') { + throw new Error('Explicit context provider not found'); + } + const provider = entry.provider as vscode.ChatExplicitContextProvider; + const result = (await provider.provideChatContext(token)) ?? []; + return this._convertItems(handle, result); + } + + async $resolveExplicitChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise { + const entry = this._providers.get(handle); + if (!entry || entry.type !== 'explicit') { + throw new Error('Explicit context provider not found'); + } + const provider = entry.provider as vscode.ChatExplicitContextProvider; + const extItem = this._globalItems.get(context.handle); + if (!extItem) { + throw new Error('Chat context item not found'); + } + return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token); + } + + // Resource context provider methods + + async $provideResourceChatContext(handle: number, options: { resource: UriComponents; withValue: boolean }, token: CancellationToken): Promise { + const entry = this._providers.get(handle); + if (!entry || entry.type !== 'resource') { + throw new Error('Resource context provider not found'); + } + const provider = entry.provider as vscode.ChatResourceContextProvider; + + const result = await provider.provideChatContext({ resource: URI.revive(options.resource) }, token); + if (!result) { + return undefined; + } + const itemHandle = this._addTrackedItem(handle, result); + + const item: IChatContextItem = { + handle: itemHandle, + icon: result.icon, + label: result.label, + modelDescription: result.modelDescription, + tooltip: result.tooltip ? MarkdownString.from(result.tooltip) : undefined, + value: options.withValue ? result.value : undefined, + command: result.command ? { id: result.command.command } : undefined + }; + if (options.withValue && !item.value) { + const resolved = await provider.resolveChatContext(result, token); + item.value = resolved?.value; + item.tooltip = resolved?.tooltip ? MarkdownString.from(resolved.tooltip) : item.tooltip; + } + + return item; + } + + async $resolveResourceChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise { + const entry = this._providers.get(handle); + if (!entry || entry.type !== 'resource') { + throw new Error('Resource context provider not found'); + } + const provider = entry.provider as vscode.ChatResourceContextProvider; + const extItem = this._globalItems.get(context.handle); + if (!extItem) { + throw new Error('Chat context item not found'); + } + return this._doResolve(provider.resolveChatContext.bind(provider), context, extItem, token); + } + + // Command execution + + async $executeChatContextItemCommand(itemHandle: number): Promise { + const extItem = this._globalItems.get(itemHandle); + if (!extItem) { + throw new Error('Chat context item not found'); + } + if (!extItem.command) { + throw new Error('Chat context item has no command'); + } + // Execute the command with the original extension item as an argument (reference equality) + const args = extItem.command.arguments ? [extItem, ...extItem.command.arguments] : [extItem]; + await this._commands.executeCommand(extItem.command.command, ...args); + } + + // Registration methods + + registerChatWorkspaceContextProvider(id: string, provider: vscode.ChatWorkspaceContextProvider): vscode.Disposable { + const handle = this._handlePool++; + const disposables = new DisposableStore(); + this._providers.set(handle, { type: 'workspace', provider, disposables }); + this._listenForWorkspaceContextChanges(handle, provider, disposables); + this._proxy.$registerChatWorkspaceContextProvider(handle, id); + + return { + dispose: () => { + this._providers.delete(handle); + this._clearProviderItems(handle); + this._providerItems.delete(handle); + this._proxy.$unregisterChatContextProvider(handle); + disposables.dispose(); + } + }; + } + + registerChatExplicitContextProvider(id: string, provider: vscode.ChatExplicitContextProvider): vscode.Disposable { + const handle = this._handlePool++; + const disposables = new DisposableStore(); + this._providers.set(handle, { type: 'explicit', provider, disposables }); + this._proxy.$registerChatExplicitContextProvider(handle, id); + + return { + dispose: () => { + this._providers.delete(handle); + this._clearProviderItems(handle); + this._providerItems.delete(handle); + this._proxy.$unregisterChatContextProvider(handle); + disposables.dispose(); + } + }; + } + + registerChatResourceContextProvider(selector: vscode.DocumentSelector, id: string, provider: vscode.ChatResourceContextProvider): vscode.Disposable { + const handle = this._handlePool++; + const disposables = new DisposableStore(); + this._providers.set(handle, { type: 'resource', provider, disposables }); + this._proxy.$registerChatResourceContextProvider(handle, id, DocumentSelector.from(selector)); + + return { + dispose: () => { + this._providers.delete(handle); + this._clearProviderItems(handle); + this._providerItems.delete(handle); + this._proxy.$unregisterChatContextProvider(handle); + disposables.dispose(); + } + }; + } + + /** + * @deprecated Use registerChatWorkspaceContextProvider, registerChatExplicitContextProvider, or registerChatResourceContextProvider instead. + */ + registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { + const disposables: vscode.Disposable[] = []; + + // Register workspace context provider if the provider supports it + if (provider.provideWorkspaceChatContext) { + const workspaceProvider: vscode.ChatWorkspaceContextProvider = { + onDidChangeWorkspaceChatContext: provider.onDidChangeWorkspaceChatContext, + provideChatContext: (token) => provider.provideWorkspaceChatContext!(token) + }; + disposables.push(this.registerChatWorkspaceContextProvider(id, workspaceProvider)); + } + + // Register explicit context provider if the provider supports it + if (provider.provideChatContextExplicit) { + const explicitProvider: vscode.ChatExplicitContextProvider = { + provideChatContext: (token) => provider.provideChatContextExplicit!(token), + resolveChatContext: provider.resolveChatContext + ? (context, token) => provider.resolveChatContext!(context, token) + : (context) => context + }; + disposables.push(this.registerChatExplicitContextProvider(id, explicitProvider)); + } + + // Register resource context provider if the provider supports it and has a selector + if (provider.provideChatContextForResource && selector) { + const resourceProvider: vscode.ChatResourceContextProvider = { + provideChatContext: (options, token) => provider.provideChatContextForResource!(options, token), + resolveChatContext: provider.resolveChatContext + ? (context, token) => provider.resolveChatContext!(context, token) + : (context) => context + }; + disposables.push(this.registerChatResourceContextProvider(selector, id, resourceProvider)); + } + + return { + dispose: () => { + for (const disposable of disposables) { + disposable.dispose(); + } + } + }; + } + + // Helper methods + private _clearProviderItems(handle: number): void { const itemHandles = this._providerItems.get(handle); if (itemHandles) { @@ -76,39 +263,30 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext return itemHandle; } - async $provideChatContextForResource(handle: number, options: { resource: UriComponents; withValue: boolean }, token: CancellationToken): Promise { - const provider = this._getProvider(handle); - - if (!provider.provideChatContextForResource) { - throw new Error('provideChatContextForResource not implemented'); + private _convertItems(handle: number, items: vscode.ChatContextItem[]): IChatContextItem[] { + const result: IChatContextItem[] = []; + for (const item of items) { + const itemHandle = this._addTrackedItem(handle, item); + result.push({ + handle: itemHandle, + icon: item.icon, + label: item.label, + modelDescription: item.modelDescription, + tooltip: item.tooltip ? MarkdownString.from(item.tooltip) : undefined, + value: item.value, + command: item.command ? { id: item.command.command } : undefined + }); } - - const result = await provider.provideChatContextForResource({ resource: URI.revive(options.resource) }, token); - if (!result) { - return undefined; - } - const itemHandle = this._addTrackedItem(handle, result); - - const item: IChatContextItem = { - handle: itemHandle, - icon: result.icon, - label: result.label, - modelDescription: result.modelDescription, - tooltip: result.tooltip ? MarkdownString.from(result.tooltip) : undefined, - value: options.withValue ? result.value : undefined, - command: result.command ? { id: result.command.command } : undefined - }; - if (options.withValue && !item.value && provider.resolveChatContext) { - const resolved = await provider.resolveChatContext(result, token); - item.value = resolved?.value; - item.tooltip = resolved?.tooltip ? MarkdownString.from(resolved.tooltip) : item.tooltip; - } - - return item; + return result; } - private async _doResolve(provider: vscode.ChatContextProvider, context: IChatContextItem, extItem: vscode.ChatContextItem, token: CancellationToken): Promise { - const extResult = await provider.resolveChatContext(extItem, token); + private async _doResolve( + resolveFn: (item: vscode.ChatContextItem, token: CancellationToken) => vscode.ProviderResult, + context: IChatContextItem, + extItem: vscode.ChatContextItem, + token: CancellationToken + ): Promise { + const extResult = await resolveFn(extItem, token); if (extResult) { return { handle: context.handle, @@ -123,71 +301,13 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext return context; } - async $resolveChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise { - const provider = this._getProvider(handle); - - if (!provider.resolveChatContext) { - throw new Error('resolveChatContext not implemented'); - } - const extItem = this._globalItems.get(context.handle); - if (!extItem) { - throw new Error('Chat context item not found'); - } - return this._doResolve(provider, context, extItem, token); - } - - async $executeChatContextItemCommand(itemHandle: number): Promise { - const extItem = this._globalItems.get(itemHandle); - if (!extItem) { - throw new Error('Chat context item not found'); - } - if (!extItem.command) { - throw new Error('Chat context item has no command'); - } - // Execute the command with the original extension item as an argument (reference equality) - const args = extItem.command.arguments ? [extItem, ...extItem.command.arguments] : [extItem]; - await this._commands.executeCommand(extItem.command.command, ...args); - } - - registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { - const handle = this._handlePool++; - const disposables = new DisposableStore(); - this._listenForWorkspaceContextChanges(handle, provider, disposables); - this._providers.set(handle, { provider, disposables }); - this._proxy.$registerChatContextProvider(handle, `${id}`, selector ? DocumentSelector.from(selector) : undefined, {}, { supportsResource: !!provider.provideChatContextForResource, supportsResolve: !!provider.resolveChatContext }); - - return { - dispose: () => { - this._providers.delete(handle); - this._clearProviderItems(handle); // Clean up tracked items - this._providerItems.delete(handle); - this._proxy.$unregisterChatContextProvider(handle); - disposables.dispose(); - } - }; - } - - private _listenForWorkspaceContextChanges(handle: number, provider: vscode.ChatContextProvider, disposables: DisposableStore): void { - if (!provider.onDidChangeWorkspaceChatContext || !provider.provideWorkspaceChatContext) { + private _listenForWorkspaceContextChanges(handle: number, provider: vscode.ChatWorkspaceContextProvider, disposables: DisposableStore): void { + if (!provider.onDidChangeWorkspaceChatContext) { return; } const provideWorkspaceContext = async () => { - const workspaceContexts = await provider.provideWorkspaceChatContext!(CancellationToken.None); - const resolvedContexts: IChatContextItem[] = []; - for (const item of workspaceContexts ?? []) { - const itemHandle = this._addTrackedItem(handle, item); - const contextItem: IChatContextItem = { - icon: item.icon, - label: item.label, - modelDescription: item.modelDescription, - tooltip: item.tooltip ? MarkdownString.from(item.tooltip) : undefined, - value: item.value, - handle: itemHandle, - command: item.command ? { id: item.command.command } : undefined - }; - const resolved = await this._doResolve(provider, contextItem, item, CancellationToken.None); - resolvedContexts.push(resolved); - } + const workspaceContexts = await provider.provideChatContext(CancellationToken.None); + const resolvedContexts = this._convertItems(handle, workspaceContexts ?? []); return this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); }; @@ -196,13 +316,6 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext provideWorkspaceContext(); } - private _getProvider(handle: number): vscode.ChatContextProvider { - if (!this._providers.has(handle)) { - throw new Error('Chat context provider not found'); - } - return this._providers.get(handle)!.provider; - } - public override dispose(): void { super.dispose(); for (const { disposables } of this._providers.values()) { diff --git a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts index 7f26d4cb095..2641884c833 100644 --- a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts +++ b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts @@ -7,7 +7,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { LanguageSelector, score } from '../../../../../editor/common/languageSelector.js'; import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; import { IChatContextPicker, IChatContextPickerItem, IChatContextPickService } from '../attachments/chatContextPickService.js'; -import { IChatContextItem, IChatContextProvider } from '../../common/contextContrib/chatContext.js'; +import { IChatContextItem, IChatExplicitContextProvider, IChatResourceContextProvider, IChatWorkspaceContextProvider } from '../../common/contextContrib/chatContext.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { IChatRequestWorkspaceVariableEntry, IGenericChatRequestVariableEntry, StringChatContextValue } from '../../common/attachments/chatVariableEntries.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; @@ -21,9 +21,11 @@ export interface IChatContextService extends ChatContextService { } interface IChatContextProviderEntry { picker?: { title: string; icon: ThemeIcon }; - chatContextProvider?: { - selector: LanguageSelector | undefined; - provider: IChatContextProvider; + workspaceProvider?: IChatWorkspaceContextProvider; + explicitProvider?: IChatExplicitContextProvider; + resourceProvider?: { + selector: LanguageSelector; + provider: IChatResourceContextProvider; }; } @@ -33,7 +35,7 @@ export class ChatContextService extends Disposable { private readonly _providers = new Map(); private readonly _workspaceContext = new Map(); private readonly _registeredPickers = this._register(new DisposableMap()); - private _lastResourceContext: Map = new Map(); + private _lastResourceContext: Map = new Map(); private _executeCommandCallback: ((itemHandle: number) => Promise) | undefined; constructor( @@ -55,7 +57,7 @@ export class ChatContextService extends Disposable { } setChatContextProvider(id: string, picker: { title: string; icon: ThemeIcon }): void { - const providerEntry = this._providers.get(id) ?? { picker: undefined }; + const providerEntry = this._providers.get(id) ?? {}; providerEntry.picker = picker; this._providers.set(id, providerEntry); this._registerWithPickService(id); @@ -63,20 +65,32 @@ export class ChatContextService extends Disposable { private _registerWithPickService(id: string): void { const providerEntry = this._providers.get(id); - if (!providerEntry || !providerEntry.picker || !providerEntry.chatContextProvider) { + if (!providerEntry || !providerEntry.picker || !providerEntry.explicitProvider) { return; } const title = `${providerEntry.picker.title.replace(/\.+$/, '')}...`; this._registeredPickers.set(id, this._contextPickService.registerChatContextItem(this._asPicker(title, providerEntry.picker.icon, id))); } - registerChatContextProvider(id: string, selector: LanguageSelector | undefined, provider: IChatContextProvider): void { - const providerEntry = this._providers.get(id) ?? { picker: undefined }; - providerEntry.chatContextProvider = { selector, provider }; + registerChatWorkspaceContextProvider(id: string, provider: IChatWorkspaceContextProvider): void { + const providerEntry = this._providers.get(id) ?? {}; + providerEntry.workspaceProvider = provider; + this._providers.set(id, providerEntry); + } + + registerChatExplicitContextProvider(id: string, provider: IChatExplicitContextProvider): void { + const providerEntry = this._providers.get(id) ?? {}; + providerEntry.explicitProvider = provider; this._providers.set(id, providerEntry); this._registerWithPickService(id); } + registerChatResourceContextProvider(id: string, selector: LanguageSelector, provider: IChatResourceContextProvider): void { + const providerEntry = this._providers.get(id) ?? {}; + providerEntry.resourceProvider = { selector, provider }; + this._providers.set(id, providerEntry); + } + unregisterChatContextProvider(id: string): void { this._providers.delete(id); this._registeredPickers.deleteAndDispose(id); @@ -110,20 +124,20 @@ export class ChatContextService extends Disposable { } private async _contextForResource(uri: URI, withValue: boolean, language?: string): Promise { - const scoredProviders: Array<{ score: number; provider: IChatContextProvider }> = []; + const scoredProviders: Array<{ score: number; provider: IChatResourceContextProvider }> = []; for (const providerEntry of this._providers.values()) { - if (!providerEntry.chatContextProvider?.provider.provideChatContextForResource || (providerEntry.chatContextProvider.selector === undefined)) { + if (!providerEntry.resourceProvider) { continue; } - const matchScore = score(providerEntry.chatContextProvider.selector, uri, language ?? '', true, undefined, undefined); - scoredProviders.push({ score: matchScore, provider: providerEntry.chatContextProvider.provider }); + const matchScore = score(providerEntry.resourceProvider.selector, uri, language ?? '', true, undefined, undefined); + scoredProviders.push({ score: matchScore, provider: providerEntry.resourceProvider.provider }); } scoredProviders.sort((a, b) => b.score - a.score); if (scoredProviders.length === 0 || scoredProviders[0].score <= 0) { return; } const provider = scoredProviders[0].provider; - const context = (await provider.provideChatContextForResource!(uri, withValue, CancellationToken.None)); + const context = (await provider.provideChatContext(uri, withValue, CancellationToken.None)); if (!context) { return; } @@ -154,7 +168,7 @@ export class ChatContextService extends Disposable { context.modelDescription = resolved?.modelDescription; context.tooltip = resolved?.tooltip; return context; - } else if (item.provider.resolveChatContext) { + } else { const resolved = await item.provider.resolveChatContext(item.originalItem, CancellationToken.None); if (resolved) { context.value = resolved.value; @@ -174,15 +188,15 @@ export class ChatContextService extends Disposable { } const picks = async (): Promise => { - if (providerEntry && !providerEntry.chatContextProvider) { + if (providerEntry && !providerEntry.explicitProvider) { // Activate the extension providing the chat context provider await this._extensionService.activateByEvent(`onChatContextProvider:${id}`); providerEntry = this._providers.get(id); - if (!providerEntry?.chatContextProvider) { + if (!providerEntry?.explicitProvider) { return []; } } - const results = await providerEntry?.chatContextProvider!.provider.provideChatContext({}, CancellationToken.None); + const results = await providerEntry?.explicitProvider!.provideChatContext(CancellationToken.None); return results || []; }; @@ -193,8 +207,8 @@ export class ChatContextService extends Disposable { iconClass: ThemeIcon.asClassName(item.icon), asAttachment: async (): Promise => { let contextValue = item; - if ((contextValue.value === undefined) && providerEntry?.chatContextProvider?.provider!.resolveChatContext) { - contextValue = await providerEntry.chatContextProvider.provider.resolveChatContext(item, CancellationToken.None); + if ((contextValue.value === undefined) && providerEntry?.explicitProvider) { + contextValue = await providerEntry.explicitProvider.resolveChatContext(item, CancellationToken.None); } return { kind: 'generic', diff --git a/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts index 6661e3e5130..7c6b273acd3 100644 --- a/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts +++ b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts @@ -21,13 +21,16 @@ export interface IChatContextItem { }; } -export interface IChatContextSupport { - supportsResource: boolean; - supportsResolve: boolean; +export interface IChatWorkspaceContextProvider { + provideWorkspaceChatContext(token: CancellationToken): Promise; } -export interface IChatContextProvider { - provideChatContext(options: {}, token: CancellationToken): Promise; - provideChatContextForResource?(resource: URI, withValue: boolean, token: CancellationToken): Promise; - resolveChatContext?(context: IChatContextItem, token: CancellationToken): Promise; +export interface IChatExplicitContextProvider { + provideChatContext(token: CancellationToken): Promise; + resolveChatContext(context: IChatContextItem, token: CancellationToken): Promise; +} + +export interface IChatResourceContextProvider { + provideChatContext(resource: URI, withValue: boolean, token: CancellationToken): Promise; + resolveChatContext(context: IChatContextItem, token: CancellationToken): Promise; } diff --git a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index 47a9284099d..b1f3ea58067 100644 --- a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts @@ -11,16 +11,45 @@ declare module 'vscode' { export namespace chat { /** - * Register a chat context provider. Chat context can be provided: - * - For a resource. Make sure to pass a selector that matches the resource you want to provide context for. - * Providers registered without a selector will not be called for resource-based context. - * - Explicitly. These context items are shown as options when the user explicitly attaches context. + * Register a chat workspace context provider. Workspace context is automatically included in all chat requests. * * To ensure your extension is activated when chat context is requested, make sure to include the following activations events: * - If your extension implements `provideWorkspaceChatContext` or `provideChatContextForResource`, find an activation event which is a good signal to activate. * Ex: `onLanguage:`, `onWebviewPanel:`, etc.` * - If your extension implements `provideChatContextExplicit`, your extension will be automatically activated when the user requests explicit context. * + * @param id Unique identifier for the provider. + * @param provider The chat workspace context provider. + */ + export function registerChatWorkspaceContextProvider(id: string, provider: ChatWorkspaceContextProvider): Disposable; + + /** + * Register a chat explicit context provider. Explicit context items are shown as options when the user explicitly attaches context. + * + * To ensure your extension is activated when chat context is requested, make sure to include the `onChatContextProvider:` activation event in your `package.json`. + * + * @param id Unique identifier for the provider. + * @param provider The chat explicit context provider. + */ + export function registerChatExplicitContextProvider(id: string, provider: ChatExplicitContextProvider): Disposable; + + /** + * Register a chat resource context provider. Resource context is provided for a specific resource. + * Make sure to pass a selector that matches the resource you want to provide context for. + * + * To ensure your extension is activated when chat context is requested, make sure to include the `onChatContextProvider:` activation event in your `package.json`. + * + * @param selector Document selector to filter which resources the provider is called for. + * @param id Unique identifier for the provider. + * @param provider The chat resource context provider. + */ + export function registerChatResourceContextProvider(selector: DocumentSelector, id: string, provider: ChatResourceContextProvider): Disposable; + + /** + * Register a chat context provider. + * + * @deprecated Use {@link registerChatWorkspaceContextProvider}, {@link registerChatExplicitContextProvider}, or {@link registerChatResourceContextProvider} instead. + * * @param selector Optional document selector to filter which resources the provider is called for. If omitted, the provider will only be called for explicit context requests. * @param id Unique identifier for the provider. * @param provider The chat context provider. @@ -57,7 +86,7 @@ declare module 'vscode' { command?: Command; } - export interface ChatContextProvider { + export interface ChatWorkspaceContextProvider { /** * An optional event that should be fired when the workspace chat context has changed. @@ -65,15 +94,16 @@ declare module 'vscode' { onDidChangeWorkspaceChatContext?: Event; /** - * TODO @API: should this be a separate provider interface? - * * Provide a list of chat context items to be included as workspace context for all chat requests. * This should be used very sparingly to avoid providing useless context and to avoid using up the context window. * A good example use case is to provide information about which branch the user is working on in a source control context. * * @param token A cancellation token. */ - provideWorkspaceChatContext?(token: CancellationToken): ProviderResult; + provideChatContext(token: CancellationToken): ProviderResult; + } + + export interface ChatExplicitContextProvider { /** * Provide a list of chat context items that a user can choose from. These context items are shown as options when the user explicitly attaches context. @@ -82,7 +112,18 @@ declare module 'vscode' { * * @param token A cancellation token. */ - provideChatContextExplicit?(token: CancellationToken): ProviderResult; + provideChatContext(token: CancellationToken): ProviderResult; + + /** + * If a chat context item is provided without a `value`, this method is called to resolve the `value` for the item. + * + * @param context The context item to resolve. + * @param token A cancellation token. + */ + resolveChatContext(context: T, token: CancellationToken): ProviderResult; + } + + export interface ChatResourceContextProvider { /** * Given a particular resource, provide a chat context item for it. This is used for implicit context (see the settings `chat.implicitContext.enabled` and `chat.implicitContext.suggestedContext`). @@ -94,10 +135,10 @@ declare module 'vscode' { * @param options Options include the resource for which to provide context. * @param token A cancellation token. */ - provideChatContextForResource?(options: { resource: Uri }, token: CancellationToken): ProviderResult; + provideChatContext(options: { resource: Uri }, token: CancellationToken): ProviderResult; /** - * If a chat context item is provided without a `value`, from either of the `provide` methods, this method is called to resolve the `value` for the item. + * If a chat context item is provided without a `value`, this method is called to resolve the `value` for the item. * * @param context The context item to resolve. * @param token A cancellation token. @@ -105,4 +146,40 @@ declare module 'vscode' { resolveChatContext(context: T, token: CancellationToken): ProviderResult; } + /** + * @deprecated Use {@link ChatWorkspaceContextProvider}, {@link ChatExplicitContextProvider}, or {@link ChatResourceContextProvider} instead. + */ + export interface ChatContextProvider { + + /** + * An optional event that should be fired when the workspace chat context has changed. + * @deprecated Use {@link ChatWorkspaceContextProvider.onDidChangeWorkspaceChatContext} instead. + */ + onDidChangeWorkspaceChatContext?: Event; + + /** + * Provide a list of chat context items to be included as workspace context for all chat requests. + * @deprecated Use {@link ChatWorkspaceContextProvider.provideChatContext} instead. + */ + provideWorkspaceChatContext?(token: CancellationToken): ProviderResult; + + /** + * Provide a list of chat context items that a user can choose from. + * @deprecated Use {@link ChatExplicitContextProvider.provideChatContext} instead. + */ + provideChatContextExplicit?(token: CancellationToken): ProviderResult; + + /** + * Given a particular resource, provide a chat context item for it. + * @deprecated Use {@link ChatResourceContextProvider.provideChatContext} instead. + */ + provideChatContextForResource?(options: { resource: Uri }, token: CancellationToken): ProviderResult; + + /** + * If a chat context item is provided without a `value`, this method is called to resolve the `value` for the item. + * @deprecated Use the `resolveChatContext` method on the specific provider type instead. + */ + resolveChatContext?(context: T, token: CancellationToken): ProviderResult; + } + }