Add API for custom slash command providers (#181604)

Likely temporary API for experimentation
This commit is contained in:
Rob Lourens
2023-05-05 10:00:21 +02:00
committed by GitHub
parent 1894181c08
commit aebaec6caf
8 changed files with 153 additions and 8 deletions
@@ -39,6 +39,29 @@ export class MainThreadInteractiveSession extends Disposable implements MainThre
}));
}
async $registerSlashCommandProvider(handle: number, chatProviderId: string): Promise<void> {
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<void> {
this._providerRegistrations.deleteAndDispose(handle);
}
async $registerInteractiveSessionProvider(handle: number, id: string): Promise<void> {
if (this.productService.quality === 'stable') {
this.logService.trace(`The interactive session API is not supported in stable VS Code.`);
@@ -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,
@@ -1125,6 +1125,9 @@ export interface MainThreadInteractiveSessionShape extends IDisposable {
$sendInteractiveRequestToProvider(providerId: string, message: IInteractiveSessionDynamicRequest): void;
$unregisterInteractiveSessionProvider(handle: number): Promise<void>;
$acceptInteractiveResponseProgress(handle: number, sessionId: number, progress: IInteractiveProgress): void;
$registerSlashCommandProvider(handle: number, chatProviderId: string): Promise<void>;
$unregisterSlashCommandProvider(handle: number): Promise<void>;
}
export interface ExtHostInteractiveSessionShape {
@@ -1136,6 +1139,9 @@ export interface ExtHostInteractiveSessionShape {
$provideSlashCommands(handle: number, sessionId: number, token: CancellationToken): Promise<IInteractiveSlashCommand[] | undefined>;
$releaseSession(sessionId: number): void;
$onDidPerformUserAction(event: IInteractiveSessionUserActionEvent): Promise<void>;
$provideProviderSlashCommands(handle: number, token: CancellationToken): Promise<IInteractiveSlashCommand[] | undefined>;
$resolveSlashCommand(handle: number, command: string, token: CancellationToken): Promise<string | undefined>;
}
export interface ExtHostUrlsShape {
@@ -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<T> {
private static _pool = 0;
@@ -23,14 +24,15 @@ class InteractiveSessionProviderWrapper {
constructor(
readonly extension: Readonly<IRelaxedExtensionDescription>,
readonly provider: vscode.InteractiveSessionProvider,
readonly provider: T,
) { }
}
export class ExtHostInteractiveSession implements ExtHostInteractiveSessionShape {
private static _nextId = 0;
private readonly _interactiveSessionProvider = new Map<number, InteractiveSessionProviderWrapper>();
private readonly _interactiveSessionProvider = new Map<number, InteractiveSessionProviderWrapper<vscode.InteractiveSessionProvider>>();
private readonly _slashCommandProvider = new Map<number, InteractiveSessionProviderWrapper<vscode.InteractiveSlashCommandProvider>>();
private readonly _interactiveSessions = new Map<number, vscode.InteractiveSession>();
// private readonly _providerResponsesByRequestId = new Map<number, { response: vscode.ProviderResult<vscode.InteractiveResponse | vscode.InteractiveResponseForProgress>; sessionId: number }>();
@@ -243,4 +245,37 @@ export class ExtHostInteractiveSession implements ExtHostInteractiveSessionShape
}
//#endregion
registerSlashCommandProvider(extension: Readonly<IRelaxedExtensionDescription>, 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<IInteractiveSlashCommand[] | undefined> {
const entry = this._slashCommandProvider.get(handle);
if (!entry) {
return undefined;
}
const slashCommands = await entry.provider.provideSlashCommands(token);
return slashCommands?.map(c => (<IInteractiveSlashCommand>{
...c,
kind: typeConvert.CompletionItemKind.from(c.kind)
}));
}
async $resolveSlashCommand(handle: number, command: string, token: CancellationToken): Promise<string | undefined> {
const entry = this._slashCommandProvider.get(handle);
if (!entry) {
return undefined;
}
const resolved = await entry.provider.resolveSlashCommand(command, token);
return withNullAsUndefined(resolved);
}
}
@@ -58,8 +58,15 @@ export interface IInteractiveProvider {
provideSlashCommands?(session: IInteractiveSession, token: CancellationToken): ProviderResult<IInteractiveSlashCommand[]>;
}
export interface IInteractiveSlashCommandProvider {
chatProviderId: string;
provideSlashCommands(token: CancellationToken): ProviderResult<IInteractiveSlashCommand[]>;
resolveSlashCommand(command: string, token: CancellationToken): ProviderResult<string>;
}
export interface IInteractiveSlashCommand {
command: string;
provider?: IInteractiveSlashCommandProvider;
sortText?: string;
detail?: string;
}
@@ -167,6 +174,7 @@ export const IInteractiveSessionService = createDecorator<IInteractiveSessionSer
export interface IInteractiveSessionService {
_serviceBrand: undefined;
registerProvider(provider: IInteractiveProvider): IDisposable;
registerSlashCommandProvider(provider: IInteractiveSlashCommandProvider): IDisposable;
getProviderInfos(): IInteractiveProviderInfo[];
startSession(providerId: string, token: CancellationToken): InteractiveSessionModel | undefined;
retrieveSession(sessionId: string): IInteractiveSessionModel | undefined;
@@ -20,7 +20,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContextKeys';
import { ISerializableInteractiveSessionData, ISerializableInteractiveSessionsData, InteractiveSessionModel, InteractiveSessionWelcomeMessageModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';
import { IInteractiveProgress, IInteractiveProvider, IInteractiveProviderInfo, IInteractiveSession, IInteractiveSessionCompleteResponse, IInteractiveSessionDetail, IInteractiveSessionDynamicRequest, IInteractiveSessionReplyFollowup, IInteractiveSessionService, IInteractiveSessionUserActionEvent, IInteractiveSlashCommand, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IInteractiveProgress, IInteractiveProvider, IInteractiveProviderInfo, IInteractiveSession, IInteractiveSessionCompleteResponse, IInteractiveSessionDetail, IInteractiveSessionDynamicRequest, IInteractiveSessionReplyFollowup, IInteractiveSessionService, IInteractiveSessionUserActionEvent, IInteractiveSlashCommand, IInteractiveSlashCommandProvider, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
const serializedInteractiveSessionKey = 'interactive.sessions';
@@ -109,6 +109,7 @@ export class InteractiveSessionService extends Disposable implements IInteractiv
declare _serviceBrand: undefined;
private readonly _providers = new Map<string, IInteractiveProvider>();
private readonly _slashCommandProviders = new Set<IInteractiveSlashCommandProvider>();
private readonly _sessionModels = new Map<string, InteractiveSessionModel>();
private readonly _pendingRequests = new Map<string, CancelablePromise<void>>();
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<void> {
private async _sendRequestAsync(model: InteractiveSessionModel, provider: IInteractiveProvider, message: string | IInteractiveSessionReplyFollowup): Promise<void> {
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<string> {
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<IInteractiveSlashCommand[] | undefined> {
@@ -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<void> {
@@ -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 {
@@ -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',
@@ -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<InteractiveSessionSlashCommand[]>;
resolveSlashCommand(command: string, token: CancellationToken): ProviderResult<string>;
}
// export interface
export interface InteractiveSlashCommandProvider2 {
provideSlashCommands(token: CancellationToken): ProviderResult<InteractiveSessionSlashCommand[]>;
resolveSlashCommand(command: string, token: CancellationToken): ProviderResult<string>;
}
export namespace interactiveSlashCommands {
export function registerSlashCommandProvider(chatProviderId: string, provider: InteractiveSlashCommandProvider): Disposable;
}
}