Move welcome message to chat agent (#204163)

* Move welcome message provider to default agent

* Remove welcome message/sample questions from interactive provider

* Remove provider display name and icon

* Add default agent for tests

* And proposal
This commit is contained in:
Rob Lourens 2024-02-02 14:41:13 -03:00 committed by GitHub
parent c535f3dc1f
commit 5e3a912d7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 121 additions and 106 deletions

View File

@ -7,6 +7,7 @@
"enabledApiProposals": [
"authSession",
"chatAgents2",
"defaultChatAgent",
"contribViewsRemote",
"contribStatusBarItems",
"createFileSystemWatcher",

View File

@ -36,6 +36,7 @@ suite('chat', () => {
deferred.complete(request);
return null;
});
agent.isDefault = true;
agent.subCommandProvider = {
provideSubCommands: (_token) => {
return [{ name: 'hello', description: 'Hello' }];

View File

@ -49,7 +49,6 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape {
const unreg = this._chatService.registerProvider({
id,
displayName: registration.label,
prepareSession: async (token) => {
const session = await this._proxy.$prepareChat(handle, token);
if (!session) {
@ -75,12 +74,6 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape {
}
};
},
provideWelcomeMessage: (token) => {
return this._proxy.$provideWelcomeMessage(handle, token);
},
provideSampleQuestions: (token) => {
return this._proxy.$provideSampleQuestions(handle, token);
},
});
this._providerRegistrations.set(handle, unreg);

View File

@ -103,6 +103,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
}
lastSlashCommands = await this._proxy.$provideSlashCommands(handle, token);
return lastSlashCommands;
},
provideWelcomeMessage: (token: CancellationToken) => {
return this._proxy.$provideWelcomeMessage(handle, token);
},
provideSampleQuestions: (token: CancellationToken) => {
return this._proxy.$provideSampleQuestions(handle, token);
}
});
this._agents.set(handle, {

View File

@ -1222,6 +1222,8 @@ export interface ExtHostChatAgentsShape2 {
$acceptFeedback(handle: number, sessionId: string, requestId: string, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void;
$acceptAction(handle: number, sessionId: string, requestId: string, action: IChatUserActionEvent): void;
$invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise<IChatAgentCompletionItem[]>;
$provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined>;
$provideSampleQuestions(handle: number, token: CancellationToken): Promise<IChatReplyFollowup[] | undefined>;
$releaseSession(sessionId: string): void;
}
@ -1305,8 +1307,6 @@ export interface MainThreadChatShape extends IDisposable {
export interface ExtHostChatShape {
$prepareChat(handle: number, token: CancellationToken): Promise<IChatDto | undefined>;
$provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IMarkdownString | IChatReplyFollowup[])[] | undefined>;
$provideSampleQuestions(handle: number, token: CancellationToken): Promise<IChatReplyFollowup[] | undefined>;
$releaseSession(sessionId: number): void;
}

View File

@ -4,13 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Iterable } from 'vs/base/common/iterator';
import { toDisposable } from 'vs/base/common/lifecycle';
import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtHostChatShape, IChatDto, IMainContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import { IChatReplyFollowup } from 'vs/workbench/contrib/chat/common/chatService';
import type * as vscode from 'vscode';
class ChatProviderWrapper<T> {
@ -89,49 +86,6 @@ export class ExtHostChat implements ExtHostChatShape {
};
}
async $provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IMarkdownString | IChatReplyFollowup[])[] | undefined> {
const entry = this._chatProvider.get(handle);
if (!entry) {
return undefined;
}
if (!entry.provider.provideWelcomeMessage) {
return undefined;
}
const content = await entry.provider.provideWelcomeMessage(token);
if (!content) {
return undefined;
}
return content.map(item => {
if (typeof item === 'string') {
return item;
} else if (Array.isArray(item)) {
return item.map(f => typeConvert.ChatReplyFollowup.from(f));
} else {
return typeConvert.MarkdownString.from(item);
}
});
}
async $provideSampleQuestions(handle: number, token: CancellationToken): Promise<IChatReplyFollowup[] | undefined> {
const entry = this._chatProvider.get(handle);
if (!entry) {
return undefined;
}
if (!entry.provider.provideSampleQuestions) {
return undefined;
}
const rawFollowups = await entry.provider.provideSampleQuestions(token);
if (!rawFollowups) {
return undefined;
}
return rawFollowups?.map(f => typeConvert.ChatReplyFollowup.from(f));
}
$releaseSession(sessionId: number) {
this._chatSessions.delete(sessionId);
}

View File

@ -8,6 +8,7 @@ import { DeferredPromise, raceCancellation } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Emitter } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { StopWatch } from 'vs/base/common/stopwatch';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
@ -19,7 +20,7 @@ import { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import * as extHostTypes from 'vs/workbench/api/common/extHostTypes';
import { IChatAgentCommand, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatFollowup, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import type * as vscode from 'vscode';
@ -218,12 +219,30 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
const items = await agent.invokeCompletionProvider(query, token);
return items.map(typeConvert.ChatAgentCompletionItem.from);
}
async $provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined> {
const agent = this._agents.get(handle);
if (!agent) {
return;
}
return await agent.provideWelcomeMessage(token);
}
async $provideSampleQuestions(handle: number, token: CancellationToken): Promise<IChatReplyFollowup[] | undefined> {
const agent = this._agents.get(handle);
if (!agent) {
return;
}
return await agent.provideSampleQuestions(token);
}
}
class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
private _subCommandProvider: vscode.ChatAgentSubCommandProvider | undefined;
private _followupProvider: vscode.FollowupProvider<TResult> | undefined;
private _followupProvider: vscode.ChatAgentFollowupProvider<TResult> | undefined;
private _description: string | undefined;
private _fullName: string | undefined;
private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined;
@ -236,6 +255,7 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
private _onDidPerformAction = new Emitter<vscode.ChatAgentUserActionEvent>();
private _supportIssueReporting: boolean | undefined;
private _agentVariableProvider?: { provider: vscode.ChatAgentCompletionItemProvider; triggerCharacters: string[] };
private _welcomeMessageProvider?: vscode.ChatAgentWelcomeMessageProvider | undefined;
constructor(
public readonly extension: IExtensionDescription,
@ -290,6 +310,35 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
return followups.map(f => typeConvert.ChatFollowup.from(f));
}
async provideWelcomeMessage(token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined> {
if (!this._welcomeMessageProvider) {
return [];
}
const content = await this._welcomeMessageProvider.provideWelcomeMessage(token);
if (!content) {
return [];
}
return content.map(item => {
if (typeof item === 'string') {
return item;
} else {
return typeConvert.MarkdownString.from(item);
}
});
}
async provideSampleQuestions(token: CancellationToken): Promise<IChatReplyFollowup[]> {
if (!this._welcomeMessageProvider || !this._welcomeMessageProvider.provideSampleQuestions) {
return [];
}
const content = await this._welcomeMessageProvider.provideSampleQuestions(token);
if (!content) {
return [];
}
return content?.map(f => typeConvert.ChatReplyFollowup.from(f));
}
get apiAgent(): vscode.ChatAgent2<TResult> {
let disposed = false;
let updateScheduled = false;
@ -444,6 +493,13 @@ class ExtHostChatAgent<TResult extends vscode.ChatAgentResult2> {
get agentVariableProvider() {
return that._agentVariableProvider;
},
set welcomeMessageProvider(v) {
that._welcomeMessageProvider = v;
updateMetadataSoon();
},
get welcomeMessageProvider() {
return that._welcomeMessageProvider;
},
onDidPerformAction: !isProposedApiEnabled(this.extension, 'chatAgents2Additions')
? undefined!
: this._onDidPerformAction.event

View File

@ -10,9 +10,10 @@ import { Iterable } from 'vs/base/common/iterator';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri';
import { ProviderResult } from 'vs/editor/common/languages';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatFollowup, IChatProgress, IChatReplyFollowup, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';
//#region agent service, commands etc
@ -33,6 +34,8 @@ export interface IChatAgent extends IChatAgentData {
provideFollowups?(sessionId: string, token: CancellationToken): Promise<IChatFollowup[]>;
lastSlashCommands?: IChatAgentCommand[];
provideSlashCommands(token: CancellationToken): Promise<IChatAgentCommand[]>;
provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>;
provideSampleQuestions?(token: CancellationToken): ProviderResult<IChatReplyFollowup[] | undefined>;
}
export interface IChatAgentCommand {

View File

@ -3,13 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export interface IChatProviderContribution {
id: string;
label: string;
extensionIcon?: URI;
when?: string;
}

View File

@ -156,11 +156,7 @@ export type IChatProgress =
export interface IChatProvider {
readonly id: string;
readonly displayName: string;
readonly iconUrl?: string;
prepareSession(token: CancellationToken): ProviderResult<IChat | undefined>;
provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString | IChatReplyFollowup[])[] | undefined>;
provideSampleQuestions?(token: CancellationToken): ProviderResult<IChatReplyFollowup[] | undefined>;
}
export interface IChatReplyFollowup {
@ -269,7 +265,6 @@ export interface IChatDetail {
export interface IChatProviderInfo {
id: string;
displayName: string;
}
export interface IChatTransferredSessionData {

View File

@ -384,11 +384,16 @@ export class ChatService extends Disposable implements IChatService {
this.trace('startSession', `Provider returned session`);
const welcomeMessage = model.welcomeMessage ? undefined : await provider.provideWelcomeMessage?.(token) ?? undefined;
const defaultAgent = this.chatAgentService.getDefaultAgent();
if (!defaultAgent) {
throw new Error('No default agent');
}
const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(token) ?? undefined;
const welcomeModel = welcomeMessage && new ChatWelcomeMessageModel(
model,
welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item),
await provider.provideSampleQuestions?.(token) ?? []
await defaultAgent.provideSampleQuestions?.(token) ?? []
);
model.initialize(session, welcomeModel);
@ -733,7 +738,6 @@ export class ChatService extends Disposable implements IChatService {
return Array.from(this._providers.values()).map(provider => {
return {
id: provider.id,
displayName: provider.displayName
};
});
}

View File

@ -172,7 +172,6 @@ suite('Chat', () => {
const id = 'testProvider';
testDisposables.add(testService.registerProvider({
id,
displayName: 'Test',
prepareSession: function (token: CancellationToken): ProviderResult<IChat | undefined> {
throw new Error('Function not implemented.');
}
@ -181,7 +180,6 @@ suite('Chat', () => {
assert.throws(() => {
testDisposables.add(testService.registerProvider({
id,
displayName: 'Test',
prepareSession: function (token: CancellationToken): ProviderResult<IChat | undefined> {
throw new Error('Function not implemented.');
}

View File

@ -3,41 +3,41 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize, localize2 } from 'vs/nls';
import { ICommandQuickPick, CommandsHistory } from 'vs/platform/quickinput/browser/commandsQuickAccess';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IMenuService, MenuId, MenuItemAction, SubmenuItemAction, Action2 } from 'vs/platform/actions/common/actions';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { CancellationToken } from 'vs/base/common/cancellation';
import { isFirefox } from 'vs/base/browser/browser';
import { raceTimeout, timeout } from 'vs/base/common/async';
import { AbstractEditorCommandsQuickAccessProvider } from 'vs/editor/contrib/quickAccess/browser/commandsQuickAccess';
import { IEditor } from 'vs/editor/common/editorCommon';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Codicon } from 'vs/base/common/codicons';
import { stripIcons } from 'vs/base/common/iconLabels';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Language } from 'vs/base/common/platform';
import { ThemeIcon } from 'vs/base/common/themables';
import { IEditor } from 'vs/editor/common/editorCommon';
import { AbstractEditorCommandsQuickAccessProvider } from 'vs/editor/contrib/quickAccess/browser/commandsQuickAccess';
import { localize, localize2 } from 'vs/nls';
import { isLocalizedString } from 'vs/platform/action/common/action';
import { Action2, IMenuService, MenuId, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IProductService } from 'vs/platform/product/common/productService';
import { CommandsHistory, ICommandQuickPick } from 'vs/platform/quickinput/browser/commandsQuickAccess';
import { TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess';
import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchQuickAccessConfiguration } from 'vs/workbench/browser/quickaccess';
import { Codicon } from 'vs/base/common/codicons';
import { ThemeIcon } from 'vs/base/common/themables';
import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
import { stripIcons } from 'vs/base/common/iconLabels';
import { isFirefox } from 'vs/base/browser/browser';
import { IProductService } from 'vs/platform/product/common/productService';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ASK_QUICK_QUESTION_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
import { CommandInformationResult, IAiRelatedInformationService, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkbenchQuickAccessConfiguration } from 'vs/workbench/browser/quickaccess';
import { CHAT_OPEN_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { isLocalizedString } from 'vs/platform/action/common/action';
import { ASK_QUICK_QUESTION_ACTION_ID } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { CommandInformationResult, IAiRelatedInformationService, RelatedInformationType } from 'vs/workbench/services/aiRelatedInformation/common/aiRelatedInformation';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAccessProvider {
@ -77,7 +77,7 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
@IPreferencesService private readonly preferencesService: IPreferencesService,
@IProductService private readonly productService: IProductService,
@IAiRelatedInformationService private readonly aiRelatedInformationService: IAiRelatedInformationService,
@IChatService private readonly chatService: IChatService
@IChatAgentService private readonly chatAgentService: IChatAgentService,
) {
super({
showAlias: !Language.isDefaultVariant(),
@ -172,10 +172,10 @@ export class CommandsQuickAccessProvider extends AbstractEditorCommandsQuickAcce
});
}
const info = this.chatService.getProviderInfos()[0];
if (info) {
const defaultAgent = this.chatAgentService.getDefaultAgent();
if (defaultAgent) {
additionalPicks.push({
label: localize('askXInChat', "Ask {0}: {1}", info.displayName, filter),
label: localize('askXInChat', "Ask {0}: {1}", defaultAgent.metadata.fullName, filter),
commandId: this.configuration.experimental.askChatLocation === 'quickChat' ? ASK_QUICK_QUESTION_ACTION_ID : CHAT_OPEN_ACTION_ID,
args: [filter]
});

View File

@ -181,7 +181,7 @@ declare module 'vscode' {
/**
* Will be invoked once after each request to get suggested followup questions to show the user. The user can click the followup to send it to the chat.
*/
export interface FollowupProvider<TResult extends ChatAgentResult2> {
export interface ChatAgentFollowupProvider<TResult extends ChatAgentResult2> {
/**
*
* @param result The same instance of the result object that was returned by the chat agent, and it can be extended with arbitrary properties if needed.
@ -229,7 +229,7 @@ declare module 'vscode' {
/**
* This provider will be called once after each request to retrieve suggested followup questions.
*/
followupProvider?: FollowupProvider<TResult>;
followupProvider?: ChatAgentFollowupProvider<TResult>;
/**
* When the user clicks this agent in `/help`, this text will be submitted to this subCommand

View File

@ -5,6 +5,13 @@
declare module 'vscode' {
export type ChatAgentWelcomeMessageContent = string | MarkdownString;
export interface ChatAgentWelcomeMessageProvider {
provideWelcomeMessage(token: CancellationToken): ProviderResult<ChatAgentWelcomeMessageContent[]>;
provideSampleQuestions?(token: CancellationToken): ProviderResult<ChatAgentReplyFollowup[]>;
}
export interface ChatAgent2<TResult extends ChatAgentResult2> {
/**
* When true, this agent is invoked by default when no other agent is being invoked
@ -26,5 +33,7 @@ declare module 'vscode' {
* A string that will be appended after the listing of chat agents in `/help`.
*/
helpTextPostfix?: string | MarkdownString;
welcomeMessageProvider?: ChatAgentWelcomeMessageProvider;
}
}

View File

@ -124,8 +124,6 @@ declare module 'vscode' {
export type InteractiveWelcomeMessageContent = string | MarkdownString | ChatAgentReplyFollowup[];
export interface InteractiveSessionProvider<S extends InteractiveSession = InteractiveSession> {
provideWelcomeMessage?(token: CancellationToken): ProviderResult<InteractiveWelcomeMessageContent[]>;
provideSampleQuestions?(token: CancellationToken): ProviderResult<ChatAgentReplyFollowup[]>;
prepareSession(token: CancellationToken): ProviderResult<S>;
}