Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/intents/node/newIntent.ts
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 * as l10n from '@vscode/l10n';
7
import { Raw } from '@vscode/prompt-tsx';
8
import { parse } from 'jsonc-parser';
9
import type * as vscode from 'vscode';
10
import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher';
11
import { ChatLocation } from '../../../platform/chat/common/commonTypes';
12
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
13
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
14
import { FileType } from '../../../platform/filesystem/common/fileTypes';
15
import { GithubRepositoryItem, IGithubRepositoryService } from '../../../platform/github/common/githubService';
16
import { IChatEndpoint } from '../../../platform/networking/common/networking';
17
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
18
import { extractCodeBlocks } from '../../../util/common/markdown';
19
import { createServiceIdentifier } from '../../../util/common/services';
20
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
21
import * as path from '../../../util/vs/base/common/path';
22
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
23
import { ChatResponseFileTreePart, MarkdownString, Uri } from '../../../vscodeTypes';
24
import { Intent } from '../../common/constants';
25
import { commandUri } from '../../linkify/common/commands';
26
import { convertFileTreeToChatResponseFileTree } from '../../prompt/common/fileTreeParser';
27
import { IBuildPromptContext } from '../../prompt/common/intents';
28
import { IIntent, IIntentInvocation, IIntentInvocationContext, IIntentSlashCommandInfo, IntentLinkificationOptions, IResponseProcessorContext } from '../../prompt/node/intents';
29
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
30
import { NewWorkspaceGithubContentMetadata, NewWorkspacePrompt } from '../../prompts/node/panel/newWorkspace/newWorkspace';
31
import { NewWorkspaceContentsPromptProps } from '../../prompts/node/panel/newWorkspace/newWorkspaceContents';
32
import { FileContentsGenerator, ProjectSpecificationGenerator } from './generateNewWorkspaceContent';
33
34
35
interface FileTreeDataWithContent extends vscode.ChatResponseFileTree {
36
content?: Promise<Uint8Array | undefined>;
37
ctime?: number;
38
type?: FileType;
39
}
40
41
export const INewWorkspacePreviewContentManager = createServiceIdentifier<INewWorkspacePreviewContentManager>('INewWorkspacePreviewContentManager');
42
export interface INewWorkspacePreviewContentManager {
43
readonly _serviceBrand: undefined;
44
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any): void;
45
get(uri: Uri): FileTreeDataWithContent | undefined;
46
getFileTree(responseId: string): ChatResponseFileTreePart | undefined;
47
}
48
49
export const CreateProjectCommand = 'github.copilot.createProject';
50
export const CreateFileCommand = 'github.copilot.createFile';
51
export const OpenFileCommand = 'github.copilot.openFile';
52
53
export class NewWorkspacePreviewContentManagerImpl implements INewWorkspacePreviewContentManager {
54
declare readonly _serviceBrand: undefined;
55
private readonly copilotContentManager: NewWorkspaceCopilotContentManager;
56
private readonly githubContentManager: NewWorkspaceGitHubContentManager;
57
private readonly fileContentManager: NewWorkspaceFileContentManager;
58
private responseScopedData = new Map<string, ChatResponseFileTreePart>();
59
private prevResponseId: string | undefined;
60
private prevFileContents = new Map<string, string>();
61
62
constructor(
63
@IInstantiationService instantiationService: IInstantiationService,
64
) {
65
this.copilotContentManager = instantiationService.createInstance(NewWorkspaceCopilotContentManager);
66
this.githubContentManager = instantiationService.createInstance(NewWorkspaceGitHubContentManager);
67
this.fileContentManager = new NewWorkspaceFileContentManager();
68
}
69
70
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {
71
this.responseScopedData.set(responseId, fileTree);
72
if (isGithubWorkspaceUri(fileTree.baseUri)) {
73
this.githubContentManager.set(responseId, projectName, fileTree, serviceArgs);
74
} else if (isCopiltoFileWorkspaceUri(fileTree.baseUri)) {
75
this.fileContentManager.set(responseId, projectName, fileTree, serviceArgs);
76
} else {
77
this.copilotContentManager.set(responseId, projectName, fileTree, serviceArgs);
78
}
79
}
80
81
get(uri: Uri): FileTreeDataWithContent | undefined {
82
if (this.prevResponseId !== uri.authority) {
83
this.prevFileContents.clear();
84
this.prevResponseId = uri.authority;
85
}
86
87
let fileContents: FileTreeDataWithContent | undefined;
88
if (isGithubWorkspaceUri(uri)) {
89
fileContents = this.githubContentManager.get(uri.authority, uri.path);
90
} else if (isCopiltoFileWorkspaceUri(uri)) {
91
fileContents = this.fileContentManager.get(uri.authority, uri.path);
92
} else {
93
fileContents = this.copilotContentManager.get(uri.authority, uri.path, this.prevFileContents);
94
}
95
96
fileContents?.content?.then((content) => {
97
if (this.prevFileContents.has(uri.path)) {
98
return;
99
}
100
const decoder = new TextDecoder();
101
const fileContentStr = decoder.decode(content);
102
this.prevFileContents.set(uri.path, fileContentStr);
103
});
104
105
return fileContents;
106
}
107
108
getFileTree(responseId: string): ChatResponseFileTreePart | undefined {
109
return this.responseScopedData.get(responseId);
110
}
111
}
112
113
interface ProjectData {
114
userPrompt: string;
115
projectStructure: string;
116
projectSpecification: Promise<string>;
117
fileTree: ChatResponseFileTreePart;
118
chatMessages: Raw.ChatMessage[];
119
}
120
121
export class NewWorkspaceCopilotContentManager {
122
123
declare readonly _serviceBrand: undefined;
124
private promises: Promise<unknown>[] = [];
125
126
private responseScopedData = new Map<string, Map<string, ProjectData>>();
127
private generatePlanPrompt = this.instantiationService.createInstance(ProjectSpecificationGenerator);
128
private generateFilePrompt = this.instantiationService.createInstance(FileContentsGenerator);
129
130
constructor(
131
@IInstantiationService private readonly instantiationService: IInstantiationService,
132
) { }
133
134
// TODO@joyceerhl persistence between reloads
135
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {
136
const { userPrompt, projectStructure, chatMessages } = serviceArgs;
137
const promptArgs: NewWorkspaceContentsPromptProps = {
138
query: userPrompt,
139
fileTreeStr: projectStructure,
140
history: chatMessages
141
};
142
143
const projectSpecificationPromise = this.generatePlanPrompt.generate(promptArgs, CancellationToken.None);
144
this.promises.push(projectSpecificationPromise);
145
146
const sessionScopedData = this._getResponseScopedData(responseId);
147
148
const projectData: ProjectData = { userPrompt, projectSpecification: projectSpecificationPromise, projectStructure, fileTree: fileTree, chatMessages };
149
sessionScopedData.set(projectName, projectData);
150
}
151
152
get(responseId: string, path: string, prevFileContents: Map<string, string>): FileTreeDataWithContent | undefined {
153
const { projectName, path: relativePath } = this._getProjectMetadata(path);
154
const responseScopedData = this._getResponseScopedData(responseId);
155
const data = responseScopedData.get(projectName);
156
if (!data) {
157
return;
158
}
159
160
const fileNodes: FileTreeDataWithContent[] = data.fileTree.value;
161
const currentNode = findMatchingNodeFromPath(fileNodes, relativePath);
162
if (currentNode && !currentNode?.content) {
163
const nodeWithMissingContent = currentNode;
164
nodeWithMissingContent.content = this._getFileContent(data.userPrompt, data.projectStructure, data.projectSpecification, path, data.chatMessages, prevFileContents).catch(() => nodeWithMissingContent.content = undefined);
165
}
166
return currentNode;
167
}
168
169
private _prefetch(userPrompt: string, projectStructure: string, projectSpecification: Promise<string>, fileTree: vscode.ChatResponseFileTree, chatMessages: Raw.ChatMessage[]): FileTreeDataWithContent {
170
const ctime = Date.now();
171
if (fileTree.children) {
172
return { ...fileTree, type: FileType.Directory, children: fileTree.children.map((child) => this._prefetch(userPrompt, projectStructure, projectSpecification, child, chatMessages)), ctime };
173
}
174
// Disable prefetching for now
175
// node.content = this._getFileContent(userPrompt, projectStructure, projectSpecification, fileTreeData.uri.path, chatMessages).catch(() => node.content = undefined);
176
return { ...fileTree, type: FileType.File, content: undefined, ctime };
177
}
178
179
private async _getFileContent(projectDescription: string, projectStructure: string, projectSpecPromise: Promise<string>, filePath: string, chatMessages: Raw.ChatMessage[], prevFileContents: Map<string, string>): Promise<Uint8Array> {
180
const promptArgs: NewWorkspaceContentsPromptProps = {
181
query: projectDescription,
182
fileTreeStr: projectStructure,
183
filePath: filePath,
184
projectSpecification: await projectSpecPromise,
185
history: chatMessages,
186
relavantFiles: prevFileContents.has(filePath) ? new Map([[filePath, prevFileContents.get(filePath)!]]) : undefined
187
};
188
return this.generateFilePrompt.generate(promptArgs, CancellationToken.None).then((response) => Buffer.from(response));
189
}
190
191
private _getResponseScopedData(responseId: string) {
192
let responseScopedData = this.responseScopedData.get(responseId);
193
if (!responseScopedData) {
194
responseScopedData = new Map<string, ProjectData>();
195
this.responseScopedData.set(responseId, responseScopedData);
196
}
197
return responseScopedData;
198
}
199
200
private _getProjectMetadata(fullPath: string) {
201
// Format: vscode-copilot-workspace://<sessionId>/<projectName>/<filePath>
202
const [, projectName, ...path] = fullPath.split('/');
203
return { projectName, path };
204
}
205
}
206
207
interface GithubData {
208
org: string;
209
repo: string;
210
path: string;
211
fileTree: ChatResponseFileTreePart;
212
}
213
class NewWorkspaceGitHubContentManager {
214
215
private responseScopedData = new Map<string, Map<string, GithubData>>();
216
217
constructor(
218
@IGithubRepositoryService private readonly repositoryService: IGithubRepositoryService
219
) { }
220
221
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {
222
const githubContentMetadata = serviceArgs as NewWorkspaceGithubContentMetadata;
223
const sessionScopedData = this._getResponseScopedData(responseId);
224
const githubData: GithubData = { ...githubContentMetadata, fileTree };
225
sessionScopedData.set(projectName, githubData);
226
}
227
228
get(responseId: string, filePath: string): FileTreeDataWithContent | undefined {
229
const { projectName, path: relativePath } = this._getProjectMetadata(filePath);
230
const responseScopedData = this._getResponseScopedData(responseId);
231
const rootNode = responseScopedData.get(projectName);
232
if (!rootNode) {
233
return;
234
}
235
const fileNodes: FileTreeDataWithContent[] = rootNode.fileTree.value;
236
const currentNode = findMatchingNodeFromPath(fileNodes, relativePath);
237
if (currentNode && !currentNode?.content && !currentNode?.children) {
238
const nodeWithMissingContent = currentNode;
239
const folderPath = rootNode.path === '.' ? path.posix.relative(rootNode.repo, filePath) : path.posix.relative(rootNode.path, filePath.slice(1));
240
nodeWithMissingContent.content = this.repositoryService.getRepositoryItemContent(rootNode.org, rootNode.repo, folderPath).catch(() => nodeWithMissingContent.content = undefined);
241
}
242
return currentNode;
243
}
244
245
private _getProjectMetadata(fullPath: string) {
246
// Format: vscode-copilot-github-workspace://<sessionId>/<projectName>/<filePath>
247
const [, projectName, ...path] = fullPath.split('/');
248
return { projectName, path };
249
}
250
251
private _getResponseScopedData(responseId: string) {
252
let responseScopedData = this.responseScopedData.get(responseId);
253
if (!responseScopedData) {
254
responseScopedData = new Map<string, GithubData>();
255
this.responseScopedData.set(responseId, responseScopedData);
256
}
257
return responseScopedData;
258
}
259
}
260
261
interface FileData {
262
fileTree: ChatResponseFileTreePart;
263
content: string;
264
}
265
266
class NewWorkspaceFileContentManager {
267
268
private responseScopedData = new Map<string, Map<string, FileData>>();
269
270
constructor() {
271
}
272
273
set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {
274
const fileContents = serviceArgs as string;
275
const sessionScopedData = this._getResponseScopedData(responseId);
276
const fileContentData: FileData = { content: fileContents, fileTree };
277
sessionScopedData.set(projectName, fileContentData);
278
}
279
280
get(responseId: string, filePath: string): FileTreeDataWithContent | undefined {
281
const { projectName, path: relativePath } = this._getFileMetadata(filePath);
282
const responseScopedData = this._getResponseScopedData(responseId);
283
const rootNode = responseScopedData.get(projectName);
284
if (!rootNode) {
285
return;
286
}
287
const fileNodes: FileTreeDataWithContent[] = rootNode.fileTree.value;
288
const currentNode = findMatchingNodeFromPath(fileNodes, relativePath);
289
if (currentNode && !currentNode?.content && !currentNode?.children) {
290
currentNode.content = Promise.resolve(new Uint8Array(new TextEncoder().encode(rootNode.content)));
291
}
292
return currentNode;
293
}
294
295
private _getFileMetadata(fullPath: string) {
296
// Format: vscode-copilot-file://<sessionId>/<projectName>/<filePath>
297
const [, projectName, ...path] = fullPath.split('/');
298
return { projectName, path };
299
}
300
301
private _getResponseScopedData(responseId: string) {
302
let responseScopedData = this.responseScopedData.get(responseId);
303
if (!responseScopedData) {
304
responseScopedData = new Map<string, FileData>();
305
this.responseScopedData.set(responseId, responseScopedData);
306
}
307
return responseScopedData;
308
}
309
}
310
311
function findMatchingNodeFromPath(fileTree: vscode.ChatResponseFileTree[], pathElements: string[]): FileTreeDataWithContent | undefined {
312
let currentNode: FileTreeDataWithContent | undefined = undefined;
313
for (const element of pathElements) {
314
if (currentNode) {
315
if (currentNode.children) {
316
currentNode = currentNode.children.find(node => node.name === element) ?? currentNode;
317
}
318
} else {
319
currentNode = fileTree.find(node => node.name === element);
320
}
321
}
322
return currentNode;
323
}
324
325
export const newId = 'new';
326
327
export class NewWorkspaceIntent implements IIntent {
328
329
static readonly ID = Intent.New;
330
readonly id: string = Intent.New;
331
readonly locations = [ChatLocation.Panel];
332
readonly description: string = l10n.t('Scaffold code for a new file or project in a workspace');
333
334
readonly commandInfo: IIntentSlashCommandInfo = {
335
allowsEmptyArgs: false,
336
defaultEnablement: true,
337
};
338
339
constructor(
340
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
341
@IInstantiationService private readonly instantiationService: IInstantiationService,
342
) { }
343
344
async invoke(invocationContext: IIntentInvocationContext): Promise<IIntentInvocation> {
345
346
const location = invocationContext.location;
347
const endpoint = await this.endpointProvider.getChatEndpoint(invocationContext.request);
348
return this.instantiationService.createInstance(NewWorkspaceIntentInvocation, this, endpoint, location);
349
}
350
}
351
function createProjectCommand(fileTree: ChatResponseFileTreePart, workspaceRoot: Uri | undefined): vscode.Command {
352
return {
353
command: CreateProjectCommand,
354
arguments: [fileTree, workspaceRoot],
355
title: l10n.t('Create Workspace...'),
356
};
357
}
358
359
function createFileCommand(fileTree: ChatResponseFileTreePart): vscode.Command {
360
return {
361
command: CreateFileCommand,
362
arguments: [fileTree],
363
title: l10n.t('Create File...'),
364
};
365
}
366
367
export class NewWorkspaceIntentInvocation implements IIntentInvocation {
368
369
private githubContentMetadata?: NewWorkspaceGithubContentMetadata;
370
371
readonly linkification: IntentLinkificationOptions = { disable: true };
372
373
constructor(
374
readonly intent: NewWorkspaceIntent,
375
readonly endpoint: IChatEndpoint,
376
readonly location: ChatLocation,
377
@IInstantiationService private readonly instantiationService: IInstantiationService,
378
@IConfigurationService private readonly configurationService: IConfigurationService,
379
@INewWorkspacePreviewContentManager private readonly newWorkspacePreviewContentManager: INewWorkspacePreviewContentManager,
380
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
381
) { }
382
383
async getShouldUseProjectTemplate() {
384
const useProjectTemplates = this.configurationService.getConfig(ConfigKey.UseProjectTemplates);
385
if (useProjectTemplates !== undefined) {
386
return useProjectTemplates;
387
}
388
return false;
389
}
390
391
async buildPrompt(promptContext: IBuildPromptContext, progress: vscode.Progress<vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart>, token: vscode.CancellationToken) {
392
// TODO: @bhavyaus enable using project templates with variables
393
const { query, history, chatVariables } = promptContext;
394
const useTemplates = !chatVariables.hasVariables() && history[history.length - 1]?.request?.message !== query && await this.getShouldUseProjectTemplate();
395
const renderer = PromptRenderer.create(this.instantiationService, this.endpoint, NewWorkspacePrompt, {
396
promptContext,
397
useTemplates: useTemplates,
398
endpoint: this.endpoint,
399
});
400
401
const result = await renderer.render(progress, token);
402
const metadata = result.metadata.get(NewWorkspaceGithubContentMetadata);
403
if (metadata) {
404
this.githubContentMetadata = metadata;
405
}
406
407
return result;
408
}
409
410
processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: CancellationToken): Promise<void> {
411
const responseProcessor = new NewWorkspaceResponseProcessor(this.newWorkspacePreviewContentManager, this.workspaceService, this.githubContentMetadata);
412
return responseProcessor.processResponse(context, inputStream, outputStream, token);
413
}
414
}
415
416
417
function convertGitHubItemsToChatResponseFileTree(items: GithubRepositoryItem[], baseUri: Uri, isRepoRoot: boolean): ChatResponseFileTreePart {
418
let paths: string[];
419
if (isRepoRoot) {
420
paths = items.map(item => [baseUri.path, item.path].join('/'));
421
} else {
422
paths = items.map(item => item.path);
423
}
424
const rootName = paths[0].split('/')[0];
425
const root: vscode.ChatResponseFileTree = { name: rootName, children: [] };
426
const result: { [key: string]: vscode.ChatResponseFileTree } = { rootName: root };
427
for (const path of paths) {
428
const pathParts = path.split('/');
429
let currentPath = rootName;
430
let currentNode = root;
431
for (let i = 1; i < pathParts.length; i++) {
432
const pathPart = pathParts[i];
433
currentPath += `/${pathPart}`;
434
if (!result[currentPath]) {
435
const newNode: vscode.ChatResponseFileTree = { name: pathPart };
436
if (currentNode.children === undefined) {
437
currentNode.children = [];
438
}
439
currentNode.children.push(newNode);
440
result[currentPath] = newNode;
441
}
442
currentNode = result[currentPath];
443
}
444
}
445
let baseTree: vscode.ChatResponseFileTree[];
446
if (isRepoRoot) {
447
baseTree = root.children?.[0].children ?? [];
448
} else {
449
baseTree = root.children ?? [];
450
}
451
const sortedTree = baseTree?.sort((a, b) => (a.children && !b.children) ? -1 : 1) ?? [];
452
return new ChatResponseFileTreePart([{ name: rootName, children: sortedTree }], baseUri);
453
}
454
455
export const CopilotWorkspaceScheme = 'vscode-copilot-workspace';
456
export const GithubWorkspaceScheme = 'vscode-copilot-github-workspace';
457
export const CopilotFileScheme = 'vscode-copilot-file';
458
459
function getNewPreviewUri(requestId: string | undefined, filePath?: string, isGithubRepo: boolean = false,) {
460
return Uri.from({
461
scheme: isGithubRepo ? GithubWorkspaceScheme : CopilotWorkspaceScheme,
462
authority: requestId ?? '',
463
path: filePath ? `/${filePath}` : undefined
464
});
465
}
466
467
class NewWorkspaceResponseProcessor {
468
469
private _appliedText = '';
470
private _p = Promise.resolve('');
471
472
constructor(
473
private readonly newWorkspacePreviewContentManager: INewWorkspacePreviewContentManager,
474
private readonly workspaceService: IWorkspaceService,
475
private readonly githubContentMetadata?: NewWorkspaceGithubContentMetadata
476
) { }
477
478
async processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<void> {
479
const { turn, messages } = context;
480
481
let isBufferingFileTree = false;
482
let projectStructure = '';
483
const fileTreeStartRegex = /```filetree\n/;
484
const chatMessages = messages.filter(message => message.role !== Raw.ChatRole.System); // Exclude system messages as we want to use a different identity for the additional prompts we run
485
let hasReportingStarted = false;
486
487
for await (const { delta } of inputStream) {
488
if (token.isCancellationRequested) {
489
break;
490
}
491
492
const incomingText = delta.text;
493
this._p = this._p.then(async (): Promise<string> => {
494
const requestId = turn.id;
495
496
497
if (!incomingText) {
498
return this._appliedText;
499
}
500
501
this._appliedText += incomingText;
502
if (!this._appliedText.startsWith('#')) {
503
const userPrompt = turn.request.message;
504
const hasWholeCodeBlock = this._appliedText.match(/```filetree\n([\s\S]+?)\n```/);
505
if (hasWholeCodeBlock && (isBufferingFileTree || !hasReportingStarted)) {
506
isBufferingFileTree = false;
507
const [before, after] = this._appliedText.split(hasWholeCodeBlock[0]);
508
if (!hasReportingStarted) {
509
// We have the whole codeblock but we haven't started reporting yet.
510
// This only happens in test when the entire response is in the incomingText.
511
outputStream.markdown(before);
512
}
513
514
projectStructure = hasWholeCodeBlock[1];
515
const { chatResponseTree, projectName } = convertFileTreeToChatResponseFileTree(projectStructure, fp => getNewPreviewUri(requestId, fp));
516
outputStream.progress(l10n.t('Generating workspace preview...'));
517
outputStream.push(chatResponseTree);
518
outputStream.markdown(after);
519
520
this.newWorkspacePreviewContentManager.set(requestId, projectName, chatResponseTree, { userPrompt, projectStructure, chatMessages });
521
} else if ((this._appliedText.match(fileTreeStartRegex)) && !isBufferingFileTree && !hasWholeCodeBlock) {
522
isBufferingFileTree = true;
523
524
const [_, after] = this._appliedText.split(fileTreeStartRegex);
525
projectStructure += after;
526
527
outputStream.progress(l10n.t('Generating workspace preview...'));
528
} else if (isBufferingFileTree) {
529
projectStructure += incomingText;
530
} else if (!isBufferingFileTree && (!this._appliedText.match(/```/))) {
531
hasReportingStarted = true;
532
outputStream.markdown(incomingText);
533
}
534
} else if (/(?:.*\n){1,}/.test(this._appliedText)) {
535
outputStream.markdown(incomingText);
536
}
537
return this._appliedText;
538
});
539
}
540
541
await this._p;
542
543
if (turn.id &&
544
this.githubContentMetadata &&
545
this.githubContentMetadata.org &&
546
this.githubContentMetadata.repo &&
547
this.githubContentMetadata.path &&
548
this.githubContentMetadata.githubRepoItems &&
549
!this.newWorkspacePreviewContentManager.getFileTree(turn.id)) {
550
551
outputStream.reference(Uri.parse(this.githubContentMetadata.githubRepoItems[0].html_url));
552
553
outputStream.progress(l10n.t('Generating workspace preview...'));
554
const isRepoRoot = this.githubContentMetadata.path === '.';
555
const projectName = isRepoRoot ? this.githubContentMetadata.repo : this.githubContentMetadata.path.split('/')[0];
556
const chatResponseTree = convertGitHubItemsToChatResponseFileTree(this.githubContentMetadata.githubRepoItems, getNewPreviewUri(turn.id, projectName, true), isRepoRoot);
557
outputStream.push(chatResponseTree);
558
559
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
560
outputStream.button(createProjectCommand(chatResponseTree, workspaceFolders.length > 0 ? workspaceFolders[0] : undefined));
561
562
this.newWorkspacePreviewContentManager.set(turn.id, projectName, chatResponseTree, this.githubContentMetadata);
563
const query = encodeURIComponent(`["/${newId} ${turn.request.message}"]`);
564
const markdownString = new MarkdownString(l10n.t(`Hint: You can [regenerate this project without using this sample](command:workbench.action.chat.open?{0}) or use this [setting](command:workbench.action.openSettings?%5B%22github.copilot.chat.useProjectTemplates%22%5D) to configure the behavior.`, query));
565
markdownString.isTrusted = { enabledCommands: ['workbench.action.openSettings', 'workbench.action.chat.open'] };
566
outputStream.markdown(markdownString);
567
}
568
else {
569
const fileContentGeneration = extractCodeBlocks(this._appliedText);
570
if (fileContentGeneration.length === 2) {
571
let fileName;
572
try {
573
fileName = parse(fileContentGeneration[1].code);
574
} catch (e) {
575
throw e;
576
}
577
578
const baseUri = Uri.from({
579
scheme: CopilotFileScheme,
580
authority: turn.id,
581
path: `/${fileName.fileName}`
582
});
583
584
const fileTree = new ChatResponseFileTreePart([{ name: `${fileName.fileName}` }], baseUri);
585
const commandstr = commandUri(OpenFileCommand, [fileTree]);
586
const markdownString = new MarkdownString(`[${fileName.fileName}](${commandstr})`);
587
markdownString.isTrusted = { enabledCommands: [OpenFileCommand] };
588
outputStream.markdown(l10n.t('Sure, here is the file you requested:'));
589
outputStream.markdown(markdownString);
590
this.newWorkspacePreviewContentManager.set(turn.id, fileName.fileName, fileTree, fileContentGeneration[0].code);
591
}
592
}
593
594
this.pushCommands(turn.id, outputStream);
595
}
596
597
pushCommands(turnRequestId: string, outputStream: vscode.ChatResponseStream): void {
598
// Extract the Repo structure here
599
const fileTree = this.newWorkspacePreviewContentManager.getFileTree(turnRequestId);
600
if (!fileTree) {
601
return;
602
}
603
604
if (isGithubWorkspaceUri(fileTree.baseUri)) {
605
return;
606
}
607
else if (isCopiltoFileWorkspaceUri(fileTree.baseUri)) {
608
outputStream.button(createFileCommand(fileTree));
609
return;
610
}
611
612
const workspaceFolders = this.workspaceService.getWorkspaceFolders();
613
outputStream.button(createProjectCommand(fileTree, workspaceFolders.length > 0 ? workspaceFolders[0] : undefined));
614
}
615
}
616
617
function isGithubWorkspaceUri(uri: Uri): boolean {
618
return uri.scheme === GithubWorkspaceScheme;
619
}
620
621
function isCopiltoFileWorkspaceUri(uri: Uri): boolean {
622
return uri.scheme === CopilotFileScheme;
623
}
624
625