Path: blob/main/extensions/copilot/src/extension/inlineChat2/node/inlineChatPrompt.tsx
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 { AssistantMessage, PromptElement, PromptElementProps, PromptReference, PromptSizing, SystemMessage, ToolMessage, useKeepWith, UserMessage } from '@vscode/prompt-tsx';6import { ChatResponsePart } from '@vscode/prompt-tsx/dist/base/vscodeTypes';7import type { CancellationToken, ExtendedLanguageModelToolResult, Position, Progress } from 'vscode';8import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';9import { CacheType } from '../../../platform/endpoint/common/endpointTypes';10import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';11import { ChatRequest, ChatRequestEditorData, Range } from '../../../vscodeTypes';12import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';13import { IToolCall } from '../../prompt/common/intents';14import { CopilotIdentityRules } from '../../prompts/node/base/copilotIdentity';15import { SafetyRules } from '../../prompts/node/base/safetyRules';16import { Tag } from '../../prompts/node/base/tag';17import { ChatVariables, UserQuery } from '../../prompts/node/panel/chatVariables';18import { CodeBlock } from '../../prompts/node/panel/safeElements';19import { ToolResult } from '../../prompts/node/panel/toolCalling';202122/**23* Threshold in lines above which a file is considered "large" and gets cropped in the prompt.24*/25export const LARGE_FILE_LINE_THRESHOLD = 250;2627/** How many context lines to show around the cursor/selection in large files. */28const LARGE_FILE_CONTEXT_LINES = 100;2930/** Context lines above/below selection in large files. */31const LARGE_FILE_SELECTION_CONTEXT_LINES = 25;3233export interface ICompletedToolCallRound {34readonly calls: readonly [IToolCall, ExtendedLanguageModelToolResult][];35}3637export type InlineChat2PromptProps = PromptElementProps<{38request: ChatRequest;39snapshotAtRequest: TextDocumentSnapshot;40data: ChatRequestEditorData;41exitToolName: string;42previousRounds: readonly ICompletedToolCallRound[];43hasFailedEdits: boolean;44isLargeFile?: boolean;45readToolName?: string;46}>;4748export class InlineChat2Prompt extends PromptElement<InlineChat2PromptProps> {4950constructor(51props: InlineChat2PromptProps,52@IPromptPathRepresentationService private readonly _promptPathRepresentationService: IPromptPathRepresentationService,53) {54super(props);55}565758override render(state: void, sizing: PromptSizing): Promise<any> {5960const snapshotAtRequest = this.props.snapshotAtRequest;6162const selection = this.props.data.selection;63const isLargeFile = this.props.isLargeFile ?? false;64const readToolName = this.props.readToolName;6566const variables = new ChatVariablesCollection(this.props.request.references);67const filepath = this._promptPathRepresentationService.getFilePath(snapshotAtRequest.uri);6869// TODO@jrieken APPLY_PATCH_INSTRUCTIONS70return (71<>72<SystemMessage priority={1000}>73<CopilotIdentityRules />74<SafetyRules />75<Tag name='instructions'>76You are an AI coding assistant that is used for quick, inline code changes. Changes are scoped to a single file or to some selected code in that file. You can ONLY edit that file and must use a tool to make these edits.<br />77The user is interested in code changes grounded in the user's prompt. So, focus on coding, no wordy explanations, and do not ask back for clarifications.<br />78Make all changes in a single invocation of the edit-tool (there is no tool calling loop).<br />79{isLargeFile && readToolName && <>80The file is large and only a portion is shown below. If you need to see more of the file to make the requested change, use the {readToolName} tool to read additional parts of this file before editing. Do NOT use it to read other files.<br />81</>}82Do not make code changes that are not directly and logically related to the user's prompt. When you cannot make a code change, reply with just a few words.<br />83</Tag>84<cacheBreakpoint type={CacheType} />85</SystemMessage>86<UserMessage>87{isLargeFile88? <CroppedFileContentElement snapshot={snapshotAtRequest} selection={selection} filepath={filepath} />89: <>90<>91The filepath is `{filepath}` and this is its content:<br />92</>93<Tag name='file'>94<CodeBlock includeFilepath={false} languageId={snapshotAtRequest.languageId} uri={snapshotAtRequest.uri} references={[new PromptReference(snapshotAtRequest.uri, undefined, undefined)]} code={snapshotAtRequest.getText()} />95</Tag>96</>97}98{selection.isEmpty99? <FileContextElement snapshot={snapshotAtRequest} position={selection.start} />100: <FileSelectionElement snapshot={snapshotAtRequest} selection={selection} />101}102<ChatVariables flexGrow={3} priority={898} chatVariables={variables} useFixCookbook={true} />103<Tag name='reminder'>104{selection.isEmpty105? <>Make changes or write new code anywhere in the file.<br /></>106: <>Focus on the selection, and try to make changes to the selected code and its context.<br /></>107}108Do not make code changes that are not directly and logically related to the user's prompt.<br />109ONLY change the `{filepath}` file, make all changes in a single invocation of the edit-tool, and change NO other file.110</Tag>111<cacheBreakpoint type={CacheType} />112</UserMessage>113<UserMessage>114<Tag name='prompt'>115<UserQuery flexGrow={7} priority={900} chatVariables={variables} query={this.props.request.prompt} />116</Tag>117<cacheBreakpoint type={CacheType} />118</UserMessage>119<ToolCallRoundsElement120previousRounds={this.props.previousRounds}121hasFailedEdits={this.props.hasFailedEdits}122data={this.props.data}123documentVersionAtRequest={this.props.snapshotAtRequest.version}124isLargeFile={this.props.isLargeFile ?? false}125selection={selection}126filepath={filepath}127/>128</>129);130}131}132133134type CroppedFileContentElementProps = PromptElementProps<{135snapshot: TextDocumentSnapshot;136selection: Range;137filepath: string;138}>;139140/**141* Renders a cropped view of a large file, centered around the cursor/selection.142*/143class CroppedFileContentElement extends PromptElement<CroppedFileContentElementProps> {144145override render() {146const { snapshot, selection, filepath } = this.props;147const totalLines = snapshot.lineCount;148149let cropStart: number;150let cropEnd: number;151152if (selection.isEmpty) {153// Cursor only: show LARGE_FILE_CONTEXT_LINES centered on cursor, biased downward154const cursorLine = selection.start.line;155const linesAbove = Math.floor(LARGE_FILE_CONTEXT_LINES * 0.4);156const linesBelow = LARGE_FILE_CONTEXT_LINES - linesAbove;157cropStart = Math.max(0, cursorLine - linesAbove);158cropEnd = Math.min(totalLines - 1, cursorLine + linesBelow);159} else {160// Selection: always include the full selection, plus context around it161const selStart = selection.start.line;162const selEnd = selection.end.line;163cropStart = Math.max(0, selStart - LARGE_FILE_SELECTION_CONTEXT_LINES);164cropEnd = Math.min(totalLines - 1, selEnd + LARGE_FILE_SELECTION_CONTEXT_LINES);165}166167const croppedText = snapshot.getText(new Range(168selection.start.with({ line: cropStart, character: 0 }),169selection.start.with({ line: cropEnd, character: Number.MAX_SAFE_INTEGER }),170));171172// 1-based line numbers for the hint173const shownFrom = cropStart + 1;174const shownTo = cropEnd + 1;175176return <>177<>178The filepath is `{filepath}` ({totalLines} lines total). Showing lines {shownFrom}-{shownTo}:<br />179</>180<Tag name='file'>181<CodeBlock includeFilepath={false} languageId={snapshot.languageId} uri={snapshot.uri} references={[new PromptReference(snapshot.uri, undefined, undefined)]} code={croppedText} />182</Tag>183</>;184}185}186187188export type FileContextElementProps = PromptElementProps<{189snapshot: TextDocumentSnapshot;190position: Position;191}>;192193export class FileContextElement extends PromptElement<FileContextElementProps> {194195override render(state: void, sizing: PromptSizing, _progress?: Progress<ChatResponsePart>, _token?: CancellationToken) {196197let startLine = this.props.position.line;198let endLine = this.props.position.line;199let n = 0;200let seenNonEmpty = false;201while (startLine > 0) {202seenNonEmpty = seenNonEmpty || !this.props.snapshot.lineAt(startLine).isEmptyOrWhitespace;203startLine--;204n++;205if (n >= 3 && seenNonEmpty) {206break;207}208}209n = 0;210seenNonEmpty = false;211while (endLine < this.props.snapshot.lineCount - 1) {212seenNonEmpty = seenNonEmpty || !this.props.snapshot.lineAt(endLine).isEmptyOrWhitespace;213endLine++;214n++;215if (n >= 3 && seenNonEmpty) {216break;217}218}219220const textBefore = this.props.snapshot.getText(new Range(this.props.position.with({ line: startLine, character: 0 }), this.props.position));221const textAfter = this.props.snapshot.getText(new Range(this.props.position, this.props.position.with({ line: endLine, character: Number.MAX_SAFE_INTEGER })));222223const code = `${textBefore}$CURSOR$${textAfter}`;224225return <>226<Tag name='file-cursor-context'>227<CodeBlock includeFilepath={false} languageId={this.props.snapshot.languageId} uri={this.props.snapshot.uri} references={[new PromptReference(this.props.snapshot.uri, undefined, undefined)]} code={code} />228</Tag>229</>;230}231}232233234export type FileSelectionElementProps = PromptElementProps<{235snapshot: TextDocumentSnapshot;236selection: Range;237}>;238239export class FileSelectionElement extends PromptElement<FileSelectionElementProps> {240241override render(state: void, sizing: PromptSizing, progress?: Progress<ChatResponsePart>, token?: CancellationToken) {242243// the full lines of the selection244// TODO@jrieken245// * use the true selected text (now we extend to full lines)246247const selectedLines = this.props.snapshot.getText(this.props.selection.with({248start: this.props.selection.start.with({ character: 0 }),249end: this.props.selection.end.with({ character: Number.MAX_SAFE_INTEGER }),250}));251252return <>253<Tag name='file-selection'>254<CodeBlock includeFilepath={false} languageId={this.props.snapshot.languageId} uri={this.props.snapshot.uri} references={[new PromptReference(this.props.snapshot.uri, undefined, undefined)]} code={selectedLines} />255</Tag>256</>;257}258}259260261type ToolCallRoundsElementProps = PromptElementProps<{262previousRounds: readonly ICompletedToolCallRound[];263hasFailedEdits: boolean;264data: ChatRequestEditorData;265documentVersionAtRequest: number;266isLargeFile: boolean;267selection: Range;268filepath: string;269}>;270271/**272* Renders all previous tool call rounds in order, each as an AssistantMessage/ToolMessage pair.273* If any edits failed, appends a single feedback UserMessage at the end.274*/275export class ToolCallRoundsElement extends PromptElement<ToolCallRoundsElementProps> {276277override render() {278if (this.props.previousRounds.length === 0) {279return;280}281282const documentNow = this.props.data.document;283284return <>285{this.props.previousRounds.map(round => {286const KeepWith = useKeepWith();287return <>288<AssistantMessage toolCalls={round.calls.map(([toolCall]) => ({289type: 'function' as const,290id: toolCall.id,291function: { name: toolCall.name, arguments: toolCall.arguments },292keepWith: KeepWith293}))} />294{round.calls.map(([toolCall, result]) => (295<KeepWith>296<ToolMessage toolCallId={toolCall.id}>297<ToolResult content={result.content} toolCallId={toolCall.id} />298</ToolMessage>299</KeepWith>300))}301</>;302})}303{this.props.hasFailedEdits && <UserMessage>304{documentNow.version === this.props.documentVersionAtRequest && <>305<Tag name='feedback'>306Editing this file did not produce the desired result. No changes were made. Understand the previous edit attempts and the original file content, and <br />307produce a better edit.<br />308</Tag>309</>}310{documentNow.version !== this.props.documentVersionAtRequest && <>311<Tag name='feedback'>312Editing this file did not produce the desired result. Understand the previous edit attempts and the current file content, and <br />313produce a better edit. This is the current file content:<br />314</Tag>315{this.props.isLargeFile316? <CroppedFileContentElement snapshot={TextDocumentSnapshot.create(documentNow)} selection={this.props.selection} filepath={this.props.filepath} />317: <Tag name='file'>318<CodeBlock includeFilepath={false} languageId={documentNow.languageId} uri={documentNow.uri} references={[new PromptReference(documentNow.uri, undefined, undefined)]} code={documentNow.getText()} />319</Tag>320}321</>}322</UserMessage>}323</>;324}325}326327328