diff --git a/src/vs/workbench/api/browser/mainThreadChatContext.ts b/src/vs/workbench/api/browser/mainThreadChatContext.ts index d94ff704768..917aeb8c02a 100644 --- a/src/vs/workbench/api/browser/mainThreadChatContext.ts +++ b/src/vs/workbench/api/browser/mainThreadChatContext.ts @@ -23,12 +23,13 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatContext); + 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: (token: CancellationToken) => { + provideChatContext: (_options: {}, token: CancellationToken) => { return this._proxy.$provideChatContext(handle, token); }, resolveChatContext: support.supportsResolve ? (context: IChatContextItem, token: CancellationToken) => { @@ -36,7 +37,7 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC } : undefined, provideChatContextForResource: support.supportsResource ? (resource: URI, withValue: boolean, token: CancellationToken) => { return this._proxy.$provideChatContextForResource(handle, { resource, withValue }, token); - } : undefined + } : undefined, }); } @@ -56,4 +57,8 @@ export class MainThreadChatContext extends Disposable implements MainThreadChatC } this._chatContextService.updateWorkspaceContextItems(provider.id, items); } + + $executeChatContextItemCommand(itemHandle: number): Promise { + return this._proxy.$executeChatContextItemCommand(itemHandle); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 2a0d70fdbdd..906ddd5efd8 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -229,7 +229,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol, extHostLanguageModels)); const extHostChatSessions = rpcProtocol.set(ExtHostContext.ExtHostChatSessions, new ExtHostChatSessions(extHostCommands, extHostLanguageModels, rpcProtocol, extHostLogService)); const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostDocumentsAndEditors, extHostLanguageModels, extHostDiagnostics, extHostLanguageModelTools)); - const extHostChatContext = rpcProtocol.set(ExtHostContext.ExtHostChatContext, new ExtHostChatContext(rpcProtocol)); + const extHostChatContext = rpcProtocol.set(ExtHostContext.ExtHostChatContext, new ExtHostChatContext(rpcProtocol, extHostCommands)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); const extHostAiSettingsSearch = rpcProtocol.set(ExtHostContext.ExtHostAiSettingsSearch, new ExtHostAiSettingsSearch(rpcProtocol)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3906ecde7f7..498dc1cd162 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1358,12 +1358,14 @@ 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; + $executeChatContextItemCommand(itemHandle: number): Promise; } export interface MainThreadChatContextShape extends IDisposable { $registerChatContextProvider(handle: number, id: string, selector: IDocumentFilterDto[] | undefined, options: {}, support: IChatContextSupport): void; $unregisterChatContextProvider(handle: number): void; $updateWorkspaceContextItems(handle: number, items: IChatContextItem[]): void; + $executeChatContextItemCommand(itemHandle: number): Promise; } export interface MainThreadEmbeddingsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostChatContext.ts b/src/vs/workbench/api/common/extHostChatContext.ts index 0e3aaac540b..761fca70dbb 100644 --- a/src/vs/workbench/api/common/extHostChatContext.ts +++ b/src/vs/workbench/api/common/extHostChatContext.ts @@ -11,6 +11,7 @@ import { DocumentSelector } from './extHostTypeConverters.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatContext.js'; import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; +import { IExtHostCommands } from './extHostCommands.js'; export class ExtHostChatContext extends Disposable implements ExtHostChatContextShape { declare _serviceBrand: undefined; @@ -19,16 +20,21 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext private _handlePool: number = 0; private _providers: Map = new Map(); private _itemPool: number = 0; - private _items: Map> = new Map(); // handle -> itemHandle -> item + /** Global map of itemHandle -> original item for command execution with reference equality */ + private _globalItems: Map = new Map(); + /** Track which items belong to which provider for cleanup */ + private _providerItems: Map> = new Map(); // providerHandle -> Set - constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService, + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService, + @IExtHostCommands private readonly _commands: IExtHostCommands, ) { super(); this._proxy = extHostRpc.getProxy(MainContext.MainThreadChatContext); } async $provideChatContext(handle: number, token: CancellationToken): Promise { - this._items.delete(handle); // clear previous items + this._clearProviderItems(handle); // clear previous items for this provider const provider = this._getProvider(handle); if (!provider.provideChatContextExplicit) { throw new Error('provideChatContext not implemented'); @@ -42,18 +48,30 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext icon: item.icon, label: item.label, modelDescription: item.modelDescription, - value: item.value + value: item.value, + command: item.command ? { id: item.command.command } : undefined }); } return items; } - private _addTrackedItem(handle: number, item: vscode.ChatContextItem): number { - const itemHandle = this._itemPool++; - if (!this._items.has(handle)) { - this._items.set(handle, new Map()); + private _clearProviderItems(handle: number): void { + const itemHandles = this._providerItems.get(handle); + if (itemHandles) { + for (const itemHandle of itemHandles) { + this._globalItems.delete(itemHandle); + } + itemHandles.clear(); } - this._items.get(handle)!.set(itemHandle, item); + } + + private _addTrackedItem(providerHandle: number, item: vscode.ChatContextItem): number { + const itemHandle = this._itemPool++; + this._globalItems.set(itemHandle, item); + if (!this._providerItems.has(providerHandle)) { + this._providerItems.set(providerHandle, new Set()); + } + this._providerItems.get(providerHandle)!.add(itemHandle); return itemHandle; } @@ -75,7 +93,8 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext icon: result.icon, label: result.label, modelDescription: result.modelDescription, - value: options.withValue ? result.value : 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); @@ -87,14 +106,17 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext 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 { - handle: context.handle, - icon: result.icon, - label: result.label, - modelDescription: result.modelDescription, - value: result.value - }; + if (extResult) { + return { + handle: context.handle, + icon: extResult.icon, + label: extResult.label, + modelDescription: extResult.modelDescription, + value: extResult.value, + command: extResult.command ? { id: extResult.command.command } : undefined + }; + } + return context; } async $resolveChatContext(handle: number, context: IChatContextItem, token: CancellationToken): Promise { @@ -103,13 +125,26 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext if (!provider.resolveChatContext) { throw new Error('resolveChatContext not implemented'); } - const extItem = this._items.get(handle)?.get(context.handle); + 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(); @@ -120,6 +155,8 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext return { dispose: () => { this._providers.delete(handle); + this._clearProviderItems(handle); // Clean up tracked items + this._providerItems.delete(handle); this._proxy.$unregisterChatContextProvider(handle); disposables.dispose(); } @@ -134,12 +171,14 @@ export class ExtHostChatContext extends Disposable implements ExtHostChatContext 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, value: item.value, - handle: this._itemPool++ + handle: itemHandle, + command: item.command ? { id: item.command.command } : undefined }; const resolved = await this._doResolve(provider, contextItem, item, CancellationToken.None); resolvedContexts.push(resolved); diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts index df8d5a18201..478d6c0e848 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentWidgets.ts @@ -57,10 +57,11 @@ import { toHistoryItemHoverContent } from '../../../scm/browser/scmHistory.js'; import { getHistoryItemEditorTitle } from '../../../scm/browser/util.js'; import { ITerminalService } from '../../../terminal/browser/terminal.js'; import { IChatContentReference } from '../../common/chatService/chatService.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry } from '../../common/attachments/chatVariableEntries.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry, IElementVariableEntry, INotebookOutputVariableEntry, IPromptFileVariableEntry, IPromptTextVariableEntry, ISCMHistoryItemVariableEntry, OmittedState, PromptFileVariableKind, ChatRequestToolReferenceEntry, ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemChangeRangeVariableEntry, ITerminalVariableEntry, isStringVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../common/languageModels.js'; import { ILanguageModelToolsService, ToolSet } from '../../common/tools/languageModelToolsService.js'; import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js'; +import { IChatContextService } from '../contextContrib/chatContextService.js'; const commonHoverOptions: Partial = { style: HoverStyle.Pointer, @@ -584,6 +585,16 @@ export class DefaultChatAttachmentWidget extends AbstractChatAttachmentWidget { this._register(this.instantiationService.invokeFunction(hookUpSymbolAttachmentDragAndContextMenu, this.element, scopedContextKeyService, { ...attachment, kind: attachment.symbolKind }, MenuId.ChatInputSymbolAttachmentContext)); } + // Handle click for string context attachments with context commands + if (isStringVariableEntry(attachment) && attachment.commandId) { + this.element.style.cursor = 'pointer'; + const contextItemHandle = attachment.handle; + this._register(dom.addDisposableListener(this.element, dom.EventType.CLICK, async () => { + const chatContextService = this.instantiationService.invokeFunction(accessor => accessor.get(IChatContextService)); + await chatContextService.executeChatContextItemCommand(contextItemHandle); + })); + } + if (resource) { this.addResourceOpenHandlers(resource, range); } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts b/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts index eac7f983360..48aee8ff887 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.ts @@ -388,7 +388,9 @@ export class ChatImplicitContext extends Disposable implements IChatRequestImpli value: this.value.value ?? this.name, modelDescription: this.modelDescription, icon: this.value.icon, - uri: this.value.uri + uri: this.value.uri, + handle: this.value.handle, + commandId: this.value.commandId } ]; } diff --git a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts index 5a4815ff15d..cc939a7ff93 100644 --- a/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts +++ b/src/vs/workbench/contrib/chat/browser/attachments/implicitContextAttachment.ts @@ -209,7 +209,9 @@ export class ImplicitContextAttachmentWidget extends Disposable { name: this.attachment.name, icon: this.attachment.value.icon, modelDescription: this.attachment.value.modelDescription, - uri: this.attachment.value.uri + uri: this.attachment.value.uri, + commandId: this.attachment.value.commandId, + handle: this.attachment.value.handle }; this.attachmentModel.addContext(context); } else { diff --git a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts index 5538c84c4b1..57850cea5ca 100644 --- a/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts +++ b/src/vs/workbench/contrib/chat/browser/contextContrib/chatContextService.ts @@ -34,6 +34,7 @@ export class ChatContextService extends Disposable { private readonly _workspaceContext = new Map(); private readonly _registeredPickers = this._register(new DisposableMap()); private _lastResourceContext: Map = new Map(); + private _executeCommandCallback: ((itemHandle: number) => Promise) | undefined; constructor( @IChatContextPickService private readonly _contextPickService: IChatContextPickService, @@ -42,6 +43,17 @@ export class ChatContextService extends Disposable { super(); } + setExecuteCommandCallback(callback: (itemHandle: number) => Promise): void { + this._executeCommandCallback = callback; + } + + async executeChatContextItemCommand(handle: number): Promise { + if (!this._executeCommandCallback) { + return; + } + await this._executeCommandCallback(handle); + } + setChatContextProvider(id: string, picker: { title: string; icon: ThemeIcon }): void { const providerEntry = this._providers.get(id) ?? { picker: undefined }; providerEntry.picker = picker; @@ -110,7 +122,8 @@ export class ChatContextService extends Disposable { if (scoredProviders.length === 0 || scoredProviders[0].score <= 0) { return; } - const context = (await scoredProviders[0].provider.provideChatContextForResource!(uri, withValue, CancellationToken.None)); + const provider = scoredProviders[0].provider; + const context = (await provider.provideChatContextForResource!(uri, withValue, CancellationToken.None)); if (!context) { return; } @@ -119,10 +132,12 @@ export class ChatContextService extends Disposable { name: context.label, icon: context.icon, uri: uri, - modelDescription: context.modelDescription + modelDescription: context.modelDescription, + commandId: context.command?.id, + handle: context.handle }; this._lastResourceContext.clear(); - this._lastResourceContext.set(contextValue, { originalItem: context, provider: scoredProviders[0].provider }); + this._lastResourceContext.set(contextValue, { originalItem: context, provider }); return contextValue; } @@ -183,7 +198,7 @@ export class ChatContextService extends Disposable { id: contextValue.label, name: contextValue.label, icon: contextValue.icon, - value: contextValue.value + value: contextValue.value, }; } })); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index a98c158ad8c..00f852701f3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -86,7 +86,7 @@ import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes import { IChatFollowup, IChatService } from '../../../common/chatService/chatService.js'; import { IChatSessionFileChange, IChatSessionProviderOptionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; import { getChatSessionType } from '../../../common/model/chatUri.js'; -import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; import { IChatResponseViewModel } from '../../../common/model/chatViewModel.js'; import { ChatHistoryNavigator } from '../../../common/widget/chatWidgetHistoryService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../../../common/constants.js'; @@ -217,7 +217,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const contextArr = this.getAttachedContext(sessionResource); - if ((this.implicitContext?.enabled && this.implicitContext?.value) || (this.implicitContext && !URI.isUri(this.implicitContext.value) && this.configurationService.getValue('chat.implicitContext.suggestedContext'))) { + if ((this.implicitContext?.enabled && this.implicitContext?.value) || (this.implicitContext && !URI.isUri(this.implicitContext.value) && !isStringImplicitContextValue(this.implicitContext.value) && this.configurationService.getValue('chat.implicitContext.suggestedContext'))) { const implicitChatVariables = this.implicitContext.toBaseEntries(); contextArr.add(...implicitChatVariables); } diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 54452201d19..c45c4899abc 100644 --- a/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -73,6 +73,11 @@ export interface StringChatContextValue { modelDescription?: string; icon: ThemeIcon; uri: URI; + /** + * Command ID to execute when this context item is clicked. + */ + readonly commandId?: string; + readonly handle: number; } export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVariableEntry { @@ -90,6 +95,11 @@ export interface IChatRequestStringVariableEntry extends IBaseChatRequestVariabl readonly modelDescription?: string; readonly icon: ThemeIcon; readonly uri: URI; + /** + * Command ID to execute when this context item is clicked. + */ + readonly commandId?: string; + readonly handle: number; } export interface IChatRequestWorkspaceVariableEntry extends IBaseChatRequestVariableEntry { @@ -329,7 +339,6 @@ export namespace IChatRequestVariableEntry { } } - export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { return obj.kind === 'implicit'; } diff --git a/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts index 854d415e208..6973240728d 100644 --- a/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts +++ b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts @@ -13,6 +13,9 @@ export interface IChatContextItem { modelDescription?: string; handle: number; value?: string; + command?: { + id: string; + }; } export interface IChatContextSupport { diff --git a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts index e2309591824..81515d97480 100644 --- a/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatContextProvider.d.ts @@ -43,6 +43,11 @@ declare module 'vscode' { * The value of the context item. Can be omitted when returned from one of the `provide` methods if the provider supports `resolveChatContext`. */ value?: string; + /** + * An optional command that is executed when the context item is clicked. + * The original context item will be passed as the first argument to the command. + */ + command?: Command; } export interface ChatContextProvider {