diff --git a/src/vs/workbench/api/browser/mainThreadInteractiveSession.ts b/src/vs/workbench/api/browser/mainThreadInteractiveSession.ts index 2b91c13634f..a38e8c18faf 100644 --- a/src/vs/workbench/api/browser/mainThreadInteractiveSession.ts +++ b/src/vs/workbench/api/browser/mainThreadInteractiveSession.ts @@ -39,6 +39,29 @@ export class MainThreadInteractiveSession extends Disposable implements MainThre })); } + async $registerSlashCommandProvider(handle: number, chatProviderId: string): Promise { + if (this.productService.quality === 'stable') { + this.logService.trace(`The interactive session API is not supported in stable VS Code.`); + return; + } + + const unreg = this._interactiveSessionService.registerSlashCommandProvider({ + chatProviderId, + provideSlashCommands: async token => { + return this._proxy.$provideProviderSlashCommands(handle, token); + }, + resolveSlashCommand: async (command, token) => { + return this._proxy.$resolveSlashCommand(handle, command, token); + } + }); + + this._providerRegistrations.set(handle, unreg); + } + + async $unregisterSlashCommandProvider(handle: number): Promise { + this._providerRegistrations.deleteAndDispose(handle); + } + async $registerInteractiveSessionProvider(handle: number, id: string): Promise { if (this.productService.quality === 'stable') { this.logService.trace(`The interactive session API is not supported in stable VS Code.`); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index ce3dcc74026..73d4e92fd71 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1271,6 +1271,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } }; + const interactiveSlashCommands: typeof vscode.interactiveSlashCommands = { + registerSlashCommandProvider(chatProviderId: string, provider: vscode.InteractiveSlashCommandProvider) { + checkProposedApiEnabled(extension, 'interactiveSlashCommands'); + return extHostInteractiveSession.registerSlashCommandProvider(extension, chatProviderId, provider); + } + }; + // namespace: ai const ai: typeof vscode.ai = { registerSemanticSimilarityProvider(provider: vscode.SemanticSimilarityProvider) { @@ -1290,6 +1297,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I env, extensions, interactive, + interactiveSlashCommands, l10n, languages, notebooks, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 657e9b324c9..06976e0f7f8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1125,6 +1125,9 @@ export interface MainThreadInteractiveSessionShape extends IDisposable { $sendInteractiveRequestToProvider(providerId: string, message: IInteractiveSessionDynamicRequest): void; $unregisterInteractiveSessionProvider(handle: number): Promise; $acceptInteractiveResponseProgress(handle: number, sessionId: number, progress: IInteractiveProgress): void; + + $registerSlashCommandProvider(handle: number, chatProviderId: string): Promise; + $unregisterSlashCommandProvider(handle: number): Promise; } export interface ExtHostInteractiveSessionShape { @@ -1136,6 +1139,9 @@ export interface ExtHostInteractiveSessionShape { $provideSlashCommands(handle: number, sessionId: number, token: CancellationToken): Promise; $releaseSession(sessionId: number): void; $onDidPerformUserAction(event: IInteractiveSessionUserActionEvent): Promise; + + $provideProviderSlashCommands(handle: number, token: CancellationToken): Promise; + $resolveSlashCommand(handle: number, command: string, token: CancellationToken): Promise; } export interface ExtHostUrlsShape { diff --git a/src/vs/workbench/api/common/extHostInteractiveSession.ts b/src/vs/workbench/api/common/extHostInteractiveSession.ts index 285b8af8168..176f0796c60 100644 --- a/src/vs/workbench/api/common/extHostInteractiveSession.ts +++ b/src/vs/workbench/api/common/extHostInteractiveSession.ts @@ -7,6 +7,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { toDisposable } from 'vs/base/common/lifecycle'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { withNullAsUndefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; @@ -15,7 +16,7 @@ import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import { IInteractiveSessionFollowup, IInteractiveSessionReplyFollowup, IInteractiveSessionUserActionEvent, IInteractiveSlashCommand } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; import type * as vscode from 'vscode'; -class InteractiveSessionProviderWrapper { +class InteractiveSessionProviderWrapper { private static _pool = 0; @@ -23,14 +24,15 @@ class InteractiveSessionProviderWrapper { constructor( readonly extension: Readonly, - readonly provider: vscode.InteractiveSessionProvider, + readonly provider: T, ) { } } export class ExtHostInteractiveSession implements ExtHostInteractiveSessionShape { private static _nextId = 0; - private readonly _interactiveSessionProvider = new Map(); + private readonly _interactiveSessionProvider = new Map>(); + private readonly _slashCommandProvider = new Map>(); private readonly _interactiveSessions = new Map(); // private readonly _providerResponsesByRequestId = new Map; sessionId: number }>(); @@ -243,4 +245,37 @@ export class ExtHostInteractiveSession implements ExtHostInteractiveSessionShape } //#endregion + + registerSlashCommandProvider(extension: Readonly, chatProviderId: string, provider: vscode.InteractiveSlashCommandProvider): vscode.Disposable { + const wrapper = new InteractiveSessionProviderWrapper(extension, provider); + this._slashCommandProvider.set(wrapper.handle, wrapper); + this._proxy.$registerSlashCommandProvider(wrapper.handle, chatProviderId); + return toDisposable(() => { + this._proxy.$unregisterSlashCommandProvider(wrapper.handle); + this._slashCommandProvider.delete(wrapper.handle); + }); + } + + async $provideProviderSlashCommands(handle: number, token: CancellationToken): Promise { + const entry = this._slashCommandProvider.get(handle); + if (!entry) { + return undefined; + } + + const slashCommands = await entry.provider.provideSlashCommands(token); + return slashCommands?.map(c => ({ + ...c, + kind: typeConvert.CompletionItemKind.from(c.kind) + })); + } + + async $resolveSlashCommand(handle: number, command: string, token: CancellationToken): Promise { + const entry = this._slashCommandProvider.get(handle); + if (!entry) { + return undefined; + } + + const resolved = await entry.provider.resolveSlashCommand(command, token); + return withNullAsUndefined(resolved); + } } diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts index a6659a3e031..973a8bf44df 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts @@ -58,8 +58,15 @@ export interface IInteractiveProvider { provideSlashCommands?(session: IInteractiveSession, token: CancellationToken): ProviderResult; } +export interface IInteractiveSlashCommandProvider { + chatProviderId: string; + provideSlashCommands(token: CancellationToken): ProviderResult; + resolveSlashCommand(command: string, token: CancellationToken): ProviderResult; +} + export interface IInteractiveSlashCommand { command: string; + provider?: IInteractiveSlashCommandProvider; sortText?: string; detail?: string; } @@ -167,6 +174,7 @@ export const IInteractiveSessionService = createDecorator(); + private readonly _slashCommandProviders = new Set(); private readonly _sessionModels = new Map(); private readonly _pendingRequests = new Map>(); private readonly _persistedSessions: ISerializableInteractiveSessionsData; @@ -328,8 +329,11 @@ export class InteractiveSessionService extends Disposable implements IInteractiv return true; } - private _sendRequestAsync(model: InteractiveSessionModel, provider: IInteractiveProvider, message: string | IInteractiveSessionReplyFollowup): CancelablePromise { + private async _sendRequestAsync(model: InteractiveSessionModel, provider: IInteractiveProvider, message: string | IInteractiveSessionReplyFollowup): Promise { const request = model.addRequest(message); + + const resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message; + let gotProgress = false; const requestType = typeof message === 'string' ? (message.startsWith('/') ? 'slashCommand' : 'string') : @@ -365,7 +369,7 @@ export class InteractiveSessionService extends Disposable implements IInteractiv model.cancelRequest(request); }); - let rawResponse = await provider.provideReply({ session: model.session!, message: request.message }, progressCallback, token); + let rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token); if (token.isCancellationRequested) { return; } else { @@ -399,7 +403,19 @@ export class InteractiveSessionService extends Disposable implements IInteractiv rawResponsePromise.finally(() => { this._pendingRequests.delete(model.sessionId); }); - return rawResponsePromise; + } + + private async handleSlashCommand(sessionId: string, command: string): Promise { + const start = Date.now(); + const slashCommands = await this.getSlashCommands(sessionId, CancellationToken.None); + for (const slashCommand of slashCommands ?? []) { + if (command.startsWith(`/${slashCommand.command}`) && slashCommand.provider) { + return await slashCommand.provider.resolveSlashCommand(command, CancellationToken.None) ?? command; + } + } + + console.log(`${Date.now() - start}ms to resolve slash command`); + return command; } async getSlashCommands(sessionId: string, token: CancellationToken): Promise { @@ -418,7 +434,23 @@ export class InteractiveSessionService extends Disposable implements IInteractiv return; } - return withNullAsUndefined(await provider.provideSlashCommands(model.session!, token)); + const mainProviderRequest = provider.provideSlashCommands(model.session!, token); + const slashCommandProviders = Array.from(this._slashCommandProviders).filter(p => p.chatProviderId === model.providerId); + const providerResults = Promise.all([ + mainProviderRequest, + ...slashCommandProviders.map(p => Promise.resolve(p.provideSlashCommands(token)) + .then(commands => commands?.map(c => ({ ...c, provider: p })))) + ]); + + try { + const slashCommands = (await providerResults).filter(c => !!c) as IInteractiveSlashCommand[][]; + return withNullAsUndefined(slashCommands.flat()); + } catch (e) { + this.logService.error(e); + + // If one of the other contributed providers fails, return the main provider's result + return withNullAsUndefined(await mainProviderRequest); + } } async addInteractiveRequest(context: any): Promise { @@ -513,6 +545,16 @@ export class InteractiveSessionService extends Disposable implements IInteractiv }); } + registerSlashCommandProvider(provider: IInteractiveSlashCommandProvider): IDisposable { + this.trace('registerProvider', `Adding new interactive slash command provider`); + + this._slashCommandProviders.add(provider); + return toDisposable(() => { + this.trace('registerProvider', `Disposing interactive slash command provider`); + this._slashCommandProviders.delete(provider); + }); + } + getProviderInfos(): IInteractiveProviderInfo[] { return Array.from(this._providers.values()).map(provider => { return { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 4af35783b55..3862b32588d 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -47,6 +47,7 @@ export const allApiProposals = Object.freeze({ indentSize: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.indentSize.d.ts', inlineCompletionsAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', interactive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactive.d.ts', + interactiveSlashCommands: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveSlashCommands.d.ts', interactiveWindow: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.interactiveWindow.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', diff --git a/src/vscode-dts/vscode.proposed.interactiveSlashCommands.d.ts b/src/vscode-dts/vscode.proposed.interactiveSlashCommands.d.ts new file mode 100644 index 00000000000..8a8c4be59fc --- /dev/null +++ b/src/vscode-dts/vscode.proposed.interactiveSlashCommands.d.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface InteractiveSlashCommandProvider { + provideSlashCommands(token: CancellationToken): ProviderResult; + resolveSlashCommand(command: string, token: CancellationToken): ProviderResult; + } + + // export interface + export interface InteractiveSlashCommandProvider2 { + provideSlashCommands(token: CancellationToken): ProviderResult; + resolveSlashCommand(command: string, token: CancellationToken): ProviderResult; + } + + export namespace interactiveSlashCommands { + export function registerSlashCommandProvider(chatProviderId: string, provider: InteractiveSlashCommandProvider): Disposable; + } +}