diff --git a/src/vs/workbench/api/browser/mainThreadChatContext.ts b/src/vs/workbench/api/browser/mainThreadChatContext.ts index 38cab5806a0..c17babbcc46 100644 --- a/src/vs/workbench/api/browser/mainThreadChatContext.ts +++ b/src/vs/workbench/api/browser/mainThreadChatContext.ts @@ -15,7 +15,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, @@ -25,7 +25,7 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatContext); } - $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[], _options: { icon: ThemeIcon }, support: IChatContextSupport): void { + $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: (token: CancellationToken) => { @@ -48,4 +48,12 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC this._chatContextService.unregisterChatContextProvider(provider.id); this._providers.delete(handle); } + + $updateWorkspaceContextItems(handle: number, items: IChatContextItem[]): void { + const provider = this._providers.get(handle); + if (!provider) { + return; + } + this._chatContextService.updateWorkspaceContextItems(provider.id, items); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index da16fe2d2e4..a976c20189f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1537,9 +1537,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatOutputRenderer'); return extHostChatOutputRenderer.registerChatOutputRenderer(extension, viewType, renderer); }, - registerChatContextProvider(selector: vscode.DocumentSelector, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { + registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'chatContextProvider'); - return extHostChatContext.registerChatContextProvider(checkSelector(selector), `${extension.id}-${id}`, provider); + 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 5b8ca6ea90f..ea42783cd42 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1329,8 +1329,9 @@ export interface ExtHostChatContextShape { } export interface MainThreadChatContextShape extends IDisposable { - $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[], options: {}, support: IChatContextSupport): void; + $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[] | undefined, options: {}, support: IChatContextSupport): void; $unregisterChatContextProvider(handle: number): void; + $updateWorkspaceContextItems(handle: number, items: IChatContextItem[]): void; } export interface MainThreadEmbeddingsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 8ad7bbd595e..e2ee3c42e36 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -10,18 +10,20 @@ import { ExtHostChatContextShape, MainContext, MainThreadChatContextShape } from import { DocumentSelector } from './extHostTypeConverters.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import { IChatContextItem } from '../../contrib/chat/common/chatContext.js'; +import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; -export class ExtHostChatContext implements ExtHostChatContextShape { +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; private _items: Map> = new Map(); // handle -> itemHandle -> item constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService, ) { + super(); this._proxy = extHostRpc.getProxy(MainContext.MainThreadChatContext); } @@ -83,16 +85,7 @@ export class ExtHostChatContext implements ExtHostChatContextShape { return item; } - 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._items.get(handle)?.get(context.handle); - if (!extItem) { - throw new Error('Chat context item not found'); - } + private async _doResolve(provider: vscode.ChatContextProvider, context: IChatContextItem, extItem: vscode.ChatContextItem, token: CancellationToken): Promise { const extResult = await provider.resolveChatContext(extItem, token); const result = extResult ?? context; return { @@ -104,23 +97,69 @@ export class ExtHostChatContext implements ExtHostChatContextShape { }; } - registerChatContextProvider(selector: vscode.DocumentSelector, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { + 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._items.get(handle)?.get(context.handle); + if (!extItem) { + throw new Error('Chat context item not found'); + } + return this._doResolve(provider, context, extItem, token); + } + + registerChatContextProvider(selector: vscode.DocumentSelector | undefined, id: string, provider: vscode.ChatContextProvider): vscode.Disposable { const handle = this._handlePool++; - this._providers.set(handle, provider); - this._proxy.$registerChatContextProvider(handle, `${id}`, DocumentSelector.from(selector), {}, { supportsResource: !!provider.provideChatContextForResource, supportsResolve: !!provider.resolveChatContext }); + 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._proxy.$unregisterChatContextProvider(handle); + disposables.dispose(); } }; } + private _listenForWorkspaceContextChanges(handle: number, provider: vscode.ChatContextProvider, disposables: DisposableStore): void { + if (!provider.onDidChangeWorkspaceChatContext || !provider.provideWorkspaceChatContext) { + return; + } + disposables.add(provider.onDidChangeWorkspaceChatContext(async () => { + const workspaceContexts = await provider.provideWorkspaceChatContext!(CancellationToken.None); + const resolvedContexts: IChatContextItem[] = []; + for (const item of workspaceContexts ?? []) { + const contextItem: IChatContextItem = { + icon: item.icon, + label: item.label, + modelDescription: item.modelDescription, + value: item.value, + handle: this._itemPool++ + }; + const resolved = await this._doResolve(provider, contextItem, item, CancellationToken.None); + resolvedContexts.push(resolved); + } + + this._proxy.$updateWorkspaceContextItems(handle, resolvedContexts); + })); + } + private _getProvider(handle: number): vscode.ChatContextProvider { if (!this._providers.has(handle)) { throw new Error('Chat context provider not found'); } - return this._providers.get(handle)!; + return this._providers.get(handle)!.provider; + } + + public override dispose(): void { + super.dispose(); + for (const { disposables } of this._providers.values()) { + disposables.dispose(); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index b7972a34222..56f5b0c247f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -11,7 +11,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ResourceLabels } from '../../../../browser/labels.js'; -import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isTerminalVariableEntry, OmittedState } from '../../common/chatVariableEntries.js'; +import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isTerminalVariableEntry, isWorkspaceVariableEntry, OmittedState } from '../../common/chatVariableEntries.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js'; @@ -153,6 +153,9 @@ export class ChatAttachmentsContentPart extends Disposable { widget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels); } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { widget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels); + } else if (isWorkspaceVariableEntry(attachment)) { + // skip workspace attachments + return; } else { widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContextService.ts b/src/vs/workbench/contrib/chat/browser/chatContextService.ts index 7222696e1d8..0d858bf3180 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContextService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContextService.ts @@ -9,7 +9,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { IChatContextPicker, IChatContextPickerItem, IChatContextPickService } from './chatContextPickService.js'; import { IChatContextItem, IChatContextProvider } from '../common/chatContext.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IGenericChatRequestVariableEntry, StringChatContextValue } from '../common/chatVariableEntries.js'; +import { IChatRequestWorkspaceVariableEntry, IGenericChatRequestVariableEntry, StringChatContextValue } from '../common/chatVariableEntries.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Disposable, DisposableMap, IDisposable } from '../../../../base/common/lifecycle.js'; @@ -22,7 +22,7 @@ export interface IChatContextService extends ChatContextService { } interface IChatContextProviderEntry { picker?: { title: string; icon: ThemeIcon }; chatContextProvider?: { - selector: LanguageSelector; + selector: LanguageSelector | undefined; provider: IChatContextProvider; }; } @@ -31,6 +31,7 @@ export class ChatContextService extends Disposable { _serviceBrand: undefined; private readonly _providers = new Map(); + private readonly _workspaceContext = new Map(); private readonly _registeredPickers = this._register(new DisposableMap()); private _lastResourceContext: Map = new Map(); @@ -56,7 +57,7 @@ export class ChatContextService extends Disposable { this._registeredPickers.set(id, this._contextPickService.registerChatContextItem(this._asPicker(providerEntry.picker.title, providerEntry.picker.icon, id))); } - registerChatContextProvider(id: string, selector: LanguageSelector, provider: IChatContextProvider): void { + registerChatContextProvider(id: string, selector: LanguageSelector | undefined, provider: IChatContextProvider): void { const providerEntry = this._providers.get(id) ?? { picker: undefined }; providerEntry.chatContextProvider = { selector, provider }; this._providers.set(id, providerEntry); @@ -68,6 +69,29 @@ export class ChatContextService extends Disposable { this._registeredPickers.deleteAndDispose(id); } + updateWorkspaceContextItems(id: string, items: IChatContextItem[]): void { + this._workspaceContext.set(id, items); + } + + getWorkspaceContextItems(): IChatRequestWorkspaceVariableEntry[] { + const items: IChatRequestWorkspaceVariableEntry[] = []; + for (const workspaceContexts of this._workspaceContext.values()) { + for (const item of workspaceContexts) { + if (!item.value) { + continue; + } + items.push({ + value: item.value, + name: item.label, + modelDescription: item.modelDescription, + id: item.label, + kind: 'workspace' + }); + } + } + return items; + } + async contextForResource(uri: URI): Promise { return this._contextForResource(uri, false); } @@ -75,7 +99,7 @@ export class ChatContextService extends Disposable { private async _contextForResource(uri: URI, withValue: boolean): Promise { const scoredProviders: Array<{ score: number; provider: IChatContextProvider }> = []; for (const providerEntry of this._providers.values()) { - if (!providerEntry.chatContextProvider?.provider.provideChatContextForResource) { + if (!providerEntry.chatContextProvider?.provider.provideChatContextForResource || (providerEntry.chatContextProvider.selector === undefined)) { continue; } const matchScore = score(providerEntry.chatContextProvider.selector, uri, '', true, undefined, undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 5f7211875e5..2724953d42b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -105,6 +105,7 @@ import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js'; import { resizeImage } from './imageUtils.js'; import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modelPicker/modePickerActionItem.js'; +import { IChatContextService } from './chatContextService.js'; const $ = dom.$; @@ -179,7 +180,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public getAttachedContext(sessionResource: URI) { const contextArr = new ChatRequestVariableSet(); - contextArr.add(...this.attachmentModel.attachments); + contextArr.add(...this.attachmentModel.attachments, ...this.chatContextService.getWorkspaceContextItems()); return contextArr; } @@ -411,6 +412,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge @ILanguageModelToolsService private readonly toolService: ILanguageModelToolsService, @IChatService private readonly chatService: IChatService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IChatContextService private readonly chatContextService: IChatContextService, ) { super(); this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); diff --git a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts index 95170b9f6d7..68f198d1158 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts @@ -90,6 +90,13 @@ export interface IChatRequestStringVariableEntry extends IBaseChatRequestVariabl readonly uri: URI; } +export interface IChatRequestWorkspaceVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'workspace'; + readonly value: string; + readonly modelDescription?: string; +} + + export interface IChatRequestPasteVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'paste'; readonly code: string; @@ -260,7 +267,7 @@ export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChat | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry | IPromptFileVariableEntry | IPromptTextVariableEntry | ISCMHistoryItemVariableEntry | ISCMHistoryItemChangeVariableEntry | ISCMHistoryItemChangeRangeVariableEntry | ITerminalVariableEntry - | IChatRequestStringVariableEntry; + | IChatRequestStringVariableEntry | IChatRequestWorkspaceVariableEntry; export namespace IChatRequestVariableEntry { @@ -293,6 +300,10 @@ export function isPasteVariableEntry(obj: IChatRequestVariableEntry): obj is ICh return obj.kind === 'paste'; } +export function isWorkspaceVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestWorkspaceVariableEntry { + return obj.kind === 'workspace'; +} + export function isImageVariableEntry(obj: IChatRequestVariableEntry): obj is IImageVariableEntry { return obj.kind === 'image'; } diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 85dc306faf5..af92bb099a8 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -85,6 +85,7 @@ import { CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatConfigKeys, InlineChatResponse import { TestWorkerService } from './testWorkerService.js'; import { URI } from '../../../../../base/common/uri.js'; import { ChatWidgetService } from '../../../chat/browser/chatWidgetService.js'; +import { ChatContextService, IChatContextService } from '../../../chat/browser/chatContextService.js'; suite('InlineChatController', function () { @@ -256,6 +257,8 @@ suite('InlineChatController', function () { model.setEOL(EndOfLineSequence.LF); editor = store.add(instantiateTestCodeEditor(instaService, model)); + instaService.set(IChatContextService, store.add(instaService.createInstance(ChatContextService))); + store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, { async invoke(request, progress, history, token) { progress([{ diff --git a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index 38ea26573a3..173c5dd11f8 100644 --- a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts @@ -10,28 +10,61 @@ declare module 'vscode' { export namespace chat { - // TODO@alexr00 API: - // selector is confusing - export function registerChatContextProvider(selector: DocumentSelector, id: string, provider: ChatContextProvider): Disposable; + /** + * 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. + * + * 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 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. + */ + export function registerChatContextProvider(selector: DocumentSelector | undefined, id: string, provider: ChatContextProvider): Disposable; } export interface ChatContextItem { + /** + * Icon for the context item. + */ icon: ThemeIcon; + /** + * Human readable label for the context item. + */ label: string; + /** + * An optional description of the context item, e.g. to describe the item to the language model. + */ modelDescription?: string; + /** + * The value of the context item. Can be omitted when returned from one of the `provide` methods if the provider supports `resolveChatContext`. + */ value?: string; } export interface ChatContextProvider { + /** + * An optional event that should be fired when the workspace chat context has changed. + */ + onDidChangeWorkspaceChatContext?: Event; + + /** + * Provide a list of chat context items to be included as workspace context for all chat sessions. + * + * @param token A cancellation token. + */ + provideWorkspaceChatContext?(token: CancellationToken): ProviderResult; + /** * 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. * Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`. * `resolveChatContext` is only called for items that do not have a `value`. * - * @param options - * @param token + * @param token A cancellation token. */ provideChatContextExplicit?(token: CancellationToken): ProviderResult; @@ -40,17 +73,16 @@ declare module 'vscode' { * Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`. * `resolveChatContext` is only called for items that do not have a `value`. * - * @param resource - * @param options - * @param token + * @param options Options include the resource for which to provide context. + * @param token A cancellation token. */ provideChatContextForResource?(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. * - * @param context - * @param token + * @param context The context item to resolve. + * @param token A cancellation token. */ resolveChatContext(context: T, token: CancellationToken): ProviderResult; }