Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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": {
Expand Down
58 changes: 57 additions & 1 deletion src/extension/chatSessions/vscode-node/chatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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,
) {
Expand Down Expand Up @@ -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());
Copy link

Copilot AI Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context key should be set in the constructor or activation, not during conditional registration. This ensures the context is always accurate regardless of whether Copilot Cloud is enabled. Consider moving this line to the constructor.

Copilot uses AI. Check for mistakes.

// Register the Copilot Cloud chat participant
this.copilotCloudRegistrations = new DisposableStore();
const copilotSessionsProvider = this.copilotCloudRegistrations.add(
Expand Down Expand Up @@ -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,
Expand All @@ -174,11 +179,62 @@ 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) {
this.copilotCloudRegistrations.dispose();
this.copilotCloudRegistrations = undefined;
}
}

private isPullRequestExtensionInstalled(): boolean {
const extension = vscode.extensions.getExtension(GHPR_EXTENSION_ID);
return extension !== undefined;
}

private async ensurePullRequestExtensionInstalled(): Promise<void> {
if (this.isPullRequestExtensionInstalled()) {
return;
}

const isInsiders = this.envService.getEditorInfo().version.includes('insider');
Copy link

Copilot AI Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version check for 'insider' is case-sensitive but VS Code Insiders versions may use different casing. Use case-insensitive matching: this.envService.getEditorInfo().version.toLowerCase().includes('insider')

Suggested change
const isInsiders = this.envService.getEditorInfo().version.includes('insider');
const isInsiders = this.envService.getEditorInfo().version.toLowerCase().includes('insider');

Copilot uses AI. Check for mistakes.
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.');
Comment on lines +216 to +238
Copy link

Copilot AI Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The polling mechanism blocks the extension host thread unnecessarily. Consider using the vscode.extensions.onDidChange event to listen for extension installation instead of polling.

Suggested change
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.');
// Wait for the extension to be installed, using onDidChange event
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
disposeListener();
reject(new Error('GitHub Pull Request extension installation timed out.'));
}, maxWaitTime);
const checkAndResolve = async () => {
if (this.isPullRequestExtensionInstalled()) {
clearTimeout(timeout);
disposeListener();
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');
}
resolve();
}
};
const listener = vscode.extensions.onDidChange(() => {
void checkAndResolve();
});
function disposeListener() {
listener.dispose();
}
// Check immediately in case the extension is already installed
void checkAndResolve();
});

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot lets do that but I want a global listener for if the user just installs this themselves. but in the explicit install case we should continue showing the notification to reload

}
}