Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliPromptResolver.ts
13405 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 type { Attachment } from '@github/copilot/sdk';
7
import type * as vscode from 'vscode';
8
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
9
import { IIgnoreService } from '../../../../platform/ignore/common/ignoreService';
10
import { ILogService } from '../../../../platform/log/common/logService';
11
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
12
import { isLocation, toLocation } from '../../../../util/common/types';
13
import { raceCancellation } from '../../../../util/vs/base/common/async';
14
import { ResourceMap } from '../../../../util/vs/base/common/map';
15
import { Schemas } from '../../../../util/vs/base/common/network';
16
import * as path from '../../../../util/vs/base/common/path';
17
import { extUriBiasedIgnorePathCase, relativePath } from '../../../../util/vs/base/common/resources';
18
import { URI } from '../../../../util/vs/base/common/uri';
19
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
20
import { ChatReferenceBinaryData, ChatReferenceDiagnostic, FileType, Location } from '../../../../vscodeTypes';
21
import { ChatVariablesCollection, isCustomizationsIndex, isInstructionFile, isPromptFile, PromptVariable } from '../../../prompt/common/chatVariablesCollection';
22
import { generateUserPrompt } from '../../../prompts/node/agent/copilotCLIPrompt';
23
import { getWorkingDirectory, isIsolationEnabled, IWorkspaceInfo } from '../../common/workspaceInfo';
24
import { ICopilotCLIImageSupport, isImageMimeType } from './copilotCLIImageSupport';
25
import { ICopilotCLISkills } from './copilotCLISkills';
26
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
27
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
28
29
export class CopilotCLIPromptResolver {
30
constructor(
31
@ICopilotCLIImageSupport private readonly imageSupport: ICopilotCLIImageSupport,
32
@ILogService private readonly logService: ILogService,
33
@IFileSystemService private readonly fileSystemService: IFileSystemService,
34
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
35
@IInstantiationService private readonly instantiationService: IInstantiationService,
36
@IIgnoreService private readonly ignoreService: IIgnoreService,
37
@ICopilotCLISkills private readonly skillsService: ICopilotCLISkills,
38
@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,
39
) { }
40
41
/**
42
* Generates the final prompt for the Copilot CLI agent, resolving variables and preparing attachments.
43
* @param prompt Provide a prompt to override the request prompt
44
*/
45
public async resolvePrompt(request: vscode.ChatRequest, prompt: string | undefined, additionalReferences: vscode.ChatPromptReference[], workspaceInfo: IWorkspaceInfo, additionalWorkspaces: IWorkspaceInfo[], token: vscode.CancellationToken): Promise<{ prompt: string; attachments: Attachment[]; references: vscode.ChatPromptReference[] }> {
46
const allReferences = new ChatVariablesCollection(request.references.concat(additionalReferences.filter(ref => !request.references.includes(ref))));
47
prompt = prompt ?? request.prompt;
48
const [variables, attachments] = await this.constructChatVariablesAndAttachments(allReferences, workspaceInfo, additionalWorkspaces, token);
49
if (token.isCancellationRequested) {
50
return { prompt, attachments: [], references: [] };
51
}
52
prompt = await raceCancellation(generateUserPrompt(request, prompt, variables, this.instantiationService), token);
53
const references = Array.from(variables).map(v => v.reference);
54
return { prompt: prompt ?? '', attachments, references };
55
}
56
57
/**
58
* Builds a map from workspace folder URIs to their corresponding worktree URIs.
59
* Used for multi-folder path translation when isolation is enabled.
60
*/
61
private buildFolderToWorktreeMap(primaryWorkspaceInfo: IWorkspaceInfo, additionalWorkspaces: IWorkspaceInfo[]): ResourceMap<vscode.Uri> {
62
const map = new ResourceMap<vscode.Uri>();
63
if (primaryWorkspaceInfo.worktree && primaryWorkspaceInfo.repository) {
64
map.set(primaryWorkspaceInfo.repository, primaryWorkspaceInfo.worktree);
65
}
66
for (const ws of additionalWorkspaces) {
67
if (ws.worktree && ws.repository) {
68
map.set(ws.repository, ws.worktree);
69
}
70
}
71
return map;
72
}
73
74
private async constructChatVariablesAndAttachments(variables: ChatVariablesCollection, workspaceInfo: IWorkspaceInfo, additionalWorkspaces: IWorkspaceInfo[], token: vscode.CancellationToken): Promise<[variables: ChatVariablesCollection, Attachment[]]> {
75
const validReferences: vscode.ChatPromptReference[] = [];
76
const fileFolderReferences: vscode.ChatPromptReference[] = [];
77
const builtinSlashCommandReferences: vscode.ChatPromptReference[] = [];
78
const isolationEnabled = isIsolationEnabled(workspaceInfo) || additionalWorkspaces.some(ws => isIsolationEnabled(ws));
79
const folderToWorktreeMap = this.buildFolderToWorktreeMap(workspaceInfo, additionalWorkspaces);
80
const hasAnyWorkingDirectory = getWorkingDirectory(workspaceInfo) || additionalWorkspaces.some(ws => getWorkingDirectory(ws));
81
const knownSkillLocations = await this.skillsService.getSkillsLocations(CancellationToken.None);
82
await Promise.all(Array.from(variables).map(async variable => {
83
// Unsupported references: prompt instructions, instruction files, and the customizations index.
84
if (isInstructionFile(variable) || isCustomizationsIndex(variable)) {
85
return;
86
}
87
// No need to include skill prompt files as an attachment if CLI already knows about them.
88
const promptFileUri = isPromptFile(variable) ? variable.value : undefined;
89
if (promptFileUri) {
90
if (knownSkillLocations.some(loc => extUriBiasedIgnorePathCase.isEqualOrParent(promptFileUri, loc))) {
91
return;
92
}
93
// Exclude plan prompt file from Core.
94
const directory = URI.file(path.dirname(promptFileUri.fsPath));
95
if (promptFileUri.fsPath.endsWith('plan.prompt.md') && path.basename(directory.fsPath) === 'prompts' && extUriBiasedIgnorePathCase.isEqualOrParent(this.extensionContext.extensionUri, directory)) {
96
return;
97
}
98
}
99
// GitHub pull request references
100
if (isGitHubPullRequestReference(variable.reference)) {
101
builtinSlashCommandReferences.push(variable.reference);
102
return;
103
}
104
// Git merge changes references
105
if (isGitMergeChangesReference(variable.reference)) {
106
builtinSlashCommandReferences.push(variable.reference);
107
return;
108
}
109
// If isolation is enabled, and we have workspace repo information, skip it.
110
if (isolationEnabled && isWorkspaceRepoInformationItem(variable)) {
111
return;
112
}
113
const variableRef = (!isolationEnabled || !hasAnyWorkingDirectory) ? variable.reference : await this.translateWorkspaceRefToWorkingDirectoryRef(variable.reference, workspaceInfo, additionalWorkspaces, folderToWorktreeMap, token);
114
// Images will be attached using regular attachments via Copilot CLI SDK.
115
if (variableRef.value instanceof ChatReferenceBinaryData) {
116
if (!isImageMimeType(variableRef.value.mimeType)) {
117
validReferences.push(variableRef);
118
}
119
fileFolderReferences.push(variableRef);
120
return;
121
}
122
if (isLocation(variableRef.value)) {
123
if (await this.ignoreService.isCopilotIgnored(variableRef.value.uri)) {
124
return;
125
}
126
fileFolderReferences.push(variableRef);
127
validReferences.push(variableRef);
128
return;
129
}
130
// Notebooks are not supported yet.
131
if (URI.isUri(variableRef.value)) {
132
if (await this.ignoreService.isCopilotIgnored(variableRef.value)) {
133
return;
134
}
135
if (variableRef.value.scheme === Schemas.vscodeNotebookCellOutput || variableRef.value.scheme === Schemas.vscodeNotebookCellOutput) {
136
return;
137
}
138
139
// Files and directories will be attached using regular attachments via Copilot CLI SDK.
140
validReferences.push(variableRef);
141
fileFolderReferences.push(variableRef);
142
return;
143
}
144
145
validReferences.push(variableRef);
146
}));
147
148
const [attachments, imageAttachments] = await this.constructFileOrFolderAttachments(fileFolderReferences, token);
149
// Re-add the images after we've copied them to the image store.
150
imageAttachments.forEach(img => {
151
if (img.type === 'file') {
152
validReferences.push({
153
name: img.displayName,
154
value: URI.file(img.path),
155
id: img.path,
156
});
157
}
158
});
159
160
// Add attachments for built-in slash command references
161
for (const reference of builtinSlashCommandReferences) {
162
// GitHub pull request reference
163
if (isGitHubPullRequestReference(reference) && URI.isUri(reference.value)) {
164
attachments.push({
165
type: 'blob',
166
mimeType: 'text/plain',
167
data: reference.value.toString(),
168
});
169
}
170
171
// Git merge changes reference
172
if (isGitMergeChangesReference(reference) && typeof reference.value === 'string') {
173
attachments.push({
174
type: 'blob',
175
mimeType: 'text/plain',
176
data: reference.value,
177
});
178
}
179
}
180
181
variables = new ChatVariablesCollection(validReferences);
182
return [variables, attachments];
183
}
184
185
186
private async constructFileOrFolderAttachments(fileOrFolderReferences: vscode.ChatPromptReference[], token: vscode.CancellationToken): Promise<[Attachment[], image: Attachment[]]> {
187
const attachments: Attachment[] = [];
188
const images: Attachment[] = [];
189
await Promise.all(fileOrFolderReferences.map(async ref => {
190
if (ref.value instanceof ChatReferenceBinaryData) {
191
if (!isImageMimeType(ref.value.mimeType)) {
192
return;
193
}
194
// Handle image attachments
195
try {
196
const buffer = await ref.value.data();
197
const uri = await this.imageSupport.storeImage(buffer, ref.value.mimeType);
198
attachments.push({
199
type: 'file',
200
displayName: ref.name,
201
path: uri.fsPath
202
});
203
images.push({
204
type: 'file',
205
displayName: ref.name,
206
path: uri.fsPath
207
});
208
} catch (error) {
209
this.logService.error(`[CopilotCLISession] Failed to store image: ${error}`);
210
}
211
return;
212
}
213
214
if (isLocation(ref.value)) {
215
try {
216
// Open the document and get the text for the range.
217
const document = await raceCancellation(this.workspaceService.openTextDocument(ref.value.uri), token);
218
if (!document) {
219
return;
220
}
221
attachments.push({
222
type: 'selection',
223
displayName: ref.name,
224
filePath: ref.value.uri.fsPath,
225
selection: {
226
start: {
227
line: ref.value.range.start.line + 1,
228
character: ref.value.range.start.character + 1
229
},
230
end: {
231
line: ref.value.range.end.line + 1,
232
character: ref.value.range.end.character + 1
233
}
234
},
235
text: document.getText(ref.value.range)
236
});
237
}
238
catch (ex) {
239
this.logService.error(`[CopilotCLISession] Failed to attach location ${ref.value.uri.fsPath}: ${ex}`);
240
}
241
return;
242
}
243
244
const uri = ref.value;
245
246
if (!URI.isUri(uri)) {
247
return;
248
}
249
250
// Attachment of Source control items.
251
if (uri.scheme === 'scm-history-item') {
252
return;
253
}
254
255
try {
256
const stat = await raceCancellation(this.fileSystemService.stat(uri), token);
257
if (!stat) {
258
return;
259
}
260
const type = stat.type === FileType.Directory ? 'directory' : stat.type === FileType.File ? 'file' : undefined;
261
if (!type) {
262
this.logService.error(`[CopilotCLISession] Ignoring attachment as it's not a file/directory (${uri.fsPath})`);
263
return;
264
}
265
attachments.push({
266
type,
267
displayName: ref.name || path.basename(uri.fsPath),
268
path: uri.fsPath
269
});
270
} catch (error) {
271
this.logService.error(`[CopilotCLISession] Failed to attach ${uri.fsPath}: ${error}`);
272
}
273
}));
274
275
return [attachments, images];
276
}
277
278
private async translateWorkspaceRefToWorkingDirectoryRef(ref: vscode.ChatPromptReference, workspaceInfo: IWorkspaceInfo, additionalWorkspaces: IWorkspaceInfo[], folderToWorktreeMap: ResourceMap<vscode.Uri>, token: vscode.CancellationToken): Promise<vscode.ChatPromptReference> {
279
try {
280
if (ref.value instanceof ChatReferenceBinaryData) {
281
return ref;
282
}
283
284
if (isLocation(ref.value)) {
285
const uri = await this.translateWorkspaceUriToWorkingDirectoryUri(ref.value.uri, workspaceInfo, additionalWorkspaces, folderToWorktreeMap, token);
286
const loc = new Location(uri, toLocation(ref.value)!.range);
287
return {
288
...ref,
289
value: loc
290
};
291
} else if (URI.isUri(ref.value)) {
292
const uri = await this.translateWorkspaceUriToWorkingDirectoryUri(ref.value, workspaceInfo, additionalWorkspaces, folderToWorktreeMap, token);
293
return {
294
...ref,
295
value: uri
296
};
297
} else if (ref.value instanceof ChatReferenceDiagnostic) {
298
const diagnostics = await Promise.all(ref.value.diagnostics.map(async ([uri, diags]) => {
299
const translatedUri = await this.translateWorkspaceUriToWorkingDirectoryUri(uri, workspaceInfo, additionalWorkspaces, folderToWorktreeMap, token);
300
return [translatedUri, diags] as [vscode.Uri, vscode.Diagnostic[]];
301
}));
302
return {
303
...ref,
304
value: new ChatReferenceDiagnostic(diagnostics)
305
};
306
}
307
return ref;
308
} catch (error) {
309
this.logService.error(error, `[CopilotCLISession] Failed to translate workspace reference`);
310
return ref;
311
}
312
}
313
314
private async translateWorkspaceUriToWorkingDirectoryUri(uri: vscode.Uri, workspaceInfo: IWorkspaceInfo, additionalWorkspaces: IWorkspaceInfo[], folderToWorktreeMap: ResourceMap<vscode.Uri>, token: vscode.CancellationToken): Promise<vscode.Uri> {
315
const workspaceFolder = this.workspaceService.getWorkspaceFolder(uri);
316
const matchingWorktree = workspaceFolder ? folderToWorktreeMap.get(workspaceFolder) : undefined;
317
if (!workspaceFolder || !matchingWorktree) {
318
return (await this.findMatchingWorktree(uri, workspaceInfo, additionalWorkspaces, token)) ?? uri;
319
}
320
// Use the folder-specific worktree from the map when available; otherwise, fall back to a best-effort worktree match (or the original URI)
321
const targetDir = matchingWorktree;
322
const rel = relativePath(workspaceFolder, uri);
323
if (!rel) {
324
return uri;
325
}
326
const segments = rel.split('/');
327
const candidate = URI.joinPath(targetDir, ...segments);
328
const candidateStat = await raceCancellation(this.fileSystemService.stat(candidate), token).catch(() => undefined);
329
return candidateStat ? candidate : uri;
330
}
331
332
private async findMatchingWorktree(uri: vscode.Uri, workspaceInfo: IWorkspaceInfo, additionalWorkspaces: IWorkspaceInfo[], token: vscode.CancellationToken): Promise<vscode.Uri | undefined> {
333
// Assume the uri is `/user/abc/projects/project_abc/file.ts` and one of the items in workspaceInfo or additionalWorkspaces has a folder/repositoryUri that is /user/abc/projects/project_abc and that has a worktree at `/user/abc/projects/project_abc-worktree`, we want to translate the file uri to `/user/abc/projects/project_abc-worktree/file.ts`.
334
for (const ws of [workspaceInfo, ...additionalWorkspaces]) {
335
if (ws.repository && ws.worktree) {
336
if (extUriBiasedIgnorePathCase.isEqualOrParent(uri, ws.repository)) {
337
const rel = relativePath(ws.repository, uri);
338
if (rel) {
339
const candidate = URI.joinPath(ws.worktree, rel);
340
const candidateStat = await raceCancellation(this.fileSystemService.stat(candidate), token).catch(() => undefined);
341
return candidateStat ? candidate : uri;
342
}
343
}
344
}
345
}
346
}
347
}
348
349
/**
350
* Never include this variable in Copilot CLI prompts when using git worktrees (isolation).
351
* This causes issues as the repository information will not match the worktree state.
352
* https://github.com/microsoft/vscode/issues/279865
353
*/
354
function isWorkspaceRepoInformationItem(variable: PromptVariable): boolean {
355
const ref = variable.reference;
356
if (typeof ref.value !== 'string') {
357
return false;
358
}
359
if (!ref.modelDescription) {
360
return false;
361
}
362
return (
363
(ref.modelDescription).startsWith('Information about one of the current repositories') || (ref.modelDescription).startsWith('Information about the current repository'))
364
&&
365
ref.value.startsWith('Repository name:');
366
}
367
368
function isGitHubPullRequestReference(ref: vscode.ChatPromptReference): boolean {
369
return ref.id === 'github-pull-request';
370
}
371
372
function isGitMergeChangesReference(ref: vscode.ChatPromptReference): boolean {
373
return ref.id === 'git-merge-changes';
374
}
375
376