mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-09 02:12:20 +01:00
Add API for custom slash command providers (#181604)
Likely temporary API for experimentation
This commit is contained in:
@@ -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;
|
||||
|
||||
+47
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user