Implement shouldRepopulate and followupPlaceholder for agents (#196317)

This commit is contained in:
Rob Lourens 2023-10-23 13:25:21 -07:00 committed by GitHub
parent 09a4a2f7ab
commit c833c4d7b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 88 additions and 14 deletions

View file

@ -259,7 +259,7 @@ class ExtHostChatAgent {
return [];
}
this._lastSlashCommands = result;
return result.map(c => ({ name: c.name, description: c.description }));
return result.map(c => ({ name: c.name, description: c.description, followupPlaceholder: c.followupPlaceholder, shouldRepopulate: c.shouldRepopulate }));
}
async provideFollowups(result: vscode.ChatAgentResult2, token: CancellationToken): Promise<IChatFollowup[]> {

View file

@ -27,7 +27,7 @@ import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/brows
import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart';
import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { SelectAndInsertFileAction, dynamicReferenceDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicReferences';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
@ -41,6 +41,10 @@ const placeholderDecorationType = 'chat-session-detail';
const slashCommandTextDecorationType = 'chat-session-text';
const variableTextDecorationType = 'chat-variable-text';
function agentAndCommandToKey(agent: string, subcommand: string): string {
return `${agent}__${subcommand}`;
}
class InputEditorDecorations extends Disposable {
public readonly id = 'inputEditorDecorations';
@ -71,8 +75,12 @@ class InputEditorDecorations extends Disposable {
this.updateInputEditorDecorations();
}));
this._register(this.chatService.onDidSubmitSlashCommand((e) => {
if (e.sessionId === this.widget.viewModel?.sessionId && !this.previouslyUsedSlashCommands.has(e.slashCommand)) {
this.previouslyUsedSlashCommands.add(e.slashCommand);
if (e.sessionId === this.widget.viewModel?.sessionId) {
if ('agent' in e) {
this.previouslyUsedSlashCommands.add(agentAndCommandToKey(e.agent.id, e.slashCommand.name));
} else {
this.previouslyUsedSlashCommands.add(e.slashCommand);
}
}
}));
@ -180,6 +188,29 @@ class InputEditorDecorations extends Disposable {
}
}
const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart);
if (onlyAgentCommandAndWhitespace) {
// Agent reference and subcommand with no other text - show the placeholder
const isFollowupSlashCommand = this.previouslyUsedSlashCommands.has(agentAndCommandToKey(agentPart.agent.id, agentSubcommandPart.command.name));
const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder;
if (agentSubcommandPart?.command.description) {
placeholderDecoration = [{
range: {
startLineNumber: 1,
endLineNumber: 1,
startColumn: inputValue.length,
endColumn: 1000
},
renderOptions: {
after: {
contentText: shouldRenderFollowupPlaceholder ? agentSubcommandPart.command.followupPlaceholder : agentSubcommandPart.command.description,
color: this.getPlaceholderColor(),
}
}
}];
}
}
const onlySlashCommandAndWhitespace = slashCommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestSlashCommandPart);
if (onlySlashCommandAndWhitespace) {
// Command reference with no other text - show the placeholder
@ -237,14 +268,27 @@ class InputEditorSlashCommandMode extends Disposable {
@IChatService private readonly chatService: IChatService
) {
super();
this._register(this.chatService.onDidSubmitSlashCommand(({ slashCommand, sessionId }) => this.repopulateSlashCommand(slashCommand, sessionId)));
this._register(this.chatService.onDidSubmitSlashCommand(e => {
if (this.widget.viewModel?.sessionId !== e.sessionId) {
return;
}
if ('agent' in e) {
this.repopulateAgentCommand(e.agent, e.slashCommand);
} else {
this.repopulateSlashCommand(e.slashCommand);
}
}));
}
private async repopulateSlashCommand(slashCommand: string, sessionId: string) {
if (this.widget.viewModel?.sessionId !== sessionId) {
return;
private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand) {
if (slashCommand.shouldRepopulate) {
const value = `${chatAgentLeader}${agent.id} ${chatSubcommandLeader}${slashCommand.name} `;
this.widget.inputEditor.setValue(value);
this.widget.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 });
}
}
private async repopulateSlashCommand(slashCommand: string) {
const slashCommands = await this.widget.getSlashCommands();
if (this.widget.inputEditor.getValue().trim().length !== 0) {
@ -255,7 +299,6 @@ class InputEditorSlashCommandMode extends Disposable {
const value = `/${slashCommand} `;
this.widget.inputEditor.setValue(value);
this.widget.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 });
}
}
}

View file

@ -34,6 +34,23 @@ export interface IChatAgentFragment {
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;
}
export interface IChatAgentMetadata {

View file

@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri';
import { Range, IRange } from 'vs/editor/common/core/range';
import { ProviderResult, Location } from 'vs/editor/common/languages';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatModel, ChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';
@ -271,7 +271,7 @@ export interface IChatService {
_serviceBrand: undefined;
transferredSessionData: IChatTransferredSessionData | undefined;
onDidSubmitSlashCommand: Event<{ slashCommand: string; sessionId: string }>;
onDidSubmitSlashCommand: Event<{ slashCommand: string; sessionId: string } | { agent: IChatAgentData; slashCommand: IChatAgentCommand; sessionId: string }>;
onDidRegisterProvider: Event<{ providerId: string }>;
registerProvider(provider: IChatProvider): IDisposable;
hasSessions(providerId: string): boolean;

View file

@ -21,7 +21,7 @@ import { Progress } from 'vs/platform/progress/common/progress';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatAgentCommand, IChatAgentData, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, ISerializableChatData, ISerializableChatsData, isCompleteInteractiveProgressTreeData } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } from 'vs/workbench/contrib/chat/common/chatParserTypes';
@ -143,7 +143,7 @@ export class ChatService extends Disposable implements IChatService {
private readonly _onDidPerformUserAction = this._register(new Emitter<IChatUserActionEvent>());
public readonly onDidPerformUserAction: Event<IChatUserActionEvent> = this._onDidPerformUserAction.event;
private readonly _onDidSubmitSlashCommand = this._register(new Emitter<{ slashCommand: string; sessionId: string }>());
private readonly _onDidSubmitSlashCommand = this._register(new Emitter<{ slashCommand: string; sessionId: string } | { agent: IChatAgentData; slashCommand: IChatAgentCommand; sessionId: string }>());
public readonly onDidSubmitSlashCommand = this._onDidSubmitSlashCommand.event;
private readonly _onDidDisposeSession = this._register(new Emitter<{ sessionId: string; providerId: string; reason: 'initializationFailed' | 'cleared' }>());
@ -501,6 +501,8 @@ export class ChatService extends Disposable implements IChatService {
try {
if (usedSlashCommand?.command) {
this._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });
} else if (agentPart && agentSlashCommandPart?.command) {
this._onDidSubmitSlashCommand.fire({ agent: agentPart.agent, slashCommand: agentSlashCommandPart.command, sessionId: model.sessionId });
}
let rawResponse: IChatResponse | null | undefined;

View file

@ -33,7 +33,6 @@ declare module 'vscode' {
}
export interface ChatAgentSlashCommand {
/**
* A short name by which this command is referred to in the UI, e.g. `fix` or
* `explain` for commands that fix an issue or explain code.
@ -44,6 +43,19 @@ declare module 'vscode' {
* Human-readable description explaining what this command does.
*/
readonly description: string;
/**
* Whether executing the command puts the
* chat into a persistent mode, where the
* slash command is prepended to the chat input.
*/
readonly shouldRepopulate?: boolean;
/**
* Placeholder text to render in the chat input
* when the slash command has been repopulated.
* Has no effect if `shouldRepopulate` is `false`.
*/
readonly followupPlaceholder?: string;
}
export interface ChatAgentSlashCommandProvider {