Skip to content

Commit 0cae183

Browse files
authored
Include conversation history context to provideCommands (microsoft#205867)
* Include conversation history context to `provideCommands` so that chat participant commands can be dynamic to the conversation microsoft#199908 * Fix tests * Fix test
1 parent 7a40a10 commit 0cae183

26 files changed

+223
-144
lines changed

src/vs/workbench/api/browser/mainThreadChatAgents2.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
2020
import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart';
2121
import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables';
2222
import { IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
23+
import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
2324
import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes';
2425
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
2526
import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
@@ -76,7 +77,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
7677
}
7778

7879
$registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata): void {
79-
let lastSlashCommands: IChatAgentCommand[] | undefined;
80+
const lastSlashCommands: WeakMap<IChatModel, IChatAgentCommand[]> = new WeakMap();
8081
const d = this._chatAgentService.registerAgent({
8182
id: name,
8283
extensionId: extension,
@@ -96,15 +97,19 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
9697

9798
return this._proxy.$provideFollowups(request, handle, result, token);
9899
},
99-
get lastSlashCommands() {
100-
return lastSlashCommands;
100+
getLastSlashCommands: (model: IChatModel) => {
101+
return lastSlashCommands.get(model);
101102
},
102-
provideSlashCommands: async (token) => {
103+
provideSlashCommands: async (model, history, token) => {
103104
if (!this._agents.get(handle)?.hasSlashCommands) {
104105
return []; // save an IPC call
105106
}
106-
lastSlashCommands = await this._proxy.$provideSlashCommands(handle, token);
107-
return lastSlashCommands;
107+
const commands = await this._proxy.$provideSlashCommands(handle, { history }, token);
108+
if (model) {
109+
lastSlashCommands.set(model, commands);
110+
}
111+
112+
return commands;
108113
},
109114
provideWelcomeMessage: (token: CancellationToken) => {
110115
return this._proxy.$provideWelcomeMessage(handle, token);

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1227,7 +1227,7 @@ export type IChatAgentHistoryEntryDto = {
12271227

12281228
export interface ExtHostChatAgentsShape2 {
12291229
$invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise<IChatAgentResult | undefined>;
1230-
$provideSlashCommands(handle: number, token: CancellationToken): Promise<IChatAgentCommand[]>;
1230+
$provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise<IChatAgentCommand[]>;
12311231
$provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise<IChatFollowup[]>;
12321232
$acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void;
12331233
$acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void;

src/vs/workbench/api/common/extHostChatAgents2.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
190190

191191
const stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._logService, this.commands.converter, sessionDisposables);
192192
try {
193-
const convertedHistory = await this.prepareHistoryTurns(request, context);
193+
const convertedHistory = await this.prepareHistoryTurns(request.agentId, context);
194194
const task = agent.invoke(
195195
typeConvert.ChatAgentRequest.to(request),
196196
{ history: convertedHistory },
@@ -220,13 +220,13 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
220220
}
221221
}
222222

223-
private async prepareHistoryTurns(request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> {
223+
private async prepareHistoryTurns(agentId: string, context: { history: IChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> {
224224

225225
const res: (vscode.ChatRequestTurn | vscode.ChatResponseTurn)[] = [];
226226

227227
for (const h of context.history) {
228228
const ehResult = typeConvert.ChatAgentResult.to(h.result);
229-
const result: vscode.ChatResult = request.agentId === h.request.agentId ?
229+
const result: vscode.ChatResult = agentId === h.request.agentId ?
230230
ehResult :
231231
{ ...ehResult, metadata: undefined };
232232

@@ -245,13 +245,16 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 {
245245
this._sessionDisposables.deleteAndDispose(sessionId);
246246
}
247247

248-
async $provideSlashCommands(handle: number, token: CancellationToken): Promise<IChatAgentCommand[]> {
248+
async $provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise<IChatAgentCommand[]> {
249249
const agent = this._agents.get(handle);
250250
if (!agent) {
251251
// this is OK, the agent might have disposed while the request was in flight
252252
return [];
253253
}
254-
return agent.provideSlashCommands(token);
254+
255+
const convertedHistory = await this.prepareHistoryTurns(agent.id, context);
256+
257+
return agent.provideSlashCommands({ history: convertedHistory }, token);
255258
}
256259

257260
async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise<IChatFollowup[]> {
@@ -386,11 +389,11 @@ class ExtHostChatAgent {
386389
return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? [];
387390
}
388391

389-
async provideSlashCommands(token: CancellationToken): Promise<IChatAgentCommand[]> {
392+
async provideSlashCommands(context: vscode.ChatContext, token: CancellationToken): Promise<IChatAgentCommand[]> {
390393
if (!this._commandProvider) {
391394
return [];
392395
}
393-
const result = await this._commandProvider.provideCommands(token);
396+
const result = await this._commandProvider.provideCommands(context, token);
394397
if (!result) {
395398
return [];
396399
}

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
266266
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` };
267267
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
268268
const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`;
269-
const commands = await a.provideSlashCommands(CancellationToken.None);
269+
const commands = await a.provideSlashCommands(undefined, [], CancellationToken.None);
270270
const commandText = commands.map(c => {
271271
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` };
272272
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));

src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget';
2727
import { SelectAndInsertFileAction, dynamicVariableDecorationType } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables';
2828
import { IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
2929
import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors';
30+
import { getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel';
3031
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
3132
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
3233
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
@@ -398,7 +399,7 @@ class AgentCompletions extends Disposable {
398399
}
399400

400401
const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart;
401-
const commands = await usedAgent.agent.provideSlashCommands(token); // Refresh the cache here
402+
const commands = await usedAgent.agent.provideSlashCommands(widget.viewModel.model, getHistoryEntriesFromModel(widget.viewModel.model), token); // Refresh the cache here
402403

403404
return <CompletionList>{
404405
suggestions: commands.map((c, i) => {
@@ -421,7 +422,8 @@ class AgentCompletions extends Disposable {
421422
triggerCharacters: ['/'],
422423
provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => {
423424
const widget = this.chatWidgetService.getWidgetByInputUri(model.uri);
424-
if (!widget) {
425+
const viewModel = widget?.viewModel;
426+
if (!widget || !viewModel) {
425427
return;
426428
}
427429

@@ -431,7 +433,7 @@ class AgentCompletions extends Disposable {
431433
}
432434

433435
const agents = this.chatAgentService.getAgents();
434-
const all = agents.map(agent => agent.provideSlashCommands(token));
436+
const all = agents.map(agent => agent.provideSlashCommands(viewModel.model, getHistoryEntriesFromModel(viewModel.model), token));
435437
const commands = await raceCancellation(Promise.all(all), token);
436438

437439
if (!commands) {

src/vs/workbench/contrib/chat/common/chatAgents.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri';
1313
import { ProviderResult } from 'vs/editor/common/languages';
1414
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
1515
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
16-
import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel';
16+
import { IChatModel, IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel';
1717
import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService';
1818

1919
//#region agent service, commands etc
@@ -33,8 +33,8 @@ export interface IChatAgentData {
3333
export interface IChatAgent extends IChatAgentData {
3434
invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult>;
3535
provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise<IChatFollowup[]>;
36-
lastSlashCommands?: IChatAgentCommand[];
37-
provideSlashCommands(token: CancellationToken): Promise<IChatAgentCommand[]>;
36+
getLastSlashCommands(model: IChatModel): IChatAgentCommand[] | undefined;
37+
provideSlashCommands(model: IChatModel | undefined, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentCommand[]>;
3838
provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>;
3939
provideSampleQuestions?(token: CancellationToken): ProviderResult<IChatFollowup[] | undefined>;
4040
}
@@ -155,7 +155,6 @@ export class ChatAgentService extends Disposable implements IChatAgentService {
155155
throw new Error(`No agent with id ${id} registered`);
156156
}
157157
data.agent.metadata = { ...data.agent.metadata, ...updateMetadata };
158-
data.agent.provideSlashCommands(CancellationToken.None); // Update the cached slash commands
159158
this._onDidChangeAgents.fire();
160159
}
161160

src/vs/workbench/contrib/chat/common/chatModel.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { URI, UriComponents, UriDto } from 'vs/base/common/uri';
1414
import { generateUuid } from 'vs/base/common/uuid';
1515
import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange';
1616
import { ILogService } from 'vs/platform/log/common/log';
17-
import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
18-
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
17+
import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
18+
import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
1919
import { IChat, IChatAgentMarkdownContentWithVulnerability, IChatCommandButton, IChatContent, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTreeData, IChatUsedContext, InteractiveSessionVoteDirection, isIUsedContext } from 'vs/workbench/contrib/chat/common/chatService';
2020
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';
2121

@@ -843,3 +843,37 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel {
843843
return this.session.responderAvatarIconUri;
844844
}
845845
}
846+
847+
export function getHistoryEntriesFromModel(model: IChatModel): IChatAgentHistoryEntry[] {
848+
const history: IChatAgentHistoryEntry[] = [];
849+
for (const request of model.getRequests()) {
850+
if (!request.response) {
851+
continue;
852+
}
853+
854+
const promptTextResult = getPromptText(request.message);
855+
const historyRequest: IChatAgentRequest = {
856+
sessionId: model.sessionId,
857+
requestId: request.id,
858+
agentId: request.response.agent?.id ?? '',
859+
message: promptTextResult.message,
860+
command: request.response.slashCommand?.name,
861+
variables: updateRanges(request.variableData, promptTextResult.diff) // TODO bit of a hack
862+
};
863+
history.push({ request: historyRequest, response: request.response.response.value, result: request.response.result ?? {} });
864+
}
865+
866+
return history;
867+
}
868+
869+
export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData {
870+
return {
871+
variables: variableData.variables.map(v => ({
872+
...v,
873+
range: {
874+
start: v.range.start - diff,
875+
endExclusive: v.range.endExclusive - diff
876+
}
877+
}))
878+
};
879+
}

src/vs/workbench/contrib/chat/common/chatRequestParser.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange';
77
import { IPosition, Position } from 'vs/editor/common/core/position';
88
import { Range } from 'vs/editor/common/core/range';
99
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
10+
import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
1011
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
12+
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
1113
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
1214
import { IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables';
1315

@@ -19,12 +21,14 @@ export class ChatRequestParser {
1921
constructor(
2022
@IChatAgentService private readonly agentService: IChatAgentService,
2123
@IChatVariablesService private readonly variableService: IChatVariablesService,
22-
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService
24+
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService,
25+
@IChatService private readonly chatService: IChatService
2326
) { }
2427

2528
parseChatRequest(sessionId: string, message: string): IParsedChatRequest {
2629
const parts: IParsedChatRequestPart[] = [];
2730
const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls
31+
const model = this.chatService.getSession(sessionId)!;
2832

2933
let lineNumber = 1;
3034
let column = 1;
@@ -38,7 +42,7 @@ export class ChatRequestParser {
3842
} else if (char === chatAgentLeader) {
3943
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts);
4044
} else if (char === chatSubcommandLeader) {
41-
newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts);
45+
newPart = this.tryToParseSlashCommand(model, message.slice(i), message, i, new Position(lineNumber, column), parts);
4246
}
4347

4448
if (!newPart) {
@@ -138,7 +142,7 @@ export class ChatRequestParser {
138142
return;
139143
}
140144

141-
private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined {
145+
private tryToParseSlashCommand(model: IChatModel, remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined {
142146
const nextSlashMatch = remainingMessage.match(slashReg);
143147
if (!nextSlashMatch) {
144148
return;
@@ -167,7 +171,7 @@ export class ChatRequestParser {
167171
return;
168172
}
169173

170-
const subCommands = usedAgent.agent.lastSlashCommands;
174+
const subCommands = usedAgent.agent.getLastSlashCommands(model);
171175
const subCommand = subCommands?.find(c => c.name === command);
172176
if (subCommand) {
173177
// Valid agent subcommand

src/vs/workbench/contrib/chat/common/chatServiceImpl.ts

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import { Progress } from 'vs/platform/progress/common/progress';
2020
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
2121
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2222
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
23-
import { IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
23+
import { IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
2424
import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
25-
import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatsData } from 'vs/workbench/contrib/chat/common/chatModel';
25+
import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel';
2626
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes';
2727
import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
2828
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
@@ -531,23 +531,7 @@ export class ChatService extends Disposable implements IChatService {
531531
const defaultAgent = this.chatAgentService.getDefaultAgent();
532532
if (agentPart || (defaultAgent && !commandPart)) {
533533
const agent = (agentPart?.agent ?? defaultAgent)!;
534-
const history: IChatAgentHistoryEntry[] = [];
535-
for (const request of model.getRequests()) {
536-
if (!request.response) {
537-
continue;
538-
}
539-
540-
const promptTextResult = getPromptText(request.message);
541-
const historyRequest: IChatAgentRequest = {
542-
sessionId,
543-
requestId: request.id,
544-
agentId: request.response.agent?.id ?? '',
545-
message: promptTextResult.message,
546-
command: request.response.slashCommand?.name,
547-
variables: updateRanges(request.variableData, promptTextResult.diff) // TODO bit of a hack
548-
};
549-
history.push({ request: historyRequest, response: request.response.response.value, result: request.response.result ?? {} });
550-
}
534+
const history = getHistoryEntriesFromModel(model);
551535

552536
const initVariableData: IChatRequestVariableData = { variables: [] };
553537
request = model.addRequest(parsedRequest, initVariableData, agent, agentSlashCommandPart?.command);
@@ -764,15 +748,3 @@ export class ChatService extends Disposable implements IChatService {
764748
this.trace('transferChatSession', `Transferred session ${model.sessionId} to workspace ${toWorkspace.toString()}`);
765749
}
766750
}
767-
768-
function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData {
769-
return {
770-
variables: variableData.variables.map(v => ({
771-
...v,
772-
range: {
773-
start: v.range.start - diff,
774-
endExclusive: v.range.endExclusive - diff
775-
}
776-
}))
777-
};
778-
}

0 commit comments

Comments
 (0)