diff --git a/package.json b/package.json index a08187f994..23e87792f4 100644 --- a/package.json +++ b/package.json @@ -2250,6 +2250,11 @@ { "command": "github.copilot.cloud.sessions.proxy.closeChatSessionPullRequest", "title": "%github.copilot.command.closeChatSessionPullRequest.title%" + }, + { + "command": "github.copilot.cloud.sessions.installPullRequestExtension", + "title": "Install GitHub Pull Requests Extension", + "icon": "$(extensions)" } ], "configuration": [ @@ -3831,6 +3836,13 @@ "when": "chatSessionType == copilot-cloud-agent", "group": "context" } + ], + "chat/multiDiff/context": [ + { + "command": "github.copilot.cloud.sessions.installPullRequestExtension", + "when": "chatSessionType == copilot-cloud-agent && !github.copilot.prExtensionInstalled", + "group": "inline" + } ] }, "icons": { diff --git a/src/extension/chatSessions/vscode-node/chatSessions.ts b/src/extension/chatSessions/vscode-node/chatSessions.ts index f6341b85d1..1bffb0e12d 100644 --- a/src/extension/chatSessions/vscode-node/chatSessions.ts +++ b/src/extension/chatSessions/vscode-node/chatSessions.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; +import { IEnvService } from '../../../platform/env/common/envService'; import { IOctoKitService } from '../../../platform/github/common/githubService'; import { OctoKitService } from '../../../platform/github/common/octoKitServiceImpl'; import { ILogService } from '../../../platform/log/common/logService'; @@ -19,7 +20,9 @@ import { CopilotCLIAgentManager } from '../../agents/copilotcli/node/copilotcliA import { CopilotCLISessionService, ICopilotCLISessionService } from '../../agents/copilotcli/node/copilotcliSessionService'; import { ILanguageModelServer, LanguageModelServer } from '../../agents/node/langModelServer'; import { IExtensionContribution } from '../../common/contributions'; +import { prExtensionInstalledContextKey } from '../../contextKeys/vscode-node/contextKeys.contribution'; import { ChatSummarizerProvider } from '../../prompt/node/summarizer'; +import { GHPR_EXTENSION_ID } from '../vscode/chatSessionsUriHandler'; import { ClaudeChatSessionContentProvider } from './claudeChatSessionContentProvider'; import { ClaudeChatSessionItemProvider } from './claudeChatSessionItemProvider'; import { ClaudeChatSessionParticipant } from './claudeChatSessionParticipant'; @@ -54,6 +57,7 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib constructor( @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvService private readonly envService: IEnvService, @ILogService private readonly logService: ILogService, @IOctoKitService private readonly octoKitService: IOctoKitService, ) { @@ -131,6 +135,8 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib const enabled = this.configurationService.getConfig(ConfigKey.Internal.CopilotCloudEnabled); if (enabled && !this.copilotCloudRegistrations) { + vscode.commands.executeCommand('setContext', prExtensionInstalledContextKey, this.isPullRequestExtensionInstalled()); + // Register the Copilot Cloud chat participant this.copilotCloudRegistrations = new DisposableStore(); const copilotSessionsProvider = this.copilotCloudRegistrations.add( @@ -159,7 +165,6 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib ); this.copilotCloudRegistrations.add( vscode.commands.registerCommand(CLOSE_SESSION_PR_CMD, async (ctx: CrossChatSessionWithPR) => { - // await this.installPullRequestExtension(); try { const success = await this.octoKitService.closePullRequest( ctx.pullRequestDetails.repository.owner.login, @@ -174,6 +179,16 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib } }) ); + this.copilotCloudRegistrations.add( + vscode.commands.registerCommand('github.copilot.cloud.sessions.installPullRequestExtension', async () => { + try { + await this.ensurePullRequestExtensionInstalled(); + } catch (e) { + this.logService.error(`Failed to install GitHub Pull Request extension: ${e}`); + vscode.window.showErrorMessage(vscode.l10n.t('Failed to install GitHub Pull Request extension. Please install it manually from the Extensions view.')); + } + }) + ); return copilotSessionsProvider; } else if (!enabled && this.copilotCloudRegistrations) { @@ -181,4 +196,45 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib this.copilotCloudRegistrations = undefined; } } + + private isPullRequestExtensionInstalled(): boolean { + const extension = vscode.extensions.getExtension(GHPR_EXTENSION_ID); + return extension !== undefined; + } + + private async ensurePullRequestExtensionInstalled(): Promise { + if (this.isPullRequestExtensionInstalled()) { + return; + } + + const isInsiders = this.envService.getEditorInfo().version.includes('insider'); + const installOptions = { enable: true, installPreReleaseVersion: isInsiders }; + + await vscode.commands.executeCommand('workbench.extensions.installExtension', GHPR_EXTENSION_ID, installOptions); + + const maxWaitTime = 10_000; // 10 seconds + const pollInterval = 100; // 100ms + let elapsed = 0; + + while (elapsed < maxWaitTime) { + if (this.isPullRequestExtensionInstalled()) { + await vscode.commands.executeCommand('setContext', prExtensionInstalledContextKey, true); + + const reloadAction = vscode.l10n.t('Reload Window'); + const result = await vscode.window.showInformationMessage( + vscode.l10n.t('GitHub Pull Request extension installed successfully. Reload VS Code to activate it.'), + reloadAction + ); + + if (result === reloadAction) { + await vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + return; + } + await new Promise(resolve => setTimeout(resolve, pollInterval)); + elapsed += pollInterval; + } + + throw new Error('GitHub Pull Request extension installation timed out.'); + } }