Path: blob/main/extensions/copilot/src/extension/prompts/node/panel/editCodePrompt.tsx
13405 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 { AssistantMessage, BasePromptElementProps, Chunk, PrioritizedList, PromptElement, PromptReference, PromptSizing, SystemMessage, TextChunk, UserMessage } from '@vscode/prompt-tsx';7import { ChatLocation } from '../../../../platform/chat/common/commonTypes';8import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';9import { NotebookDocumentSnapshot } from '../../../../platform/editing/common/notebookDocumentSnapshot';10import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';11import { modelPrefersInstructionsAfterHistory } from '../../../../platform/endpoint/common/chatModelCapabilities';12import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';13import { IChatEndpoint } from '../../../../platform/networking/common/networking';14import { IPromptPathRepresentationService } from '../../../../platform/prompts/common/promptPathRepresentationService';15import { IExperimentationService } from '../../../../platform/telemetry/common/nullExperimentationService';16import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';17import { filepathCodeBlockMarker } from '../../../../util/common/markdown';18import { isLocation, isUri } from '../../../../util/common/types';19import { ResourceMap } from '../../../../util/vs/base/common/map';20import { Schemas } from '../../../../util/vs/base/common/network';21import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';22import { Range, Uri } from '../../../../vscodeTypes';23import { GenericBasePromptElementProps } from '../../../context/node/resolvers/genericPanelIntentInvocation';24import { IEditStepBuildPromptContext, PreviousEditCodeStep } from '../../../intents/node/editCodeStep';25import { ChatVariablesCollection } from '../../../prompt/common/chatVariablesCollection';26import { Turn } from '../../../prompt/common/conversation';27import { INotebookWorkingSetEntry, isTextDocumentWorkingSetEntry, ITextDocumentWorkingSetEntry, IWorkingSet, WorkingSetEntryState } from '../../../prompt/common/intents';28import { CompositeElement } from '../base/common';29import { CopilotIdentityRules } from '../base/copilotIdentity';30import { InstructionMessage } from '../base/instructionMessage';31import { ResponseTranslationRules } from '../base/responseTranslationRules';32import { LegacySafetyRules } from '../base/safetyRules';33import { Tag } from '../base/tag';34import { DocumentSummarizer, NotebookDocumentSummarizer } from '../inline/summarizedDocument/summarizeDocumentHelpers';35import { ChatToolReferences, ChatVariables, UserQuery } from './chatVariables';36import { EXISTING_CODE_MARKER } from './codeBlockFormattingRules';37import { CustomInstructions } from './customInstructions';38import { fileVariableCostFn } from './fileVariable';39import { NotebookFormat, NotebookReminderInstructions } from './notebookEditCodePrompt';40import { ProjectLabels } from './projectLabels';41import { CodeBlock, ExampleCodeBlock } from './safeElements';42import { ChatToolCalls } from './toolCalling';4344export interface EditCodePromptProps extends GenericBasePromptElementProps {45readonly promptContext: IEditStepBuildPromptContext;46readonly endpoint: IChatEndpoint;47readonly location: ChatLocation;48}4950export class EditCodePrompt extends PromptElement<EditCodePromptProps> {51constructor(52props: EditCodePromptProps,53@IConfigurationService private readonly configurationService: IConfigurationService,54@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,55) {56super(props);57}58async render(state: void, sizing: PromptSizing) {59const tsExampleFilePath = '/Users/someone/proj01/example.ts';6061const instructionsAfterHistory = modelPrefersInstructionsAfterHistory(this.props.endpoint.family);62const hasFilesInWorkingSet = this.props.promptContext.workingSet.length > 0;63const instructions = <InstructionMessage priority={900}>64{hasFilesInWorkingSet65? <>The user has a request for modifying one or more files.<br /></>66: <>If the user asks a question, then answer it.<br />67If you need to change existing files and it's not clear which files should be changed, then refuse and answer with "Please add the files to be modified to the working set{(this.configurationService.getConfig(ConfigKey.CodeSearchAgentEnabled) || this.configurationService.getConfig(ConfigKey.Advanced.CodeSearchAgentEnabled)) ? ', or use `#codebase` in your request to automatically discover working set files.' : ''}".<br />68The only exception is if you need to create new files. In that case, follow the following instructions.<br /></>}691. Please come up with a solution that you first describe step-by-step.<br />702. Group your changes by file. Use the file path as the header.<br />713. For each file, give a short summary of what needs to be changed followed by a code block that contains the code changes.<br />724. The code block should start with four backticks followed by the language.<br />735. On the first line of the code block add a comment containing the filepath. This includes Markdown code blocks.<br />746. Use a single code block per file that needs to be modified, even if there are multiple changes for a file.<br />757. The user is very smart and can understand how to merge your code blocks into their files, you just need to provide minimal hints.<br />768. Avoid repeating existing code, instead use comments to represent regions of unchanged code. The user prefers that you are as concise as possible. For example: <br />77<ExampleCodeBlock languageId='languageId' examplePath={'/path/to/file'} includeFilepath={true} minNumberOfBackticks={4}78code={79[80`// ${EXISTING_CODE_MARKER}`,81`{ changed code }`,82`// ${EXISTING_CODE_MARKER}`,83`{ changed code }`,84`// ${EXISTING_CODE_MARKER}`85].join('\n')86}87/><br />8889<br />90<ResponseTranslationRules />91Here is an example of how you should format a code block belonging to the file example.ts in your response:<br />92<Tag name='example'>93### {this.promptPathRepresentationService.getExampleFilePath(tsExampleFilePath)}<br />94<br />95Add a new property 'age' and a new method 'getAge' to the class Person.<br />96<br />97<ExampleCodeBlock languageId='typescript' examplePath={tsExampleFilePath} includeFilepath={true} minNumberOfBackticks={4}98code={99[100`class Person {`,101` // ${EXISTING_CODE_MARKER}`,102` age: number;`,103` // ${EXISTING_CODE_MARKER}`,104` getAge() {`,105` return this.age;`,106` }`,107`}`,108].join('\n')109}110/><br />111</Tag>112</InstructionMessage>;113114return (115<>116<SystemMessage priority={1000}>117You are an AI programming assistant.<br />118<CopilotIdentityRules />119<LegacySafetyRules />120</SystemMessage>121{instructionsAfterHistory ? undefined : instructions}122<EditCodeConversationHistory flexGrow={1} priority={700} workingSet={this.props.promptContext.workingSet} history={this.props.promptContext.history} promptInstructions={this.props.promptContext.promptInstructions} chatVariables={this.props.promptContext.chatVariables} />123{instructionsAfterHistory ? instructions : undefined}124<EditCodeUserMessage flexGrow={2} priority={900} {...this.props} />125<ChatToolCalls priority={899} flexGrow={3} promptContext={this.props.promptContext} toolCallRounds={this.props.promptContext.toolCallRounds} toolCallResults={this.props.promptContext.toolCallResults} />126</>127);128}129}130131interface EditCodeReadonlyInstructionsProps extends BasePromptElementProps {132readonly chatVariables: ChatVariablesCollection;133readonly workingSet: IWorkingSet;134}135136export class EditCodeReadonlyInstructions extends PromptElement<EditCodeReadonlyInstructionsProps> {137constructor(138props: EditCodeReadonlyInstructionsProps,139@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,140) {141super(props);142}143144override render() {145const { readonlyUris } = this;146147if (!readonlyUris.length) {148return <></>;149}150151return <TextChunk>152{'<fileRestrictions>'}<br />153The following files are readonly. Making edits to any of these file paths is FORBIDDEN. If you cannot accomplish the task without editing the files, briefly explain why, but NEVER edit any of these files:<br />154{readonlyUris.map(uri => `\t- ${this.promptPathRepresentationService.getFilePath(uri)}`).join('\n')}<br />155{'</fileRestrictions>'}156</TextChunk>;157}158159/**160* List of {@link Uri}s for all `readonly` variables, if any.161*/162private get readonlyUris(): Uri[] {163164// get list of URIs for readonly variables inside the working set165const readonlyUris = [];166for (const entry of this.props.workingSet) {167if (entry.isMarkedReadonly) {168readonlyUris.push(entry.document.uri);169}170}171for (const variable of this.props.chatVariables) {172if (variable.isMarkedReadonly) {173if (isUri(variable.value)) {174readonlyUris.push(variable.value);175} else if (isLocation(variable.value)) {176readonlyUris.push(variable.value.uri);177}178}179}180return readonlyUris;181}182}183184interface EditCodeConversationHistoryProps extends BasePromptElementProps {185readonly workingSet: IWorkingSet;186readonly promptInstructions: readonly TextDocumentSnapshot[];187readonly chatVariables: ChatVariablesCollection;188readonly history: readonly Turn[];189readonly priority: number;190}191192class EditCodeConversationHistory extends PromptElement<EditCodeConversationHistoryProps> {193194constructor(195props: EditCodeConversationHistoryProps,196@IPromptPathRepresentationService private readonly _promptPathRepresentationService: IPromptPathRepresentationService197) {198super(props);199}200201202override async render(state: void, sizing: PromptSizing) {203// Here we will keep track of which [file,version] pairs that are already in the prompt204const includedFilesAtVersions = new ResourceMap<number[]>();205206// Populate with the current state207for (const entry of this.props.workingSet) {208includedFilesAtVersions.set(entry.document.uri, [entry.document.version]);209}210211// Ditto for prompt instruction files212const includedPromptInstructions = new ResourceMap<number[]>();213for (const promptInstruction of this.props.promptInstructions) {214includedPromptInstructions.set(promptInstruction.uri, [promptInstruction.version]);215}216217const history: (UserMessage | AssistantMessage)[] = [];218for (const turn of this.props.history) {219const editCodeStep = PreviousEditCodeStep.fromTurn(turn);220if (editCodeStep) {221history.push(this._renderUserMessageWithoutFiles(editCodeStep, includedFilesAtVersions, includedPromptInstructions));222history.push(this._renderAssistantMessageWithoutFileTags(editCodeStep.response));223}224}225226return (<PrioritizedList priority={this.props.priority} descending={false}>{history}</PrioritizedList>);227}228229private _renderAssistantMessageWithoutFileTags(message: string): AssistantMessage {230message = message.replace(/<\/?file>/g, '');231return (232<AssistantMessage>{message}</AssistantMessage>233);234}235236private _renderUserMessageWithoutFiles(editCodeStep: PreviousEditCodeStep, includedFilesAtVersion: ResourceMap<number[]>, includedPromptInstructions: ResourceMap<number[]>): UserMessage {237const filesToRemove: Uri[] = [];238for (const entry of editCodeStep.workingSet) {239const versions = includedFilesAtVersion.get(entry.document.uri) ?? [];240const isAlreadyIncluded = versions.some(version => version === entry.document.version);241if (isAlreadyIncluded) {242filesToRemove.push(entry.document.uri);243} else {244versions.push(entry.document.version);245includedFilesAtVersion.set(entry.document.uri, versions);246}247}248249const promptInstructionsToRemove: Uri[] = [];250for (const entry of editCodeStep.promptInstructions) {251const versions = includedPromptInstructions.get(entry.document.uri) ?? [];252const isAlreadyIncluded = versions.some(version => version === entry.document.version);253if (isAlreadyIncluded) {254promptInstructionsToRemove.push(entry.document.uri);255}256}257258let userMessage = this._removePromptInstructionsFromPastUserMessage(editCodeStep.request, promptInstructionsToRemove);259userMessage = this._removeFilesFromPastUserMessage(userMessage, filesToRemove);260userMessage = this._removeReminders(userMessage);261return (262<UserMessage>{userMessage}</UserMessage>263);264}265266private _removePromptInstructionsFromPastUserMessage(userMessage: string, shouldRemove: Uri[]) {267const interestingFilePaths = shouldRemove.map(uri => this._promptPathRepresentationService.getFilePath(uri));268return userMessage.replace(/<instructions>[\s\S]*?<\/instructions>/g, (match) => {269if (interestingFilePaths.some(path => match.includes(path))) {270return '';271}272return match;273});274}275276private _removeFilesFromPastUserMessage(userMessage: string, shouldRemove: Uri[]) {277const interestingFilePaths = shouldRemove.map(uri => `${filepathCodeBlockMarker} ${this._promptPathRepresentationService.getFilePath(uri)}`);278return userMessage.replace(/<file(-selection)?>[\s\S]*?<\/file(-selection)?>/g, (match) => {279if (interestingFilePaths.some(path => match.includes(path))) {280return '';281}282return match;283});284}285286private _removeReminders(userMessage: string) {287return userMessage.replace(/^<reminder>[\s\S]*?^<\/reminder>/gm, (match) => {288return '';289});290}291}292293export class EditCodeUserMessage extends PromptElement<EditCodePromptProps> {294constructor(295props: EditCodePromptProps,296@IExperimentationService private readonly experimentationService: IExperimentationService,297@IConfigurationService private readonly _configurationService: IConfigurationService,298) {299super(props);300}301302async render(state: void, sizing: PromptSizing) {303const { query, chatVariables, workingSet } = this.props.promptContext;304const useProjectLabels = this._configurationService.getExperimentBasedConfig(ConfigKey.Advanced.ProjectLabelsChat, this.experimentationService);305return (306<>307<UserMessage>308{useProjectLabels && <ProjectLabels flexGrow={1} priority={600} />}309<CustomInstructions flexGrow={6} priority={750} languageId={undefined} chatVariables={chatVariables} />310<NotebookFormat flexGrow={5} priority={810} chatVariables={workingSet} query={query} />311<ChatToolReferences flexGrow={4} priority={898} promptContext={this.props.promptContext} documentContext={this.props.documentContext} />312<ChatVariables flexGrow={3} priority={898} chatVariables={chatVariables} />313<WorkingSet flexGrow={3} flexReserve={sizing.tokenBudget * 0.8} priority={810} workingSet={workingSet} /><br />314<Tag name='reminder' flexGrow={2} priority={899} >315Avoid repeating existing code, instead use a line comment with `{EXISTING_CODE_MARKER}` to represent regions of unchanged code.<br />316The code block for each file being edited must start with a comment containing the filepath. This includes Markdown code blocks.<br />317For existing files, make sure the filepath exactly matches the filepath of the original file.<br />318<NotebookReminderInstructions chatVariables={chatVariables} query={query} />319<NewFilesLocationHint />320</Tag>321{query && <Tag name='prompt'><UserQuery flexGrow={7} priority={900} chatVariables={chatVariables} query={query} /></Tag>}322<EditCodeReadonlyInstructions chatVariables={chatVariables} workingSet={workingSet} />323</UserMessage>324</>325);326}327}328329interface WorkingSetPromptProps extends BasePromptElementProps {330readonly workingSet: IWorkingSet;331}332333export class WorkingSet extends PromptElement<WorkingSetPromptProps> {334public override render(state: void, sizing: PromptSizing) {335const { workingSet } = this.props;336return (337workingSet.length ?338<>339The user has provided the following files as input. Always make changes to these files unless the user asks to create a new file.<br />340Untitled files are files that are not yet named. Make changes to them like regular files.<br />341{workingSet.map((entry, index) => (342isTextDocumentWorkingSetEntry(entry) ?343<TextDocumentWorkingSetEntry entry={entry} flexGrow={index} /> :344<NotebookWorkingSetEntry entry={entry} flexGrow={index} />345))}346</> :347<></>348);349}350}351352export class NewFilesLocationHint extends PromptElement {353constructor(354props: BasePromptElementProps,355@IWorkspaceService private readonly _workspaceService: IWorkspaceService,356@IPromptPathRepresentationService private readonly _promptPathRepresentationService: IPromptPathRepresentationService,357) {358super(props);359}360361public override render(state: void, sizing: PromptSizing) {362const workspaceFolders = this._workspaceService.getWorkspaceFolders();363if (workspaceFolders.length === 1) {364return <>When suggesting to create new files, pick a location inside `{this._promptPathRepresentationService.getFilePath(workspaceFolders[0])}`.</>;365} else if (workspaceFolders.length > 0) {366return <>When suggesting to create new files, pick a location inside one of these root folders: {workspaceFolders.map(f => `${this._promptPathRepresentationService.getFilePath(f)}`).join(', ')}.</>;367} else {368const untitledRoot = Uri.from({ scheme: Schemas.untitled, authority: 'untitled' });369return <>When suggesting to create new files, pick a location inside `{this._promptPathRepresentationService.getFilePath(untitledRoot)}`.</>;370}371}372}373374interface TextDocumentWorkingSetEntryPromptProps extends BasePromptElementProps {375readonly entry: ITextDocumentWorkingSetEntry;376}377378class TextDocumentWorkingSetEntry extends PromptElement<TextDocumentWorkingSetEntryPromptProps> {379constructor(380props: TextDocumentWorkingSetEntryPromptProps,381@IIgnoreService private readonly _ignoreService: IIgnoreService,382@IInstantiationService private readonly instantiationService: IInstantiationService,383) {384super(props);385}386387async render(state: void, sizing: PromptSizing) {388const { document, range: selection, state: workingSetEntryState } = this.props.entry;389390const isIgnored = await this._ignoreService.isCopilotIgnored(document.uri);391if (isIgnored) {392return <ignoredFiles value={[document.uri]} />;393}394395const s = this.instantiationService.createInstance(DocumentSummarizer);396const summarized = await s.summarizeDocument(document, undefined, selection, sizing.tokenBudget, {397costFnOverride: fileVariableCostFn,398});399400const promptReferenceOptions = !summarized.isOriginal401? { status: { description: l10n.t('Part of this file was not sent to the model due to context window limitations. Try attaching specific selections from your file instead.'), kind: 2 } }402: undefined;403404let userActionStateFragment = '';405if (workingSetEntryState === WorkingSetEntryState.Accepted) {406userActionStateFragment = 'I applied your suggestions for this file and accepted them. Here is the updated file:';407} else if (workingSetEntryState === WorkingSetEntryState.Rejected) {408userActionStateFragment = 'I considered your suggestions for this file but rejected them. Here is the file:';409} else if (workingSetEntryState === WorkingSetEntryState.Undecided) {410userActionStateFragment = 'I applied your suggestions for this file but haven\'t decided yet if I accept or reject them. Here is the updated file:';411}412413return (414<CompositeElement priority={this.props.priority}>415<Chunk priority={2}>416<Tag name='file'>417{418userActionStateFragment && <>419<br />420<status>421{userActionStateFragment}422</status>423<br />424</>425}426<CodeBlock includeFilepath={true} languageId={document.languageId} uri={document.uri} references={[new PromptReference(document.uri, undefined, promptReferenceOptions)]} code={summarized.text} />427</Tag>428</Chunk>429{!!selection && <FileSelection document={document} selection={selection} priority={1} />}430</CompositeElement>431);432}433}434435interface NotebookWorkingSetEntryPromptProps extends BasePromptElementProps {436readonly entry: INotebookWorkingSetEntry;437}438439class NotebookWorkingSetEntry extends PromptElement<NotebookWorkingSetEntryPromptProps> {440constructor(441props: NotebookWorkingSetEntryPromptProps,442@IIgnoreService private readonly _ignoreService: IIgnoreService,443@IInstantiationService private readonly instantiationService: IInstantiationService,444) {445super(props);446}447448async render(state: void, sizing: PromptSizing) {449const { document, range: selection, state: workingSetEntryState } = this.props.entry;450451const isIgnored = await this._ignoreService.isCopilotIgnored(document.uri);452if (isIgnored) {453return <ignoredFiles value={[document.uri]} />;454}455456// TODO@rebornix ensure notebook is open457const s = this.instantiationService.createInstance(NotebookDocumentSummarizer);458const summarized = await s.summarizeDocument(document, undefined, selection, sizing.tokenBudget, {459costFnOverride: fileVariableCostFn,460});461462const promptReferenceOptions = !summarized.isOriginal463? { status: { description: l10n.t('Part of this file was not sent to the model due to context window limitations. Try attaching specific selections from your file instead.'), kind: 2 } }464: undefined;465466let userActionStateFragment = '';467if (workingSetEntryState === WorkingSetEntryState.Accepted) {468userActionStateFragment = 'I applied your suggestions for this file and accepted them. Here is the updated file:';469} else if (workingSetEntryState === WorkingSetEntryState.Rejected) {470userActionStateFragment = 'I considered your suggestions for this file but rejected them. Here is the file:';471} else if (workingSetEntryState === WorkingSetEntryState.Undecided) {472userActionStateFragment = 'I applied your suggestions for this file but haven\'t decided yet if I accept or reject them. Here is the updated file:';473}474// Kernel variables are useful only if we're in inline chat mode.475// This is the logic we used to have with inline chat for notebooks.476return (477<CompositeElement priority={this.props.priority}>478<Chunk priority={2}>479This is a notebook file: <br />480<Tag name='file'>481{482userActionStateFragment && <>483<br />484<status>485{userActionStateFragment}486</status>487<br />488</>489}490<CodeBlock includeFilepath={true} languageId={document.languageId} uri={document.uri} references={[new PromptReference(document.uri, undefined, promptReferenceOptions)]} code={summarized.text} />491</Tag>492</Chunk>493{!!selection && <FileSelection document={document} selection={selection} priority={1} />}494</CompositeElement>495);496}497}498499500interface CurrentFileSelectionPromptProps extends BasePromptElementProps {501document: TextDocumentSnapshot | NotebookDocumentSnapshot | undefined;502selection: Range | undefined;503}504505class FileSelection extends PromptElement<CurrentFileSelectionPromptProps> {506constructor(507props: CurrentFileSelectionPromptProps,508@IIgnoreService private readonly _ignoreService: IIgnoreService509) {510super(props);511}512513async render(state: void, sizing: PromptSizing) {514const { document, selection } = this.props;515516if (!document || !selection) {517return undefined;518}519520const isIgnored = await this._ignoreService.isCopilotIgnored(document.uri);521if (isIgnored) {522return <ignoredFiles value={[document.uri]} />;523}524525if (document.lineCount >= 4) {526const selectionLines: string[] = [];527const charactersInSelectionLines = () => selectionLines.reduce((acc, line) => acc + line.length, 0);528let selectionStartLine = Math.min(529document.lineCount - 1,530Math.max(0, selection.start.line));531let selectionEndLine = Math.min(document.lineCount - 1, selection.end.line);532if (selectionEndLine > selectionStartLine && selection.end.character === 0) {533selectionEndLine--;534}535if (selectionStartLine < selectionEndLine && selection.start.character === document.lineAt(selectionStartLine).text.length) {536selectionStartLine++;537}538for (let i = selectionStartLine; i <= selectionEndLine; i++) {539const line = document.lineAt(i);540selectionLines.push(line.text);541}542// render at least 4 lines as selected543let above = selectionStartLine - 1;544let below = selectionEndLine + 1;545while (selectionLines.length < 4 && charactersInSelectionLines() < 10) {546if (above >= 0) {547selectionLines.unshift(document.lineAt(above).text);548above--;549}550if (below < document.lineCount) {551selectionLines.push(document.lineAt(below).text);552below++;553}554}555556// TODO@tags: adopt tags here once <Tag> fixes whitespace problems557return (558<Chunk>559<file-selection>560<CodeBlock includeFilepath={true} languageId={document.languageId} uri={document.uri} references={[new PromptReference(document.uri, undefined)]} code={selectionLines.join('\n')} shouldTrim={false} /><br />561</file-selection>562</Chunk>563);564} else {565return undefined;566}567}568}569570571