diff --git a/package.json b/package.json index 2aa2b185e6..2478f69664 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,8 @@ "contribEditorContentMenu", "chatPromptFiles", "mcpServerDefinitions", - "tabInputMultiDiff" + "tabInputMultiDiff", + "workspaceTrust" ], "contributes": { "languageModelTools": [ @@ -2140,6 +2141,12 @@ "icon": "$(terminal)", "category": "Copilot CLI" }, + { + "command": "github.copilot.cli.sessions.openRepository", + "title": "%github.copilot.command.cli.sessions.openRepository%", + "icon": "$(folder-opened)", + "category": "Copilot CLI" + }, { "command": "github.copilot.chat.replay", "title": "Start Chat Replay", @@ -4527,6 +4534,10 @@ "command": "github.copilot.cli.sessions.resumeInTerminal", "when": "false" }, + { + "command": "github.copilot.cli.sessions.openRepository", + "when": "false" + }, { "command": "github.copilot.cloud.sessions.openInBrowser", "when": "false" diff --git a/package.nls.json b/package.nls.json index c7bff62be0..34da136b9f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -377,6 +377,7 @@ "github.copilot.config.nextEditSuggestions.preferredModel": "Preferred model for next edit suggestions.", "github.copilot.command.deleteAgentSession": "Delete...", "github.copilot.command.cli.sessions.resumeInTerminal": "Resume in Terminal", + "github.copilot.command.cli.sessions.openRepository": "Open Repository", "github.copilot.command.openCopilotAgentSessionsInBrowser": "Open in Browser", "github.copilot.command.closeChatSessionPullRequest.title": "Close Pull Request", "github.copilot.command.installPRExtension.title": "Install GitHub Pull Request Extension", diff --git a/src/extension/chatSessions/common/chatSessionWorkspaceFolderService.ts b/src/extension/chatSessions/common/chatSessionWorkspaceFolderService.ts index a715374a48..570d22c277 100644 --- a/src/extension/chatSessions/common/chatSessionWorkspaceFolderService.ts +++ b/src/extension/chatSessions/common/chatSessionWorkspaceFolderService.ts @@ -15,6 +15,7 @@ export const IChatSessionWorkspaceFolderService = createServiceIdentifier; /** * Track workspace folder selection for a session (for folders without git repos in multi-root workspaces) diff --git a/src/extension/chatSessions/vscode-node/chatSessionWorkspaceFolderServiceImpl.ts b/src/extension/chatSessions/vscode-node/chatSessionWorkspaceFolderServiceImpl.ts index 2c2aef8c0c..0caf2bda75 100644 --- a/src/extension/chatSessions/vscode-node/chatSessionWorkspaceFolderServiceImpl.ts +++ b/src/extension/chatSessions/vscode-node/chatSessionWorkspaceFolderServiceImpl.ts @@ -92,6 +92,21 @@ export class ChatSessionWorkspaceFolderService extends Disposable implements ICh this.logService.trace(`[ChatSessionWorkspaceFolderService] Cleaned up old entries, kept ${entriesToKeep.length}`); } + public getRecentFolders(): { folder: vscode.Uri; lastAccessTime: number }[] { + const data = this.extensionContext.globalState.get>(CHAT_SESSION_WORKSPACE_FOLDER_MEMENTO_KEY, {}); + const recentFolders: { folder: vscode.Uri; lastAccessTime: number }[] = []; + for (const entry of Object.values(data)) { + if (typeof entry === 'string') { + continue; + } + if (isUntitledSessionId(entry.folderPath)) { + continue; // Skip untitled sessions that may have been saved. + } + recentFolders.push({ folder: vscode.Uri.file(entry.folderPath), lastAccessTime: entry.timestamp }); + } + recentFolders.sort((a, b) => b.lastAccessTime - a.lastAccessTime); + return recentFolders; + } async deleteTrackedWorkspaceFolder(sessionId: string): Promise { this._sessionWorkspaceFolders.delete(sessionId); const data = this.extensionContext.globalState.get>(CHAT_SESSION_WORKSPACE_FOLDER_MEMENTO_KEY, {}); diff --git a/src/extension/chatSessions/vscode-node/chatSessions.ts b/src/extension/chatSessions/vscode-node/chatSessions.ts index c82ff42a1c..6e3628a039 100644 --- a/src/extension/chatSessions/vscode-node/chatSessions.ts +++ b/src/extension/chatSessions/vscode-node/chatSessions.ts @@ -134,7 +134,6 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib const copilotcliChatSessionParticipant = this._register(copilotcliAgentInstaService.createInstance( CopilotCLIChatSessionParticipant, - copilotcliSessionIsolationManager, copilotcliChatSessionContentProvider, promptResolver, copilotcliSessionItemProvider, @@ -145,7 +144,7 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib const copilotCLIWorkspaceFolderSessions = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(IChatSessionWorkspaceFolderService)); const copilotcliParticipant = vscode.chat.createChatParticipant(this.copilotcliSessionType, copilotcliChatSessionParticipant.createHandler()); this._register(vscode.chat.registerChatSessionContentProvider(this.copilotcliSessionType, copilotcliChatSessionContentProvider, copilotcliParticipant)); - this._register(registerCLIChatCommands(copilotcliSessionItemProvider, copilotCLISessionService, copilotCLIWorktreeManagerService, gitService, copilotCLIWorkspaceFolderSessions)); + this._register(registerCLIChatCommands(copilotcliSessionItemProvider, copilotCLISessionService, copilotCLIWorktreeManagerService, gitService, copilotCLIWorkspaceFolderSessions, copilotcliChatSessionContentProvider)); } private registerCopilotCloudAgent() { diff --git a/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index 99e5aedf66..9cee479493 100644 --- a/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -14,12 +14,14 @@ import { ILogService } from '../../../platform/log/common/logService'; import { IPromptsService, ParsedPromptFile } from '../../../platform/promptFiles/common/promptsService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; +import { isUri } from '../../../util/common/types'; import { disposableTimeout, raceCancellation } from '../../../util/vs/base/common/async'; import { isCancellationError } from '../../../util/vs/base/common/errors'; import { Emitter, Event } from '../../../util/vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, IReference, toDisposable } from '../../../util/vs/base/common/lifecycle'; +import { ResourceMap, ResourceSet } from '../../../util/vs/base/common/map'; import { autorun } from '../../../util/vs/base/common/observable'; -import { extUri, isEqual } from '../../../util/vs/base/common/resources'; +import { basename, extUri, isEqual } from '../../../util/vs/base/common/resources'; import { URI } from '../../../util/vs/base/common/uri'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ToolCall } from '../../agents/copilotcli/common/copilotCLITools'; @@ -42,6 +44,7 @@ import { CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider'; const AGENTS_OPTION_ID = 'agent'; const MODELS_OPTION_ID = 'model'; const REPOSITORY_OPTION_ID = 'repository'; +const OPEN_REPOSITORY_COMMAND_ID = 'github.copilot.cli.sessions.openRepository'; const UncommittedChangesStep = 'uncommitted-changes'; type ConfirmationResult = { step: string; accepted: boolean; metadata?: CLIConfirmationMetadata }; @@ -66,6 +69,15 @@ const _sessionModel: Map = new Map(); // As a temporary solution, return the same untitled session id back to core until the session is completed. const _untitledSessionIdMap = new Map(); +// We want to keep track of the date/time when a repo was added. +const untitledWorkspaceRepositories = new ResourceMap(); +// We want to keep track of the date/time when a folder was added. +const untitledWorkspaceFodlers = new ResourceMap(); + +const listOfKnownRepos = new ResourceSet(); + +const untrustedFolderMessage = l10n.t('The selected folder is not trusted. Please trust the folder to continue with the {0}.', 'Background Agent'); + export class CopilotCLISessionIsolationManager { private _sessionIsolation: Map = new Map(); @@ -262,6 +274,10 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements this._onDidChangeChatSessionOptions.fire({ resource, updates }); } + public notifyProviderOptionsChange(): void { + this._onDidChangeChatSessionProviderOptions.fire(); + } + async provideChatSessionContent(resource: Uri, token: vscode.CancellationToken): Promise { const copilotcliSessionId = SessionIdForCLI.parse(resource); const workingDirectoryValue = this.copilotCLIWorktreeManagerService.getWorktreePath(copilotcliSessionId); @@ -287,46 +303,79 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements options[MODELS_OPTION_ID] = model; } - const repositories = this.getRepositoryOptionItems(); - if (repositories.length > 1) { - // Check if this session has a workspace folder tracked (for folders without git repos) - const sessionWorkspaceFolder = this.workspaceFolderService.getSessionWorkspaceFolder(copilotcliSessionId); + // Handle repository options based on workspace type + if (this.isUntitledWorkspace()) { + // For untitled workspaces, check if session already has a repository selected const repository = await this.copilotCLIWorktreeManagerService.getWorktreeRepository(copilotcliSessionId); + const sessionRepository = this.copilotCLIWorktreeManagerService.getSessionRepository(copilotcliSessionId); if (repository) { options[REPOSITORY_OPTION_ID] = { ...toRepositoryOptionItem(repository), locked: true }; - } else if (sessionWorkspaceFolder) { - const folderName = this.workspaceService.getWorkspaceFolderName(sessionWorkspaceFolder); + } else if (sessionRepository) { options[REPOSITORY_OPTION_ID] = { - ...toWorkspaceFolderOptionItem(sessionWorkspaceFolder, folderName), + ...toRepositoryOptionItem(sessionRepository), locked: true }; } else if (isUntitledSessionId(copilotcliSessionId)) { - const firstOption = repositories[0]; - options[REPOSITORY_OPTION_ID] = firstOption.id; - // Track based on whether first option is a git repo or workspace folder - if (this.isWorkspaceFolderWithoutRepo(firstOption.id)) { - await Promise.all([ - this.workspaceFolderService.trackSessionWorkspaceFolder(copilotcliSessionId, firstOption.id), - this.copilotCLIWorktreeManagerService.deleteSessionRepository(copilotcliSessionId) - ]); + // For new untitled sessions in untitled workspaces, auto-select the first last-used repo if available + const lastUsedRepos = await this.getRepositoryOptionItemsForUntitledWorkspace(); + if (lastUsedRepos.length > 0) { + const firstRepo = lastUsedRepos[0]; + options[REPOSITORY_OPTION_ID] = firstRepo.id; + if (listOfKnownRepos.has(URI.file(firstRepo.id))) { + await this.copilotCLIWorktreeManagerService.setSessionRepository(copilotcliSessionId, firstRepo.id); + await this.workspaceFolderService.deleteTrackedWorkspaceFolder(copilotcliSessionId); + } else { + await this.workspaceFolderService.trackSessionWorkspaceFolder(copilotcliSessionId, firstRepo.id); + await this.copilotCLIWorktreeManagerService.deleteSessionRepository(copilotcliSessionId); + } + } + } + } else { + const repositories = this.getRepositoryOptionItems(); + if (repositories.length > 1) { + // Check if this session has a workspace folder tracked (for folders without git repos) + const sessionWorkspaceFolder = this.workspaceFolderService.getSessionWorkspaceFolder(copilotcliSessionId); + const repository = await this.copilotCLIWorktreeManagerService.getWorktreeRepository(copilotcliSessionId); + + if (repository) { + options[REPOSITORY_OPTION_ID] = { + ...toRepositoryOptionItem(repository), + locked: true + }; + } else if (sessionWorkspaceFolder) { + const folderName = this.workspaceService.getWorkspaceFolderName(sessionWorkspaceFolder); + options[REPOSITORY_OPTION_ID] = { + ...toWorkspaceFolderOptionItem(sessionWorkspaceFolder, folderName), + locked: true + }; + } else if (isUntitledSessionId(copilotcliSessionId)) { + const firstOption = repositories[0]; + options[REPOSITORY_OPTION_ID] = firstOption.id; + // Track based on whether first option is a git repo or workspace folder + if (await this.isWorkspaceFolderWithoutRepo(URI.file(firstOption.id))) { + await Promise.all([ + this.workspaceFolderService.trackSessionWorkspaceFolder(copilotcliSessionId, firstOption.id), + this.copilotCLIWorktreeManagerService.deleteSessionRepository(copilotcliSessionId) + ]); + } else { + await Promise.all([ + this.workspaceFolderService.deleteTrackedWorkspaceFolder(copilotcliSessionId), + this.copilotCLIWorktreeManagerService.setSessionRepository(copilotcliSessionId, firstOption.id) + ]); + } } else { - await Promise.all([ - this.workspaceFolderService.deleteTrackedWorkspaceFolder(copilotcliSessionId), - this.copilotCLIWorktreeManagerService.setSessionRepository(copilotcliSessionId, firstOption.id) - ]); + // This is an existing session without a worktree, display current workspace folder. + options[REPOSITORY_OPTION_ID] = { + id: '', + name: this.workspaceService.getWorkspaceFolders().length === 1 ? this.workspaceService.getWorkspaceFolderName(this.workspaceService.getWorkspaceFolders()[0]) : l10n.t('Current Workspace'), + icon: new vscode.ThemeIcon('repo'), + locked: true + }; } - } else { - // This is an existing session without a worktree, display current workspace folder. - options[REPOSITORY_OPTION_ID] = { - id: '', - name: this.workspaceService.getWorkspaceFolders().length === 1 ? this.workspaceService.getWorkspaceFolderName(this.workspaceService.getWorkspaceFolders()[0]) : l10n.t('Current Workspace'), - icon: new vscode.ThemeIcon('repo'), - locked: true - }; } } @@ -345,58 +394,88 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements } async provideChatSessionProviderOptions(): Promise { - const models = await this.copilotCLIModels.getModels(); + const [models, defaultModel] = await Promise.all([ + this.copilotCLIModels.getModels(), + this.copilotCLIModels.getDefaultModel(), + ]); const modelItems: vscode.ChatSessionProviderOptionItem[] = models.map(model => ({ id: model.id, name: model.name, description: model.multiplier !== undefined ? `${model.multiplier}x` : undefined, + default: model.id === defaultModel })); - const options = { - optionGroups: [ - { - id: MODELS_OPTION_ID, - name: l10n.t('Model'), - description: l10n.t('Pick Model'), - items: modelItems - } - ] - }; + const optionGroups: vscode.ChatSessionProviderOptions['optionGroups'] = [ + { + id: MODELS_OPTION_ID, + name: l10n.t('Model'), + description: l10n.t('Pick Model'), + items: modelItems + } + ]; - const repositories = this.getRepositoryOptionItems(); - if (repositories.length > 1) { - options.optionGroups.push({ + // Handle repository options based on workspace type + if (this.isUntitledWorkspace()) { + // For untitled workspaces, show last used repositories and "Open Repository..." command + const repositories = await this.getRepositoryOptionItemsForUntitledWorkspace(); + optionGroups.push({ id: REPOSITORY_OPTION_ID, name: l10n.t('Repository'), description: l10n.t('Pick Repository'), - items: repositories + items: repositories, + commands: [{ + command: OPEN_REPOSITORY_COMMAND_ID, + title: l10n.t('Open Repository') + }] }); + } else { + const repositories = this.getRepositoryOptionItems(); + if (repositories.length > 1) { + optionGroups.push({ + id: REPOSITORY_OPTION_ID, + name: l10n.t('Repository'), + description: l10n.t('Pick Repository'), + items: repositories + }); + } } - return options; + return { + optionGroups + }; + } + + /** + * Check if the current workspace is untitled (has no workspace folders). + */ + private isUntitledWorkspace(): boolean { + return this.workspaceService.getWorkspaceFolders().length === 0; } /** * Check if the given path is a workspace folder that doesn't have any git repos. */ - private isWorkspaceFolderWithoutRepo(path: string): boolean { - const workspaceFolders = this.workspaceService.getWorkspaceFolders(); - if (workspaceFolders.length <= 1) { + private async isWorkspaceFolderWithoutRepo(uri: Uri): Promise { + const repositories = this.gitService.repositories.filter(repo => repo.kind !== 'worktree'); + const repo = repositories.find(r => isEqual(r.rootUri, uri)) ?? await this.gitService.getRepository(uri, true); + if (repo) { return false; } + const workspaceFolders = this.workspaceService.getWorkspaceFolders(); + if (!workspaceFolders.length) { + return true; + } - const uri = URI.file(path); // Check if the path is a workspace folder - const isWorkspaceFolder = workspaceFolders.some(folder => folder.fsPath === uri.fsPath); + const isWorkspaceFolder = workspaceFolders.some(folder => isEqual(folder, uri)); if (!isWorkspaceFolder) { - return false; + return true; } // Check if any git repo belongs to this workspace folder - const repositories = this.gitService.repositories.filter(repo => repo.kind !== 'worktree'); for (const repo of repositories) { const folder = this.workspaceService.getWorkspaceFolder(repo.rootUri); - if (folder && folder.fsPath === uri.fsPath) { + if (folder && isEqual(folder, uri)) { return false; // This folder has a git repo } } @@ -436,6 +515,63 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements return repoItems.sort((a, b) => a.name.localeCompare(b.name)); } + /** + * Get repository option items for untitled workspaces using last used repositories. + */ + private async getRepositoryOptionItemsForUntitledWorkspace(): Promise { + const latestReposAndFolders: { uri: Uri; type: 'repo' | 'folder'; lastUsed: number }[] = []; + const seenUris = new ResourceSet(); + + untitledWorkspaceRepositories.forEach((lastUsed, uri) => { + seenUris.add(uri); + latestReposAndFolders.push({ uri, type: 'repo', lastUsed }); + }); + + untitledWorkspaceFodlers.forEach((lastUsed, uri) => { + seenUris.add(uri); + latestReposAndFolders.push({ uri, type: 'folder', lastUsed }); + }); + + // Last used git repositories + for (const repo of this.gitService.getRecentRepositories()) { + if (seenUris.has(repo.rootUri)) { + continue; + } + seenUris.add(repo.rootUri); + listOfKnownRepos.add(repo.rootUri); + latestReposAndFolders.push({ uri: repo.rootUri, type: 'repo', lastUsed: repo.lastAccessTime }); + } + + // Last used workspace folders without git repos + for (const repo of this.workspaceFolderService.getRecentFolders()) { + if (seenUris.has(repo.folder)) { + continue; + } + seenUris.add(repo.folder); + latestReposAndFolders.push({ uri: repo.folder, type: 'folder', lastUsed: repo.lastAccessTime }); + } + + // Filter out items that no longer exist. + const latest10ReposAndFolders: { uri: Uri; type: 'repo' | 'folder'; lastUsed: number }[] = []; + await Promise.all(latestReposAndFolders.slice(0, 20).map(async (repoAccess) => { + if (await checkFileExists(repoAccess.uri, this.fileSystem)) { + latest10ReposAndFolders.push(repoAccess); + } + })); + + // Sort by last used time descending and take top 10 + latest10ReposAndFolders.sort((a, b) => b.lastUsed - a.lastUsed); + const latestReposAndFoldersLimited = latest10ReposAndFolders.slice(0, 10); + + return latestReposAndFoldersLimited.map((repoAccess) => { + if (repoAccess.type === 'folder') { + return toWorkspaceFolderOptionItem(repoAccess.uri, basename(repoAccess.uri)); + } else { + return toRepositoryOptionItem(repoAccess.uri); + } + }); + } + // Handle option changes for a session (store current state in a map) async provideHandleOptionsChange(resource: Uri, updates: ReadonlyArray, token: vscode.CancellationToken): Promise { const sessionId = SessionIdForCLI.parse(resource); @@ -452,7 +588,7 @@ export class CopilotCLIChatSessionContentProvider extends Disposable implements } } else if (update.optionId === REPOSITORY_OPTION_ID && typeof update.value === 'string') { // Track based on whether selection is a git repo or workspace folder without git - if (this.isWorkspaceFolderWithoutRepo(update.value)) { + if (await this.isWorkspaceFolderWithoutRepo(URI.file(update.value))) { await Promise.all([ this.workspaceFolderService.trackSessionWorkspaceFolder(sessionId, update.value), this.copilotCLIWorktreeManagerService.deleteSessionRepository(sessionId) @@ -518,18 +654,20 @@ async function checkFileExists(filePath: Uri, fileSystem: IFileSystemService): P } } -function toRepositoryOptionItem(repository: RepoContext): ChatSessionProviderOptionItem { - const repositoryName = repository.rootUri.path.split('/').pop() ?? repository.rootUri.toString(); +function toRepositoryOptionItem(repository: RepoContext | Uri, isDefault: boolean = false): ChatSessionProviderOptionItem { + const repositoryUri = isUri(repository) ? repository : repository.rootUri; + const repositoryIcon = isUri(repository) ? 'repo' : repository.kind === 'repository' ? 'repo' : 'archive'; + const repositoryName = repositoryUri.path.split('/').pop() ?? repositoryUri.toString(); return { - id: repository.rootUri.fsPath, + id: repositoryUri.fsPath, name: repositoryName, - icon: repository.kind === 'repository' - ? new vscode.ThemeIcon('repo') - : new vscode.ThemeIcon('archive'), + icon: new vscode.ThemeIcon(repositoryIcon), + default: isDefault } satisfies vscode.ChatSessionProviderOptionItem; } + function toWorkspaceFolderOptionItem(workspaceFolderUri: URI, name: string): ChatSessionProviderOptionItem { return { id: workspaceFolderUri.fsPath, @@ -547,7 +685,6 @@ const CLI_CANCEL = l10n.t('Cancel'); export class CopilotCLIChatSessionParticipant extends Disposable { private readonly untitledSessionIdMapping = new Map(); constructor( - private readonly isolationManager: CopilotCLISessionIsolationManager, private readonly contentProvider: CopilotCLIChatSessionContentProvider, private readonly promptResolver: CopilotCLIPromptResolver, private readonly sessionItemProvider: CopilotCLIChatSessionItemProvider, @@ -599,6 +736,13 @@ export class CopilotCLIChatSessionParticipant extends Disposable { if (chatSessionContext?.chatSessionItem) { if (chatSessionContext.isUntitled) { selectedRepository = this.copilotCLIWorktreeManagerService.getSessionRepository(SessionIdForCLI.parse(chatSessionContext.chatSessionItem.resource)); + // Possible user selected a folder, and its possible the folder is a git repo + if (!selectedRepository) { + const folder = this.workspaceFolderService.getSessionWorkspaceFolder(SessionIdForCLI.parse(chatSessionContext.chatSessionItem.resource)); + if (folder) { + selectedRepository = await this.gitService.getRepository(folder, true); + } + } } else { // Existing session, get worktree repository, and no need to migrate changes. } @@ -986,21 +1130,44 @@ export class CopilotCLIChatSessionParticipant extends Disposable { workingDirectory: Uri | undefined; worktreeProperties: ChatSessionWorktreeProperties | undefined; }> { - const createWorkingTreeIfRequired = async (create: boolean, sessionId: string | undefined) => { + const createWorkingTreeIfRequired = async (sessionId: string | undefined) => { // Check if the session has a workspace folder tracked (folder without git repo) const sessionWorkspaceFolder = sessionId ? this.workspaceFolderService.getSessionWorkspaceFolder(sessionId) : undefined; - const selectedRepository = sessionId ? this.copilotCLIWorktreeManagerService.getSessionRepository(sessionId) : undefined; + let selectedRepository = sessionId ? this.copilotCLIWorktreeManagerService.getSessionRepository(sessionId) : undefined; const workingDirectory = selectedRepository ? this.workspaceService.getWorkspaceFolder(selectedRepository.rootUri) : undefined; if (!selectedRepository && sessionWorkspaceFolder) { - // Workspace folder without git repo - no worktree can be created, use folder directly - return { workingDirectory: sessionWorkspaceFolder, worktreeProperties: undefined, isWorkspaceFolderWithoutRepo: true }; + // Possible we now have a git repo in this folder, check again. + selectedRepository = await this.gitService.getRepository(sessionWorkspaceFolder, true); + if (!selectedRepository) { + // Verify this folder is trusted. + const isTrusted = await this.workspaceService.requestResourceTrust({ uri: sessionWorkspaceFolder, message: untrustedFolderMessage }); + if (!isTrusted) { + stream.warning(l10n.t('The selected workspace folder is not trusted. Proceeding without isolation.')); + return { workingDirectory: undefined, worktreeProperties: undefined, isWorkspaceFolderWithoutRepo: true }; + } + // Workspace folder without git repo - no worktree can be created, use folder directly + return { workingDirectory: sessionWorkspaceFolder, worktreeProperties: undefined, isWorkspaceFolderWithoutRepo: true }; + } } - if (!create) { + // Verify we have a git repo and it is trusted. + selectedRepository = selectedRepository ?? (sessionId ? await this.copilotCLIWorktreeManagerService.getSessionRepository(sessionId) : undefined); + + // Verify repository is trusted + if (selectedRepository) { + const isTrusted = await this.workspaceService.requestResourceTrust({ uri: selectedRepository.rootUri, message: untrustedFolderMessage }); + if (!isTrusted) { + stream.warning(l10n.t('The selected workspace folder is not trusted. Proceeding without isolation.')); + return { workingDirectory: undefined, worktreeProperties: undefined, isWorkspaceFolderWithoutRepo: true }; + } + } + + if (!selectedRepository) { return { workingDirectory, worktreeProperties: undefined, isWorkspaceFolderWithoutRepo: false }; } + const worktreeProperties = await this.copilotCLIWorktreeManagerService.createWorktree(sessionId, stream); if (worktreeProperties) { return { workingDirectory: Uri.file(worktreeProperties.worktreePath), worktreeProperties, isWorkspaceFolderWithoutRepo: false }; @@ -1019,10 +1186,9 @@ export class CopilotCLIChatSessionParticipant extends Disposable { const existingSessionId = this.untitledSessionIdMapping.get(SessionIdForCLI.parse(chatSessionContext.chatSessionItem.resource)); const id = existingSessionId ?? SessionIdForCLI.parse(chatSessionContext.chatSessionItem.resource); const isNewSession = chatSessionContext.isUntitled && !existingSessionId; - isolationEnabled = this.isolationManager.getIsolationPreference(id); if (isNewSession) { - ({ workingDirectory, worktreeProperties, isWorkspaceFolderWithoutRepo } = await createWorkingTreeIfRequired(isolationEnabled, id)); + ({ workingDirectory, worktreeProperties, isWorkspaceFolderWithoutRepo } = await createWorkingTreeIfRequired(id)); // Means we failed to create worktree or this is a workspace folder without git repo if (!worktreeProperties) { isolationEnabled = false; @@ -1040,7 +1206,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { } else { // This happens when we're delegating from a non-Background session to a new Background session isolationEnabled = this.copilotCLIWorktreeManagerService.isWorktreeSupportedObs.get(); - ({ workingDirectory, worktreeProperties, isWorkspaceFolderWithoutRepo } = await createWorkingTreeIfRequired(isolationEnabled, undefined)); + ({ workingDirectory, worktreeProperties, isWorkspaceFolderWithoutRepo } = await createWorkingTreeIfRequired(undefined)); // Means we failed to create worktree or this is a workspace folder without git repo if (!worktreeProperties) { isolationEnabled = false; @@ -1206,7 +1372,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable { } } -export function registerCLIChatCommands(copilotcliSessionItemProvider: CopilotCLIChatSessionItemProvider, copilotCLISessionService: ICopilotCLISessionService, copilotCLIWorktreeManagerService: IChatSessionWorktreeService, gitService: IGitService, copilotCliWorkspaceSession: IChatSessionWorkspaceFolderService): IDisposable { +export function registerCLIChatCommands(copilotcliSessionItemProvider: CopilotCLIChatSessionItemProvider, copilotCLISessionService: ICopilotCLISessionService, copilotCLIWorktreeManagerService: IChatSessionWorktreeService, gitService: IGitService, copilotCliWorkspaceSession: IChatSessionWorkspaceFolderService, contentProvider: CopilotCLIChatSessionContentProvider): IDisposable { const disposableStore = new DisposableStore(); disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.delete', async (sessionItem?: vscode.ChatSessionItem) => { if (sessionItem?.resource) { @@ -1250,6 +1416,56 @@ export function registerCLIChatCommands(copilotcliSessionItemProvider: CopilotCL await copilotcliSessionItemProvider.resumeCopilotCLISessionInTerminal(sessionItem); } })); + // Command to open a folder picker and select a repository for untitled workspaces + disposableStore.add(vscode.commands.registerCommand(OPEN_REPOSITORY_COMMAND_ID, async (sessionItemResource?: vscode.Uri) => { + if (!sessionItemResource) { + return; + } + // Open folder picker dialog + const folderUris = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: l10n.t('Open Folder...'), + }); + + if (!folderUris || folderUris.length === 0) { + return; + } + + const selectedFolderUri = folderUris[0]; + + // Check if the selected folder contains a git repository + const repository = await gitService.getRepository(selectedFolderUri, true); + + // If we have a session resource, update the session's repository + if (repository) { + const sessionId = SessionIdForCLI.parse(sessionItemResource); + untitledWorkspaceRepositories.set(repository.rootUri, Date.now()); + await copilotCliWorkspaceSession.deleteTrackedWorkspaceFolder(sessionId); + await copilotCLIWorktreeManagerService.setSessionRepository(sessionId, repository.rootUri.fsPath); + + // Notify VS Code that the option changed + contentProvider.notifySessionOptionsChange(sessionItemResource, [{ + optionId: REPOSITORY_OPTION_ID, + value: toRepositoryOptionItem(repository) + }]); + } else { + const sessionId = SessionIdForCLI.parse(sessionItemResource); + untitledWorkspaceFodlers.set(selectedFolderUri, Date.now()); + await copilotCliWorkspaceSession.trackSessionWorkspaceFolder(sessionId, selectedFolderUri.fsPath); + await copilotCLIWorktreeManagerService.deleteSessionRepository(sessionId); + + // Notify VS Code that the option changed + contentProvider.notifySessionOptionsChange(sessionItemResource, [{ + optionId: REPOSITORY_OPTION_ID, + value: toWorkspaceFolderOptionItem(selectedFolderUri, basename(selectedFolderUri)), + }]); + } + + // Notify that provider options have changed so the dropdown updates + contentProvider.notifyProviderOptionsChange(); + })); const applyChanges = async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => { const resource = sessionItemOrResource instanceof vscode.Uri diff --git a/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts b/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts index 4723c36030..0f2f3ded88 100644 --- a/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts +++ b/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts @@ -241,7 +241,6 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { } }(); participant = new CopilotCLIChatSessionParticipant( - isolationManager, contentProvider, promptResolver, itemProvider, @@ -284,6 +283,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => { it('uses worktree workingDirectory when isolation is enabled for a new untitled session', async () => { worktree.setSupported(true); (isolationManager.getIsolationPreference as unknown as ReturnType).mockReturnValue(true); + worktree.setSelectedRepository({ rootUri: Uri.file(`${sep}repo`) } as RepoContext); (worktree.createWorktree as unknown as ReturnType).mockResolvedValue({ autoCommit: true, baseCommit: 'deadbeef', diff --git a/src/extension/prompt/node/test/repoInfoTelemetry.spec.ts b/src/extension/prompt/node/test/repoInfoTelemetry.spec.ts index f6f19d54ab..e7abf17f42 100644 --- a/src/extension/prompt/node/test/repoInfoTelemetry.spec.ts +++ b/src/extension/prompt/node/test/repoInfoTelemetry.spec.ts @@ -77,6 +77,7 @@ suite('RepoInfoTelemetry', () => { repositories: [], isInitialized: true, getRepository: vi.fn(), + getRecentRepositories: vi.fn(), getRepositoryFetchUrls: vi.fn(), initialize: vi.fn(), log: vi.fn(), diff --git a/src/extension/test/node/notebookPromptRendering.spec.ts b/src/extension/test/node/notebookPromptRendering.spec.ts index 224290dff8..332624c22b 100644 --- a/src/extension/test/node/notebookPromptRendering.spec.ts +++ b/src/extension/test/node/notebookPromptRendering.spec.ts @@ -135,6 +135,12 @@ describe('Notebook Prompt Rendering', function () { override applyEdit(edit: vscode.WorkspaceEdit): Thenable { throw new Error('Method not implemented.'); } + override requestResourceTrust(_options: vscode.ResourceTrustRequestOptions): Thenable { + return Promise.resolve(true); + } + override requestWorkspaceTrust(_options?: vscode.WorkspaceTrustRequestOptions): Thenable { + return Promise.resolve(true); + } }); testingServiceCollection.define(IExperimentationService, new class extends NullExperimentationService { diff --git a/src/extension/vscode.proposed.workspaceTrust.d.ts b/src/extension/vscode.proposed.workspaceTrust.d.ts new file mode 100644 index 0000000000..2c8edbd9d1 --- /dev/null +++ b/src/extension/vscode.proposed.workspaceTrust.d.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/120173 + + export interface ResourceTrustRequestOptions { + /** + * An resource related to the trust request. + */ + readonly uri: Uri; + + /** + * Custom message describing the user action that requires resource + * trust. If omitted, a generic message will be displayed in the resource + * trust request dialog. + */ + readonly message?: string; + } + + /** + * The object describing the properties of the workspace trust request + */ + export interface WorkspaceTrustRequestOptions { + /** + * Custom message describing the user action that requires workspace + * trust. If omitted, a generic message will be displayed in the workspace + * trust request dialog. + */ + readonly message?: string; + } + + export namespace workspace { + /** + * Prompt the user to chose whether to trust the specified resource (ex: folder) + * @param options Object describing the properties of the resource trust request. + */ + export function requestResourceTrust(options: ResourceTrustRequestOptions): Thenable; + + /** + * Prompt the user to chose whether to trust the current workspace + * @param options Optional object describing the properties of the + * workspace trust request. + */ + export function requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable; + } +} diff --git a/src/platform/git/common/gitService.ts b/src/platform/git/common/gitService.ts index b26e247b29..17d815da98 100644 --- a/src/platform/git/common/gitService.ts +++ b/src/platform/git/common/gitService.ts @@ -5,12 +5,12 @@ import { IDisposable } from 'monaco-editor'; import { createServiceIdentifier } from '../../../util/common/services'; +import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { Event } from '../../../util/vs/base/common/event'; import { IObservable } from '../../../util/vs/base/common/observableInternal'; import { equalsIgnoreCase } from '../../../util/vs/base/common/strings'; import { URI } from '../../../util/vs/base/common/uri'; -import { Change, Commit, CommitShortStat, DiffChange, LogOptions, Ref, RefQuery, RepositoryKind, Worktree } from '../vscode/git'; -import { CancellationToken } from '../../../util/vs/base/common/cancellation'; +import { Change, Commit, CommitShortStat, DiffChange, LogOptions, Ref, RefQuery, RepositoryAccessDetails, RepositoryKind, Worktree } from '../vscode/git'; export interface RepoContext { readonly rootUri: URI; @@ -51,6 +51,7 @@ export interface IGitService extends IDisposable { readonly repositories: Array; readonly isInitialized: boolean; + getRecentRepositories(): Iterable; getRepository(uri: URI, forceOpen?: boolean): Promise; getRepositoryFetchUrls(uri: URI): Promise | undefined>; initialize(): Promise; diff --git a/src/platform/git/vscode/git.d.ts b/src/platform/git/vscode/git.d.ts index f08c37078e..7fc29e8aae 100644 --- a/src/platform/git/vscode/git.d.ts +++ b/src/platform/git/vscode/git.d.ts @@ -150,6 +150,11 @@ export interface RepositoryUIState { readonly onDidChange: Event; } +export interface RepositoryAccessDetails { + readonly rootUri: Uri; + readonly lastAccessTime: number; +} + /** * Log options. */ @@ -377,6 +382,7 @@ export interface API { readonly onDidPublish: Event; readonly git: Git; readonly repositories: Repository[]; + readonly recentRepositories: Iterable; readonly onDidOpenRepository: Event; readonly onDidCloseRepository: Event; diff --git a/src/platform/git/vscode/gitServiceImpl.ts b/src/platform/git/vscode/gitServiceImpl.ts index f44d7defa1..1c19d320aa 100644 --- a/src/platform/git/vscode/gitServiceImpl.ts +++ b/src/platform/git/vscode/gitServiceImpl.ts @@ -20,7 +20,7 @@ import { ILogService } from '../../log/common/logService'; import { IGitExtensionService } from '../common/gitExtensionService'; import { IGitService, RepoContext } from '../common/gitService'; import { parseGitRemotes } from '../common/utils'; -import { API, APIState, Change, Commit, CommitShortStat, DiffChange, LogOptions, Ref, RefQuery, Repository } from './git'; +import { API, APIState, Change, Commit, CommitShortStat, DiffChange, LogOptions, Ref, RefQuery, Repository, RepositoryAccessDetails } from './git'; export class GitServiceImpl extends Disposable implements IGitService { @@ -35,7 +35,6 @@ export class GitServiceImpl extends Disposable implements IGitService { private _onDidFinishInitialRepositoryDiscovery = new Emitter(); readonly onDidFinishInitialization: Event = this._onDidFinishInitialRepositoryDiscovery.event; private _isInitialized = observableValue(this, false); - constructor( @IGitExtensionService private readonly gitExtensionService: IGitExtensionService, @ILogService private readonly logService: ILogService @@ -100,6 +99,14 @@ export class GitServiceImpl extends Disposable implements IGitService { return this._isInitialized.get(); } + public getRecentRepositories(): Iterable { + const gitAPI = this.gitExtensionService.getExtensionApi(); + if (!gitAPI) { + return []; + } + return gitAPI.recentRepositories; + } + async getRepository(uri: URI, forceOpen = true): Promise { const gitAPI = this.gitExtensionService.getExtensionApi(); if (!gitAPI) { @@ -426,4 +433,4 @@ export class RepoContextImpl implements RepoContext { private readonly _repo: Repository ) { } -} \ No newline at end of file +} diff --git a/src/platform/ignore/node/test/mockGitService.ts b/src/platform/ignore/node/test/mockGitService.ts index 5cd144a409..2e846c882b 100644 --- a/src/platform/ignore/node/test/mockGitService.ts +++ b/src/platform/ignore/node/test/mockGitService.ts @@ -10,7 +10,7 @@ import { IObservable } from '../../../../util/vs/base/common/observableInternal' import { observableValue } from '../../../../util/vs/base/common/observableInternal/observables/observableValue'; import { URI } from '../../../../util/vs/base/common/uri'; import { IGitService, RepoContext } from '../../../git/common/gitService'; -import { Change, Commit, CommitShortStat, DiffChange, LogOptions, Ref, RefQuery } from '../../../git/vscode/git'; +import { Change, Commit, CommitShortStat, DiffChange, LogOptions, Ref, RefQuery, RepositoryAccessDetails } from '../../../git/vscode/git'; /** * A configurable mock implementation of IGitService for testing. @@ -38,6 +38,10 @@ export class MockGitService implements IGitService { this._repositoryFetchUrls = value; } + getRecentRepositories(): Iterable { + return []; + } + getRepositoryFetchUrls = vi.fn().mockImplementation((): Promise | undefined> => { this.getRepositoryFetchUrlsCallCount++; return Promise.resolve(this._repositoryFetchUrls); diff --git a/src/platform/ignore/node/test/mockWorkspaceService.ts b/src/platform/ignore/node/test/mockWorkspaceService.ts index 19d5aef748..3bee106f3c 100644 --- a/src/platform/ignore/node/test/mockWorkspaceService.ts +++ b/src/platform/ignore/node/test/mockWorkspaceService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { FileSystem, NotebookData, NotebookDocument, NotebookDocumentChangeEvent, TextDocument, TextDocumentChangeEvent, TextEditorSelectionChangeEvent, WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import type { FileSystem, NotebookData, NotebookDocument, NotebookDocumentChangeEvent, ResourceTrustRequestOptions, TextDocument, TextDocumentChangeEvent, TextEditorSelectionChangeEvent, WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersChangeEvent, WorkspaceTrustRequestOptions } from 'vscode'; import { Event } from '../../../../util/vs/base/common/event'; import { URI } from '../../../../util/vs/base/common/uri'; import { NotebookDocumentSnapshot } from '../../../editing/common/notebookDocumentSnapshot'; @@ -86,4 +86,12 @@ export class MockWorkspaceService implements IWorkspaceService { ensureWorkspaceIsFullyLoaded(): Promise { return Promise.resolve(); } + + requestResourceTrust(_options: ResourceTrustRequestOptions): Thenable { + return Promise.resolve(true); + } + + requestWorkspaceTrust(_options?: WorkspaceTrustRequestOptions): Thenable { + return Promise.resolve(true); + } } diff --git a/src/platform/test/node/simulationWorkspaceServices.ts b/src/platform/test/node/simulationWorkspaceServices.ts index 5daafc7752..298889a92c 100644 --- a/src/platform/test/node/simulationWorkspaceServices.ts +++ b/src/platform/test/node/simulationWorkspaceServices.ts @@ -29,7 +29,7 @@ import { IFileSystemService } from '../../filesystem/common/fileSystemService'; import { FileType, RelativePattern } from '../../filesystem/common/fileTypes'; import { NodeFileSystemService } from '../../filesystem/node/fileSystemServiceImpl'; import { IGitService, RepoContext } from '../../git/common/gitService'; -import { Change, CommitShortStat, DiffChange, Ref, RefQuery } from '../../git/vscode/git'; +import { Change, CommitShortStat, DiffChange, Ref, RefQuery, RepositoryAccessDetails } from '../../git/vscode/git'; import { AbstractLanguageDiagnosticsService } from '../../languages/common/languageDiagnosticsService'; import { ILanguageFeaturesService } from '../../languages/common/languageFeaturesService'; import { ILogService } from '../../log/common/logService'; @@ -123,6 +123,14 @@ export class SimulationWorkspaceService extends AbstractWorkspaceService { override applyEdit(edit: vscode.WorkspaceEdit): Thenable { return Promise.resolve(true); } + + override requestResourceTrust(options: vscode.ResourceTrustRequestOptions): Thenable { + return Promise.resolve(true); + } + + override requestWorkspaceTrust(options?: vscode.WorkspaceTrustRequestOptions): Thenable { + return Promise.resolve(true); + } } export class SimulationLanguageDiagnosticsService extends AbstractLanguageDiagnosticsService { @@ -687,6 +695,10 @@ export class TestingGitService implements IGitService { return Promise.resolve(undefined); } + getRecentRepositories(): Iterable { + return []; + } + async initialize() { return undefined; } diff --git a/src/platform/workspace/common/workspaceService.ts b/src/platform/workspace/common/workspaceService.ts index 3d53cafc52..20cdfedcb0 100644 --- a/src/platform/workspace/common/workspaceService.ts +++ b/src/platform/workspace/common/workspaceService.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { Event, FileSystem, NotebookData, NotebookDocument, NotebookDocumentChangeEvent, TextDocument, TextDocumentChangeEvent, TextEditorSelectionChangeEvent, Uri, WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import type { Event, FileSystem, NotebookData, NotebookDocument, NotebookDocumentChangeEvent, ResourceTrustRequestOptions, TextDocument, TextDocumentChangeEvent, TextEditorSelectionChangeEvent, Uri, WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersChangeEvent, WorkspaceTrustRequestOptions } from 'vscode'; import { findNotebook } from '../../../util/common/notebooks'; import { createServiceIdentifier } from '../../../util/common/services'; +import { Emitter } from '../../../util/vs/base/common/event'; +import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle'; import * as path from '../../../util/vs/base/common/path'; import { extUriBiasedIgnorePathCase, relativePath } from '../../../util/vs/base/common/resources'; import { URI } from '../../../util/vs/base/common/uri'; import { NotebookDocumentSnapshot } from '../../editing/common/notebookDocumentSnapshot'; import { TextDocumentSnapshot } from '../../editing/common/textDocumentSnapshot'; -import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle'; -import { Emitter } from '../../../util/vs/base/common/event'; export const IWorkspaceService = createServiceIdentifier('IWorkspaceService'); @@ -49,6 +49,8 @@ export interface IWorkspaceService { * has been downloaded before we can use them. */ ensureWorkspaceIsFullyLoaded(): Promise; + requestResourceTrust(options: ResourceTrustRequestOptions): Thenable; + requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable; } export abstract class AbstractWorkspaceService implements IWorkspaceService { @@ -73,6 +75,9 @@ export abstract class AbstractWorkspaceService implements IWorkspaceService { abstract showWorkspaceFolderPicker(): Promise; abstract getWorkspaceFolderName(workspaceFolderUri: URI): string; abstract applyEdit(edit: WorkspaceEdit): Thenable; + abstract requestResourceTrust(options: ResourceTrustRequestOptions): Thenable; + abstract requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable; + asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string { // Copied from the implementation in vscode/extHostWorkspace.ts let resource: URI | undefined; @@ -223,4 +228,12 @@ export class NullWorkspaceService extends AbstractWorkspaceService implements ID public dispose() { this.disposables.dispose(); } + + override requestResourceTrust(options: ResourceTrustRequestOptions): Thenable { + return Promise.resolve(true); + } + + override requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable { + return Promise.resolve(true); + } } diff --git a/src/platform/workspace/vscode/workspaceServiceImpl.ts b/src/platform/workspace/vscode/workspaceServiceImpl.ts index 7caca5e249..f24d5e8ad8 100644 --- a/src/platform/workspace/vscode/workspaceServiceImpl.ts +++ b/src/platform/workspace/vscode/workspaceServiceImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { FileSystem, NotebookData, NotebookDocument, TextDocument, Uri, window, workspace, WorkspaceFolder, type WorkspaceEdit } from 'vscode'; +import { FileSystem, NotebookData, NotebookDocument, ResourceTrustRequestOptions, TextDocument, Uri, window, workspace, WorkspaceFolder, WorkspaceTrustRequestOptions, type WorkspaceEdit } from 'vscode'; import { findNotebook } from '../../../util/common/notebooks'; import { URI } from '../../../util/vs/base/common/uri'; import { ILogService } from '../../log/common/logService'; @@ -109,4 +109,13 @@ export class ExtensionTextDocumentManager extends AbstractWorkspaceService { } return; } + + + override requestResourceTrust(options: ResourceTrustRequestOptions): Thenable { + return workspace.requestResourceTrust(options); + } + + override requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable { + return workspace.requestWorkspaceTrust(options); + } }