Path: blob/main/extensions/copilot/src/extension/intents/node/newIntent.ts
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 * as l10n from '@vscode/l10n';6import { Raw } from '@vscode/prompt-tsx';7import { parse } from 'jsonc-parser';8import type * as vscode from 'vscode';9import { IResponsePart } from '../../../platform/chat/common/chatMLFetcher';10import { ChatLocation } from '../../../platform/chat/common/commonTypes';11import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';12import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';13import { FileType } from '../../../platform/filesystem/common/fileTypes';14import { GithubRepositoryItem, IGithubRepositoryService } from '../../../platform/github/common/githubService';15import { IChatEndpoint } from '../../../platform/networking/common/networking';16import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';17import { extractCodeBlocks } from '../../../util/common/markdown';18import { createServiceIdentifier } from '../../../util/common/services';19import { CancellationToken } from '../../../util/vs/base/common/cancellation';20import * as path from '../../../util/vs/base/common/path';21import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';22import { ChatResponseFileTreePart, MarkdownString, Uri } from '../../../vscodeTypes';23import { Intent } from '../../common/constants';24import { commandUri } from '../../linkify/common/commands';25import { convertFileTreeToChatResponseFileTree } from '../../prompt/common/fileTreeParser';26import { IBuildPromptContext } from '../../prompt/common/intents';27import { IIntent, IIntentInvocation, IIntentInvocationContext, IIntentSlashCommandInfo, IntentLinkificationOptions, IResponseProcessorContext } from '../../prompt/node/intents';28import { PromptRenderer } from '../../prompts/node/base/promptRenderer';29import { NewWorkspaceGithubContentMetadata, NewWorkspacePrompt } from '../../prompts/node/panel/newWorkspace/newWorkspace';30import { NewWorkspaceContentsPromptProps } from '../../prompts/node/panel/newWorkspace/newWorkspaceContents';31import { FileContentsGenerator, ProjectSpecificationGenerator } from './generateNewWorkspaceContent';323334interface FileTreeDataWithContent extends vscode.ChatResponseFileTree {35content?: Promise<Uint8Array | undefined>;36ctime?: number;37type?: FileType;38}3940export const INewWorkspacePreviewContentManager = createServiceIdentifier<INewWorkspacePreviewContentManager>('INewWorkspacePreviewContentManager');41export interface INewWorkspacePreviewContentManager {42readonly _serviceBrand: undefined;43set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any): void;44get(uri: Uri): FileTreeDataWithContent | undefined;45getFileTree(responseId: string): ChatResponseFileTreePart | undefined;46}4748export const CreateProjectCommand = 'github.copilot.createProject';49export const CreateFileCommand = 'github.copilot.createFile';50export const OpenFileCommand = 'github.copilot.openFile';5152export class NewWorkspacePreviewContentManagerImpl implements INewWorkspacePreviewContentManager {53declare readonly _serviceBrand: undefined;54private readonly copilotContentManager: NewWorkspaceCopilotContentManager;55private readonly githubContentManager: NewWorkspaceGitHubContentManager;56private readonly fileContentManager: NewWorkspaceFileContentManager;57private responseScopedData = new Map<string, ChatResponseFileTreePart>();58private prevResponseId: string | undefined;59private prevFileContents = new Map<string, string>();6061constructor(62@IInstantiationService instantiationService: IInstantiationService,63) {64this.copilotContentManager = instantiationService.createInstance(NewWorkspaceCopilotContentManager);65this.githubContentManager = instantiationService.createInstance(NewWorkspaceGitHubContentManager);66this.fileContentManager = new NewWorkspaceFileContentManager();67}6869set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {70this.responseScopedData.set(responseId, fileTree);71if (isGithubWorkspaceUri(fileTree.baseUri)) {72this.githubContentManager.set(responseId, projectName, fileTree, serviceArgs);73} else if (isCopiltoFileWorkspaceUri(fileTree.baseUri)) {74this.fileContentManager.set(responseId, projectName, fileTree, serviceArgs);75} else {76this.copilotContentManager.set(responseId, projectName, fileTree, serviceArgs);77}78}7980get(uri: Uri): FileTreeDataWithContent | undefined {81if (this.prevResponseId !== uri.authority) {82this.prevFileContents.clear();83this.prevResponseId = uri.authority;84}8586let fileContents: FileTreeDataWithContent | undefined;87if (isGithubWorkspaceUri(uri)) {88fileContents = this.githubContentManager.get(uri.authority, uri.path);89} else if (isCopiltoFileWorkspaceUri(uri)) {90fileContents = this.fileContentManager.get(uri.authority, uri.path);91} else {92fileContents = this.copilotContentManager.get(uri.authority, uri.path, this.prevFileContents);93}9495fileContents?.content?.then((content) => {96if (this.prevFileContents.has(uri.path)) {97return;98}99const decoder = new TextDecoder();100const fileContentStr = decoder.decode(content);101this.prevFileContents.set(uri.path, fileContentStr);102});103104return fileContents;105}106107getFileTree(responseId: string): ChatResponseFileTreePart | undefined {108return this.responseScopedData.get(responseId);109}110}111112interface ProjectData {113userPrompt: string;114projectStructure: string;115projectSpecification: Promise<string>;116fileTree: ChatResponseFileTreePart;117chatMessages: Raw.ChatMessage[];118}119120export class NewWorkspaceCopilotContentManager {121122declare readonly _serviceBrand: undefined;123private promises: Promise<unknown>[] = [];124125private responseScopedData = new Map<string, Map<string, ProjectData>>();126private generatePlanPrompt = this.instantiationService.createInstance(ProjectSpecificationGenerator);127private generateFilePrompt = this.instantiationService.createInstance(FileContentsGenerator);128129constructor(130@IInstantiationService private readonly instantiationService: IInstantiationService,131) { }132133// TODO@joyceerhl persistence between reloads134set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {135const { userPrompt, projectStructure, chatMessages } = serviceArgs;136const promptArgs: NewWorkspaceContentsPromptProps = {137query: userPrompt,138fileTreeStr: projectStructure,139history: chatMessages140};141142const projectSpecificationPromise = this.generatePlanPrompt.generate(promptArgs, CancellationToken.None);143this.promises.push(projectSpecificationPromise);144145const sessionScopedData = this._getResponseScopedData(responseId);146147const projectData: ProjectData = { userPrompt, projectSpecification: projectSpecificationPromise, projectStructure, fileTree: fileTree, chatMessages };148sessionScopedData.set(projectName, projectData);149}150151get(responseId: string, path: string, prevFileContents: Map<string, string>): FileTreeDataWithContent | undefined {152const { projectName, path: relativePath } = this._getProjectMetadata(path);153const responseScopedData = this._getResponseScopedData(responseId);154const data = responseScopedData.get(projectName);155if (!data) {156return;157}158159const fileNodes: FileTreeDataWithContent[] = data.fileTree.value;160const currentNode = findMatchingNodeFromPath(fileNodes, relativePath);161if (currentNode && !currentNode?.content) {162const nodeWithMissingContent = currentNode;163nodeWithMissingContent.content = this._getFileContent(data.userPrompt, data.projectStructure, data.projectSpecification, path, data.chatMessages, prevFileContents).catch(() => nodeWithMissingContent.content = undefined);164}165return currentNode;166}167168private _prefetch(userPrompt: string, projectStructure: string, projectSpecification: Promise<string>, fileTree: vscode.ChatResponseFileTree, chatMessages: Raw.ChatMessage[]): FileTreeDataWithContent {169const ctime = Date.now();170if (fileTree.children) {171return { ...fileTree, type: FileType.Directory, children: fileTree.children.map((child) => this._prefetch(userPrompt, projectStructure, projectSpecification, child, chatMessages)), ctime };172}173// Disable prefetching for now174// node.content = this._getFileContent(userPrompt, projectStructure, projectSpecification, fileTreeData.uri.path, chatMessages).catch(() => node.content = undefined);175return { ...fileTree, type: FileType.File, content: undefined, ctime };176}177178private async _getFileContent(projectDescription: string, projectStructure: string, projectSpecPromise: Promise<string>, filePath: string, chatMessages: Raw.ChatMessage[], prevFileContents: Map<string, string>): Promise<Uint8Array> {179const promptArgs: NewWorkspaceContentsPromptProps = {180query: projectDescription,181fileTreeStr: projectStructure,182filePath: filePath,183projectSpecification: await projectSpecPromise,184history: chatMessages,185relavantFiles: prevFileContents.has(filePath) ? new Map([[filePath, prevFileContents.get(filePath)!]]) : undefined186};187return this.generateFilePrompt.generate(promptArgs, CancellationToken.None).then((response) => Buffer.from(response));188}189190private _getResponseScopedData(responseId: string) {191let responseScopedData = this.responseScopedData.get(responseId);192if (!responseScopedData) {193responseScopedData = new Map<string, ProjectData>();194this.responseScopedData.set(responseId, responseScopedData);195}196return responseScopedData;197}198199private _getProjectMetadata(fullPath: string) {200// Format: vscode-copilot-workspace://<sessionId>/<projectName>/<filePath>201const [, projectName, ...path] = fullPath.split('/');202return { projectName, path };203}204}205206interface GithubData {207org: string;208repo: string;209path: string;210fileTree: ChatResponseFileTreePart;211}212class NewWorkspaceGitHubContentManager {213214private responseScopedData = new Map<string, Map<string, GithubData>>();215216constructor(217@IGithubRepositoryService private readonly repositoryService: IGithubRepositoryService218) { }219220set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {221const githubContentMetadata = serviceArgs as NewWorkspaceGithubContentMetadata;222const sessionScopedData = this._getResponseScopedData(responseId);223const githubData: GithubData = { ...githubContentMetadata, fileTree };224sessionScopedData.set(projectName, githubData);225}226227get(responseId: string, filePath: string): FileTreeDataWithContent | undefined {228const { projectName, path: relativePath } = this._getProjectMetadata(filePath);229const responseScopedData = this._getResponseScopedData(responseId);230const rootNode = responseScopedData.get(projectName);231if (!rootNode) {232return;233}234const fileNodes: FileTreeDataWithContent[] = rootNode.fileTree.value;235const currentNode = findMatchingNodeFromPath(fileNodes, relativePath);236if (currentNode && !currentNode?.content && !currentNode?.children) {237const nodeWithMissingContent = currentNode;238const folderPath = rootNode.path === '.' ? path.posix.relative(rootNode.repo, filePath) : path.posix.relative(rootNode.path, filePath.slice(1));239nodeWithMissingContent.content = this.repositoryService.getRepositoryItemContent(rootNode.org, rootNode.repo, folderPath).catch(() => nodeWithMissingContent.content = undefined);240}241return currentNode;242}243244private _getProjectMetadata(fullPath: string) {245// Format: vscode-copilot-github-workspace://<sessionId>/<projectName>/<filePath>246const [, projectName, ...path] = fullPath.split('/');247return { projectName, path };248}249250private _getResponseScopedData(responseId: string) {251let responseScopedData = this.responseScopedData.get(responseId);252if (!responseScopedData) {253responseScopedData = new Map<string, GithubData>();254this.responseScopedData.set(responseId, responseScopedData);255}256return responseScopedData;257}258}259260interface FileData {261fileTree: ChatResponseFileTreePart;262content: string;263}264265class NewWorkspaceFileContentManager {266267private responseScopedData = new Map<string, Map<string, FileData>>();268269constructor() {270}271272set(responseId: string, projectName: string, fileTree: ChatResponseFileTreePart, serviceArgs: any) {273const fileContents = serviceArgs as string;274const sessionScopedData = this._getResponseScopedData(responseId);275const fileContentData: FileData = { content: fileContents, fileTree };276sessionScopedData.set(projectName, fileContentData);277}278279get(responseId: string, filePath: string): FileTreeDataWithContent | undefined {280const { projectName, path: relativePath } = this._getFileMetadata(filePath);281const responseScopedData = this._getResponseScopedData(responseId);282const rootNode = responseScopedData.get(projectName);283if (!rootNode) {284return;285}286const fileNodes: FileTreeDataWithContent[] = rootNode.fileTree.value;287const currentNode = findMatchingNodeFromPath(fileNodes, relativePath);288if (currentNode && !currentNode?.content && !currentNode?.children) {289currentNode.content = Promise.resolve(new Uint8Array(new TextEncoder().encode(rootNode.content)));290}291return currentNode;292}293294private _getFileMetadata(fullPath: string) {295// Format: vscode-copilot-file://<sessionId>/<projectName>/<filePath>296const [, projectName, ...path] = fullPath.split('/');297return { projectName, path };298}299300private _getResponseScopedData(responseId: string) {301let responseScopedData = this.responseScopedData.get(responseId);302if (!responseScopedData) {303responseScopedData = new Map<string, FileData>();304this.responseScopedData.set(responseId, responseScopedData);305}306return responseScopedData;307}308}309310function findMatchingNodeFromPath(fileTree: vscode.ChatResponseFileTree[], pathElements: string[]): FileTreeDataWithContent | undefined {311let currentNode: FileTreeDataWithContent | undefined = undefined;312for (const element of pathElements) {313if (currentNode) {314if (currentNode.children) {315currentNode = currentNode.children.find(node => node.name === element) ?? currentNode;316}317} else {318currentNode = fileTree.find(node => node.name === element);319}320}321return currentNode;322}323324export const newId = 'new';325326export class NewWorkspaceIntent implements IIntent {327328static readonly ID = Intent.New;329readonly id: string = Intent.New;330readonly locations = [ChatLocation.Panel];331readonly description: string = l10n.t('Scaffold code for a new file or project in a workspace');332333readonly commandInfo: IIntentSlashCommandInfo = {334allowsEmptyArgs: false,335defaultEnablement: true,336};337338constructor(339@IEndpointProvider private readonly endpointProvider: IEndpointProvider,340@IInstantiationService private readonly instantiationService: IInstantiationService,341) { }342343async invoke(invocationContext: IIntentInvocationContext): Promise<IIntentInvocation> {344345const location = invocationContext.location;346const endpoint = await this.endpointProvider.getChatEndpoint(invocationContext.request);347return this.instantiationService.createInstance(NewWorkspaceIntentInvocation, this, endpoint, location);348}349}350function createProjectCommand(fileTree: ChatResponseFileTreePart, workspaceRoot: Uri | undefined): vscode.Command {351return {352command: CreateProjectCommand,353arguments: [fileTree, workspaceRoot],354title: l10n.t('Create Workspace...'),355};356}357358function createFileCommand(fileTree: ChatResponseFileTreePart): vscode.Command {359return {360command: CreateFileCommand,361arguments: [fileTree],362title: l10n.t('Create File...'),363};364}365366export class NewWorkspaceIntentInvocation implements IIntentInvocation {367368private githubContentMetadata?: NewWorkspaceGithubContentMetadata;369370readonly linkification: IntentLinkificationOptions = { disable: true };371372constructor(373readonly intent: NewWorkspaceIntent,374readonly endpoint: IChatEndpoint,375readonly location: ChatLocation,376@IInstantiationService private readonly instantiationService: IInstantiationService,377@IConfigurationService private readonly configurationService: IConfigurationService,378@INewWorkspacePreviewContentManager private readonly newWorkspacePreviewContentManager: INewWorkspacePreviewContentManager,379@IWorkspaceService private readonly workspaceService: IWorkspaceService,380) { }381382async getShouldUseProjectTemplate() {383const useProjectTemplates = this.configurationService.getConfig(ConfigKey.UseProjectTemplates);384if (useProjectTemplates !== undefined) {385return useProjectTemplates;386}387return false;388}389390async buildPrompt(promptContext: IBuildPromptContext, progress: vscode.Progress<vscode.ChatResponseProgressPart | vscode.ChatResponseReferencePart>, token: vscode.CancellationToken) {391// TODO: @bhavyaus enable using project templates with variables392const { query, history, chatVariables } = promptContext;393const useTemplates = !chatVariables.hasVariables() && history[history.length - 1]?.request?.message !== query && await this.getShouldUseProjectTemplate();394const renderer = PromptRenderer.create(this.instantiationService, this.endpoint, NewWorkspacePrompt, {395promptContext,396useTemplates: useTemplates,397endpoint: this.endpoint,398});399400const result = await renderer.render(progress, token);401const metadata = result.metadata.get(NewWorkspaceGithubContentMetadata);402if (metadata) {403this.githubContentMetadata = metadata;404}405406return result;407}408409processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: CancellationToken): Promise<void> {410const responseProcessor = new NewWorkspaceResponseProcessor(this.newWorkspacePreviewContentManager, this.workspaceService, this.githubContentMetadata);411return responseProcessor.processResponse(context, inputStream, outputStream, token);412}413}414415416function convertGitHubItemsToChatResponseFileTree(items: GithubRepositoryItem[], baseUri: Uri, isRepoRoot: boolean): ChatResponseFileTreePart {417let paths: string[];418if (isRepoRoot) {419paths = items.map(item => [baseUri.path, item.path].join('/'));420} else {421paths = items.map(item => item.path);422}423const rootName = paths[0].split('/')[0];424const root: vscode.ChatResponseFileTree = { name: rootName, children: [] };425const result: { [key: string]: vscode.ChatResponseFileTree } = { rootName: root };426for (const path of paths) {427const pathParts = path.split('/');428let currentPath = rootName;429let currentNode = root;430for (let i = 1; i < pathParts.length; i++) {431const pathPart = pathParts[i];432currentPath += `/${pathPart}`;433if (!result[currentPath]) {434const newNode: vscode.ChatResponseFileTree = { name: pathPart };435if (currentNode.children === undefined) {436currentNode.children = [];437}438currentNode.children.push(newNode);439result[currentPath] = newNode;440}441currentNode = result[currentPath];442}443}444let baseTree: vscode.ChatResponseFileTree[];445if (isRepoRoot) {446baseTree = root.children?.[0].children ?? [];447} else {448baseTree = root.children ?? [];449}450const sortedTree = baseTree?.sort((a, b) => (a.children && !b.children) ? -1 : 1) ?? [];451return new ChatResponseFileTreePart([{ name: rootName, children: sortedTree }], baseUri);452}453454export const CopilotWorkspaceScheme = 'vscode-copilot-workspace';455export const GithubWorkspaceScheme = 'vscode-copilot-github-workspace';456export const CopilotFileScheme = 'vscode-copilot-file';457458function getNewPreviewUri(requestId: string | undefined, filePath?: string, isGithubRepo: boolean = false,) {459return Uri.from({460scheme: isGithubRepo ? GithubWorkspaceScheme : CopilotWorkspaceScheme,461authority: requestId ?? '',462path: filePath ? `/${filePath}` : undefined463});464}465466class NewWorkspaceResponseProcessor {467468private _appliedText = '';469private _p = Promise.resolve('');470471constructor(472private readonly newWorkspacePreviewContentManager: INewWorkspacePreviewContentManager,473private readonly workspaceService: IWorkspaceService,474private readonly githubContentMetadata?: NewWorkspaceGithubContentMetadata475) { }476477async processResponse(context: IResponseProcessorContext, inputStream: AsyncIterable<IResponsePart>, outputStream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<void> {478const { turn, messages } = context;479480let isBufferingFileTree = false;481let projectStructure = '';482const fileTreeStartRegex = /```filetree\n/;483const 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 run484let hasReportingStarted = false;485486for await (const { delta } of inputStream) {487if (token.isCancellationRequested) {488break;489}490491const incomingText = delta.text;492this._p = this._p.then(async (): Promise<string> => {493const requestId = turn.id;494495496if (!incomingText) {497return this._appliedText;498}499500this._appliedText += incomingText;501if (!this._appliedText.startsWith('#')) {502const userPrompt = turn.request.message;503const hasWholeCodeBlock = this._appliedText.match(/```filetree\n([\s\S]+?)\n```/);504if (hasWholeCodeBlock && (isBufferingFileTree || !hasReportingStarted)) {505isBufferingFileTree = false;506const [before, after] = this._appliedText.split(hasWholeCodeBlock[0]);507if (!hasReportingStarted) {508// We have the whole codeblock but we haven't started reporting yet.509// This only happens in test when the entire response is in the incomingText.510outputStream.markdown(before);511}512513projectStructure = hasWholeCodeBlock[1];514const { chatResponseTree, projectName } = convertFileTreeToChatResponseFileTree(projectStructure, fp => getNewPreviewUri(requestId, fp));515outputStream.progress(l10n.t('Generating workspace preview...'));516outputStream.push(chatResponseTree);517outputStream.markdown(after);518519this.newWorkspacePreviewContentManager.set(requestId, projectName, chatResponseTree, { userPrompt, projectStructure, chatMessages });520} else if ((this._appliedText.match(fileTreeStartRegex)) && !isBufferingFileTree && !hasWholeCodeBlock) {521isBufferingFileTree = true;522523const [_, after] = this._appliedText.split(fileTreeStartRegex);524projectStructure += after;525526outputStream.progress(l10n.t('Generating workspace preview...'));527} else if (isBufferingFileTree) {528projectStructure += incomingText;529} else if (!isBufferingFileTree && (!this._appliedText.match(/```/))) {530hasReportingStarted = true;531outputStream.markdown(incomingText);532}533} else if (/(?:.*\n){1,}/.test(this._appliedText)) {534outputStream.markdown(incomingText);535}536return this._appliedText;537});538}539540await this._p;541542if (turn.id &&543this.githubContentMetadata &&544this.githubContentMetadata.org &&545this.githubContentMetadata.repo &&546this.githubContentMetadata.path &&547this.githubContentMetadata.githubRepoItems &&548!this.newWorkspacePreviewContentManager.getFileTree(turn.id)) {549550outputStream.reference(Uri.parse(this.githubContentMetadata.githubRepoItems[0].html_url));551552outputStream.progress(l10n.t('Generating workspace preview...'));553const isRepoRoot = this.githubContentMetadata.path === '.';554const projectName = isRepoRoot ? this.githubContentMetadata.repo : this.githubContentMetadata.path.split('/')[0];555const chatResponseTree = convertGitHubItemsToChatResponseFileTree(this.githubContentMetadata.githubRepoItems, getNewPreviewUri(turn.id, projectName, true), isRepoRoot);556outputStream.push(chatResponseTree);557558const workspaceFolders = this.workspaceService.getWorkspaceFolders();559outputStream.button(createProjectCommand(chatResponseTree, workspaceFolders.length > 0 ? workspaceFolders[0] : undefined));560561this.newWorkspacePreviewContentManager.set(turn.id, projectName, chatResponseTree, this.githubContentMetadata);562const query = encodeURIComponent(`["/${newId} ${turn.request.message}"]`);563const 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));564markdownString.isTrusted = { enabledCommands: ['workbench.action.openSettings', 'workbench.action.chat.open'] };565outputStream.markdown(markdownString);566}567else {568const fileContentGeneration = extractCodeBlocks(this._appliedText);569if (fileContentGeneration.length === 2) {570let fileName;571try {572fileName = parse(fileContentGeneration[1].code);573} catch (e) {574throw e;575}576577const baseUri = Uri.from({578scheme: CopilotFileScheme,579authority: turn.id,580path: `/${fileName.fileName}`581});582583const fileTree = new ChatResponseFileTreePart([{ name: `${fileName.fileName}` }], baseUri);584const commandstr = commandUri(OpenFileCommand, [fileTree]);585const markdownString = new MarkdownString(`[${fileName.fileName}](${commandstr})`);586markdownString.isTrusted = { enabledCommands: [OpenFileCommand] };587outputStream.markdown(l10n.t('Sure, here is the file you requested:'));588outputStream.markdown(markdownString);589this.newWorkspacePreviewContentManager.set(turn.id, fileName.fileName, fileTree, fileContentGeneration[0].code);590}591}592593this.pushCommands(turn.id, outputStream);594}595596pushCommands(turnRequestId: string, outputStream: vscode.ChatResponseStream): void {597// Extract the Repo structure here598const fileTree = this.newWorkspacePreviewContentManager.getFileTree(turnRequestId);599if (!fileTree) {600return;601}602603if (isGithubWorkspaceUri(fileTree.baseUri)) {604return;605}606else if (isCopiltoFileWorkspaceUri(fileTree.baseUri)) {607outputStream.button(createFileCommand(fileTree));608return;609}610611const workspaceFolders = this.workspaceService.getWorkspaceFolders();612outputStream.button(createProjectCommand(fileTree, workspaceFolders.length > 0 ? workspaceFolders[0] : undefined));613}614}615616function isGithubWorkspaceUri(uri: Uri): boolean {617return uri.scheme === GithubWorkspaceScheme;618}619620function isCopiltoFileWorkspaceUri(uri: Uri): boolean {621return uri.scheme === CopilotFileScheme;622}623624625