Path: blob/main/extensions/copilot/src/extension/intents/node/editCodeIntent.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as l10n from '@vscode/l10n';6import { ChatResponseReferencePartStatusKind, MetadataMap, PromptReference, Raw } from '@vscode/prompt-tsx';7import type * as vscode from 'vscode';8import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher';9import { ChatLocation } from '../../../platform/chat/common/commonTypes';10import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';11import { isNotebookDocumentSnapshotJSON, NotebookDocumentSnapshot } from '../../../platform/editing/common/notebookDocumentSnapshot';12import { isTextDocumentSnapshotJSON, TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';13import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';14import { IEnvService } from '../../../platform/env/common/envService';15import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogService';16import { IChatEndpoint } from '../../../platform/networking/common/networking';17import { INotebookService } from '../../../platform/notebook/common/notebookService';18import { GenAiMetrics } from '../../../platform/otel/common/genAiMetrics';19import { IOTelService } from '../../../platform/otel/common/otelService';20import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';21import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';22import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';23import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';24import { isLocation } from '../../../util/common/types';25import { AsyncIterableObject } from '../../../util/vs/base/common/async';26import { CancellationToken } from '../../../util/vs/base/common/cancellation';27import { ResourceSet } from '../../../util/vs/base/common/map';28import { Schemas } from '../../../util/vs/base/common/network';29import { basename, isEqual } from '../../../util/vs/base/common/resources';30import { assertType, isObject } from '../../../util/vs/base/common/types';31import { isUriComponents, URI } from '../../../util/vs/base/common/uri';32import { generateUuid } from '../../../util/vs/base/common/uuid';33import { BrandedService, IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';34import { ChatRequestEditorData, Location, MarkdownString } from '../../../vscodeTypes';35import { CodeBlockInfo, CodeBlockProcessor, isCodeBlockWithResource } from '../../codeBlocks/node/codeBlockProcessor';36import { ICommandService } from '../../commands/node/commandService';37import { Intent } from '../../common/constants';38import { GenericInlineIntentInvocation } from '../../context/node/resolvers/genericInlineIntentInvocation';39import { ChatVariablesCollection, InstructionFileIdPrefix, isInstructionFile } from '../../prompt/common/chatVariablesCollection';40import { CodeBlock, Conversation, Turn } from '../../prompt/common/conversation';41import { IBuildPromptContext, InternalToolReference, IWorkingSet, IWorkingSetEntry, WorkingSetEntryState } from '../../prompt/common/intents';42import { ChatTelemetryBuilder } from '../../prompt/node/chatParticipantTelemetry';43import { CodebaseToolCallingLoop } from '../../prompt/node/codebaseToolCalling';44import { IntentInvocationMetadata } from '../../prompt/node/conversation';45import { DefaultIntentRequestHandler, IDefaultIntentRequestHandlerOptions } from '../../prompt/node/defaultIntentRequestHandler';46import { IDocumentContext } from '../../prompt/node/documentContext';47import { EditStrategy } from '../../prompt/node/editGeneration';48import { IBuildPromptResult, IIntent, IIntentInvocation, IIntentInvocationContext, IntentLinkificationOptions, IResponseProcessorContext } from '../../prompt/node/intents';49import { reportCitations } from '../../prompt/node/pseudoStartStopConversationCallback';50import { PromptRenderer, renderPromptElement } from '../../prompts/node/base/promptRenderer';51import { ICodeMapperService, IMapCodeRequest, IMapCodeResult } from '../../prompts/node/codeMapper/codeMapperService';52import { ChatToolReferences } from '../../prompts/node/panel/chatVariables';53import { EXISTING_CODE_MARKER } from '../../prompts/node/panel/codeBlockFormattingRules';54import { EditCodePrompt } from '../../prompts/node/panel/editCodePrompt';55import { ToolCallResultWrapper, ToolResultMetadata } from '../../prompts/node/panel/toolCalling';56import { getToolName, ToolName } from '../../tools/common/toolNames';57import { IToolsService } from '../../tools/common/toolsService';58import { CodebaseTool } from '../../tools/node/codebaseTool';59import { sendEditNotebookTelemetry } from '../../tools/node/editNotebookTool';60import { EditCodeStep, EditCodeStepTurnMetaData, PreviousEditCodeStep } from './editCodeStep';616263type IntentInvocationCtor<T extends BrandedService[]> = {64new(65intent: IIntent,66location: ChatLocation,67endpoint: IChatEndpoint,68request: vscode.ChatRequest,69intentOptions: EditCodeIntentOptions,70...args: T[]71): EditCodeIntentInvocation;72};7374export interface EditCodeIntentOptions extends EditCodeIntentInvocationOptions {75intentInvocation: IntentInvocationCtor<any>;76}7778export interface EditCodeIntentInvocationOptions {79processCodeblocks: boolean;80}8182export class EditCodeIntent implements IIntent {8384static readonly ID: Intent = Intent.Edit;8586readonly id: string = EditCodeIntent.ID;8788readonly description = l10n.t('Make changes to existing code');8990readonly locations = [ChatLocation.Editor, ChatLocation.Panel];9192constructor(93@IInstantiationService protected readonly instantiationService: IInstantiationService,94@IEndpointProvider protected readonly endpointProvider: IEndpointProvider,95@IConfigurationService protected readonly configurationService: IConfigurationService,96@IExperimentationService protected readonly expService: IExperimentationService,97@ICodeMapperService private readonly codeMapperService: ICodeMapperService,98@IWorkspaceService private readonly workspaceService: IWorkspaceService,99private readonly intentOptions: EditCodeIntentOptions = { processCodeblocks: true, intentInvocation: EditCodeIntentInvocation },100) { }101102private 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 }> {103const foundReferences: vscode.ChatPromptReference[] = [];104if ((this.configurationService.getConfig(ConfigKey.CodeSearchAgentEnabled) || this.configurationService.getConfig(ConfigKey.Advanced.CodeSearchAgentEnabled)) && request.toolReferences.find((r) => r.name === CodebaseTool.toolName && !isDirectorySemanticSearch(r))) {105106const latestTurn = conversation.getLatestTurn();107108const codebaseTool = this.instantiationService.createInstance(CodebaseToolCallingLoop, {109conversation,110toolCallLimit: 5,111request,112location,113});114115const toolCallLoopResult = await codebaseTool.run(stream, token);116117const toolCallResults = toolCallLoopResult.toolCallResults;118if (!toolCallLoopResult.chatResult?.errorDetails && toolCallResults) {119// TODO: do these new references need a lower priority?120const variables = new ChatVariablesCollection(request.references);121const endpoint = await this.endpointProvider.getChatEndpoint(request);122const { references } = await renderPromptElement(this.instantiationService, endpoint, ToolCallResultWrapper, { toolCallResults }, undefined, token);123foundReferences.push(...toNewChatReferences(variables, references));124// TODO: how should we splice in the assistant message?125conversation = new Conversation(conversation.sessionId, [...conversation.turns.slice(0, -1), new Turn(latestTurn.id, latestTurn.request, undefined, [], undefined, undefined, false, latestTurn.modeInstructions)]);126}127return { conversation, request: { ...request, references: [...request.references, ...foundReferences], toolReferences: request.toolReferences.filter((r) => r.name !== CodebaseTool.toolName) } };128}129return { conversation, request };130}131132private async _handleApplyConfirmedEdits(edits: (MappedEditsRequest & { chatRequestId: string; chatRequestModel: string })[], outputStream: vscode.ChatResponseStream, token: CancellationToken) {133const hydrateMappedEditsRequest = async (request: MappedEditsRequest): Promise<MappedEditsRequest> => {134const workingSet = await Promise.all(request.workingSet.map(async (ws): Promise<IWorkingSetEntry> => {135if (isTextDocumentSnapshotJSON(ws.document)) {136const document = await this.workspaceService.openTextDocument(ws.document.uri);137return { ...ws, document: TextDocumentSnapshot.fromJSON(document, ws.document) };138} else if (isNotebookDocumentSnapshotJSON(ws.document)) {139const document = await this.workspaceService.openNotebookDocument(ws.document.uri);140return { ...ws, document: NotebookDocumentSnapshot.fromJSON(document, ws.document) };141}142return ws;143}));144145return { ...request, workingSet };146};147148await Promise.all(edits.map(async requestDry => {149const request = await hydrateMappedEditsRequest(requestDry);150const uri = request.codeBlock.resource;151152outputStream.markdown(l10n.t`Applying edits to \`${this.workspaceService.asRelativePath(uri)}\`...\n\n`);153outputStream.textEdit(uri, []); // signal start of154155try {156return await this.codeMapperService.mapCode(request, outputStream, { chatRequestId: requestDry.chatRequestId, chatRequestModel: requestDry.chatRequestModel, chatRequestSource: `confirmed_edits_${this.id}` }, token);157} finally {158if (!token.isCancellationRequested) {159outputStream.textEdit(uri, true);160}161}162}));163}164165async 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> {166const applyEdits = request.acceptedConfirmationData?.filter(isEditsOkayConfirmation);167if (applyEdits?.length) {168await this._handleApplyConfirmedEdits(applyEdits.flatMap(e => ({ ...e.edits, chatRequestId: e.chatRequestId, chatRequestModel: request.model.id })), stream, token);169return {};170}171172({ conversation, request } = await this._handleCodesearch(conversation, request, location, stream, token, documentContext, chatTelemetry));173return this.instantiationService.createInstance(EditIntentRequestHandler, this, conversation, request, stream, token, documentContext, location, chatTelemetry, this.getIntentHandlerOptions(request), yieldRequested).getResult();174}175176protected getIntentHandlerOptions(_request: vscode.ChatRequest): IDefaultIntentRequestHandlerOptions | undefined {177return undefined;178}179180async invoke(invocationContext: IIntentInvocationContext) {181const { location, documentContext, request } = invocationContext;182const endpoint = await this.endpointProvider.getChatEndpoint(request);183184if (location === ChatLocation.Panel || location === ChatLocation.Notebook) {185return this.instantiationService.createInstance(this.intentOptions.intentInvocation, this, location, endpoint, request, this.intentOptions);186}187188if (!documentContext) {189throw new Error('Open a file to add code.');190}191return this.instantiationService.createInstance(GenericInlineIntentInvocation, this, location, endpoint, documentContext, EditStrategy.FallbackToReplaceRange);192}193}194195class EditIntentRequestHandler {196197constructor(198private readonly intent: EditCodeIntent,199private readonly conversation: Conversation,200private readonly request: vscode.ChatRequest,201private readonly stream: vscode.ChatResponseStream,202private readonly token: vscode.CancellationToken,203private readonly documentContext: IDocumentContext | undefined,204private readonly location: ChatLocation,205private readonly chatTelemetry: ChatTelemetryBuilder,206private readonly handlerOptions: IDefaultIntentRequestHandlerOptions | undefined,207private readonly yieldRequested: () => boolean,208@IInstantiationService private readonly instantiationService: IInstantiationService,209@ITelemetryService protected readonly telemetryService: ITelemetryService,210@IEditLogService private readonly editLogService: IEditLogService,211@IOTelService private readonly otelService: IOTelService,212) { }213214async getResult(): Promise<vscode.ChatResult> {215const actual = this.instantiationService.createInstance(216DefaultIntentRequestHandler,217this.intent,218this.conversation,219this.request,220this.stream,221this.token,222this.documentContext,223this.location,224this.chatTelemetry,225this.handlerOptions,226this.yieldRequested,227);228const result = await actual.getResult();229230// Record telemetry for the edit code blocks in an editing session231const turn = this.conversation.getLatestTurn();232const currentTurnMetadata = turn.getMetadata(IntentInvocationMetadata)?.value;233const editCodeStep = (currentTurnMetadata instanceof EditCodeIntentInvocation ? currentTurnMetadata._editCodeStep : undefined);234235if (editCodeStep?.telemetryInfo) {236/* __GDPR__237"panel.edit.codeblocks" : {238"owner": "joyceerhl",239"comment": "Records information about the proposed edit codeblocks in an editing session",240"conversationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Id for the current chat conversation." },241"outcome": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the request succeeded or failed." },242"workingSetCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of entries in the working set" },243"uniqueCodeblockUriCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of unique code block URIs" },244"codeblockCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of code blocks in the response" },245"codeblockWithUriCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of code blocks that had URIs" },246"codeblockWithElidedCodeCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of code blocks that had a ...existing code... comment" },247"shellCodeblockCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of shell code blocks in the response" },248"shellCodeblockWithUriCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of shell code blocks that had URIs" },249"shellCodeblockWithElidedCodeCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of shell code blocks that had a ...existing code... comment" },250"editStepCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The number of edit steps in the session so far" },251"sessionDuration": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The time since the session started" },252"intentId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The ID of the intent being executed" }253}254*/255this.telemetryService.sendMSFTTelemetryEvent('panel.edit.codeblocks', {256conversationId: this.conversation.sessionId,257outcome: Boolean(result.errorDetails) ? 'error' : 'success',258intentId: this.intent.id259}, {260workingSetCount: editCodeStep.workingSet.length,261uniqueCodeblockUriCount: editCodeStep.telemetryInfo.codeblockUris.size,262codeblockCount: editCodeStep.telemetryInfo.codeblockCount,263codeblockWithUriCount: editCodeStep.telemetryInfo.codeblockWithUriCount,264codeblockWithElidedCodeCount: editCodeStep.telemetryInfo.codeblockWithElidedCodeCount,265shellCodeblockCount: editCodeStep.telemetryInfo.shellCodeblockCount,266shellCodeblockWithUriCount: editCodeStep.telemetryInfo.shellCodeblockWithUriCount,267shellCodeblockWithElidedCodeCount: editCodeStep.telemetryInfo.shellCodeblockWithElidedCodeCount,268editStepCount: this.conversation.turns.length,269sessionDuration: Date.now() - turn.startTime,270});271GenAiMetrics.incrementAgentEditResponseCount(this.otelService, Boolean(result.errorDetails) ? 'error' : 'success');272}273274await this.editLogService.markCompleted(turn.id, result.errorDetails ? 'error' : 'success');275276return result;277}278}279280type MappedEditsRequest = IMapCodeRequest & { workingSet: IWorkingSet };281282const enum ConfirmationIds {283EditsOkay = '4e6e0e05-5dab-48d0-b2cd-6a14c8e3e8a2', // random string284}285286interface IEditsOkayConfirmation {287id: ConfirmationIds.EditsOkay;288chatRequestId: string;289edits: MappedEditsRequest;290}291292const makeEditsConfirmation = (chatRequestId: string, edits: MappedEditsRequest): IEditsOkayConfirmation => ({293id: ConfirmationIds.EditsOkay,294chatRequestId,295edits,296});297298const isEditsOkayConfirmation = (obj: unknown): obj is IEditsOkayConfirmation =>299isObject(obj) && (obj as IEditsOkayConfirmation).id === ConfirmationIds.EditsOkay;300301export class EditCodeIntentInvocation implements IIntentInvocation {302303public _editCodeStep: EditCodeStep | undefined = undefined;304305/**306* Stable codebase invocation so that their {@link InternalToolReference.id ids}307* are reused across multiple turns.308*/309protected stableToolReferences = this.request.toolReferences.map(InternalToolReference.from);310311public get linkification(): IntentLinkificationOptions {312return { disable: false };313}314315public readonly codeblocksRepresentEdits: boolean = true;316317constructor(318readonly intent: IIntent,319readonly location: ChatLocation,320readonly endpoint: IChatEndpoint,321protected readonly request: vscode.ChatRequest,322private readonly intentOptions: EditCodeIntentInvocationOptions,323@IInstantiationService protected readonly instantiationService: IInstantiationService,324@ICodeMapperService private readonly codeMapperService: ICodeMapperService,325@IEnvService private readonly envService: IEnvService,326@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,327@IEndpointProvider private readonly endpointProvider: IEndpointProvider,328@IWorkspaceService private readonly workspaceService: IWorkspaceService,329@IToolsService protected readonly toolsService: IToolsService,330@IConfigurationService protected readonly configurationService: IConfigurationService,331@IEditLogService private readonly editLogService: IEditLogService,332@ICommandService protected readonly commandService: ICommandService,333@ITelemetryService protected readonly telemetryService: ITelemetryService,334@INotebookService private readonly notebookService: INotebookService,335@IOTelService protected readonly otelService: IOTelService,336) { }337338getAvailableTools(): vscode.LanguageModelToolInformation[] | Promise<vscode.LanguageModelToolInformation[]> | undefined {339return undefined;340}341342async buildPrompt(343promptContext: IBuildPromptContext,344progress: vscode.Progress<vscode.ChatResponseReferencePart | vscode.ChatResponseProgressPart>,345token: vscode.CancellationToken346): Promise<IBuildPromptResult> {347348// Add any references from the codebase invocation to the request349const codebase = await this._getCodebaseReferences(promptContext, token);350351let variables = promptContext.chatVariables;352let toolReferences: vscode.ChatPromptReference[] = [];353if (codebase) {354toolReferences = toNewChatReferences(variables, codebase.references);355variables = new ChatVariablesCollection([...this.request.references, ...toolReferences]);356}357358if (this.request.location2 instanceof ChatRequestEditorData) {359const editorRequestReference: vscode.ChatPromptReference = {360id: '',361name: this.request.location2.document.fileName,362value: new Location(this.request.location2.document.uri, this.request.location2.wholeRange)363};364variables = new ChatVariablesCollection([...this.request.references, ...toolReferences, editorRequestReference]);365}366367368369const tools = await this.getAvailableTools();370const toolTokens = tools?.length ? await this.endpoint.acquireTokenizer().countToolTokens(tools) : 0;371const endpoint = toolTokens > 0 ? this.endpoint.cloneWithTokenOverride(Math.floor((this.endpoint.modelMaxPromptTokens - toolTokens) * 0.85)) : this.endpoint;372const { editCodeStep, chatVariables } = await EditCodeStep.create(this.instantiationService, promptContext.history, variables, endpoint);373this._editCodeStep = editCodeStep;374375const commandToolReferences: InternalToolReference[] = [];376let query = promptContext.query;377const command = this.request.command && this.commandService.getCommand(this.request.command, this.location);378if (command) {379if (command.toolEquivalent) {380commandToolReferences.push({381id: `${this.request.command}->${generateUuid()}`,382name: getToolName(command.toolEquivalent)383});384}385query = query ? `${command.details}.\n${query}` : command.details;386}387388// Reserve extra space when tools are involved due to token counting issues389const renderer = PromptRenderer.create(this.instantiationService, endpoint, EditCodePrompt, {390endpoint,391promptContext: {392...promptContext,393query,394chatVariables,395workingSet: editCodeStep.workingSet,396promptInstructions: editCodeStep.promptInstructions,397toolCallResults: { ...promptContext.toolCallResults, ...codebase?.toolCallResults },398tools: promptContext.tools && {399...promptContext.tools,400toolReferences: this.stableToolReferences.filter((r) => r.name !== ToolName.Codebase).concat(commandToolReferences),401},402},403location: this.location404});405const start = Date.now();406const result = await renderer.render(progress, token);407const duration = Date.now() - start;408this.sendPromptRenderTelemetry(duration);409const lastMessage = result.messages[result.messages.length - 1];410if (lastMessage.role === Raw.ChatRole.User) {411this._editCodeStep.setUserMessage(lastMessage);412}413414return {415...result,416// The codebase tool is not actually called/referenced in the edit prompt, so we need to417// merge its metadata so that its output is not lost and it's not called repeatedly every turn418// todo@connor4312/joycerhl: this seems a bit janky419metadata: codebase ? mergeMetadata(result.metadata, codebase.metadatas) : result.metadata,420// Don't report file references that came in via chat variables in an editing session, unless they have warnings,421// because they are already displayed as part of the working set422references: result.references.filter((ref) => this.shouldKeepReference(editCodeStep, ref, toolReferences, chatVariables)),423};424}425426private sendPromptRenderTelemetry(duration: number) {427/* __GDPR__428"editCodeIntent.promptRender" : {429"owner": "roblourens",430"comment": "Understanding the performance of the edit code intent rendering",431"promptRenderDurationIncludingRunningTools": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Duration of the prompt rendering, includes running tools" },432"isAgentMode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Whether the prompt was for agent mode" }433}434*/435this.telemetryService.sendMSFTTelemetryEvent('editCodeIntent.promptRender', {436}, {437promptRenderDurationIncludingRunningTools: duration,438isAgentMode: this.intent.id === Intent.Agent ? 1 : 0,439});440}441442protected async _getCodebaseReferences(443promptContext: IBuildPromptContext,444token: vscode.CancellationToken,445) {446const codebaseTools = this.stableToolReferences.filter(t => t.name === ToolName.Codebase);447if (!codebaseTools.length) {448return;449}450451const history = promptContext.history;452const endpoint = await this.endpointProvider.getChatEndpoint(this.request);453454const { 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);455return { toolCallResults: getToolCallResults(metadatas), references, metadatas };456}457458private shouldKeepReference(editCodeStep: EditCodeStep, ref: PromptReference, toolReferences: vscode.ChatPromptReference[], chatVariables: ChatVariablesCollection): boolean {459if (ref.options?.status && ref.options?.status?.kind !== ChatResponseReferencePartStatusKind.Complete) {460// Always show references for files which have warnings461return true;462}463const uri = getUriOfReference(ref);464if (!uri) {465// This reference doesn't have an URI466return true;467}468if (toolReferences.find(entry => (URI.isUri(entry.value) && isEqual(entry.value, uri) || (isLocation(entry.value) && isEqual(entry.value.uri, uri))))) {469// If this reference came in via resolving #codebase, we should show it470// TODO@joyceerhl if this reference is subsequently modified and joins the working set, should we suppress it again in the UI?471return true;472}473const PROMPT_INSTRUCTION_ROOT_PREFIX = `${InstructionFileIdPrefix}.root`;474const promptInstruction = chatVariables.find((variable) => isInstructionFile(variable) && URI.isUri(variable.value) && isEqual(variable.value, uri));475if (promptInstruction) {476// Report references for root prompt instruction files and not their children477return promptInstruction.reference.id.startsWith(PROMPT_INSTRUCTION_ROOT_PREFIX);478}479const workingSetEntry = editCodeStep.workingSet.find(entry => isEqual(entry.document.uri, uri));480if (!workingSetEntry) {481// This reference wasn't part of the working set482return true;483}484return false;485}486487private async shouldConfirmBeforeFileEdits(uri: URI) {488for (const tool of this.request.toolReferences) {489const ownTool = this.toolsService.getCopilotTool(tool.name as ToolName);490if (!ownTool) {491continue;492}493494const filter = await ownTool.filterEdits?.(uri);495if (filter) {496return filter;497}498}499500return undefined;501}502503async processResponse?(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<vscode.ChatResult> {504assertType(this._editCodeStep);505506const codeMapperWork: Promise<IMapCodeResult | undefined>[] = [];507508const allReceivedMarkdown: string[] = [];509510const textStream = (511AsyncIterableObject512.map(inputStream, part => {513reportCitations(part.delta, outputStream);514return part.delta.text;515})516.map(piece => {517allReceivedMarkdown.push(piece);518return piece;519})520);521const remoteName = this.envService.remoteName;522const createUriFromResponsePath = this._createUriFromResponsePath.bind(this);523if (this.intentOptions.processCodeblocks) {524for await (const codeBlock of getCodeBlocksFromResponse(textStream, outputStream, createUriFromResponsePath, remoteName)) {525526if (token.isCancellationRequested) {527break;528}529530const isShellScript = codeBlock.language === 'sh';531if (isCodeBlockWithResource(codeBlock)) {532this._editCodeStep.telemetryInfo.codeblockUris.add(codeBlock.resource);533this._editCodeStep.telemetryInfo.codeblockWithUriCount += 1;534if (isShellScript) {535this._editCodeStep.telemetryInfo.shellCodeblockWithUriCount += 1;536}537538// The model proposed an edit for this URI539this._editCodeStep.setWorkingSetEntryState(codeBlock.resource, WorkingSetEntryState.Undecided);540541if (codeBlock.code.includes(EXISTING_CODE_MARKER)) {542this._editCodeStep.telemetryInfo.codeblockWithElidedCodeCount += 1;543if (isShellScript) {544this._editCodeStep.telemetryInfo.shellCodeblockWithElidedCodeCount += 1;545}546}547const request: MappedEditsRequest = {548workingSet: [...this._editCodeStep.workingSet],549codeBlock550};551552const confirmEdits = await this.shouldConfirmBeforeFileEdits(codeBlock.resource);553if (confirmEdits) {554outputStream.confirmation(confirmEdits.title, confirmEdits.message, makeEditsConfirmation(context.turn.id, request));555continue;556}557const isNotebookDocument = this.notebookService.hasSupportedNotebooks(codeBlock.resource);558if (isNotebookDocument) {559outputStream.notebookEdit(codeBlock.resource, []);560} else {561outputStream.textEdit(codeBlock.resource, []); // signal start562}563const task = this.codeMapperService.mapCode(request, outputStream, {564chatRequestId: context.turn.id,565chatRequestModel: this.endpoint.model,566chatSessionId: context.chatSessionId,567chatRequestSource: `${this.intent.id}_${ChatLocation.toString(this.location)}`,568}, token).finally(() => {569if (!token.isCancellationRequested) {570// signal being done with this uri571if (isNotebookDocument) {572outputStream.notebookEdit(codeBlock.resource, true);573sendEditNotebookTelemetry(this.telemetryService, undefined, 'editCodeIntent', codeBlock.resource, this.request.id, undefined, this.endpoint);574} else {575outputStream.textEdit(codeBlock.resource, true);576}577}578});579codeMapperWork.push(task);580} else {581this._editCodeStep.telemetryInfo.codeblockCount += 1;582if (isShellScript) {583this._editCodeStep.telemetryInfo.shellCodeblockCount += 1;584}585}586}587} else {588for await (const part of textStream) {589if (token.isCancellationRequested) {590break;591}592593outputStream.markdown(part);594}595}596597const results = await Promise.all(codeMapperWork);598for (const result of results) {599if (!result) {600context.addAnnotations([{ severity: 'error', label: 'cancelled', message: 'CodeMapper cancelled' }]);601} else if (result.annotations) {602context.addAnnotations(result.annotations);603}604}605for (const result of results) {606if (result && result.errorDetails) {607return {608errorDetails: result.errorDetails609};610}611}612613const response = allReceivedMarkdown.join('');614this._editCodeStep.setAssistantReply(response);615this.editLogService.logEditChatRequest(context.turn.id, context.messages, response);616617const historyEditCodeStep = PreviousEditCodeStep.fromEditCodeStep(this._editCodeStep);618context.turn.setMetadata(new EditCodeStepTurnMetaData(historyEditCodeStep));619return {620metadata: historyEditCodeStep.toChatResultMetaData(),621};622}623624private _createUriFromResponsePath(path: string): URI | undefined {625assertType(this._editCodeStep);626627// ok to modify entries from the working set628for (const entry of this._editCodeStep.workingSet) {629if (this.promptPathRepresentationService.getFilePath(entry.document.uri) === path) {630return entry.document.uri;631}632}633634const uri = this.promptPathRepresentationService.resolveFilePath(path, this._editCodeStep.getPredominantScheme());635if (!uri) {636return undefined;637}638639// ok to make changes in the workspace640if (this.workspaceService.getWorkspaceFolder(uri)) {641return uri;642}643if (uri.scheme === Schemas.file || uri.scheme === Schemas.vscodeRemote) {644// do not directly modify files outside the workspace. Create an untitled file instead, let the user save when ok645return URI.from({ scheme: Schemas.untitled, path: uri.path });646}647return uri;648}649}650651652const fileHeadingLineStart = '### ';653654export function getCodeBlocksFromResponse(textStream: AsyncIterable<string>, outputStream: vscode.ChatResponseStream, createUriFromResponsePath: (p: string) => URI | undefined, remoteName: string | undefined): AsyncIterable<CodeBlock> {655656return new AsyncIterableObject<CodeBlock>(async (emitter) => {657658let currentCodeBlock: CodeBlockInfo | undefined = undefined;659const codeblockProcessor = new CodeBlockProcessor(660path => {661return createUriFromResponsePath(path);662},663(markdown: MarkdownString, codeBlockInfo: CodeBlockInfo | undefined, vulnerabilities: vscode.ChatVulnerability[] | undefined) => {664if (vulnerabilities) {665outputStream.markdownWithVulnerabilities(markdown, vulnerabilities);666} else {667outputStream.markdown(markdown);668}669if (codeBlockInfo && codeBlockInfo.resource && codeBlockInfo !== currentCodeBlock) {670// first time we see this code block671currentCodeBlock = codeBlockInfo;672outputStream.codeblockUri(codeBlockInfo.resource, true);673}674},675codeBlock => {676emitter.emitOne(codeBlock);677},678{679matchesLineStart(linePart, inCodeBlock) {680return !inCodeBlock && linePart.startsWith(fileHeadingLineStart.substring(0, linePart.length));681},682process(line, inCodeBlock) {683const header = line.value.substring(fileHeadingLineStart.length).trim(); // remove the ### and trim684let fileUri = createUriFromResponsePath(header);685if (fileUri) {686if (remoteName) {687fileUri = URI.from({ scheme: Schemas.vscodeRemote, authority: remoteName, path: fileUri.path });688}689const headerLine = `### [${basename(fileUri)}](${fileUri.toString()})\n`;690return new MarkdownString(headerLine);691} else {692// likely not a file path, just keep the original line693return line;694}695},696}697698);699700for await (const text of textStream) {701codeblockProcessor.processMarkdown(text);702}703codeblockProcessor.flush();704});705}706707function getUriOfReference(ref: PromptReference): vscode.Uri | undefined {708if ('variableName' in ref.anchor) {709return _extractUri(ref.anchor.value);710}711return _extractUri(ref.anchor);712}713714function _extractUri(something: vscode.Uri | vscode.Location | undefined): vscode.Uri | undefined {715if (isLocation(something)) {716return something.uri;717}718return something;719}720721export function toNewChatReferences(chatVariables: ChatVariablesCollection, promptReferences: PromptReference[]): vscode.ChatPromptReference[] {722const toolReferences: vscode.ChatPromptReference[] = [];723const seen = new ResourceSet();724725for (const reference of promptReferences) {726if (isLocation(reference.anchor)) {727const uri = reference.anchor.uri;728if (seen.has(uri) || chatVariables.find((v) => URI.isUri(v.value) && isEqual(v.value, uri))) {729continue;730}731seen.add(uri);732toolReferences.push({ id: uri.toString(), name: uri.toString(), value: reference.anchor });733} else if (isUriComponents(reference.anchor) || URI.isUri(reference.anchor)) {734const uri = URI.revive(reference.anchor);735if (seen.has(uri) || chatVariables.find((v) => URI.isUri(v.value) && isEqual(v.value, uri))) {736continue;737}738seen.add(uri);739toolReferences.push({ id: uri.toString(), name: uri.toString(), value: uri });740}741}742743return toolReferences;744}745746function getToolCallResults(metadatas: MetadataMap) {747const toolCallResults: Record<string, vscode.LanguageModelToolResult2> = {};748for (const metadata of metadatas.getAll(ToolResultMetadata)) {749toolCallResults[metadata.toolCallId] = metadata.result;750}751752return toolCallResults;753}754755export function mergeMetadata(m1: MetadataMap, m2: MetadataMap): MetadataMap {756return {757get: key => m1.get(key) ?? m2.get(key),758getAll: key => m1.getAll(key).concat(m2.getAll(key)),759};760}761762function isDirectorySemanticSearch(toolCall: vscode.ChatLanguageModelToolReference) {763if (toolCall.name !== ToolName.Codebase) {764return false;765}766767const input = (toolCall as any).input;768if (!input) {769return false;770}771772const scopedDirectories = input.scopedDirectories;773if (!Array.isArray(scopedDirectories)) {774return false;775}776777return scopedDirectories.length > 0;778}779780781