diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessions.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessions.ts index 91185bb22b6..f6341b85d1e 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessions.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessions.ts @@ -5,7 +5,6 @@ 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'; @@ -20,9 +19,7 @@ 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'; @@ -57,8 +54,8 @@ 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, ) { super(); @@ -134,8 +131,6 @@ 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( @@ -162,34 +157,20 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib copilotSessionsProvider.openSessionsInBrowser(chatSessionItem); }) ); - // this.copilotCloudRegistrations.add( - // vscode.commands.registerCommand('github.copilot.cloud.sessions.proxy.checkoutFromDescription', async (ctx: { path: string } | undefined) => { - // await this.installPullRequestExtension(); - // try { - // await vscode.commands.executeCommand('pr.checkoutFromDescription', ctx?.path); - // } catch (e) { - // this.logService.error(e); - // } - // }) - // ); - // this.copilotCloudRegistrations.add( - // vscode.commands.registerCommand('github.copilot.cloud.sessions.proxy.applyChangesFromDescription', async (ctx: { path: string } | undefined) => { - // await this.installPullRequestExtension(); - // try { - // await vscode.commands.executeCommand('pr.applyChangesFromDescription', ctx?.path); - // } catch (e) { - // this.logService.error(e); - // } - // }) - // ); this.copilotCloudRegistrations.add( vscode.commands.registerCommand(CLOSE_SESSION_PR_CMD, async (ctx: CrossChatSessionWithPR) => { - await this.installPullRequestExtension(); + // await this.installPullRequestExtension(); try { - await vscode.commands.executeCommand('pr.closeChatSessionPullRequest', ctx); + const success = await this.octoKitService.closePullRequest( + ctx.pullRequestDetails.repository.owner.login, + ctx.pullRequestDetails.repository.name, + ctx.pullRequestDetails.number); + if (!success) { + this.logService.error(`${CLOSE_SESSION_PR_CMD}: Failed to close PR #${ctx.pullRequestDetails.number}`); + } copilotSessionsProvider.refresh(); } catch (e) { - this.logService.error(`${CLOSE_SESSION_PR_CMD}: ${e}`); + this.logService.error(`${CLOSE_SESSION_PR_CMD}: Exception ${e}`); } }) ); @@ -200,33 +181,4 @@ 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 installPullRequestExtension() { - 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()) { - vscode.window.showInformationMessage(vscode.l10n.t('GitHub Pull Request extension installed successfully.')); - break; - } - await new Promise(resolve => setTimeout(resolve, pollInterval)); - elapsed += pollInterval; - } - if (!this.isPullRequestExtensionInstalled()) { - throw new Error('Extension installation timed out.'); - } - await vscode.commands.executeCommand('setContext', prExtensionInstalledContextKey, true); - } } diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts index 9afbec6b366..dd55c3c56c3 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts @@ -210,7 +210,15 @@ export class CopilotChatSessionsProvider extends Disposable implements vscode.Ch this.chatSessions.set(pr.number, pr); return session; })); - const filteredSessions = sessionItems.filter(item => item !== undefined); + const filteredSessions = sessionItems + // Remove any undefined sessions + .filter(item => item !== undefined) + // Only keep sessions with attached PRs not CLOSED or MERGED + .filter(item => { + const pr = item.pullRequestDetails; + const state = pr.state.toUpperCase(); + return state !== 'CLOSED' && state !== 'MERGED'; + }); vscode.commands.executeCommand('setContext', 'github.copilot.chat.cloudSessionsEmpty', filteredSessions.length === 0); return filteredSessions; diff --git a/extensions/copilot/src/platform/github/common/githubAPI.ts b/extensions/copilot/src/platform/github/common/githubAPI.ts index 01be705650a..dbb3601b3ef 100644 --- a/extensions/copilot/src/platform/github/common/githubAPI.ts +++ b/extensions/copilot/src/platform/github/common/githubAPI.ts @@ -312,6 +312,39 @@ export async function addPullRequestCommentGraphQLRequest( return result?.data?.addComment?.commentEdge?.node || null; } +export async function closePullRequest( + fetcherService: IFetcherService, + logService: ILogService, + telemetry: ITelemetryService, + host: string, + token: string | undefined, + owner: string, + repo: string, + pullNumber: number, +): Promise { + logService.debug(`[GitHubAPI] Closing pull request ${owner}/${repo}#${pullNumber}`); + + const result = await makeGitHubAPIRequest( + fetcherService, + logService, + telemetry, + host, + `repos/${owner}/${repo}/pulls/${pullNumber}`, + 'POST', + token, + { state: 'closed' }, + '2022-11-28' + ); + + const success = result?.state === 'closed'; + if (success) { + logService.debug(`[GitHubAPI] Successfully closed pull request ${owner}/${repo}#${pullNumber}`); + } else { + logService.error(`[GitHubAPI] Failed to close pull request ${owner}/${repo}#${pullNumber}. Its state is ${result?.state}`); + } + return success; +} + export async function makeGitHubAPIRequestWithPagination( fetcherService: IFetcherService, logService: ILogService, diff --git a/extensions/copilot/src/platform/github/common/githubService.ts b/extensions/copilot/src/platform/github/common/githubService.ts index 05c959165c4..4919145f8d6 100644 --- a/extensions/copilot/src/platform/github/common/githubService.ts +++ b/extensions/copilot/src/platform/github/common/githubService.ts @@ -9,7 +9,7 @@ import { ICAPIClientService } from '../../endpoint/common/capiClient'; import { ILogService } from '../../log/common/logService'; import { IFetcherService } from '../../networking/common/fetcherService'; import { ITelemetryService } from '../../telemetry/common/telemetry'; -import { addPullRequestCommentGraphQLRequest, getPullRequestFromGlobalId, makeGitHubAPIRequest, makeGitHubAPIRequestWithPagination, makeSearchGraphQLRequest, PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI'; +import { addPullRequestCommentGraphQLRequest, closePullRequest, getPullRequestFromGlobalId, makeGitHubAPIRequest, makeGitHubAPIRequestWithPagination, makeSearchGraphQLRequest, PullRequestComment, PullRequestSearchItem, SessionInfo } from './githubAPI'; export type IGetRepositoryInfoResponseData = Endpoints["GET /repos/{owner}/{repo}"]["response"]["data"]; @@ -252,6 +252,15 @@ export interface IOctoKitService { * @returns An array of changed files with their metadata */ getPullRequestFiles(owner: string, repo: string, pullNumber: number): Promise; + + /** + * Closes a pull request. + * @param owner The repository owner + * @param repo The repository name + * @param pullNumber The pull request number + * @returns A promise that resolves to true if the PR was successfully closed + */ + closePullRequest(owner: string, repo: string, pullNumber: number): Promise; } /** @@ -335,4 +344,8 @@ export class BaseOctoKitService { const result = await makeGitHubAPIRequest(this._fetcherService, this._logService, this._telemetryService, this._capiClientService.dotcomAPIURL, `repos/${owner}/${repo}/pulls/${pullNumber}/files`, 'GET', token, undefined, '2022-11-28'); return result || []; } + + protected async closePullRequestWithToken(owner: string, repo: string, pullNumber: number, token: string): Promise { + return closePullRequest(this._fetcherService, this._logService, this._telemetryService, this._capiClientService.dotcomAPIURL, token, owner, repo, pullNumber); + } } diff --git a/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts b/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts index 5368d42977b..8ffdae9e3e7 100644 --- a/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts +++ b/extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts @@ -170,4 +170,12 @@ export class OctoKitService extends BaseOctoKitService implements IOctoKitServic } return this.getPullRequestFilesWithToken(owner, repo, pullNumber, authToken); } + + async closePullRequest(owner: string, repo: string, pullNumber: number): Promise { + const authToken = (await this._authService.getAnyGitHubSession())?.accessToken; + if (!authToken) { + return false; + } + return this.closePullRequestWithToken(owner, repo, pullNumber, authToken); + } } \ No newline at end of file