diff --git a/package.json b/package.json index 121adffe67..085c444640 100644 --- a/package.json +++ b/package.json @@ -311,6 +311,11 @@ "default": false, "description": "%githubPullRequests.hideViewedFiles.description%" }, + "githubPullRequests.markAsViewedOnOpen": { + "type": "boolean", + "default": false, + "description": "%githubPullRequests.markAsViewedOnOpen.description%" + }, "githubPullRequests.defaultDeletionMethod.selectLocalBranch": { "type": "boolean", "default": true, diff --git a/package.nls.json b/package.nls.json index 73c02b7684..531e993a38 100644 --- a/package.nls.json +++ b/package.nls.json @@ -37,6 +37,7 @@ "githubPullRequests.notifications.description": "If GitHub notifications should be shown to the user.", "githubPullRequests.fileListLayout.description": "The layout to use when displaying changed files list.", "githubPullRequests.hideViewedFiles.description": "Hide files that have been marked as viewed in the pull request changes tree.", + "githubPullRequests.markAsViewedOnOpen.description": "Automatically mark files as viewed when opened in the editor.", "githubPullRequests.defaultDeletionMethod.selectLocalBranch.description": "When true, the option to delete the local branch will be selected by default when deleting a branch from a pull request.", "githubPullRequests.defaultDeletionMethod.selectRemote.description": "When true, the option to delete the remote will be selected by default when deleting a branch from a pull request.", "githubPullRequests.terminalLinksHandler.description": "Default handler for terminal links.", diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index 71520fa1ec..aa7001a3d2 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -105,6 +105,7 @@ declare module 'vscode' { isComplete?: boolean; toolSpecificData?: ChatTerminalToolInvocationData; fromSubAgent?: boolean; + presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; constructor(toolName: string, toolCallId: string, isError?: boolean); } diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index bd4e624430..772fc387b9 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -95,6 +95,11 @@ declare module 'vscode' { */ description?: string | MarkdownString; + /** + * An optional badge that provides additional context about the chat session. + */ + badge?: string | MarkdownString; + /** * An optional status indicating the current state of the session. */ diff --git a/src/common/settingKeys.ts b/src/common/settingKeys.ts index 1376c4c7bc..1a63c63e56 100644 --- a/src/common/settingKeys.ts +++ b/src/common/settingKeys.ts @@ -10,6 +10,7 @@ export const BRANCH_LIST_TIMEOUT = 'branchListTimeout'; export const USE_REVIEW_MODE = 'useReviewMode'; export const FILE_LIST_LAYOUT = 'fileListLayout'; export const HIDE_VIEWED_FILES = 'hideViewedFiles'; +export const MARK_AS_VIEWED_ON_OPEN = 'markAsViewedOnOpen'; export const ASSIGN_TO = 'assignCreated'; export const PUSH_BRANCH = 'pushBranch'; export const IGNORE_PR_BRANCHES = 'ignoredPullRequestBranches'; diff --git a/src/extension.ts b/src/extension.ts index f91a82d43f..668ec6c597 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,524 +1,578 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import TelemetryReporter from '@vscode/extension-telemetry'; -import * as vscode from 'vscode'; - -import { LiveShare } from 'vsls/vscode.js'; -import { PostCommitCommandsProvider, Repository } from './api/api'; -import { GitApiImpl } from './api/api1'; -import { registerCommands } from './commands'; -import { COPILOT_SWE_AGENT } from './common/copilot'; -import { commands } from './common/executeCommands'; -import { isSubmodule } from './common/gitUtils'; -import Logger from './common/logger'; -import * as PersistentState from './common/persistentState'; -import { parseRepositoryRemotes } from './common/remote'; -import { BRANCH_PUBLISH, EXPERIMENTAL_CHAT, FILE_LIST_LAYOUT, GIT, IGNORE_SUBMODULES, OPEN_DIFF_ON_CLICK, PR_SETTINGS_NAMESPACE, SHOW_INLINE_OPEN_FILE_ACTION } from './common/settingKeys'; -import { initBasedOnSettingChange } from './common/settingsUtils'; -import { TemporaryState } from './common/temporaryState'; -import { Schemes } from './common/uri'; -import { isDescendant } from './common/utils'; -import { EXTENSION_ID, FOCUS_REVIEW_MODE } from './constants'; -import { createExperimentationService, ExperimentationTelemetry } from './experimentationService'; -import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent'; -import { CredentialStore } from './github/credentials'; -import { FolderRepositoryManager } from './github/folderRepositoryManager'; -import { OverviewRestorer } from './github/overviewRestorer'; -import { RepositoriesManager } from './github/repositoriesManager'; -import { registerBuiltinGitProvider, registerLiveShareGitProvider } from './gitProviders/api'; -import { GitHubContactServiceProvider } from './gitProviders/GitHubContactServiceProvider'; -import { GitLensIntegration } from './integrations/gitlens/gitlensImpl'; -import { IssueFeatureRegistrar } from './issues/issueFeatureRegistrar'; -import { StateManager } from './issues/stateManager'; -import { IssueContextProvider } from './lm/issueContextProvider'; -import { ChatParticipant, ChatParticipantState } from './lm/participants'; -import { PullRequestContextProvider } from './lm/pullRequestContextProvider'; -import { registerTools } from './lm/tools/tools'; -import { migrate } from './migrations'; -import { NotificationsFeatureRegister } from './notifications/notificationsFeatureRegistar'; -import { NotificationsManager } from './notifications/notificationsManager'; -import { NotificationsProvider } from './notifications/notificationsProvider'; -import { ThemeWatcher } from './themeWatcher'; -import { resumePendingCheckout, UriHandler } from './uriHandler'; -import { CommentDecorationProvider } from './view/commentDecorationProvider'; -import { CommitsDecorationProvider } from './view/commitsDecorationProvider'; -import { CompareChanges } from './view/compareChangesTreeDataProvider'; -import { CreatePullRequestHelper } from './view/createPullRequestHelper'; -import { EmojiCompletionProvider } from './view/emojiCompletionProvider'; -import { FileTypeDecorationProvider } from './view/fileTypeDecorationProvider'; -import { GitHubCommitFileSystemProvider } from './view/githubFileContentProvider'; -import { getInMemPRFileSystemProvider } from './view/inMemPRContentProvider'; -import { PullRequestChangesTreeDataProvider } from './view/prChangesTreeDataProvider'; -import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider'; -import { PrsTreeModel } from './view/prsTreeModel'; -import { ReviewManager, ShowPullRequest } from './view/reviewManager'; -import { ReviewsManager } from './view/reviewsManager'; -import { TreeDecorationProviders } from './view/treeDecorationProviders'; -import { WebviewViewCoordinator } from './view/webviewViewCoordinator'; - -const ingestionKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255'; - -let telemetry: ExperimentationTelemetry; - -const ACTIVATION = 'Activation'; - -async function init( - context: vscode.ExtensionContext, - git: GitApiImpl, - credentialStore: CredentialStore, - repositories: Repository[], - tree: PullRequestsTreeDataProvider, - liveshareApiPromise: Promise, - showPRController: ShowPullRequest, - reposManager: RepositoriesManager, - createPrHelper: CreatePullRequestHelper, - copilotRemoteAgentManager: CopilotRemoteAgentManager, - themeWatcher: ThemeWatcher, - prsTreeModel: PrsTreeModel, -): Promise { - context.subscriptions.push(Logger); - Logger.appendLine('Git repository found, initializing review manager and pr tree view.', ACTIVATION); - - context.subscriptions.push(credentialStore.onDidChangeSessions(async e => { - if (e.provider.id === 'github') { - await reposManager.clearCredentialCache(); - if (reviewsManager) { - reviewsManager.reviewManagers.forEach(reviewManager => reviewManager.updateState(true)); - } - } - })); - - context.subscriptions.push( - git.onDidPublish(async e => { - // Only notify on branch publish events - if (!e.branch) { - return; - } - - if (vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'ask' | 'never' | undefined>(BRANCH_PUBLISH) !== 'ask') { - return; - } - - const reviewManager = reviewsManager.reviewManagers.find( - manager => manager.repository.rootUri.toString() === e.repository.rootUri.toString(), - ); - if (reviewManager?.isCreatingPullRequest) { - return; - } - - const folderManager = reposManager.folderManagers.find( - manager => manager.repository.rootUri.toString() === e.repository.rootUri.toString()); - - if (!folderManager || folderManager.gitHubRepositories.length === 0) { - return; - } - - const defaults = await folderManager.getPullRequestDefaults(); - if (defaults.base === e.branch) { - return; - } - - const create = vscode.l10n.t('Create Pull Request...'); - const dontShowAgain = vscode.l10n.t('Don\'t Show Again'); - const result = await vscode.window.showInformationMessage( - vscode.l10n.t('Would you like to create a Pull Request for branch \'{0}\'?', e.branch), - create, - dontShowAgain, - ); - if (result === create) { - reviewManager?.createPullRequest(e.branch); - } else if (result === dontShowAgain) { - await vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).update(BRANCH_PUBLISH, 'never', vscode.ConfigurationTarget.Global); - } - }), - ); - - // Sort the repositories to match folders in a multiroot workspace (if possible). - const workspaceFolders = vscode.workspace.workspaceFolders; - if (workspaceFolders) { - repositories = repositories.sort((a, b) => { - let indexA = workspaceFolders.length; - let indexB = workspaceFolders.length; - for (let i = 0; i < workspaceFolders.length; i++) { - if (workspaceFolders[i].uri.toString() === a.rootUri.toString()) { - indexA = i; - } else if (workspaceFolders[i].uri.toString() === b.rootUri.toString()) { - indexB = i; - } - if (indexA !== workspaceFolders.length && indexB !== workspaceFolders.length) { - break; - } - } - return indexA - indexB; - }); - } - - liveshareApiPromise.then(api => { - if (api) { - // register the pull request provider to suggest PR contacts - api.registerContactServiceProvider('github-pr', new GitHubContactServiceProvider(reposManager)); - } - }); - - const changesTree = new PullRequestChangesTreeDataProvider(git, reposManager); - context.subscriptions.push(changesTree); - - const activePrViewCoordinator = new WebviewViewCoordinator(context); - context.subscriptions.push(activePrViewCoordinator); - - let reviewManagerIndex = 0; - const reviewManagers = reposManager.folderManagers.map( - folderManager => new ReviewManager(reviewManagerIndex++, context, folderManager.repository, folderManager, telemetry, changesTree, tree, showPRController, activePrViewCoordinator, createPrHelper, git), - ); - const treeDecorationProviders = new TreeDecorationProviders(reposManager); - context.subscriptions.push(treeDecorationProviders); - treeDecorationProviders.registerProviders([new FileTypeDecorationProvider(), new CommentDecorationProvider(reposManager), new CommitsDecorationProvider(reposManager)]); - - const notificationsProvider = new NotificationsProvider(credentialStore, reposManager); - context.subscriptions.push(notificationsProvider); - - const notificationsManager = new NotificationsManager(notificationsProvider, credentialStore, reposManager, context); - context.subscriptions.push(notificationsManager); - - const reviewsManager = new ReviewsManager(context, reposManager, reviewManagers, prsTreeModel, tree, changesTree, telemetry, credentialStore, git, copilotRemoteAgentManager, notificationsManager); - context.subscriptions.push(reviewsManager); - - context.subscriptions.push(vscode.languages.registerCompletionItemProvider( - { scheme: Schemes.Comment }, - new EmojiCompletionProvider(context), - ':' - )); - - git.onDidChangeState(() => { - Logger.appendLine(`Git initialization state changed: state=${git.state}`, ACTIVATION); - reviewsManager.reviewManagers.forEach(reviewManager => reviewManager.updateState(true)); - }); - - git.onDidOpenRepository(repo => { - function addRepo() { - // Make sure we don't already have a folder manager for this repo. - const existing = reposManager.folderManagers.find(manager => manager.repository.rootUri.toString() === repo.rootUri.toString()); - if (existing) { - Logger.appendLine(`Repo ${repo.rootUri} has already been setup.`, ACTIVATION); - return; - } - - // Check if submodules should be ignored - const ignoreSubmodules = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(IGNORE_SUBMODULES, false); - if (ignoreSubmodules && isSubmodule(repo, git)) { - Logger.appendLine(`Repo ${repo.rootUri} is a submodule and will be ignored due to ${IGNORE_SUBMODULES} setting.`, ACTIVATION); - return; - } - - const newFolderManager = new FolderRepositoryManager(reposManager.folderManagers.length, context, repo, telemetry, git, credentialStore, createPrHelper, themeWatcher); - reposManager.insertFolderManager(newFolderManager); - const newReviewManager = new ReviewManager( - reviewManagerIndex++, - context, - newFolderManager.repository, - newFolderManager, - telemetry, - changesTree, - tree, - showPRController, - activePrViewCoordinator, - createPrHelper, - git - ); - reviewsManager.addReviewManager(newReviewManager); - } - - // Check if repo is in one of the workspace folders or vice versa - Logger.debug(`Checking if repo ${repo.rootUri.fsPath} is in a workspace folder.`, ACTIVATION); - Logger.debug(`Workspace folders: ${workspaceFolders?.map(folder => folder.uri.fsPath).join(', ')}`, ACTIVATION); - if (workspaceFolders && !workspaceFolders.some(folder => isDescendant(folder.uri.fsPath, repo.rootUri.fsPath, true) || isDescendant(repo.rootUri.fsPath, folder.uri.fsPath, true))) { - Logger.appendLine(`Repo ${repo.rootUri} is not in a workspace folder, ignoring.`, ACTIVATION); - return; - } - addRepo(); - const disposable = repo.state.onDidChange(() => { - Logger.appendLine(`Repo state for ${repo.rootUri} changed.`, ACTIVATION); - addRepo(); - disposable.dispose(); - }); - }); - - git.onDidCloseRepository(repo => { - reposManager.removeRepo(repo); - reviewsManager.removeReviewManager(repo); - }); - - tree.initialize(reviewsManager.reviewManagers.map(manager => manager.reviewModel), notificationsManager); - - registerCommands(context, reposManager, reviewsManager, telemetry, copilotRemoteAgentManager, notificationsManager, prsTreeModel); - - const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(FILE_LIST_LAYOUT); - await vscode.commands.executeCommand('setContext', 'fileListLayout:flat', layout === 'flat'); - - const issueStateManager = new StateManager(git, reposManager, context); - const issuesFeatures = new IssueFeatureRegistrar(git, reposManager, reviewsManager, context, telemetry, issueStateManager, copilotRemoteAgentManager); - context.subscriptions.push(issuesFeatures); - await issuesFeatures.initialize(); - - const pullRequestContextProvider = new PullRequestContextProvider(prsTreeModel, reposManager, git); - vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-PullRequestOverview**' }, 'githubpr', pullRequestContextProvider); - vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-IssueOverview**' }, 'githubissue', new IssueContextProvider(issueStateManager, reposManager)); - pullRequestContextProvider.initialize(); - - const notificationsFeatures = new NotificationsFeatureRegister(credentialStore, reposManager, telemetry, notificationsManager); - context.subscriptions.push(notificationsFeatures); - - context.subscriptions.push(new GitLensIntegration()); - - context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context.extensionUri, credentialStore)); - - await vscode.commands.executeCommand('setContext', 'github:initialized', true); - - registerPostCommitCommandsProvider(context, reposManager, git); - - // Resume any pending checkout request stored before workspace reopened. - await resumePendingCheckout(reviewsManager, context, reposManager); - - initChat(context, credentialStore, reposManager, copilotRemoteAgentManager, telemetry, prsTreeModel); - context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git))); - - // Make sure any compare changes tabs, which come from the create flow, are closed. - CompareChanges.closeTabs(); - /* __GDPR__ - "startup" : {} - */ - telemetry.sendTelemetryEvent('startup'); -} - -function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager, copilotRemoteManager: CopilotRemoteAgentManager, telemetry: ExperimentationTelemetry, prsTreeModel: PrsTreeModel) { - const createParticipant = () => { - const chatParticipantState = new ChatParticipantState(); - context.subscriptions.push(new ChatParticipant(context, chatParticipantState)); - registerTools(context, credentialStore, reposManager, chatParticipantState, copilotRemoteManager, telemetry, prsTreeModel); - }; - - const chatEnabled = () => vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(EXPERIMENTAL_CHAT, false); - if (chatEnabled()) { - createParticipant(); - } else { - initBasedOnSettingChange(PR_SETTINGS_NAMESPACE, EXPERIMENTAL_CHAT, chatEnabled, createParticipant, context.subscriptions); - } -} - -export async function activate(context: vscode.ExtensionContext): Promise { - Logger.appendLine(`Extension version: ${vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON.version}`, 'Activation'); - - // @ts-ignore - if (EXTENSION_ID === 'GitHub.vscode-pull-request-github-insiders') { - const stable = vscode.extensions.getExtension('github.vscode-pull-request-github'); - if (stable !== undefined) { - throw new Error( - 'GitHub Pull Requests and Issues Nightly cannot be used while GitHub Pull Requests and Issues is also installed. Please ensure that only one version of the extension is installed.', - ); - } - } - - const showPRController = new ShowPullRequest(); - vscode.commands.registerCommand('github.api.preloadPullRequest', async (shouldShow: boolean) => { - await vscode.commands.executeCommand('setContext', FOCUS_REVIEW_MODE, true); - await commands.focusView('github:activePullRequest:welcome'); - showPRController.shouldShow = shouldShow; - }); - await setGitSettingContexts(context); - - Logger.debug('Creating API implementation.', 'Activation'); - - telemetry = new ExperimentationTelemetry(new TelemetryReporter(ingestionKey)); - context.subscriptions.push(telemetry); - - return await deferredActivate(context, showPRController); -} - -async function setGitSettingContexts(context: vscode.ExtensionContext) { - // We set contexts instead of using the config directly in package.json because the git extension might not actually be available. - const settings: [string, () => void][] = [ - ['openDiffOnClick', () => vscode.workspace.getConfiguration(GIT, null).get(OPEN_DIFF_ON_CLICK, true)], - ['showInlineOpenFileAction', () => vscode.workspace.getConfiguration(GIT, null).get(SHOW_INLINE_OPEN_FILE_ACTION, true)] - ]; - for (const [contextName, setting] of settings) { - commands.setContext(contextName, setting()); - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(`${GIT}.${contextName}`)) { - commands.setContext(contextName, setting()); - } - })); - } -} - -async function doRegisterBuiltinGitProvider(context: vscode.ExtensionContext, credentialStore: CredentialStore, apiImpl: GitApiImpl): Promise { - const builtInGitProvider = await registerBuiltinGitProvider(credentialStore, apiImpl); - if (builtInGitProvider) { - context.subscriptions.push(builtInGitProvider); - return true; - } - return false; -} - -function registerPostCommitCommandsProvider(context: vscode.ExtensionContext, reposManager: RepositoriesManager, git: GitApiImpl) { - const componentId = 'GitPostCommitCommands'; - class Provider implements PostCommitCommandsProvider { - - getCommands(repository: Repository) { - Logger.appendLine(`Looking for remote. Comparing ${repository.state.remotes.length} local repo remotes with ${reposManager.folderManagers.reduce((prev, curr) => prev + curr.gitHubRepositories.length, 0)} GitHub repositories.`, componentId); - const repoRemotes = parseRepositoryRemotes(repository); - - const found = reposManager.folderManagers.find(folderManager => folderManager.findRepo(githubRepo => { - return !!repoRemotes.find(remote => { - return remote.equals(githubRepo.remote); - }); - })); - Logger.appendLine(`Found ${found ? 'a repo' : 'no repos'} when getting post commit commands.`, componentId); - return found ? [{ - command: 'pr.pushAndCreate', - title: vscode.l10n.t('{0} Commit & Create Pull Request', '$(git-pull-request-create)'), - tooltip: vscode.l10n.t('Commit & Create Pull Request') - }] : []; - } - } - - function hasGitHubRepos(): boolean { - return reposManager.folderManagers.some(folderManager => folderManager.gitHubRepositories.length > 0); - } - function tryRegister(): boolean { - Logger.appendLine('Trying to register post commit commands.', 'GitPostCommitCommands'); - if (hasGitHubRepos()) { - Logger.appendLine('GitHub remote(s) found, registering post commit commands.', componentId); - context.subscriptions.push(git.registerPostCommitCommandsProvider(new Provider())); - return true; - } - return false; - } - - if (!tryRegister()) { - const reposDisposable = reposManager.onDidLoadAnyRepositories(() => { - if (tryRegister()) { - reposDisposable.dispose(); - } - }); - } -} - -async function deferredActivateRegisterBuiltInGitProvider(context: vscode.ExtensionContext, apiImpl: GitApiImpl, credentialStore: CredentialStore) { - Logger.appendLine('Registering built in git provider.', 'Activation'); - if (!(await doRegisterBuiltinGitProvider(context, credentialStore, apiImpl))) { - const extensionsChangedDisposable = vscode.extensions.onDidChange(async () => { - if (await doRegisterBuiltinGitProvider(context, credentialStore, apiImpl)) { - extensionsChangedDisposable.dispose(); - } - }); - context.subscriptions.push(extensionsChangedDisposable); - } -} - -async function deferredActivate(context: vscode.ExtensionContext, showPRController: ShowPullRequest) { - Logger.debug('Initializing state.', 'Activation'); - PersistentState.init(context); - await migrate(context); - TemporaryState.init(context); - Logger.debug('Creating credential store.', 'Activation'); - const credentialStore = new CredentialStore(telemetry, context); - context.subscriptions.push(credentialStore); - const experimentationService = await createExperimentationService(context, telemetry); - await experimentationService.initializePromise; - await experimentationService.isCachedFlightEnabled('githubaa'); - await credentialStore.create(); - - const reposManager = new RepositoriesManager(credentialStore, telemetry); - context.subscriptions.push(reposManager); - - const prsTreeModel = new PrsTreeModel(telemetry, reposManager, context); - context.subscriptions.push(prsTreeModel); - - // API - const apiImpl = new GitApiImpl(reposManager); - context.subscriptions.push(apiImpl); - - deferredActivateRegisterBuiltInGitProvider(context, apiImpl, credentialStore); - - Logger.debug('Registering live share git provider.', 'Activation'); - const liveshareGitProvider = registerLiveShareGitProvider(apiImpl); - context.subscriptions.push(liveshareGitProvider); - const liveshareApiPromise = liveshareGitProvider.initialize(); - - context.subscriptions.push(apiImpl); - - Logger.debug('Creating tree view.', 'Activation'); - - const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, apiImpl, prsTreeModel); - context.subscriptions.push(copilotRemoteAgentManager); - if (vscode.chat?.registerChatSessionItemProvider) { - const chatParticipant = vscode.chat.createChatParticipant(COPILOT_SWE_AGENT, async (request, context, stream, token) => - await copilotRemoteAgentManager.chatParticipantImpl(request, context, stream, token) - ); - context.subscriptions.push(chatParticipant); - - const provider = new class implements vscode.ChatSessionContentProvider, vscode.ChatSessionItemProvider { - label = vscode.l10n.t('GitHub Copilot Coding Agent'); - async provideChatSessionItems(token: vscode.CancellationToken) { - return await copilotRemoteAgentManager.provideChatSessions(token); - } - async provideChatSessionContent(resource: vscode.Uri, token: vscode.CancellationToken) { - return await copilotRemoteAgentManager.provideChatSessionContent(resource, token); - } - onDidChangeChatSessionItems = copilotRemoteAgentManager.onDidChangeChatSessions; - onDidCommitChatSessionItem = copilotRemoteAgentManager.onDidCommitChatSession; - }(); - - context.subscriptions.push(vscode.chat?.registerChatSessionItemProvider( - COPILOT_SWE_AGENT, - provider - )); - - context.subscriptions.push(vscode.chat?.registerChatSessionContentProvider( - COPILOT_SWE_AGENT, - provider, - chatParticipant, - { supportsInterruptions: true } - )); - } - - const prTree = new PullRequestsTreeDataProvider(prsTreeModel, telemetry, context, reposManager, copilotRemoteAgentManager); - context.subscriptions.push(prTree); - context.subscriptions.push(credentialStore.onDidGetSession(() => prTree.refreshAll(true))); - Logger.appendLine('Looking for git repository', ACTIVATION); - const repositories = apiImpl.repositories; - Logger.appendLine(`Found ${repositories.length} repositories during activation`, ACTIVATION); - const createPrHelper = new CreatePullRequestHelper(); - context.subscriptions.push(createPrHelper); - - const themeWatcher = new ThemeWatcher(); - context.subscriptions.push(themeWatcher); - - let folderManagerIndex = 0; - const folderManagers = repositories.map( - repository => new FolderRepositoryManager(folderManagerIndex++, context, repository, telemetry, apiImpl, credentialStore, createPrHelper, themeWatcher), - ); - context.subscriptions.push(...folderManagers); - for (const folderManager of folderManagers) { - reposManager.insertFolderManager(folderManager); - } - - const inMemPRFileSystemProvider = getInMemPRFileSystemProvider({ reposManager, gitAPI: apiImpl, credentialStore })!; - const readOnlyMessage = new vscode.MarkdownString(vscode.l10n.t('Cannot edit this pull request file. [Check out](command:pr.checkoutFromReadonlyFile) this pull request to edit.')); - readOnlyMessage.isTrusted = { enabledCommands: ['pr.checkoutFromReadonlyFile'] }; - context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.Pr, inMemPRFileSystemProvider, { isReadonly: readOnlyMessage })); - const githubFilesystemProvider = new GitHubCommitFileSystemProvider(reposManager, apiImpl, credentialStore); - context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.GitHubCommit, githubFilesystemProvider, { isReadonly: new vscode.MarkdownString(vscode.l10n.t('GitHub commits cannot be edited')) })); - - await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager, createPrHelper, copilotRemoteAgentManager, themeWatcher, prsTreeModel); - return apiImpl; -} - -export async function deactivate() { - if (telemetry) { - telemetry.dispose(); - } -} +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import TelemetryReporter from '@vscode/extension-telemetry'; +import * as vscode from 'vscode'; + +import { LiveShare } from 'vsls/vscode.js'; +import { PostCommitCommandsProvider, Repository } from './api/api'; +import { GitApiImpl } from './api/api1'; +import { registerCommands } from './commands'; +import { COPILOT_SWE_AGENT } from './common/copilot'; +import { commands } from './common/executeCommands'; +import { isSubmodule } from './common/gitUtils'; +import Logger from './common/logger'; +import * as PersistentState from './common/persistentState'; +import { parseRepositoryRemotes } from './common/remote'; +import { BRANCH_PUBLISH, EXPERIMENTAL_CHAT, FILE_LIST_LAYOUT, GIT, IGNORE_SUBMODULES, MARK_AS_VIEWED_ON_OPEN, OPEN_DIFF_ON_CLICK, PR_SETTINGS_NAMESPACE, SHOW_INLINE_OPEN_FILE_ACTION } from './common/settingKeys'; +import { initBasedOnSettingChange } from './common/settingsUtils'; +import { TemporaryState } from './common/temporaryState'; +import { fromPRUri, Schemes } from './common/uri'; +import { isDescendant } from './common/utils'; +import { EXTENSION_ID, FOCUS_REVIEW_MODE } from './constants'; +import { createExperimentationService, ExperimentationTelemetry } from './experimentationService'; +import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent'; +import { CredentialStore } from './github/credentials'; +import { FolderRepositoryManager } from './github/folderRepositoryManager'; +import { OverviewRestorer } from './github/overviewRestorer'; +import { RepositoriesManager } from './github/repositoriesManager'; +import { registerBuiltinGitProvider, registerLiveShareGitProvider } from './gitProviders/api'; +import { GitHubContactServiceProvider } from './gitProviders/GitHubContactServiceProvider'; +import { GitLensIntegration } from './integrations/gitlens/gitlensImpl'; +import { IssueFeatureRegistrar } from './issues/issueFeatureRegistrar'; +import { StateManager } from './issues/stateManager'; +import { IssueContextProvider } from './lm/issueContextProvider'; +import { ChatParticipant, ChatParticipantState } from './lm/participants'; +import { PullRequestContextProvider } from './lm/pullRequestContextProvider'; +import { registerTools } from './lm/tools/tools'; +import { migrate } from './migrations'; +import { NotificationsFeatureRegister } from './notifications/notificationsFeatureRegistar'; +import { NotificationsManager } from './notifications/notificationsManager'; +import { NotificationsProvider } from './notifications/notificationsProvider'; +import { ThemeWatcher } from './themeWatcher'; +import { resumePendingCheckout, UriHandler } from './uriHandler'; +import { CommentDecorationProvider } from './view/commentDecorationProvider'; +import { CommitsDecorationProvider } from './view/commitsDecorationProvider'; +import { CompareChanges } from './view/compareChangesTreeDataProvider'; +import { CreatePullRequestHelper } from './view/createPullRequestHelper'; +import { EmojiCompletionProvider } from './view/emojiCompletionProvider'; +import { FileTypeDecorationProvider } from './view/fileTypeDecorationProvider'; +import { GitHubCommitFileSystemProvider } from './view/githubFileContentProvider'; +import { getInMemPRFileSystemProvider } from './view/inMemPRContentProvider'; +import { PullRequestChangesTreeDataProvider } from './view/prChangesTreeDataProvider'; +import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider'; +import { PrsTreeModel } from './view/prsTreeModel'; +import { ReviewManager, ShowPullRequest } from './view/reviewManager'; +import { ReviewsManager } from './view/reviewsManager'; +import { TreeDecorationProviders } from './view/treeDecorationProviders'; +import { WebviewViewCoordinator } from './view/webviewViewCoordinator'; + +const ingestionKey = '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255'; + +let telemetry: ExperimentationTelemetry; + +const ACTIVATION = 'Activation'; + +async function init( + context: vscode.ExtensionContext, + git: GitApiImpl, + credentialStore: CredentialStore, + repositories: Repository[], + tree: PullRequestsTreeDataProvider, + liveshareApiPromise: Promise, + showPRController: ShowPullRequest, + reposManager: RepositoriesManager, + createPrHelper: CreatePullRequestHelper, + copilotRemoteAgentManager: CopilotRemoteAgentManager, + themeWatcher: ThemeWatcher, + prsTreeModel: PrsTreeModel, +): Promise { + context.subscriptions.push(Logger); + Logger.appendLine('Git repository found, initializing review manager and pr tree view.', ACTIVATION); + + context.subscriptions.push(credentialStore.onDidChangeSessions(async e => { + if (e.provider.id === 'github') { + await reposManager.clearCredentialCache(); + if (reviewsManager) { + reviewsManager.reviewManagers.forEach(reviewManager => reviewManager.updateState(true)); + } + } + })); + + context.subscriptions.push( + git.onDidPublish(async e => { + // Only notify on branch publish events + if (!e.branch) { + return; + } + + if (vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'ask' | 'never' | undefined>(BRANCH_PUBLISH) !== 'ask') { + return; + } + + const reviewManager = reviewsManager.reviewManagers.find( + manager => manager.repository.rootUri.toString() === e.repository.rootUri.toString(), + ); + if (reviewManager?.isCreatingPullRequest) { + return; + } + + const folderManager = reposManager.folderManagers.find( + manager => manager.repository.rootUri.toString() === e.repository.rootUri.toString()); + + if (!folderManager || folderManager.gitHubRepositories.length === 0) { + return; + } + + const defaults = await folderManager.getPullRequestDefaults(); + if (defaults.base === e.branch) { + return; + } + + const create = vscode.l10n.t('Create Pull Request...'); + const dontShowAgain = vscode.l10n.t('Don\'t Show Again'); + const result = await vscode.window.showInformationMessage( + vscode.l10n.t('Would you like to create a Pull Request for branch \'{0}\'?', e.branch), + create, + dontShowAgain, + ); + if (result === create) { + reviewManager?.createPullRequest(e.branch); + } else if (result === dontShowAgain) { + await vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).update(BRANCH_PUBLISH, 'never', vscode.ConfigurationTarget.Global); + } + }), + ); + + // Sort the repositories to match folders in a multiroot workspace (if possible). + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders) { + repositories = repositories.sort((a, b) => { + let indexA = workspaceFolders.length; + let indexB = workspaceFolders.length; + for (let i = 0; i < workspaceFolders.length; i++) { + if (workspaceFolders[i].uri.toString() === a.rootUri.toString()) { + indexA = i; + } else if (workspaceFolders[i].uri.toString() === b.rootUri.toString()) { + indexB = i; + } + if (indexA !== workspaceFolders.length && indexB !== workspaceFolders.length) { + break; + } + } + return indexA - indexB; + }); + } + + liveshareApiPromise.then(api => { + if (api) { + // register the pull request provider to suggest PR contacts + api.registerContactServiceProvider('github-pr', new GitHubContactServiceProvider(reposManager)); + } + }); + + const changesTree = new PullRequestChangesTreeDataProvider(git, reposManager); + context.subscriptions.push(changesTree); + + const activePrViewCoordinator = new WebviewViewCoordinator(context); + context.subscriptions.push(activePrViewCoordinator); + + let reviewManagerIndex = 0; + const reviewManagers = reposManager.folderManagers.map( + folderManager => new ReviewManager(reviewManagerIndex++, context, folderManager.repository, folderManager, telemetry, changesTree, tree, showPRController, activePrViewCoordinator, createPrHelper, git), + ); + const treeDecorationProviders = new TreeDecorationProviders(reposManager); + context.subscriptions.push(treeDecorationProviders); + treeDecorationProviders.registerProviders([new FileTypeDecorationProvider(), new CommentDecorationProvider(reposManager), new CommitsDecorationProvider(reposManager)]); + + const notificationsProvider = new NotificationsProvider(credentialStore, reposManager); + context.subscriptions.push(notificationsProvider); + + const notificationsManager = new NotificationsManager(notificationsProvider, credentialStore, reposManager, context); + context.subscriptions.push(notificationsManager); + + const reviewsManager = new ReviewsManager(context, reposManager, reviewManagers, prsTreeModel, tree, changesTree, telemetry, credentialStore, git, copilotRemoteAgentManager, notificationsManager); + context.subscriptions.push(reviewsManager); + + context.subscriptions.push(vscode.languages.registerCompletionItemProvider( + { scheme: Schemes.Comment }, + new EmojiCompletionProvider(context), + ':' + )); + + git.onDidChangeState(() => { + Logger.appendLine(`Git initialization state changed: state=${git.state}`, ACTIVATION); + reviewsManager.reviewManagers.forEach(reviewManager => reviewManager.updateState(true)); + }); + + git.onDidOpenRepository(repo => { + function addRepo() { + // Make sure we don't already have a folder manager for this repo. + const existing = reposManager.folderManagers.find(manager => manager.repository.rootUri.toString() === repo.rootUri.toString()); + if (existing) { + Logger.appendLine(`Repo ${repo.rootUri} has already been setup.`, ACTIVATION); + return; + } + + // Check if submodules should be ignored + const ignoreSubmodules = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(IGNORE_SUBMODULES, false); + if (ignoreSubmodules && isSubmodule(repo, git)) { + Logger.appendLine(`Repo ${repo.rootUri} is a submodule and will be ignored due to ${IGNORE_SUBMODULES} setting.`, ACTIVATION); + return; + } + + const newFolderManager = new FolderRepositoryManager(reposManager.folderManagers.length, context, repo, telemetry, git, credentialStore, createPrHelper, themeWatcher); + reposManager.insertFolderManager(newFolderManager); + const newReviewManager = new ReviewManager( + reviewManagerIndex++, + context, + newFolderManager.repository, + newFolderManager, + telemetry, + changesTree, + tree, + showPRController, + activePrViewCoordinator, + createPrHelper, + git + ); + reviewsManager.addReviewManager(newReviewManager); + } + + // Check if repo is in one of the workspace folders or vice versa + Logger.debug(`Checking if repo ${repo.rootUri.fsPath} is in a workspace folder.`, ACTIVATION); + Logger.debug(`Workspace folders: ${workspaceFolders?.map(folder => folder.uri.fsPath).join(', ')}`, ACTIVATION); + if (workspaceFolders && !workspaceFolders.some(folder => isDescendant(folder.uri.fsPath, repo.rootUri.fsPath, true) || isDescendant(repo.rootUri.fsPath, folder.uri.fsPath, true))) { + Logger.appendLine(`Repo ${repo.rootUri} is not in a workspace folder, ignoring.`, ACTIVATION); + return; + } + addRepo(); + const disposable = repo.state.onDidChange(() => { + Logger.appendLine(`Repo state for ${repo.rootUri} changed.`, ACTIVATION); + addRepo(); + disposable.dispose(); + }); + }); + + git.onDidCloseRepository(repo => { + reposManager.removeRepo(repo); + reviewsManager.removeReviewManager(repo); + }); + + // Auto-mark files as viewed when opened + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument(async (document) => { + // Check if the feature is enabled + if (!vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(MARK_AS_VIEWED_ON_OPEN, false)) { + return; + } + + const uri = document.uri; + + // Only process files that are part of a PR + if (uri.scheme !== Schemes.Pr && uri.scheme !== 'file') { + return; + } + + try { + // Find the manager for this file + const manager = reposManager.getManagerForFile(uri); + if (!manager) { + return; + } + + // Find the pull request from the URI + let pullRequest; + if (uri.scheme === Schemes.Pr) { + const prQuery = fromPRUri(uri); + if (prQuery) { + for (const githubRepo of manager.gitHubRepositories) { + const prNumber = Number(prQuery.prNumber); + pullRequest = githubRepo.getExistingPullRequestModel(prNumber); + if (pullRequest) { + break; + } + } + } + } else { + // For regular file scheme, use the active PR + pullRequest = manager.activePullRequest; + } + + if (!pullRequest) { + return; + } + + // Mark the file as viewed + await pullRequest.markFiles([uri.path], false, 'viewed'); + manager.setFileViewedContext(); + } catch (e) { + // Silently fail to avoid disrupting the user experience + Logger.debug(`Failed to auto-mark file as viewed: ${e}`, 'AutoMarkViewed'); + } + }) + ); + + tree.initialize(reviewsManager.reviewManagers.map(manager => manager.reviewModel), notificationsManager); + + registerCommands(context, reposManager, reviewsManager, telemetry, copilotRemoteAgentManager, notificationsManager, prsTreeModel); + + const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(FILE_LIST_LAYOUT); + await vscode.commands.executeCommand('setContext', 'fileListLayout:flat', layout === 'flat'); + + const issueStateManager = new StateManager(git, reposManager, context); + const issuesFeatures = new IssueFeatureRegistrar(git, reposManager, reviewsManager, context, telemetry, issueStateManager, copilotRemoteAgentManager); + context.subscriptions.push(issuesFeatures); + await issuesFeatures.initialize(); + + const pullRequestContextProvider = new PullRequestContextProvider(prsTreeModel, reposManager, git); + vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-PullRequestOverview**' }, 'githubpr', pullRequestContextProvider); + vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-IssueOverview**' }, 'githubissue', new IssueContextProvider(issueStateManager, reposManager)); + pullRequestContextProvider.initialize(); + + const notificationsFeatures = new NotificationsFeatureRegister(credentialStore, reposManager, telemetry, notificationsManager); + context.subscriptions.push(notificationsFeatures); + + context.subscriptions.push(new GitLensIntegration()); + + context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context.extensionUri, credentialStore)); + + await vscode.commands.executeCommand('setContext', 'github:initialized', true); + + registerPostCommitCommandsProvider(context, reposManager, git); + + // Resume any pending checkout request stored before workspace reopened. + await resumePendingCheckout(reviewsManager, context, reposManager); + + initChat(context, credentialStore, reposManager, copilotRemoteAgentManager, telemetry, prsTreeModel); + context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git))); + + // Make sure any compare changes tabs, which come from the create flow, are closed. + CompareChanges.closeTabs(); + /* __GDPR__ + "startup" : {} + */ + telemetry.sendTelemetryEvent('startup'); +} + +function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager, copilotRemoteManager: CopilotRemoteAgentManager, telemetry: ExperimentationTelemetry, prsTreeModel: PrsTreeModel) { + const createParticipant = () => { + const chatParticipantState = new ChatParticipantState(); + context.subscriptions.push(new ChatParticipant(context, chatParticipantState)); + registerTools(context, credentialStore, reposManager, chatParticipantState, copilotRemoteManager, telemetry, prsTreeModel); + }; + + const chatEnabled = () => vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(EXPERIMENTAL_CHAT, false); + if (chatEnabled()) { + createParticipant(); + } else { + initBasedOnSettingChange(PR_SETTINGS_NAMESPACE, EXPERIMENTAL_CHAT, chatEnabled, createParticipant, context.subscriptions); + } +} + +export async function activate(context: vscode.ExtensionContext): Promise { + Logger.appendLine(`Extension version: ${vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON.version}`, 'Activation'); + + // @ts-ignore + if (EXTENSION_ID === 'GitHub.vscode-pull-request-github-insiders') { + const stable = vscode.extensions.getExtension('github.vscode-pull-request-github'); + if (stable !== undefined) { + throw new Error( + 'GitHub Pull Requests and Issues Nightly cannot be used while GitHub Pull Requests and Issues is also installed. Please ensure that only one version of the extension is installed.', + ); + } + } + + const showPRController = new ShowPullRequest(); + vscode.commands.registerCommand('github.api.preloadPullRequest', async (shouldShow: boolean) => { + await vscode.commands.executeCommand('setContext', FOCUS_REVIEW_MODE, true); + await commands.focusView('github:activePullRequest:welcome'); + showPRController.shouldShow = shouldShow; + }); + await setGitSettingContexts(context); + + Logger.debug('Creating API implementation.', 'Activation'); + + telemetry = new ExperimentationTelemetry(new TelemetryReporter(ingestionKey)); + context.subscriptions.push(telemetry); + + return await deferredActivate(context, showPRController); +} + +async function setGitSettingContexts(context: vscode.ExtensionContext) { + // We set contexts instead of using the config directly in package.json because the git extension might not actually be available. + const settings: [string, () => void][] = [ + ['openDiffOnClick', () => vscode.workspace.getConfiguration(GIT, null).get(OPEN_DIFF_ON_CLICK, true)], + ['showInlineOpenFileAction', () => vscode.workspace.getConfiguration(GIT, null).get(SHOW_INLINE_OPEN_FILE_ACTION, true)] + ]; + for (const [contextName, setting] of settings) { + commands.setContext(contextName, setting()); + context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(`${GIT}.${contextName}`)) { + commands.setContext(contextName, setting()); + } + })); + } +} + +async function doRegisterBuiltinGitProvider(context: vscode.ExtensionContext, credentialStore: CredentialStore, apiImpl: GitApiImpl): Promise { + const builtInGitProvider = await registerBuiltinGitProvider(credentialStore, apiImpl); + if (builtInGitProvider) { + context.subscriptions.push(builtInGitProvider); + return true; + } + return false; +} + +function registerPostCommitCommandsProvider(context: vscode.ExtensionContext, reposManager: RepositoriesManager, git: GitApiImpl) { + const componentId = 'GitPostCommitCommands'; + class Provider implements PostCommitCommandsProvider { + + getCommands(repository: Repository) { + Logger.appendLine(`Looking for remote. Comparing ${repository.state.remotes.length} local repo remotes with ${reposManager.folderManagers.reduce((prev, curr) => prev + curr.gitHubRepositories.length, 0)} GitHub repositories.`, componentId); + const repoRemotes = parseRepositoryRemotes(repository); + + const found = reposManager.folderManagers.find(folderManager => folderManager.findRepo(githubRepo => { + return !!repoRemotes.find(remote => { + return remote.equals(githubRepo.remote); + }); + })); + Logger.appendLine(`Found ${found ? 'a repo' : 'no repos'} when getting post commit commands.`, componentId); + return found ? [{ + command: 'pr.pushAndCreate', + title: vscode.l10n.t('{0} Commit & Create Pull Request', '$(git-pull-request-create)'), + tooltip: vscode.l10n.t('Commit & Create Pull Request') + }] : []; + } + } + + function hasGitHubRepos(): boolean { + return reposManager.folderManagers.some(folderManager => folderManager.gitHubRepositories.length > 0); + } + function tryRegister(): boolean { + Logger.appendLine('Trying to register post commit commands.', 'GitPostCommitCommands'); + if (hasGitHubRepos()) { + Logger.appendLine('GitHub remote(s) found, registering post commit commands.', componentId); + context.subscriptions.push(git.registerPostCommitCommandsProvider(new Provider())); + return true; + } + return false; + } + + if (!tryRegister()) { + const reposDisposable = reposManager.onDidLoadAnyRepositories(() => { + if (tryRegister()) { + reposDisposable.dispose(); + } + }); + } +} + +async function deferredActivateRegisterBuiltInGitProvider(context: vscode.ExtensionContext, apiImpl: GitApiImpl, credentialStore: CredentialStore) { + Logger.appendLine('Registering built in git provider.', 'Activation'); + if (!(await doRegisterBuiltinGitProvider(context, credentialStore, apiImpl))) { + const extensionsChangedDisposable = vscode.extensions.onDidChange(async () => { + if (await doRegisterBuiltinGitProvider(context, credentialStore, apiImpl)) { + extensionsChangedDisposable.dispose(); + } + }); + context.subscriptions.push(extensionsChangedDisposable); + } +} + +async function deferredActivate(context: vscode.ExtensionContext, showPRController: ShowPullRequest) { + Logger.debug('Initializing state.', 'Activation'); + PersistentState.init(context); + await migrate(context); + TemporaryState.init(context); + Logger.debug('Creating credential store.', 'Activation'); + const credentialStore = new CredentialStore(telemetry, context); + context.subscriptions.push(credentialStore); + const experimentationService = await createExperimentationService(context, telemetry); + await experimentationService.initializePromise; + await experimentationService.isCachedFlightEnabled('githubaa'); + await credentialStore.create(); + + const reposManager = new RepositoriesManager(credentialStore, telemetry); + context.subscriptions.push(reposManager); + + const prsTreeModel = new PrsTreeModel(telemetry, reposManager, context); + context.subscriptions.push(prsTreeModel); + + // API + const apiImpl = new GitApiImpl(reposManager); + context.subscriptions.push(apiImpl); + + deferredActivateRegisterBuiltInGitProvider(context, apiImpl, credentialStore); + + Logger.debug('Registering live share git provider.', 'Activation'); + const liveshareGitProvider = registerLiveShareGitProvider(apiImpl); + context.subscriptions.push(liveshareGitProvider); + const liveshareApiPromise = liveshareGitProvider.initialize(); + + context.subscriptions.push(apiImpl); + + Logger.debug('Creating tree view.', 'Activation'); + + const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, apiImpl, prsTreeModel); + context.subscriptions.push(copilotRemoteAgentManager); + if (vscode.chat?.registerChatSessionItemProvider) { + const chatParticipant = vscode.chat.createChatParticipant(COPILOT_SWE_AGENT, async (request, context, stream, token) => + await copilotRemoteAgentManager.chatParticipantImpl(request, context, stream, token) + ); + context.subscriptions.push(chatParticipant); + + const provider = new class implements vscode.ChatSessionContentProvider, vscode.ChatSessionItemProvider { + label = vscode.l10n.t('GitHub Copilot Coding Agent'); + async provideChatSessionItems(token: vscode.CancellationToken) { + return await copilotRemoteAgentManager.provideChatSessions(token); + } + async provideChatSessionContent(resource: vscode.Uri, token: vscode.CancellationToken) { + return await copilotRemoteAgentManager.provideChatSessionContent(resource, token); + } + onDidChangeChatSessionItems = copilotRemoteAgentManager.onDidChangeChatSessions; + onDidCommitChatSessionItem = copilotRemoteAgentManager.onDidCommitChatSession; + }(); + + context.subscriptions.push(vscode.chat?.registerChatSessionItemProvider( + COPILOT_SWE_AGENT, + provider + )); + + context.subscriptions.push(vscode.chat?.registerChatSessionContentProvider( + COPILOT_SWE_AGENT, + provider, + chatParticipant, + { supportsInterruptions: true } + )); + } + + const prTree = new PullRequestsTreeDataProvider(prsTreeModel, telemetry, context, reposManager, copilotRemoteAgentManager); + context.subscriptions.push(prTree); + context.subscriptions.push(credentialStore.onDidGetSession(() => prTree.refreshAll(true))); + Logger.appendLine('Looking for git repository', ACTIVATION); + const repositories = apiImpl.repositories; + Logger.appendLine(`Found ${repositories.length} repositories during activation`, ACTIVATION); + const createPrHelper = new CreatePullRequestHelper(); + context.subscriptions.push(createPrHelper); + + const themeWatcher = new ThemeWatcher(); + context.subscriptions.push(themeWatcher); + + let folderManagerIndex = 0; + const folderManagers = repositories.map( + repository => new FolderRepositoryManager(folderManagerIndex++, context, repository, telemetry, apiImpl, credentialStore, createPrHelper, themeWatcher), + ); + context.subscriptions.push(...folderManagers); + for (const folderManager of folderManagers) { + reposManager.insertFolderManager(folderManager); + } + + const inMemPRFileSystemProvider = getInMemPRFileSystemProvider({ reposManager, gitAPI: apiImpl, credentialStore })!; + const readOnlyMessage = new vscode.MarkdownString(vscode.l10n.t('Cannot edit this pull request file. [Check out](command:pr.checkoutFromReadonlyFile) this pull request to edit.')); + readOnlyMessage.isTrusted = { enabledCommands: ['pr.checkoutFromReadonlyFile'] }; + context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.Pr, inMemPRFileSystemProvider, { isReadonly: readOnlyMessage })); + const githubFilesystemProvider = new GitHubCommitFileSystemProvider(reposManager, apiImpl, credentialStore); + context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.GitHubCommit, githubFilesystemProvider, { isReadonly: new vscode.MarkdownString(vscode.l10n.t('GitHub commits cannot be edited')) })); + + await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager, createPrHelper, copilotRemoteAgentManager, themeWatcher, prsTreeModel); + return apiImpl; +} + +export async function deactivate() { + if (telemetry) { + telemetry.dispose(); + } +}