Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make vscode own /help, add API for agents to customize it #197964

Merged
merged 2 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 47 additions & 1 deletion src/vs/workbench/api/common/extHostChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ class ExtHostChatAgent {
private _fullName: string | undefined;
private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined;
private _isDefault: boolean | undefined;
private _helpTextPrefix: string | vscode.MarkdownString | undefined;
private _helpTextPostfix: string | vscode.MarkdownString | undefined;
private _sampleRequest?: string;
private _isSecondary: boolean | undefined;
private _onDidReceiveFeedback = new Emitter<vscode.ChatAgentResult2Feedback>();
private _onDidPerformAction = new Emitter<vscode.ChatAgentUserActionEvent>();
Expand Down Expand Up @@ -259,7 +262,14 @@ class ExtHostChatAgent {
return [];
}
this._lastSlashCommands = result;
return result.map(c => ({ name: c.name, description: c.description, followupPlaceholder: c.followupPlaceholder, shouldRepopulate: c.shouldRepopulate }));
return result
.map(c => ({
name: c.name,
description: c.description,
followupPlaceholder: c.followupPlaceholder,
shouldRepopulate: c.shouldRepopulate,
sampleRequest: c.sampleRequest
}));
}

async provideFollowups(result: vscode.ChatAgentResult2, token: CancellationToken): Promise<IChatFollowup[]> {
Expand Down Expand Up @@ -300,6 +310,9 @@ class ExtHostChatAgent {
hasFollowup: this._followupProvider !== undefined,
isDefault: this._isDefault,
isSecondary: this._isSecondary,
helpTextPrefix: (!this._helpTextPrefix || typeof this._helpTextPrefix === 'string') ? this._helpTextPrefix : typeConvert.MarkdownString.from(this._helpTextPrefix),
helpTextPostfix: (!this._helpTextPostfix || typeof this._helpTextPostfix === 'string') ? this._helpTextPostfix : typeConvert.MarkdownString.from(this._helpTextPostfix),
sampleRequest: this._sampleRequest,
});
updateScheduled = false;
});
Expand Down Expand Up @@ -354,6 +367,32 @@ class ExtHostChatAgent {
that._isDefault = v;
updateMetadataSoon();
},
get helpTextPrefix() {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
return that._helpTextPrefix;
},
set helpTextPrefix(v) {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
if (!that._isDefault) {
throw new Error('helpTextPrefix is only available on the default chat agent');
}

that._helpTextPrefix = v;
updateMetadataSoon();
},
get helpTextPostfix() {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
return that._helpTextPostfix;
},
set helpTextPostfix(v) {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
if (!that._isDefault) {
throw new Error('helpTextPostfix is only available on the default chat agent');
}

that._helpTextPostfix = v;
updateMetadataSoon();
},
get isSecondary() {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
return that._isSecondary;
Expand All @@ -363,6 +402,13 @@ class ExtHostChatAgent {
that._isSecondary = v;
updateMetadataSoon();
},
get sampleRequest() {
return that._sampleRequest;
},
set sampleRequest(v) {
that._sampleRequest = v;
updateMetadataSoon();
},
get onDidReceiveFeedback() {
return that._onDidReceiveFeedback.event;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,15 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';

export interface IChatExecuteActionContext {
widget: IChatWidget;
widget?: IChatWidget;
inputValue?: string;
}

export function isExecuteActionContext(thing: unknown): thing is IChatExecuteActionContext {
return typeof thing === 'object' && thing !== null && 'widget' in thing;
}

export class SubmitAction extends Action2 {
static readonly ID = 'workbench.action.chat.submit';

Expand All @@ -44,12 +40,11 @@ export class SubmitAction extends Action2 {
}

run(accessor: ServicesAccessor, ...args: any[]) {
const context = args[0];
if (!isExecuteActionContext(context)) {
return;
}
const context: IChatExecuteActionContext = args[0];

context.widget.acceptInput(context.inputValue);
const widgetService = accessor.get(IChatWidgetService);
const widget = context.widget ?? widgetService.lastFocusedWidget;
widget?.acceptInput(context.inputValue);
}
}

Expand All @@ -76,8 +71,8 @@ export function registerChatExecuteActions() {
}

run(accessor: ServicesAccessor, ...args: any[]) {
const context = args[0];
if (!isExecuteActionContext(context)) {
const context: IChatExecuteActionContext = args[0];
if (!context.widget) {
return;
}

Expand Down
44 changes: 41 additions & 3 deletions src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/ed
import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { registerChatCodeBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions';
import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions';
import { registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions';
import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport';
Expand All @@ -45,7 +45,7 @@ import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { ChatProviderService, IChatProviderService } from 'vs/workbench/contrib/chat/common/chatProvider';
import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions';
Expand All @@ -56,6 +56,8 @@ import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/a
import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick';
import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables';
import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { CancellationToken } from 'vs/base/common/cancellation';

// Register configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
Expand Down Expand Up @@ -221,16 +223,52 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
constructor(
@IChatSlashCommandService slashCommandService: IChatSlashCommandService,
@ICommandService commandService: ICommandService,
@IChatAgentService chatAgentService: IChatAgentService,
) {
super();
this._store.add(slashCommandService.registerSlashCommand({
command: 'clear',
detail: nls.localize('clear', "Clear the session"),
sortText: 'z_clear',
sortText: 'z2_clear',
executeImmediately: true
}, async () => {
commandService.executeCommand(ACTION_ID_CLEAR_CHAT);
}));
this._store.add(slashCommandService.registerSlashCommand({
command: 'help',
detail: '',
sortText: 'z1_help',
executeImmediately: true
}, async (prompt, progress) => {
const defaultAgent = chatAgentService.getDefaultAgent();
const agents = chatAgentService.getAgents();
if (defaultAgent?.metadata.helpTextPrefix) {
progress.report({ content: defaultAgent.metadata.helpTextPrefix });
progress.report({ content: '\n\n' });
}

const agentText = (await Promise.all(agents
.filter(a => a.id !== defaultAgent?.id)
.map(async a => {
const agentWithLeader = `${chatAgentLeader}${a.id}`;
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`;
const commands = await a.provideSlashCommands(CancellationToken.None);
const commandText = commands.map(c => {
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${c.description}`;
}).join('\n');

return agentLine + '\n' + commandText;
}))).join('\n');
progress.report({ content: new MarkdownString(agentText, { isTrusted: { enabledCommands: [SubmitAction.ID] } }) });
if (defaultAgent?.metadata.helpTextPostfix) {
progress.report({ content: '\n\n' });
progress.report({ content: defaultAgent.metadata.helpTextPostfix });
}
}));
}
}

Expand Down
15 changes: 10 additions & 5 deletions src/vs/workbench/contrib/chat/common/chatAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
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 { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';

//#region agent service, commands etc
Expand All @@ -27,40 +28,44 @@ export interface IChatAgent extends IChatAgentData {
provideSlashCommands(token: CancellationToken): Promise<IChatAgentCommand[]>;
}

export interface IChatAgentFragment {
content: string | { treeData: IChatResponseProgressFileTreeData };
}

export interface IChatAgentCommand {
name: string;
description: string;

/**
* Whether the command should execute as soon
* as it is entered. Defaults to `false`.
*/
executeImmediately?: boolean;

/**
* Whether executing the command puts the
* chat into a persistent mode, where the
* slash command is prepended to the chat input.
*/
shouldRepopulate?: boolean;

/**
* Placeholder text to render in the chat input
* when the slash command has been repopulated.
* Has no effect if `shouldRepopulate` is `false`.
*/
followupPlaceholder?: string;

sampleRequest?: string;
}

export interface IChatAgentMetadata {
description?: string;
isDefault?: boolean; // The agent invoked when no agent is specified
helpTextPrefix?: string | IMarkdownString;
helpTextPostfix?: string | IMarkdownString;
isSecondary?: boolean; // Invoked by ctrl/cmd+enter
fullName?: string;
icon?: URI;
iconDark?: URI;
themeIcon?: ThemeIcon;
sampleRequest?: string;
}

export interface IChatAgentRequest {
Expand Down
6 changes: 5 additions & 1 deletion src/vs/workbench/contrib/chat/common/chatModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,11 @@ export class Response implements IResponse {
} else if (lastResponsePart) {
// Combine this part with the last, non-resolving string part
if (isMarkdownString(responsePart)) {
this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + responsePart.value, responsePart) };
// Merge all enabled commands
const lastPartEnabledCommands = typeof lastResponsePart.string.isTrusted === 'object' ? lastResponsePart.string.isTrusted.enabledCommands : [];
const thisPartEnabledCommands = typeof responsePart.isTrusted === 'object' ? responsePart.isTrusted.enabledCommands : [];
const enabledCommands = [...lastPartEnabledCommands, ...thisPartEnabledCommands];
this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + responsePart.value, { isTrusted: { enabledCommands } }) };
} else {
this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + responsePart, lastResponsePart.string) };
}
Expand Down
8 changes: 3 additions & 5 deletions src/vs/workbench/contrib/chat/common/chatServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashC
import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatRequest, IChatResponse, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ISlashCommand, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService, IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';

Expand Down Expand Up @@ -567,10 +567,8 @@ export class ChatService extends Disposable implements IChatService {
history.push({ role: ChatMessageRole.User, content: request.message.text });
history.push({ role: ChatMessageRole.Assistant, content: request.response.response.asString() });
}
const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress<IChatSlashFragment>(p => {
const { content } = p;
const data = isCompleteInteractiveProgressTreeData(content) ? content : { content };
progressCallback(data);
const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress<IChatProgress>(p => {
progressCallback(p);
}), history, token);
agentOrCommandFollowups = Promise.resolve(commandResult?.followUp);
rawResponse = { session: model.session! };
Expand Down
36 changes: 9 additions & 27 deletions src/vs/workbench/contrib/chat/common/chatSlashCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProgress } from 'vs/platform/progress/common/progress';
import { IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
import { IChatFollowup, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';

//#region slash service, commands etc
Expand All @@ -29,21 +29,18 @@ export interface IChatSlashData {
export interface IChatSlashFragment {
content: string | { treeData: IChatResponseProgressFileTreeData };
}

export type IChatSlashCallback = { (prompt: string, progress: IProgress<IChatSlashFragment>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> };
export type IChatSlashCallback = { (prompt: string, progress: IProgress<IChatProgress>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> };

export const IChatSlashCommandService = createDecorator<IChatSlashCommandService>('chatSlashCommandService');

/**
* This currently only exists to drive /clear. Delete this when the agent service can handle that scenario
* This currently only exists to drive /clear and /help
*/
export interface IChatSlashCommandService {
_serviceBrand: undefined;
readonly onDidChangeCommands: Event<void>;
registerSlashData(data: IChatSlashData): IDisposable;
registerSlashCallback(id: string, command: IChatSlashCallback): IDisposable;
registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable;
executeCommand(id: string, prompt: string, progress: IProgress<IChatSlashFragment>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>;
executeCommand(id: string, prompt: string, progress: IProgress<IChatProgress>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>;
getCommands(): Array<IChatSlashData>;
hasCommand(id: string): boolean;
}
Expand All @@ -68,11 +65,12 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom
this._commands.clear();
}

registerSlashData(data: IChatSlashData): IDisposable {
registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable {
if (this._commands.has(data.command)) {
throw new Error(`Already registered a command with id ${data.command}}`);
}
this._commands.set(data.command, { data });

this._commands.set(data.command, { data, command });
this._onDidChangeCommands.fire();

return toDisposable(() => {
Expand All @@ -82,22 +80,6 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom
});
}

registerSlashCallback(id: string, command: IChatSlashCallback): IDisposable {
const data = this._commands.get(id);
if (!data) {
throw new Error(`No command with id ${id} registered`);
}
data.command = command;
return toDisposable(() => data.command = undefined);
}

registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable {
return combinedDisposable(
this.registerSlashData(data),
this.registerSlashCallback(data.command, command)
);
}

getCommands(): Array<IChatSlashData> {
return Array.from(this._commands.values(), v => v.data);
}
Expand All @@ -106,7 +88,7 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom
return this._commands.has(id);
}

async executeCommand(id: string, prompt: string, progress: IProgress<IChatSlashFragment>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> {
async executeCommand(id: string, prompt: string, progress: IProgress<IChatProgress>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> {
const data = this._commands.get(id);
if (!data) {
throw new Error('No command with id ${id} NOT registered');
Expand Down