Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/intents/node/editCodeIntent.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as l10n from '@vscode/l10n';
7
import { ChatResponseReferencePartStatusKind, MetadataMap, PromptReference, Raw } from '@vscode/prompt-tsx';
8
import type * as vscode from 'vscode';
9
import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher';
10
import { ChatLocation } from '../../../platform/chat/common/commonTypes';
11
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
12
import { isNotebookDocumentSnapshotJSON, NotebookDocumentSnapshot } from '../../../platform/editing/common/notebookDocumentSnapshot';
13
import { isTextDocumentSnapshotJSON, TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';
14
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
15
import { IEnvService } from '../../../platform/env/common/envService';
16
import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogService';
17
import { IChatEndpoint } from '../../../platform/networking/common/networking';
18
import { INotebookService } from '../../../platform/notebook/common/notebookService';
19
import { GenAiMetrics } from '../../../platform/otel/common/genAiMetrics';
20
import { IOTelService } from '../../../platform/otel/common/otelService';
21
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
22
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
23
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
24
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
25
import { isLocation } from '../../../util/common/types';
26
import { AsyncIterableObject } from '../../../util/vs/base/common/async';
27
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
28
import { ResourceSet } from '../../../util/vs/base/common/map';
29
import { Schemas } from '../../../util/vs/base/common/network';
30
import { basename, isEqual } from '../../../util/vs/base/common/resources';
31
import { assertType, isObject } from '../../../util/vs/base/common/types';
32
import { isUriComponents, URI } from '../../../util/vs/base/common/uri';
33
import { generateUuid } from '../../../util/vs/base/common/uuid';
34
import { BrandedService, IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
35
import { ChatRequestEditorData, Location, MarkdownString } from '../../../vscodeTypes';
36
import { CodeBlockInfo, CodeBlockProcessor, isCodeBlockWithResource } from '../../codeBlocks/node/codeBlockProcessor';
37
import { ICommandService } from '../../commands/node/commandService';
38
import { Intent } from '../../common/constants';
39
import { GenericInlineIntentInvocation } from '../../context/node/resolvers/genericInlineIntentInvocation';
40
import { ChatVariablesCollection, InstructionFileIdPrefix, isInstructionFile } from '../../prompt/common/chatVariablesCollection';
41
import { CodeBlock, Conversation, Turn } from '../../prompt/common/conversation';
42
import { IBuildPromptContext, InternalToolReference, IWorkingSet, IWorkingSetEntry, WorkingSetEntryState } from '../../prompt/common/intents';
43
import { ChatTelemetryBuilder } from '../../prompt/node/chatParticipantTelemetry';
44
import { CodebaseToolCallingLoop } from '../../prompt/node/codebaseToolCalling';
45
import { IntentInvocationMetadata } from '../../prompt/node/conversation';
46
import { DefaultIntentRequestHandler, IDefaultIntentRequestHandlerOptions } from '../../prompt/node/defaultIntentRequestHandler';
47
import { IDocumentContext } from '../../prompt/node/documentContext';
48
import { EditStrategy } from '../../prompt/node/editGeneration';
49
import { IBuildPromptResult, IIntent, IIntentInvocation, IIntentInvocationContext, IntentLinkificationOptions, IResponseProcessorContext } from '../../prompt/node/intents';
50
import { reportCitations } from '../../prompt/node/pseudoStartStopConversationCallback';
51
import { PromptRenderer, renderPromptElement } from '../../prompts/node/base/promptRenderer';
52
import { ICodeMapperService, IMapCodeRequest, IMapCodeResult } from '../../prompts/node/codeMapper/codeMapperService';
53
import { ChatToolReferences } from '../../prompts/node/panel/chatVariables';
54
import { EXISTING_CODE_MARKER } from '../../prompts/node/panel/codeBlockFormattingRules';
55
import { EditCodePrompt } from '../../prompts/node/panel/editCodePrompt';
56
import { ToolCallResultWrapper, ToolResultMetadata } from '../../prompts/node/panel/toolCalling';
57
import { getToolName, ToolName } from '../../tools/common/toolNames';
58
import { IToolsService } from '../../tools/common/toolsService';
59
import { CodebaseTool } from '../../tools/node/codebaseTool';
60
import { sendEditNotebookTelemetry } from '../../tools/node/editNotebookTool';
61
import { EditCodeStep, EditCodeStepTurnMetaData, PreviousEditCodeStep } from './editCodeStep';
62
63
64
type IntentInvocationCtor<T extends BrandedService[]> = {
65
new(
66
intent: IIntent,
67
location: ChatLocation,
68
endpoint: IChatEndpoint,
69
request: vscode.ChatRequest,
70
intentOptions: EditCodeIntentOptions,
71
...args: T[]
72
): EditCodeIntentInvocation;
73
};
74
75
export interface EditCodeIntentOptions extends EditCodeIntentInvocationOptions {
76
intentInvocation: IntentInvocationCtor<any>;
77
}
78
79
export interface EditCodeIntentInvocationOptions {
80
processCodeblocks: boolean;
81
}
82
83
export class EditCodeIntent implements IIntent {
84
85
static readonly ID: Intent = Intent.Edit;
86
87
readonly id: string = EditCodeIntent.ID;
88
89
readonly description = l10n.t('Make changes to existing code');
90
91
readonly locations = [ChatLocation.Editor, ChatLocation.Panel];
92
93
constructor(
94
@IInstantiationService protected readonly instantiationService: IInstantiationService,
95
@IEndpointProvider protected readonly endpointProvider: IEndpointProvider,
96
@IConfigurationService protected readonly configurationService: IConfigurationService,
97
@IExperimentationService protected readonly expService: IExperimentationService,
98
@ICodeMapperService private readonly codeMapperService: ICodeMapperService,
99
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
100
private readonly intentOptions: EditCodeIntentOptions = { processCodeblocks: true, intentInvocation: EditCodeIntentInvocation },
101
) { }
102
103
private async _handleCodesearch(conversation: Conversation, request: vscode.ChatRequest, location: ChatLocation, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext | undefined, chatTelemetry: ChatTelemetryBuilder): Promise<{ request: vscode.ChatRequest; conversation: Conversation }> {
104
const foundReferences: vscode.ChatPromptReference[] = [];
105
if ((this.configurationService.getConfig(ConfigKey.CodeSearchAgentEnabled) || this.configurationService.getConfig(ConfigKey.Advanced.CodeSearchAgentEnabled)) && request.toolReferences.find((r) => r.name === CodebaseTool.toolName && !isDirectorySemanticSearch(r))) {
106
107
const latestTurn = conversation.getLatestTurn();
108
109
const codebaseTool = this.instantiationService.createInstance(CodebaseToolCallingLoop, {
110
conversation,
111
toolCallLimit: 5,
112
request,
113
location,
114
});
115
116
const toolCallLoopResult = await codebaseTool.run(stream, token);
117
118
const toolCallResults = toolCallLoopResult.toolCallResults;
119
if (!toolCallLoopResult.chatResult?.errorDetails && toolCallResults) {
120
// TODO: do these new references need a lower priority?
121
const variables = new ChatVariablesCollection(request.references);
122
const endpoint = await this.endpointProvider.getChatEndpoint(request);
123
const { references } = await renderPromptElement(this.instantiationService, endpoint, ToolCallResultWrapper, { toolCallResults }, undefined, token);
124
foundReferences.push(...toNewChatReferences(variables, references));
125
// TODO: how should we splice in the assistant message?
126
conversation = new Conversation(conversation.sessionId, [...conversation.turns.slice(0, -1), new Turn(latestTurn.id, latestTurn.request, undefined, [], undefined, undefined, false, latestTurn.modeInstructions)]);
127
}
128
return { conversation, request: { ...request, references: [...request.references, ...foundReferences], toolReferences: request.toolReferences.filter((r) => r.name !== CodebaseTool.toolName) } };
129
}
130
return { conversation, request };
131
}
132
133
private async _handleApplyConfirmedEdits(edits: (MappedEditsRequest & { chatRequestId: string; chatRequestModel: string })[], outputStream: vscode.ChatResponseStream, token: CancellationToken) {
134
const hydrateMappedEditsRequest = async (request: MappedEditsRequest): Promise<MappedEditsRequest> => {
135
const workingSet = await Promise.all(request.workingSet.map(async (ws): Promise<IWorkingSetEntry> => {
136
if (isTextDocumentSnapshotJSON(ws.document)) {
137
const document = await this.workspaceService.openTextDocument(ws.document.uri);
138
return { ...ws, document: TextDocumentSnapshot.fromJSON(document, ws.document) };
139
} else if (isNotebookDocumentSnapshotJSON(ws.document)) {
140
const document = await this.workspaceService.openNotebookDocument(ws.document.uri);
141
return { ...ws, document: NotebookDocumentSnapshot.fromJSON(document, ws.document) };
142
}
143
return ws;
144
}));
145
146
return { ...request, workingSet };
147
};
148
149
await Promise.all(edits.map(async requestDry => {
150
const request = await hydrateMappedEditsRequest(requestDry);
151
const uri = request.codeBlock.resource;
152
153
outputStream.markdown(l10n.t`Applying edits to \`${this.workspaceService.asRelativePath(uri)}\`...\n\n`);
154
outputStream.textEdit(uri, []); // signal start of
155
156
try {
157
return await this.codeMapperService.mapCode(request, outputStream, { chatRequestId: requestDry.chatRequestId, chatRequestModel: requestDry.chatRequestModel, chatRequestSource: `confirmed_edits_${this.id}` }, token);
158
} finally {
159
if (!token.isCancellationRequested) {
160
outputStream.textEdit(uri, true);
161
}
162
}
163
}));
164
}
165
166
async handleRequest(conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken, documentContext: IDocumentContext | undefined, agentName: string, location: ChatLocation, chatTelemetry: ChatTelemetryBuilder, yieldRequested: () => boolean): Promise<vscode.ChatResult> {
167
const applyEdits = request.acceptedConfirmationData?.filter(isEditsOkayConfirmation);
168
if (applyEdits?.length) {
169
await this._handleApplyConfirmedEdits(applyEdits.flatMap(e => ({ ...e.edits, chatRequestId: e.chatRequestId, chatRequestModel: request.model.id })), stream, token);
170
return {};
171
}
172
173
({ conversation, request } = await this._handleCodesearch(conversation, request, location, stream, token, documentContext, chatTelemetry));
174
return this.instantiationService.createInstance(EditIntentRequestHandler, this, conversation, request, stream, token, documentContext, location, chatTelemetry, this.getIntentHandlerOptions(request), yieldRequested).getResult();
175
}
176
177
protected getIntentHandlerOptions(_request: vscode.ChatRequest): IDefaultIntentRequestHandlerOptions | undefined {
178
return undefined;
179
}
180
181
async invoke(invocationContext: IIntentInvocationContext) {
182
const { location, documentContext, request } = invocationContext;
183
const endpoint = await this.endpointProvider.getChatEndpoint(request);
184
185
if (location === ChatLocation.Panel || location === ChatLocation.Notebook) {
186
return this.instantiationService.createInstance(this.intentOptions.intentInvocation, this, location, endpoint, request, this.intentOptions);
187
}
188
189
if (!documentContext) {
190
throw new Error('Open a file to add code.');
191
}
192
return this.instantiationService.createInstance(GenericInlineIntentInvocation, this, location, endpoint, documentContext, EditStrategy.FallbackToReplaceRange);
193
}
194
}
195
196
class EditIntentRequestHandler {
197
198
constructor(
199
private readonly intent: EditCodeIntent,
200
private readonly conversation: Conversation,
201
private readonly request: vscode.ChatRequest,
202
private readonly stream: vscode.ChatResponseStream,
203
private readonly token: vscode.CancellationToken,
204
private readonly documentContext: IDocumentContext | undefined,
205
private readonly location: ChatLocation,
206
private readonly chatTelemetry: ChatTelemetryBuilder,
207
private readonly handlerOptions: IDefaultIntentRequestHandlerOptions | undefined,
208
private readonly yieldRequested: () => boolean,
209
@IInstantiationService private readonly instantiationService: IInstantiationService,
210
@ITelemetryService protected readonly telemetryService: ITelemetryService,
211
@IEditLogService private readonly editLogService: IEditLogService,
212
@IOTelService private readonly otelService: IOTelService,
213
) { }
214
215
async getResult(): Promise<vscode.ChatResult> {
216
const actual = this.instantiationService.createInstance(
217
DefaultIntentRequestHandler,
218
this.intent,
219
this.conversation,
220
this.request,
221
this.stream,
222
this.token,
223
this.documentContext,
224
this.location,
225
this.chatTelemetry,
226
this.handlerOptions,
227
this.yieldRequested,
228
);
229
const result = await actual.getResult();
230
231
// Record telemetry for the edit code blocks in an editing session
232
const turn = this.conversation.getLatestTurn();
233
const currentTurnMetadata = turn.getMetadata(IntentInvocationMetadata)?.value;
234
const editCodeStep = (currentTurnMetadata instanceof EditCodeIntentInvocation ? currentTurnMetadata._editCodeStep : undefined);
235
236
if (editCodeStep?.telemetryInfo) {
237
/* __GDPR__
238
"panel.edit.codeblocks" : {
239
"owner": "joyceerhl",
240
"comment": "Records information about the proposed edit codeblocks in an editing session",
241
"conversationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id for the current chat conversation." },
242
"outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request succeeded or failed." },
243
"workingSetCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of entries in the working set" },
244
"uniqueCodeblockUriCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of unique code block URIs" },
245
"codeblockCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of code blocks in the response" },
246
"codeblockWithUriCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of code blocks that had URIs" },
247
"codeblockWithElidedCodeCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of code blocks that had a ...existing code... comment" },
248
"shellCodeblockCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of shell code blocks in the response" },
249
"shellCodeblockWithUriCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of shell code blocks that had URIs" },
250
"shellCodeblockWithElidedCodeCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of shell code blocks that had a ...existing code... comment" },
251
"editStepCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of edit steps in the session so far" },
252
"sessionDuration": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The time since the session started" },
253
"intentId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The ID of the intent being executed" }
254
}
255
*/
256
this.telemetryService.sendMSFTTelemetryEvent('panel.edit.codeblocks', {
257
conversationId: this.conversation.sessionId,
258
outcome: Boolean(result.errorDetails) ? 'error' : 'success',
259
intentId: this.intent.id
260
}, {
261
workingSetCount: editCodeStep.workingSet.length,
262
uniqueCodeblockUriCount: editCodeStep.telemetryInfo.codeblockUris.size,
263
codeblockCount: editCodeStep.telemetryInfo.codeblockCount,
264
codeblockWithUriCount: editCodeStep.telemetryInfo.codeblockWithUriCount,
265
codeblockWithElidedCodeCount: editCodeStep.telemetryInfo.codeblockWithElidedCodeCount,
266
shellCodeblockCount: editCodeStep.telemetryInfo.shellCodeblockCount,
267
shellCodeblockWithUriCount: editCodeStep.telemetryInfo.shellCodeblockWithUriCount,
268
shellCodeblockWithElidedCodeCount: editCodeStep.telemetryInfo.shellCodeblockWithElidedCodeCount,
269
editStepCount: this.conversation.turns.length,
270
sessionDuration: Date.now() - turn.startTime,
271
});
272
GenAiMetrics.incrementAgentEditResponseCount(this.otelService, Boolean(result.errorDetails) ? 'error' : 'success');
273
}
274
275
await this.editLogService.markCompleted(turn.id, result.errorDetails ? 'error' : 'success');
276
277
return result;
278
}
279
}
280
281
type MappedEditsRequest = IMapCodeRequest & { workingSet: IWorkingSet };
282
283
const enum ConfirmationIds {
284
EditsOkay = '4e6e0e05-5dab-48d0-b2cd-6a14c8e3e8a2', // random string
285
}
286
287
interface IEditsOkayConfirmation {
288
id: ConfirmationIds.EditsOkay;
289
chatRequestId: string;
290
edits: MappedEditsRequest;
291
}
292
293
const makeEditsConfirmation = (chatRequestId: string, edits: MappedEditsRequest): IEditsOkayConfirmation => ({
294
id: ConfirmationIds.EditsOkay,
295
chatRequestId,
296
edits,
297
});
298
299
const isEditsOkayConfirmation = (obj: unknown): obj is IEditsOkayConfirmation =>
300
isObject(obj) && (obj as IEditsOkayConfirmation).id === ConfirmationIds.EditsOkay;
301
302
export class EditCodeIntentInvocation implements IIntentInvocation {
303
304
public _editCodeStep: EditCodeStep | undefined = undefined;
305
306
/**
307
* Stable codebase invocation so that their {@link InternalToolReference.id ids}
308
* are reused across multiple turns.
309
*/
310
protected stableToolReferences = this.request.toolReferences.map(InternalToolReference.from);
311
312
public get linkification(): IntentLinkificationOptions {
313
return { disable: false };
314
}
315
316
public readonly codeblocksRepresentEdits: boolean = true;
317
318
constructor(
319
readonly intent: IIntent,
320
readonly location: ChatLocation,
321
readonly endpoint: IChatEndpoint,
322
protected readonly request: vscode.ChatRequest,
323
private readonly intentOptions: EditCodeIntentInvocationOptions,
324
@IInstantiationService protected readonly instantiationService: IInstantiationService,
325
@ICodeMapperService private readonly codeMapperService: ICodeMapperService,
326
@IEnvService private readonly envService: IEnvService,
327
@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,
328
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
329
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
330
@IToolsService protected readonly toolsService: IToolsService,
331
@IConfigurationService protected readonly configurationService: IConfigurationService,
332
@IEditLogService private readonly editLogService: IEditLogService,
333
@ICommandService protected readonly commandService: ICommandService,
334
@ITelemetryService protected readonly telemetryService: ITelemetryService,
335
@INotebookService private readonly notebookService: INotebookService,
336
@IOTelService protected readonly otelService: IOTelService,
337
) { }
338
339
getAvailableTools(): vscode.LanguageModelToolInformation[] | Promise<vscode.LanguageModelToolInformation[]> | undefined {
340
return undefined;
341
}
342
343
async buildPrompt(
344
promptContext: IBuildPromptContext,
345
progress: vscode.Progress<vscode.ChatResponseReferencePart | vscode.ChatResponseProgressPart>,
346
token: vscode.CancellationToken
347
): Promise<IBuildPromptResult> {
348
349
// Add any references from the codebase invocation to the request
350
const codebase = await this._getCodebaseReferences(promptContext, token);
351
352
let variables = promptContext.chatVariables;
353
let toolReferences: vscode.ChatPromptReference[] = [];
354
if (codebase) {
355
toolReferences = toNewChatReferences(variables, codebase.references);
356
variables = new ChatVariablesCollection([...this.request.references, ...toolReferences]);
357
}
358
359
if (this.request.location2 instanceof ChatRequestEditorData) {
360
const editorRequestReference: vscode.ChatPromptReference = {
361
id: '',
362
name: this.request.location2.document.fileName,
363
value: new Location(this.request.location2.document.uri, this.request.location2.wholeRange)
364
};
365
variables = new ChatVariablesCollection([...this.request.references, ...toolReferences, editorRequestReference]);
366
}
367
368
369
370
const tools = await this.getAvailableTools();
371
const toolTokens = tools?.length ? await this.endpoint.acquireTokenizer().countToolTokens(tools) : 0;
372
const endpoint = toolTokens > 0 ? this.endpoint.cloneWithTokenOverride(Math.floor((this.endpoint.modelMaxPromptTokens - toolTokens) * 0.85)) : this.endpoint;
373
const { editCodeStep, chatVariables } = await EditCodeStep.create(this.instantiationService, promptContext.history, variables, endpoint);
374
this._editCodeStep = editCodeStep;
375
376
const commandToolReferences: InternalToolReference[] = [];
377
let query = promptContext.query;
378
const command = this.request.command && this.commandService.getCommand(this.request.command, this.location);
379
if (command) {
380
if (command.toolEquivalent) {
381
commandToolReferences.push({
382
id: `${this.request.command}->${generateUuid()}`,
383
name: getToolName(command.toolEquivalent)
384
});
385
}
386
query = query ? `${command.details}.\n${query}` : command.details;
387
}
388
389
// Reserve extra space when tools are involved due to token counting issues
390
const renderer = PromptRenderer.create(this.instantiationService, endpoint, EditCodePrompt, {
391
endpoint,
392
promptContext: {
393
...promptContext,
394
query,
395
chatVariables,
396
workingSet: editCodeStep.workingSet,
397
promptInstructions: editCodeStep.promptInstructions,
398
toolCallResults: { ...promptContext.toolCallResults, ...codebase?.toolCallResults },
399
tools: promptContext.tools && {
400
...promptContext.tools,
401
toolReferences: this.stableToolReferences.filter((r) => r.name !== ToolName.Codebase).concat(commandToolReferences),
402
},
403
},
404
location: this.location
405
});
406
const start = Date.now();
407
const result = await renderer.render(progress, token);
408
const duration = Date.now() - start;
409
this.sendPromptRenderTelemetry(duration);
410
const lastMessage = result.messages[result.messages.length - 1];
411
if (lastMessage.role === Raw.ChatRole.User) {
412
this._editCodeStep.setUserMessage(lastMessage);
413
}
414
415
return {
416
...result,
417
// The codebase tool is not actually called/referenced in the edit prompt, so we need to
418
// merge its metadata so that its output is not lost and it's not called repeatedly every turn
419
// todo@connor4312/joycerhl: this seems a bit janky
420
metadata: codebase ? mergeMetadata(result.metadata, codebase.metadatas) : result.metadata,
421
// Don't report file references that came in via chat variables in an editing session, unless they have warnings,
422
// because they are already displayed as part of the working set
423
references: result.references.filter((ref) => this.shouldKeepReference(editCodeStep, ref, toolReferences, chatVariables)),
424
};
425
}
426
427
private sendPromptRenderTelemetry(duration: number) {
428
/* __GDPR__
429
"editCodeIntent.promptRender" : {
430
"owner": "roblourens",
431
"comment": "Understanding the performance of the edit code intent rendering",
432
"promptRenderDurationIncludingRunningTools": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Duration of the prompt rendering, includes running tools" },
433
"isAgentMode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the prompt was for agent mode" }
434
}
435
*/
436
this.telemetryService.sendMSFTTelemetryEvent('editCodeIntent.promptRender', {
437
}, {
438
promptRenderDurationIncludingRunningTools: duration,
439
isAgentMode: this.intent.id === Intent.Agent ? 1 : 0,
440
});
441
}
442
443
protected async _getCodebaseReferences(
444
promptContext: IBuildPromptContext,
445
token: vscode.CancellationToken,
446
) {
447
const codebaseTools = this.stableToolReferences.filter(t => t.name === ToolName.Codebase);
448
if (!codebaseTools.length) {
449
return;
450
}
451
452
const history = promptContext.history;
453
const endpoint = await this.endpointProvider.getChatEndpoint(this.request);
454
455
const { references, metadatas } = await renderPromptElement(this.instantiationService, endpoint, ChatToolReferences, { promptContext: { requestId: promptContext.requestId, query: this.request.prompt, chatVariables: promptContext.chatVariables, history, toolCallResults: promptContext.toolCallResults, tools: { toolReferences: codebaseTools, toolInvocationToken: this.request.toolInvocationToken, availableTools: promptContext.tools?.availableTools ?? [] } }, embeddedInsideUserMessage: false }, undefined, token);
456
return { toolCallResults: getToolCallResults(metadatas), references, metadatas };
457
}
458
459
private shouldKeepReference(editCodeStep: EditCodeStep, ref: PromptReference, toolReferences: vscode.ChatPromptReference[], chatVariables: ChatVariablesCollection): boolean {
460
if (ref.options?.status && ref.options?.status?.kind !== ChatResponseReferencePartStatusKind.Complete) {
461
// Always show references for files which have warnings
462
return true;
463
}
464
const uri = getUriOfReference(ref);
465
if (!uri) {
466
// This reference doesn't have an URI
467
return true;
468
}
469
if (toolReferences.find(entry => (URI.isUri(entry.value) && isEqual(entry.value, uri) || (isLocation(entry.value) && isEqual(entry.value.uri, uri))))) {
470
// If this reference came in via resolving #codebase, we should show it
471
// TODO@joyceerhl if this reference is subsequently modified and joins the working set, should we suppress it again in the UI?
472
return true;
473
}
474
const PROMPT_INSTRUCTION_ROOT_PREFIX = `${InstructionFileIdPrefix}.root`;
475
const promptInstruction = chatVariables.find((variable) => isInstructionFile(variable) && URI.isUri(variable.value) && isEqual(variable.value, uri));
476
if (promptInstruction) {
477
// Report references for root prompt instruction files and not their children
478
return promptInstruction.reference.id.startsWith(PROMPT_INSTRUCTION_ROOT_PREFIX);
479
}
480
const workingSetEntry = editCodeStep.workingSet.find(entry => isEqual(entry.document.uri, uri));
481
if (!workingSetEntry) {
482
// This reference wasn't part of the working set
483
return true;
484
}
485
return false;
486
}
487
488
private async shouldConfirmBeforeFileEdits(uri: URI) {
489
for (const tool of this.request.toolReferences) {
490
const ownTool = this.toolsService.getCopilotTool(tool.name as ToolName);
491
if (!ownTool) {
492
continue;
493
}
494
495
const filter = await ownTool.filterEdits?.(uri);
496
if (filter) {
497
return filter;
498
}
499
}
500
501
return undefined;
502
}
503
504
async processResponse?(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<vscode.ChatResult> {
505
assertType(this._editCodeStep);
506
507
const codeMapperWork: Promise<IMapCodeResult | undefined>[] = [];
508
509
const allReceivedMarkdown: string[] = [];
510
511
const textStream = (
512
AsyncIterableObject
513
.map(inputStream, part => {
514
reportCitations(part.delta, outputStream);
515
return part.delta.text;
516
})
517
.map(piece => {
518
allReceivedMarkdown.push(piece);
519
return piece;
520
})
521
);
522
const remoteName = this.envService.remoteName;
523
const createUriFromResponsePath = this._createUriFromResponsePath.bind(this);
524
if (this.intentOptions.processCodeblocks) {
525
for await (const codeBlock of getCodeBlocksFromResponse(textStream, outputStream, createUriFromResponsePath, remoteName)) {
526
527
if (token.isCancellationRequested) {
528
break;
529
}
530
531
const isShellScript = codeBlock.language === 'sh';
532
if (isCodeBlockWithResource(codeBlock)) {
533
this._editCodeStep.telemetryInfo.codeblockUris.add(codeBlock.resource);
534
this._editCodeStep.telemetryInfo.codeblockWithUriCount += 1;
535
if (isShellScript) {
536
this._editCodeStep.telemetryInfo.shellCodeblockWithUriCount += 1;
537
}
538
539
// The model proposed an edit for this URI
540
this._editCodeStep.setWorkingSetEntryState(codeBlock.resource, WorkingSetEntryState.Undecided);
541
542
if (codeBlock.code.includes(EXISTING_CODE_MARKER)) {
543
this._editCodeStep.telemetryInfo.codeblockWithElidedCodeCount += 1;
544
if (isShellScript) {
545
this._editCodeStep.telemetryInfo.shellCodeblockWithElidedCodeCount += 1;
546
}
547
}
548
const request: MappedEditsRequest = {
549
workingSet: [...this._editCodeStep.workingSet],
550
codeBlock
551
};
552
553
const confirmEdits = await this.shouldConfirmBeforeFileEdits(codeBlock.resource);
554
if (confirmEdits) {
555
outputStream.confirmation(confirmEdits.title, confirmEdits.message, makeEditsConfirmation(context.turn.id, request));
556
continue;
557
}
558
const isNotebookDocument = this.notebookService.hasSupportedNotebooks(codeBlock.resource);
559
if (isNotebookDocument) {
560
outputStream.notebookEdit(codeBlock.resource, []);
561
} else {
562
outputStream.textEdit(codeBlock.resource, []); // signal start
563
}
564
const task = this.codeMapperService.mapCode(request, outputStream, {
565
chatRequestId: context.turn.id,
566
chatRequestModel: this.endpoint.model,
567
chatSessionId: context.chatSessionId,
568
chatRequestSource: `${this.intent.id}_${ChatLocation.toString(this.location)}`,
569
}, token).finally(() => {
570
if (!token.isCancellationRequested) {
571
// signal being done with this uri
572
if (isNotebookDocument) {
573
outputStream.notebookEdit(codeBlock.resource, true);
574
sendEditNotebookTelemetry(this.telemetryService, undefined, 'editCodeIntent', codeBlock.resource, this.request.id, undefined, this.endpoint);
575
} else {
576
outputStream.textEdit(codeBlock.resource, true);
577
}
578
}
579
});
580
codeMapperWork.push(task);
581
} else {
582
this._editCodeStep.telemetryInfo.codeblockCount += 1;
583
if (isShellScript) {
584
this._editCodeStep.telemetryInfo.shellCodeblockCount += 1;
585
}
586
}
587
}
588
} else {
589
for await (const part of textStream) {
590
if (token.isCancellationRequested) {
591
break;
592
}
593
594
outputStream.markdown(part);
595
}
596
}
597
598
const results = await Promise.all(codeMapperWork);
599
for (const result of results) {
600
if (!result) {
601
context.addAnnotations([{ severity: 'error', label: 'cancelled', message: 'CodeMapper cancelled' }]);
602
} else if (result.annotations) {
603
context.addAnnotations(result.annotations);
604
}
605
}
606
for (const result of results) {
607
if (result && result.errorDetails) {
608
return {
609
errorDetails: result.errorDetails
610
};
611
}
612
}
613
614
const response = allReceivedMarkdown.join('');
615
this._editCodeStep.setAssistantReply(response);
616
this.editLogService.logEditChatRequest(context.turn.id, context.messages, response);
617
618
const historyEditCodeStep = PreviousEditCodeStep.fromEditCodeStep(this._editCodeStep);
619
context.turn.setMetadata(new EditCodeStepTurnMetaData(historyEditCodeStep));
620
return {
621
metadata: historyEditCodeStep.toChatResultMetaData(),
622
};
623
}
624
625
private _createUriFromResponsePath(path: string): URI | undefined {
626
assertType(this._editCodeStep);
627
628
// ok to modify entries from the working set
629
for (const entry of this._editCodeStep.workingSet) {
630
if (this.promptPathRepresentationService.getFilePath(entry.document.uri) === path) {
631
return entry.document.uri;
632
}
633
}
634
635
const uri = this.promptPathRepresentationService.resolveFilePath(path, this._editCodeStep.getPredominantScheme());
636
if (!uri) {
637
return undefined;
638
}
639
640
// ok to make changes in the workspace
641
if (this.workspaceService.getWorkspaceFolder(uri)) {
642
return uri;
643
}
644
if (uri.scheme === Schemas.file || uri.scheme === Schemas.vscodeRemote) {
645
// do not directly modify files outside the workspace. Create an untitled file instead, let the user save when ok
646
return URI.from({ scheme: Schemas.untitled, path: uri.path });
647
}
648
return uri;
649
}
650
}
651
652
653
const fileHeadingLineStart = '### ';
654
655
export function getCodeBlocksFromResponse(textStream: AsyncIterable<string>, outputStream: vscode.ChatResponseStream, createUriFromResponsePath: (p: string) => URI | undefined, remoteName: string | undefined): AsyncIterable<CodeBlock> {
656
657
return new AsyncIterableObject<CodeBlock>(async (emitter) => {
658
659
let currentCodeBlock: CodeBlockInfo | undefined = undefined;
660
const codeblockProcessor = new CodeBlockProcessor(
661
path => {
662
return createUriFromResponsePath(path);
663
},
664
(markdown: MarkdownString, codeBlockInfo: CodeBlockInfo | undefined, vulnerabilities: vscode.ChatVulnerability[] | undefined) => {
665
if (vulnerabilities) {
666
outputStream.markdownWithVulnerabilities(markdown, vulnerabilities);
667
} else {
668
outputStream.markdown(markdown);
669
}
670
if (codeBlockInfo && codeBlockInfo.resource && codeBlockInfo !== currentCodeBlock) {
671
// first time we see this code block
672
currentCodeBlock = codeBlockInfo;
673
outputStream.codeblockUri(codeBlockInfo.resource, true);
674
}
675
},
676
codeBlock => {
677
emitter.emitOne(codeBlock);
678
},
679
{
680
matchesLineStart(linePart, inCodeBlock) {
681
return !inCodeBlock && linePart.startsWith(fileHeadingLineStart.substring(0, linePart.length));
682
},
683
process(line, inCodeBlock) {
684
const header = line.value.substring(fileHeadingLineStart.length).trim(); // remove the ### and trim
685
let fileUri = createUriFromResponsePath(header);
686
if (fileUri) {
687
if (remoteName) {
688
fileUri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteName, path: fileUri.path });
689
}
690
const headerLine = `### [${basename(fileUri)}](${fileUri.toString()})\n`;
691
return new MarkdownString(headerLine);
692
} else {
693
// likely not a file path, just keep the original line
694
return line;
695
}
696
},
697
}
698
699
);
700
701
for await (const text of textStream) {
702
codeblockProcessor.processMarkdown(text);
703
}
704
codeblockProcessor.flush();
705
});
706
}
707
708
function getUriOfReference(ref: PromptReference): vscode.Uri | undefined {
709
if ('variableName' in ref.anchor) {
710
return _extractUri(ref.anchor.value);
711
}
712
return _extractUri(ref.anchor);
713
}
714
715
function _extractUri(something: vscode.Uri | vscode.Location | undefined): vscode.Uri | undefined {
716
if (isLocation(something)) {
717
return something.uri;
718
}
719
return something;
720
}
721
722
export function toNewChatReferences(chatVariables: ChatVariablesCollection, promptReferences: PromptReference[]): vscode.ChatPromptReference[] {
723
const toolReferences: vscode.ChatPromptReference[] = [];
724
const seen = new ResourceSet();
725
726
for (const reference of promptReferences) {
727
if (isLocation(reference.anchor)) {
728
const uri = reference.anchor.uri;
729
if (seen.has(uri) || chatVariables.find((v) => URI.isUri(v.value) && isEqual(v.value, uri))) {
730
continue;
731
}
732
seen.add(uri);
733
toolReferences.push({ id: uri.toString(), name: uri.toString(), value: reference.anchor });
734
} else if (isUriComponents(reference.anchor) || URI.isUri(reference.anchor)) {
735
const uri = URI.revive(reference.anchor);
736
if (seen.has(uri) || chatVariables.find((v) => URI.isUri(v.value) && isEqual(v.value, uri))) {
737
continue;
738
}
739
seen.add(uri);
740
toolReferences.push({ id: uri.toString(), name: uri.toString(), value: uri });
741
}
742
}
743
744
return toolReferences;
745
}
746
747
function getToolCallResults(metadatas: MetadataMap) {
748
const toolCallResults: Record<string, vscode.LanguageModelToolResult2> = {};
749
for (const metadata of metadatas.getAll(ToolResultMetadata)) {
750
toolCallResults[metadata.toolCallId] = metadata.result;
751
}
752
753
return toolCallResults;
754
}
755
756
export function mergeMetadata(m1: MetadataMap, m2: MetadataMap): MetadataMap {
757
return {
758
get: key => m1.get(key) ?? m2.get(key),
759
getAll: key => m1.getAll(key).concat(m2.getAll(key)),
760
};
761
}
762
763
function isDirectorySemanticSearch(toolCall: vscode.ChatLanguageModelToolReference) {
764
if (toolCall.name !== ToolName.Codebase) {
765
return false;
766
}
767
768
const input = (toolCall as any).input;
769
if (!input) {
770
return false;
771
}
772
773
const scopedDirectories = input.scopedDirectories;
774
if (!Array.isArray(scopedDirectories)) {
775
return false;
776
}
777
778
return scopedDirectories.length > 0;
779
}
780
781