Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineChat2/node/inlineChatPrompt.tsx
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 { AssistantMessage, PromptElement, PromptElementProps, PromptReference, PromptSizing, SystemMessage, ToolMessage, useKeepWith, UserMessage } from '@vscode/prompt-tsx';
7
import { ChatResponsePart } from '@vscode/prompt-tsx/dist/base/vscodeTypes';
8
import type { CancellationToken, ExtendedLanguageModelToolResult, Position, Progress } from 'vscode';
9
import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';
10
import { CacheType } from '../../../platform/endpoint/common/endpointTypes';
11
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
12
import { ChatRequest, ChatRequestEditorData, Range } from '../../../vscodeTypes';
13
import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';
14
import { IToolCall } from '../../prompt/common/intents';
15
import { CopilotIdentityRules } from '../../prompts/node/base/copilotIdentity';
16
import { SafetyRules } from '../../prompts/node/base/safetyRules';
17
import { Tag } from '../../prompts/node/base/tag';
18
import { ChatVariables, UserQuery } from '../../prompts/node/panel/chatVariables';
19
import { CodeBlock } from '../../prompts/node/panel/safeElements';
20
import { ToolResult } from '../../prompts/node/panel/toolCalling';
21
22
23
/**
24
* Threshold in lines above which a file is considered "large" and gets cropped in the prompt.
25
*/
26
export const LARGE_FILE_LINE_THRESHOLD = 250;
27
28
/** How many context lines to show around the cursor/selection in large files. */
29
const LARGE_FILE_CONTEXT_LINES = 100;
30
31
/** Context lines above/below selection in large files. */
32
const LARGE_FILE_SELECTION_CONTEXT_LINES = 25;
33
34
export interface ICompletedToolCallRound {
35
readonly calls: readonly [IToolCall, ExtendedLanguageModelToolResult][];
36
}
37
38
export type InlineChat2PromptProps = PromptElementProps<{
39
request: ChatRequest;
40
snapshotAtRequest: TextDocumentSnapshot;
41
data: ChatRequestEditorData;
42
exitToolName: string;
43
previousRounds: readonly ICompletedToolCallRound[];
44
hasFailedEdits: boolean;
45
isLargeFile?: boolean;
46
readToolName?: string;
47
}>;
48
49
export class InlineChat2Prompt extends PromptElement<InlineChat2PromptProps> {
50
51
constructor(
52
props: InlineChat2PromptProps,
53
@IPromptPathRepresentationService private readonly _promptPathRepresentationService: IPromptPathRepresentationService,
54
) {
55
super(props);
56
}
57
58
59
override render(state: void, sizing: PromptSizing): Promise<any> {
60
61
const snapshotAtRequest = this.props.snapshotAtRequest;
62
63
const selection = this.props.data.selection;
64
const isLargeFile = this.props.isLargeFile ?? false;
65
const readToolName = this.props.readToolName;
66
67
const variables = new ChatVariablesCollection(this.props.request.references);
68
const filepath = this._promptPathRepresentationService.getFilePath(snapshotAtRequest.uri);
69
70
// TODO@jrieken APPLY_PATCH_INSTRUCTIONS
71
return (
72
<>
73
<SystemMessage priority={1000}>
74
<CopilotIdentityRules />
75
<SafetyRules />
76
<Tag name='instructions'>
77
You 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 />
78
The 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 />
79
Make all changes in a single invocation of the edit-tool (there is no tool calling loop).<br />
80
{isLargeFile && readToolName && <>
81
The 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 />
82
</>}
83
Do 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 />
84
</Tag>
85
<cacheBreakpoint type={CacheType} />
86
</SystemMessage>
87
<UserMessage>
88
{isLargeFile
89
? <CroppedFileContentElement snapshot={snapshotAtRequest} selection={selection} filepath={filepath} />
90
: <>
91
<>
92
The filepath is `{filepath}` and this is its content:<br />
93
</>
94
<Tag name='file'>
95
<CodeBlock includeFilepath={false} languageId={snapshotAtRequest.languageId} uri={snapshotAtRequest.uri} references={[new PromptReference(snapshotAtRequest.uri, undefined, undefined)]} code={snapshotAtRequest.getText()} />
96
</Tag>
97
</>
98
}
99
{selection.isEmpty
100
? <FileContextElement snapshot={snapshotAtRequest} position={selection.start} />
101
: <FileSelectionElement snapshot={snapshotAtRequest} selection={selection} />
102
}
103
<ChatVariables flexGrow={3} priority={898} chatVariables={variables} useFixCookbook={true} />
104
<Tag name='reminder'>
105
{selection.isEmpty
106
? <>Make changes or write new code anywhere in the file.<br /></>
107
: <>Focus on the selection, and try to make changes to the selected code and its context.<br /></>
108
}
109
Do not make code changes that are not directly and logically related to the user's prompt.<br />
110
ONLY change the `{filepath}` file, make all changes in a single invocation of the edit-tool, and change NO other file.
111
</Tag>
112
<cacheBreakpoint type={CacheType} />
113
</UserMessage>
114
<UserMessage>
115
<Tag name='prompt'>
116
<UserQuery flexGrow={7} priority={900} chatVariables={variables} query={this.props.request.prompt} />
117
</Tag>
118
<cacheBreakpoint type={CacheType} />
119
</UserMessage>
120
<ToolCallRoundsElement
121
previousRounds={this.props.previousRounds}
122
hasFailedEdits={this.props.hasFailedEdits}
123
data={this.props.data}
124
documentVersionAtRequest={this.props.snapshotAtRequest.version}
125
isLargeFile={this.props.isLargeFile ?? false}
126
selection={selection}
127
filepath={filepath}
128
/>
129
</>
130
);
131
}
132
}
133
134
135
type CroppedFileContentElementProps = PromptElementProps<{
136
snapshot: TextDocumentSnapshot;
137
selection: Range;
138
filepath: string;
139
}>;
140
141
/**
142
* Renders a cropped view of a large file, centered around the cursor/selection.
143
*/
144
class CroppedFileContentElement extends PromptElement<CroppedFileContentElementProps> {
145
146
override render() {
147
const { snapshot, selection, filepath } = this.props;
148
const totalLines = snapshot.lineCount;
149
150
let cropStart: number;
151
let cropEnd: number;
152
153
if (selection.isEmpty) {
154
// Cursor only: show LARGE_FILE_CONTEXT_LINES centered on cursor, biased downward
155
const cursorLine = selection.start.line;
156
const linesAbove = Math.floor(LARGE_FILE_CONTEXT_LINES * 0.4);
157
const linesBelow = LARGE_FILE_CONTEXT_LINES - linesAbove;
158
cropStart = Math.max(0, cursorLine - linesAbove);
159
cropEnd = Math.min(totalLines - 1, cursorLine + linesBelow);
160
} else {
161
// Selection: always include the full selection, plus context around it
162
const selStart = selection.start.line;
163
const selEnd = selection.end.line;
164
cropStart = Math.max(0, selStart - LARGE_FILE_SELECTION_CONTEXT_LINES);
165
cropEnd = Math.min(totalLines - 1, selEnd + LARGE_FILE_SELECTION_CONTEXT_LINES);
166
}
167
168
const croppedText = snapshot.getText(new Range(
169
selection.start.with({ line: cropStart, character: 0 }),
170
selection.start.with({ line: cropEnd, character: Number.MAX_SAFE_INTEGER }),
171
));
172
173
// 1-based line numbers for the hint
174
const shownFrom = cropStart + 1;
175
const shownTo = cropEnd + 1;
176
177
return <>
178
<>
179
The filepath is `{filepath}` ({totalLines} lines total). Showing lines {shownFrom}-{shownTo}:<br />
180
</>
181
<Tag name='file'>
182
<CodeBlock includeFilepath={false} languageId={snapshot.languageId} uri={snapshot.uri} references={[new PromptReference(snapshot.uri, undefined, undefined)]} code={croppedText} />
183
</Tag>
184
</>;
185
}
186
}
187
188
189
export type FileContextElementProps = PromptElementProps<{
190
snapshot: TextDocumentSnapshot;
191
position: Position;
192
}>;
193
194
export class FileContextElement extends PromptElement<FileContextElementProps> {
195
196
override render(state: void, sizing: PromptSizing, _progress?: Progress<ChatResponsePart>, _token?: CancellationToken) {
197
198
let startLine = this.props.position.line;
199
let endLine = this.props.position.line;
200
let n = 0;
201
let seenNonEmpty = false;
202
while (startLine > 0) {
203
seenNonEmpty = seenNonEmpty || !this.props.snapshot.lineAt(startLine).isEmptyOrWhitespace;
204
startLine--;
205
n++;
206
if (n >= 3 && seenNonEmpty) {
207
break;
208
}
209
}
210
n = 0;
211
seenNonEmpty = false;
212
while (endLine < this.props.snapshot.lineCount - 1) {
213
seenNonEmpty = seenNonEmpty || !this.props.snapshot.lineAt(endLine).isEmptyOrWhitespace;
214
endLine++;
215
n++;
216
if (n >= 3 && seenNonEmpty) {
217
break;
218
}
219
}
220
221
const textBefore = this.props.snapshot.getText(new Range(this.props.position.with({ line: startLine, character: 0 }), this.props.position));
222
const textAfter = this.props.snapshot.getText(new Range(this.props.position, this.props.position.with({ line: endLine, character: Number.MAX_SAFE_INTEGER })));
223
224
const code = `${textBefore}$CURSOR$${textAfter}`;
225
226
return <>
227
<Tag name='file-cursor-context'>
228
<CodeBlock includeFilepath={false} languageId={this.props.snapshot.languageId} uri={this.props.snapshot.uri} references={[new PromptReference(this.props.snapshot.uri, undefined, undefined)]} code={code} />
229
</Tag>
230
</>;
231
}
232
}
233
234
235
export type FileSelectionElementProps = PromptElementProps<{
236
snapshot: TextDocumentSnapshot;
237
selection: Range;
238
}>;
239
240
export class FileSelectionElement extends PromptElement<FileSelectionElementProps> {
241
242
override render(state: void, sizing: PromptSizing, progress?: Progress<ChatResponsePart>, token?: CancellationToken) {
243
244
// the full lines of the selection
245
// TODO@jrieken
246
// * use the true selected text (now we extend to full lines)
247
248
const selectedLines = this.props.snapshot.getText(this.props.selection.with({
249
start: this.props.selection.start.with({ character: 0 }),
250
end: this.props.selection.end.with({ character: Number.MAX_SAFE_INTEGER }),
251
}));
252
253
return <>
254
<Tag name='file-selection'>
255
<CodeBlock includeFilepath={false} languageId={this.props.snapshot.languageId} uri={this.props.snapshot.uri} references={[new PromptReference(this.props.snapshot.uri, undefined, undefined)]} code={selectedLines} />
256
</Tag>
257
</>;
258
}
259
}
260
261
262
type ToolCallRoundsElementProps = PromptElementProps<{
263
previousRounds: readonly ICompletedToolCallRound[];
264
hasFailedEdits: boolean;
265
data: ChatRequestEditorData;
266
documentVersionAtRequest: number;
267
isLargeFile: boolean;
268
selection: Range;
269
filepath: string;
270
}>;
271
272
/**
273
* Renders all previous tool call rounds in order, each as an AssistantMessage/ToolMessage pair.
274
* If any edits failed, appends a single feedback UserMessage at the end.
275
*/
276
export class ToolCallRoundsElement extends PromptElement<ToolCallRoundsElementProps> {
277
278
override render() {
279
if (this.props.previousRounds.length === 0) {
280
return;
281
}
282
283
const documentNow = this.props.data.document;
284
285
return <>
286
{this.props.previousRounds.map(round => {
287
const KeepWith = useKeepWith();
288
return <>
289
<AssistantMessage toolCalls={round.calls.map(([toolCall]) => ({
290
type: 'function' as const,
291
id: toolCall.id,
292
function: { name: toolCall.name, arguments: toolCall.arguments },
293
keepWith: KeepWith
294
}))} />
295
{round.calls.map(([toolCall, result]) => (
296
<KeepWith>
297
<ToolMessage toolCallId={toolCall.id}>
298
<ToolResult content={result.content} toolCallId={toolCall.id} />
299
</ToolMessage>
300
</KeepWith>
301
))}
302
</>;
303
})}
304
{this.props.hasFailedEdits && <UserMessage>
305
{documentNow.version === this.props.documentVersionAtRequest && <>
306
<Tag name='feedback'>
307
Editing this file did not produce the desired result. No changes were made. Understand the previous edit attempts and the original file content, and <br />
308
produce a better edit.<br />
309
</Tag>
310
</>}
311
{documentNow.version !== this.props.documentVersionAtRequest && <>
312
<Tag name='feedback'>
313
Editing this file did not produce the desired result. Understand the previous edit attempts and the current file content, and <br />
314
produce a better edit. This is the current file content:<br />
315
</Tag>
316
{this.props.isLargeFile
317
? <CroppedFileContentElement snapshot={TextDocumentSnapshot.create(documentNow)} selection={this.props.selection} filepath={this.props.filepath} />
318
: <Tag name='file'>
319
<CodeBlock includeFilepath={false} languageId={documentNow.languageId} uri={documentNow.uri} references={[new PromptReference(documentNow.uri, undefined, undefined)]} code={documentNow.getText()} />
320
</Tag>
321
}
322
</>}
323
</UserMessage>}
324
</>;
325
}
326
}
327
328