From 12e6603b1ebfa0121d4fdbbf4fd9a288e60936a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:39:46 +0000 Subject: [PATCH 1/3] Initial plan From 5bd3c7c58bb5955cae70a2de518820dcf4daa3bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:06:06 +0000 Subject: [PATCH 2/3] Add browser action to PR editors Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- package.json | 5 ++ src/commands.ts | 63 ++++++++++++++------- src/github/pullRequestOverview.ts | 25 ++++++++ src/test/extension.test.ts | 53 ++++++++++++++++- src/test/github/pullRequestOverview.test.ts | 22 +++++++ 5 files changed, 145 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index dcb1512b79..683a0c574a 100644 --- a/package.json +++ b/package.json @@ -3241,6 +3241,11 @@ } ], "editor/title": [ + { + "command": "pr.openPullRequestOnGitHub", + "group": "navigation", + "when": "activeWebviewPanelId == 'PullRequestOverview'" + }, { "command": "review.openFile", "group": "navigation", diff --git a/src/commands.ts b/src/commands.ts index 923d61832e..77fe0bdc29 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -103,14 +103,21 @@ export async function openDescription( } export async function openPullRequestOnGitHub(e: PRNode | RepositoryChangesNode | IssueModel | NotificationTreeItem, telemetry: ITelemetry) { + let url: string; if (e instanceof PRNode || e instanceof RepositoryChangesNode) { - vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.pullRequestModel.html_url)); + url = e.pullRequestModel.html_url; } else if (isNotificationTreeItem(e)) { - vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.model.html_url)); + url = e.model.html_url; } else { - vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.html_url)); + url = e.html_url; } + openPullRequestUrlOnGitHub(vscode.Uri.parse(url), telemetry); +} + +function openPullRequestUrlOnGitHub(url: vscode.Uri, telemetry: ITelemetry): void { + vscode.commands.executeCommand('vscode.open', url); + /** __GDPR__ "pr.openInGitHub" : {} */ @@ -141,6 +148,36 @@ export function findExactPullRequestNumberMatch(value: string, items: readonly P return items.find(item => item.prNumber === prNumber); } +export async function openPullRequestOnGitHubCommand( + e: PRNode | RepositoryChangesNode | PullRequestModel | vscode.Uri | undefined, + reposManager: Pick, + telemetry: ITelemetry, +): Promise { + if (!e || e instanceof vscode.Uri) { + const currentPullRequestUrl = PullRequestOverviewPanel.getCurrentPullRequestUrl(); + if (currentPullRequestUrl) { + openPullRequestUrlOnGitHub(currentPullRequestUrl, telemetry); + return; + } + + const activePullRequests: PullRequestModel[] = reposManager.folderManagers + .map(folderManager => folderManager.activePullRequest!) + .filter(activePR => !!activePR); + + if (activePullRequests.length >= 1) { + const result = await chooseItem( + activePullRequests, + itemValue => ({ label: itemValue.html_url }), + ); + if (result) { + openPullRequestOnGitHub(result, telemetry); + } + } + } else { + openPullRequestOnGitHub(e, telemetry); + } +} + function isCrossChatSessionWithPR(value: any): value is CrossChatSessionWithPR { const asCrossChatSessionWithPR = value as Partial; return !!asCrossChatSessionWithPR.pullRequestDetails; @@ -163,25 +200,7 @@ export function registerCommands( context.subscriptions.push( vscode.commands.registerCommand( 'pr.openPullRequestOnGitHub', - async (e: PRNode | RepositoryChangesNode | PullRequestModel | undefined) => { - if (!e) { - const activePullRequests: PullRequestModel[] = reposManager.folderManagers - .map(folderManager => folderManager.activePullRequest!) - .filter(activePR => !!activePR); - - if (activePullRequests.length >= 1) { - const result = await chooseItem( - activePullRequests, - itemValue => ({ label: itemValue.html_url }), - ); - if (result) { - openPullRequestOnGitHub(result, telemetry); - } - } - } else { - openPullRequestOnGitHub(e, telemetry); - } - }, + (e: PRNode | RepositoryChangesNode | PullRequestModel | vscode.Uri | undefined) => openPullRequestOnGitHubCommand(e, reposManager, telemetry), ), ); context.subscriptions.push( diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index c39e55f05d..206f8e95b3 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -130,6 +130,31 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel + repository.remote.owner.toLocaleLowerCase() === identity.owner.toLocaleLowerCase() + && repository.remote.repositoryName.toLocaleLowerCase() === identity.repo.toLocaleLowerCase() + ) ?? repositories[0])?.remote; + if (remote) { + return vscode.Uri.joinPath(vscode.Uri.parse(remote.normalizedHost), identity.owner, identity.repo, 'pull', identity.number.toString()); + } + return; + } + /** * Return the panel whose webview is currently active (focused), * or `undefined` when no PR panel is active. diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 4be8763c4e..a77cbc7b1e 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -4,10 +4,61 @@ *--------------------------------------------------------------------------------------------*/ import { default as assert } from 'assert'; +import { createSandbox, SinonSandbox } from 'sinon'; +import * as vscode from 'vscode'; import { parseDiffHunk } from '../common/diffHunk'; -import { findExactPullRequestNumberMatch } from '../commands'; +import { findExactPullRequestNumberMatch, openPullRequestOnGitHubCommand } from '../commands'; +import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; +import { MockTelemetry } from './mocks/mockTelemetry'; + +const packageJson = require('../../package.json') as { + contributes: { + commands: { command: string; icon?: string }[]; + menus: { 'editor/title': { command: string; group?: string; when?: string }[] }; + }; +}; describe('Extension Tests', function () { + describe('openPullRequestOnGitHubCommand', () => { + let sinon: SinonSandbox; + + beforeEach(() => { + sinon = createSandbox(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('opens the active PR editor in the browser', async () => { + const pullRequestUrl = vscode.Uri.parse('https://github.com/aaa/bbb/pull/123'); + const open = sinon.stub(vscode.commands, 'executeCommand').resolves(); + sinon.stub(PullRequestOverviewPanel, 'getCurrentPullRequestUrl').returns(pullRequestUrl); + + await openPullRequestOnGitHubCommand( + vscode.Uri.parse('webview-panel:/PullRequestOverview'), + { folderManagers: [] }, + new MockTelemetry(), + ); + + assert(open.calledOnceWithExactly('vscode.open', pullRequestUrl)); + }); + }); + + describe('package contributions', () => { + it('contributes a globe action to PR editors', () => { + const action = packageJson.contributes.menus['editor/title'].find(item => item.command === 'pr.openPullRequestOnGitHub' && item.when === "activeWebviewPanelId == 'PullRequestOverview'"); + const command = packageJson.contributes.commands.find(item => item.command === 'pr.openPullRequestOnGitHub'); + + assert.deepStrictEqual(action, { + command: 'pr.openPullRequestOnGitHub', + group: 'navigation', + when: "activeWebviewPanelId == 'PullRequestOverview'", + }); + assert.strictEqual(command?.icon, '$(globe)'); + }); + }); + describe('findExactPullRequestNumberMatch', () => { it('prioritizes an exact number over a title match without changing the label', () => { const items = [ diff --git a/src/test/github/pullRequestOverview.test.ts b/src/test/github/pullRequestOverview.test.ts index d0469014fa..eedeabf4c9 100644 --- a/src/test/github/pullRequestOverview.test.ts +++ b/src/test/github/pullRequestOverview.test.ts @@ -99,6 +99,28 @@ describe('PullRequestOverview', function () { assert.notStrictEqual(PullRequestOverviewPanel.findPanel('aaa', 'bbb', 1000), undefined); }); + it('builds the active PR URL before the PR has loaded', async function () { + repo.addGraphQLPullRequest(builder => { + builder.pullRequest(response => { + response.repository(r => { + r.pullRequest(pr => pr.number(1000)); + }); + }); + }); + + const prItem = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().number(1000).build(), repo); + const prModel = new PullRequestModel(credentialStore, telemetry, repo, remote, prItem); + const identity = { owner: prModel.remote.owner, repo: prModel.remote.repositoryName, number: prModel.number }; + + await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity, prModel); + const panel = PullRequestOverviewPanel.findPanel(identity.owner, identity.repo, identity.number)!; + sinon.stub(PullRequestOverviewPanel, 'getActivePanel').returns(panel); + (pullRequestManager as any)._githubRepositories = [repo]; + (panel as any)._item = undefined; + + assert.strictEqual(PullRequestOverviewPanel.getCurrentPullRequestUrl()?.toString(), 'https://github.com/aaa/bbb/pull/1000'); + }); + it('reveals an existing panel for the same PR', async function () { const createWebviewPanel = sinon.spy(vscode.window, 'createWebviewPanel'); From 3f390e734f11b912532ff5334e789774502c5cb3 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:40:09 +0200 Subject: [PATCH 3/3] Remove unhelpful tests --- src/test/extension.test.ts | 53 +------------------------------------- 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index a77cbc7b1e..4be8763c4e 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -4,61 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { default as assert } from 'assert'; -import { createSandbox, SinonSandbox } from 'sinon'; -import * as vscode from 'vscode'; import { parseDiffHunk } from '../common/diffHunk'; -import { findExactPullRequestNumberMatch, openPullRequestOnGitHubCommand } from '../commands'; -import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; -import { MockTelemetry } from './mocks/mockTelemetry'; - -const packageJson = require('../../package.json') as { - contributes: { - commands: { command: string; icon?: string }[]; - menus: { 'editor/title': { command: string; group?: string; when?: string }[] }; - }; -}; +import { findExactPullRequestNumberMatch } from '../commands'; describe('Extension Tests', function () { - describe('openPullRequestOnGitHubCommand', () => { - let sinon: SinonSandbox; - - beforeEach(() => { - sinon = createSandbox(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it('opens the active PR editor in the browser', async () => { - const pullRequestUrl = vscode.Uri.parse('https://github.com/aaa/bbb/pull/123'); - const open = sinon.stub(vscode.commands, 'executeCommand').resolves(); - sinon.stub(PullRequestOverviewPanel, 'getCurrentPullRequestUrl').returns(pullRequestUrl); - - await openPullRequestOnGitHubCommand( - vscode.Uri.parse('webview-panel:/PullRequestOverview'), - { folderManagers: [] }, - new MockTelemetry(), - ); - - assert(open.calledOnceWithExactly('vscode.open', pullRequestUrl)); - }); - }); - - describe('package contributions', () => { - it('contributes a globe action to PR editors', () => { - const action = packageJson.contributes.menus['editor/title'].find(item => item.command === 'pr.openPullRequestOnGitHub' && item.when === "activeWebviewPanelId == 'PullRequestOverview'"); - const command = packageJson.contributes.commands.find(item => item.command === 'pr.openPullRequestOnGitHub'); - - assert.deepStrictEqual(action, { - command: 'pr.openPullRequestOnGitHub', - group: 'navigation', - when: "activeWebviewPanelId == 'PullRequestOverview'", - }); - assert.strictEqual(command?.icon, '$(globe)'); - }); - }); - describe('findExactPullRequestNumberMatch', () => { it('prioritizes an exact number over a title match without changing the label', () => { const items = [