Skip to content

Commit

Permalink
Merge pull request #188297 from microsoft/joh/thoughtless-haddock
Browse files Browse the repository at this point in the history
joh/thoughtless-haddock
  • Loading branch information
jrieken authored Aug 9, 2023
2 parents 6dd6bdf + e7e6dd3 commit 2c5ddd1
Show file tree
Hide file tree
Showing 19 changed files with 875 additions and 10 deletions.
2 changes: 2 additions & 0 deletions src/vs/workbench/api/browser/extensionHost.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { StatusBarItemsExtensionPoint } from 'vs/workbench/api/browser/statusBar
// --- mainThread participants
import './mainThreadLocalization';
import './mainThreadBulkEdits';
import './mainThreadChatProvider';
import './mainThreadChatSlashCommands';
import './mainThreadCodeInsets';
import './mainThreadCLICommands';
import './mainThreadClipboard';
Expand Down
73 changes: 73 additions & 0 deletions src/vs/workbench/api/browser/mainThreadChatProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableMap } from 'vs/base/common/lifecycle';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgress, Progress } from 'vs/platform/progress/common/progress';
import { ExtHostChatProviderShape, ExtHostContext, MainContext, MainThreadChatProviderShape } from 'vs/workbench/api/common/extHost.protocol';
import { IChatResponseProviderMetadata, IChatResponseFragment, IChatProviderService, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';

@extHostNamedCustomer(MainContext.MainThreadChatProvider)
export class MainThreadChatProvider implements MainThreadChatProviderShape {

private readonly _proxy: ExtHostChatProviderShape;
private readonly _providerRegistrations = new DisposableMap<number>();
private readonly _pendingProgress = new Map<number, IProgress<IChatResponseFragment>>();

constructor(
extHostContext: IExtHostContext,
@IChatProviderService private readonly _chatProviderService: IChatProviderService,
@ILogService private readonly _logService: ILogService,
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider);
}

dispose(): void {
this._providerRegistrations.dispose();
}

$registerProvider(handle: number, identifier: string, metadata: IChatResponseProviderMetadata): void {
const registration = this._chatProviderService.registerChatResponseProvider(identifier, {
metadata,
provideChatResponse: async (messages, options, progress, token) => {
const requestId = (Math.random() * 1e6) | 0;
this._pendingProgress.set(requestId, progress);
try {
await this._proxy.$provideChatResponse(handle, requestId, messages, options, token);
} finally {
this._pendingProgress.delete(requestId);
}
}
});
this._providerRegistrations.set(handle, registration);
}

async $handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise<void> {
this._pendingProgress.get(requestId)?.report(chunk);
}

$unregisterProvider(handle: number): void {
this._providerRegistrations.deleteAndDispose(handle);
}

async $fetchResponse(extension: ExtensionIdentifier, providerId: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise<any> {
this._logService.debug('[CHAT] extension request STARTED', extension.value, requestId);

const task = this._chatProviderService.fetchChatResponse(providerId, messages, options, new Progress(value => {
this._proxy.$handleResponseFragment(requestId, value);
}), token);

task.catch(err => {
this._logService.error('[CHAT] extension request ERRORED', err, extension.value, requestId);
}).finally(() => {
this._logService.debug('[CHAT] extension request DONE', extension.value, requestId);
});

return task;
}
}
61 changes: 61 additions & 0 deletions src/vs/workbench/api/browser/mainThreadChatSlashCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { DisposableMap } from 'vs/base/common/lifecycle';
import { IProgress } from 'vs/platform/progress/common/progress';
import { ExtHostChatSlashCommandsShape, ExtHostContext, MainContext, MainThreadChatSlashCommandsShape } from 'vs/workbench/api/common/extHost.protocol';
import { IChatSlashCommandService, IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';


@extHostNamedCustomer(MainContext.MainThreadChatSlashCommands)
export class MainThreadChatSlashCommands implements MainThreadChatSlashCommandsShape {

private readonly _commands = new DisposableMap<number>;
private readonly _pendingProgress = new Map<number, IProgress<IChatSlashFragment>>();
private readonly _proxy: ExtHostChatSlashCommandsShape;

constructor(
extHostContext: IExtHostContext,
@IChatSlashCommandService private readonly _chatSlashCommandService: IChatSlashCommandService
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatSlashCommands);
}

dispose(): void {
this._commands.clearAndDisposeAll();
}

$registerCommand(handle: number, name: string, detail: string): void {

if (!this._chatSlashCommandService.hasCommand(name)) {
// dynamic slash commands!
this._chatSlashCommandService.registerSlashData({
name,
id: name,
detail
});
}

const d = this._chatSlashCommandService.registerSlashCallback(name, async (prompt, progress, history, token) => {
const requestId = Math.random();
this._pendingProgress.set(requestId, progress);
try {
await this._proxy.$executeCommand(handle, requestId, prompt, { history }, token);
} finally {
this._pendingProgress.delete(requestId);
}
});
this._commands.set(handle, d);
}

async $handleProgressChunk(requestId: number, chunk: IChatSlashFragment): Promise<void> {
this._pendingProgress.get(requestId)?.report(chunk);
}

$unregisterCommand(handle: number): void {
this._commands.deleteAndDispose(handle);
}
}
23 changes: 23 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ import { ExtHostSemanticSimilarity } from 'vs/workbench/api/common/extHostSemant
import { ExtHostIssueReporter } from 'vs/workbench/api/common/extHostIssueReporter';
import { IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets';
import { ExtHostShare } from 'vs/workbench/api/common/extHostShare';
import { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider';
import { ExtHostChatSlashCommands } from 'vs/workbench/api/common/extHostChatSlashCommand';

export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
Expand Down Expand Up @@ -203,6 +205,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol));
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInlineChat, new ExtHostInteractiveEditor(rpcProtocol, extHostCommands, extHostDocuments, extHostLogService));
const extHostChatProvider = rpcProtocol.set(ExtHostContext.ExtHostChatProvider, new ExtHostChatProvider(rpcProtocol, extHostLogService));
const extHostChatSlashCommands = rpcProtocol.set(ExtHostContext.ExtHostChatSlashCommands, new ExtHostChatSlashCommands(rpcProtocol, extHostChatProvider, extHostLogService));
const extHostChat = rpcProtocol.set(ExtHostContext.ExtHostChat, new ExtHostChat(rpcProtocol, extHostLogService));
const extHostSemanticSimilarity = rpcProtocol.set(ExtHostContext.ExtHostSemanticSimilarity, new ExtHostSemanticSimilarity(rpcProtocol));
const extHostIssueReporter = rpcProtocol.set(ExtHostContext.ExtHostIssueReporter, new ExtHostIssueReporter(rpcProtocol));
Expand Down Expand Up @@ -1328,6 +1332,22 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
}
};

// namespace: llm
const chat: typeof vscode.chat = {
registerChatResponseProvider(id: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata) {
checkProposedApiEnabled(extension, 'chatProvider');
return extHostChatProvider.registerProvider(extension.identifier, id, provider, metadata);
},
registerSlashCommand(name: string, command: vscode.SlashCommand, metadata?: vscode.SlashCommandMetadata) {
checkProposedApiEnabled(extension, 'chatSlashCommands');
return extHostChatSlashCommands.registerCommand(extension.identifier, name, command, metadata ?? { description: '' });
},
requestChatAccess(id: string) {
checkProposedApiEnabled(extension, 'chatRequestAccess');
return extHostChatProvider.requestChatResponseProvider(extension.identifier, id);
}
};

return <typeof vscode>{
version: initData.version,
// namespaces
Expand All @@ -1341,6 +1361,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
interactive,
interactiveSlashCommands,
l10n,
chat,
languages,
notebooks,
scm,
Expand All @@ -1351,6 +1372,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
// types
Breakpoint: extHostTypes.Breakpoint,
TerminalOutputAnchor: extHostTypes.TerminalOutputAnchor,
ChatMessage: extHostTypes.ChatMessage,
ChatMessageRole: extHostTypes.ChatMessageRole,
CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall,
CallHierarchyItem: extHostTypes.CallHierarchyItem,
CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall,
Expand Down
29 changes: 29 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ import { ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/qu
import * as search from 'vs/workbench/services/search/common/search';
import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { TerminalCommandMatchResult, TerminalQuickFixCommand, TerminalQuickFixOpener } from 'vscode';
import { IChatMessage, IChatResponseFragment, IChatResponseProviderMetadata } from 'vs/workbench/contrib/chat/common/chatProvider';
import { IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands';

export type TerminalQuickFix = TerminalQuickFixCommand | TerminalQuickFixOpener;

Expand Down Expand Up @@ -1124,6 +1126,29 @@ export interface MainThreadNotebookRenderersShape extends IDisposable {
export interface MainThreadInteractiveShape extends IDisposable {
}

export interface MainThreadChatProviderShape extends IDisposable {
$registerProvider(handle: number, identifier: string, metadata: IChatResponseProviderMetadata): void;
$unregisterProvider(handle: number): void;
$handleProgressChunk(requestId: number, chunk: IChatResponseFragment): Promise<void>;

$fetchResponse(extension: ExtensionIdentifier, provider: string, requestId: number, messages: IChatMessage[], options: {}, token: CancellationToken): Promise<any>;
}

export interface ExtHostChatProviderShape {
$provideChatResponse(handle: number, requestId: number, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<any>;
$handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise<void>;
}

export interface MainThreadChatSlashCommandsShape extends IDisposable {
$registerCommand(handle: number, name: string, detail: string): void;
$unregisterCommand(handle: number): void;
$handleProgressChunk(requestId: number, chunk: IChatSlashFragment): Promise<void>;
}

export interface ExtHostChatSlashCommandsShape {
$executeCommand(handle: number, requestId: number, prompt: string, context: { history: IChatMessage[] }, token: CancellationToken): Promise<any>;
}

export interface MainThreadInlineChatShape extends IDisposable {
$registerInteractiveEditorProvider(handle: number, label: string, debugName: string, supportsFeedback: boolean): Promise<void>;
$handleProgressChunk(requestId: string, chunk: { message?: string; edits?: languages.TextEdit[] }): Promise<void>;
Expand Down Expand Up @@ -2529,6 +2554,8 @@ export interface MainThreadTestingShape {
export const MainContext = {
MainThreadAuthentication: createProxyIdentifier<MainThreadAuthenticationShape>('MainThreadAuthentication'),
MainThreadBulkEdits: createProxyIdentifier<MainThreadBulkEditsShape>('MainThreadBulkEdits'),
MainThreadChatProvider: createProxyIdentifier<MainThreadChatProviderShape>('MainThreadChatProvider'),
MainThreadChatSlashCommands: createProxyIdentifier<MainThreadChatSlashCommandsShape>('MainThreadChatSlashCommands'),
MainThreadClipboard: createProxyIdentifier<MainThreadClipboardShape>('MainThreadClipboard'),
MainThreadCommands: createProxyIdentifier<MainThreadCommandsShape>('MainThreadCommands'),
MainThreadComments: createProxyIdentifier<MainThreadCommentsShape>('MainThreadComments'),
Expand Down Expand Up @@ -2647,6 +2674,8 @@ export const ExtHostContext = {
ExtHostInteractive: createProxyIdentifier<ExtHostInteractiveShape>('ExtHostInteractive'),
ExtHostInlineChat: createProxyIdentifier<ExtHostInlineChatShape>('ExtHostInlineChatShape'),
ExtHostChat: createProxyIdentifier<ExtHostChatShape>('ExtHostChat'),
ExtHostChatSlashCommands: createProxyIdentifier<ExtHostChatSlashCommandsShape>('ExtHostChatSlashCommands'),
ExtHostChatProvider: createProxyIdentifier<ExtHostChatProviderShape>('ExtHostChatProvider'),
ExtHostSemanticSimilarity: createProxyIdentifier<ExtHostSemanticSimilarityShape>('ExtHostSemanticSimilarity'),
ExtHostTheming: createProxyIdentifier<ExtHostThemingShape>('ExtHostTheming'),
ExtHostTunnelService: createProxyIdentifier<ExtHostTunnelServiceShape>('ExtHostTunnelService'),
Expand Down
107 changes: 107 additions & 0 deletions src/vs/workbench/api/common/extHostChatProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from 'vs/base/common/cancellation';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtHostChatProviderShape, IMainContext, MainContext, MainThreadChatProviderShape } from 'vs/workbench/api/common/extHost.protocol';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import type * as vscode from 'vscode';
import { Progress } from 'vs/platform/progress/common/progress';
import { IChatMessage, IChatResponseFragment } from 'vs/workbench/contrib/chat/common/chatProvider';
import { ExtensionIdentifier, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions';

type ProviderData = {
readonly extension: ExtensionIdentifier;
readonly provider: vscode.ChatResponseProvider;
};

export class ExtHostChatProvider implements ExtHostChatProviderShape {

private static _idPool = 1;

private readonly _proxy: MainThreadChatProviderShape;
private readonly _providers = new Map<number, ProviderData>();

constructor(
mainContext: IMainContext,
private readonly _logService: ILogService,
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadChatProvider);
}

registerProvider(extension: ExtensionIdentifier, identifier: string, provider: vscode.ChatResponseProvider, metadata: vscode.ChatResponseProviderMetadata): IDisposable {

const handle = ExtHostChatProvider._idPool++;
this._providers.set(handle, { extension, provider });
this._proxy.$registerProvider(handle, identifier, { extension, displayName: metadata.name ?? extension.value });

return toDisposable(() => {
this._proxy.$unregisterProvider(handle);
this._providers.delete(handle);
});
}

async $provideChatResponse(handle: number, requestId: number, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise<any> {
const data = this._providers.get(handle);
if (!data) {
return;
}
const progress = new Progress<vscode.ChatResponseFragment>(async fragment => {
if (token.isCancellationRequested) {
this._logService.warn(`[CHAT](${data.extension.value}) CANNOT send progress because the REQUEST IS CANCELLED`);
return;
}
await this._proxy.$handleProgressChunk(requestId, { index: fragment.index, part: fragment.part });
}, { async: true });

return data.provider.provideChatResponse(messages.map(typeConvert.ChatMessage.to), options, progress, token);
}

//#region --- making request

private readonly _pendingRequest = new Map<number, vscode.Progress<vscode.ChatResponseFragment>>();

private readonly _chatAccessAllowList = new ExtensionIdentifierMap<Promise<unknown>>();

allowListExtensionWhile(extension: ExtensionIdentifier, promise: Promise<unknown>): void {
this._chatAccessAllowList.set(extension, promise);
promise.finally(() => this._chatAccessAllowList.delete(extension));
}

async requestChatResponseProvider(from: ExtensionIdentifier, identifier: string): Promise<vscode.ChatAccess> {
// check if a UI command is running/active

if (!this._chatAccessAllowList.has(from)) {
throw new Error('Extension is NOT allowed to make chat requests');
}

const that = this;

return {
get isRevoked() {
return !that._chatAccessAllowList.has(from);
},
async makeRequest(messages, options, progress, token) {

if (!that._chatAccessAllowList.has(from)) {
throw new Error('Access to chat has been revoked');
}

const requestId = (Math.random() * 1e6) | 0;
that._pendingRequest.set(requestId, progress);
try {
await that._proxy.$fetchResponse(from, identifier, requestId, messages.map(typeConvert.ChatMessage.from), options, token);
} finally {
that._pendingRequest.delete(requestId);
}
},
};
}

async $handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise<void> {
this._pendingRequest.get(requestId)?.report(chunk);
}
}
Loading

0 comments on commit 2c5ddd1

Please sign in to comment.