Skip to content

Commit 2c7d2ca

Browse files
authored
Merge pull request #298110 from microsoft/copilot-worktree-2026-02-26T19-05-36
Sessions window: open pull request if created
2 parents cfa8363 + 756602d commit 2c7d2ca

File tree

4 files changed

+132
-17
lines changed

4 files changed

+132
-17
lines changed

extensions/github/package.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"main": "./out/extension.js",
2323
"type": "module",
2424
"capabilities": {
25-
"virtualWorkspaces": false,
25+
"virtualWorkspaces": true,
2626
"untrustedWorkspaces": {
2727
"supported": true
2828
}
@@ -74,6 +74,11 @@
7474
"command": "github.createPullRequest",
7575
"title": "%command.createPullRequest%",
7676
"icon": "$(git-pull-request)"
77+
},
78+
{
79+
"command": "github.openPullRequest",
80+
"title": "%command.openPullRequest%",
81+
"icon": "$(git-pull-request)"
7782
}
7883
],
7984
"continueEditSession": [
@@ -95,6 +100,10 @@
95100
"command": "github.createPullRequest",
96101
"when": "false"
97102
},
103+
{
104+
"command": "github.openPullRequest",
105+
"when": "false"
106+
},
98107
{
99108
"command": "github.graph.openOnGitHub",
100109
"when": "false"
@@ -179,7 +188,13 @@
179188
"command": "github.createPullRequest",
180189
"group": "navigation",
181190
"order": 1,
182-
"when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli"
191+
"when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli && !github.hasOpenPullRequest"
192+
},
193+
{
194+
"command": "github.openPullRequest",
195+
"group": "navigation",
196+
"order": 1,
197+
"when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli && github.hasOpenPullRequest"
183198
}
184199
]
185200
},

extensions/github/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"command.openOnGitHub": "Open on GitHub",
77
"command.openOnVscodeDev": "Open in vscode.dev",
88
"command.createPullRequest": "Create Pull Request",
9+
"command.openPullRequest": "Open Pull Request",
910
"config.branchProtection": "Controls whether to query repository rules for GitHub repositories",
1011
"config.gitAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.",
1112
"config.gitProtocol": "Controls which protocol is used to clone a GitHub repository",

extensions/github/src/commands.ts

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { API as GitAPI, RefType, Repository } from './typings/git.js';
88
import { publishRepository } from './publish.js';
99
import { DisposableStore, getRepositoryFromUrl } from './util.js';
1010
import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links.js';
11+
import { getOctokit } from './auth.js';
1112

1213
async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) {
1314
try {
@@ -34,46 +35,95 @@ async function openVscodeDevLink(gitAPI: GitAPI): Promise<vscode.Uri | undefined
3435
}
3536
}
3637

37-
async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise<void> {
38-
if (!sessionResource || !sessionMetadata?.worktreePath) {
39-
return;
38+
interface ResolvedSessionRepo {
39+
repository: Repository;
40+
remoteInfo: { owner: string; repo: string };
41+
gitRemote: { name: string; fetchUrl: string };
42+
head: { name: string; upstream?: { name: string; remote: string; commit: string } };
43+
}
44+
45+
function resolveSessionRepo(gitAPI: GitAPI, sessionMetadata: { worktreePath?: string } | undefined, showErrors: boolean): ResolvedSessionRepo | undefined {
46+
if (!sessionMetadata?.worktreePath) {
47+
return undefined;
4048
}
4149

4250
const worktreeUri = vscode.Uri.file(sessionMetadata.worktreePath);
4351
const repository = gitAPI.getRepository(worktreeUri);
4452

4553
if (!repository) {
46-
vscode.window.showErrorMessage(vscode.l10n.t('Could not find a git repository for the session worktree.'));
47-
return;
54+
if (showErrors) {
55+
vscode.window.showErrorMessage(vscode.l10n.t('Could not find a git repository for the session worktree.'));
56+
}
57+
return undefined;
4858
}
4959

50-
// Find the GitHub remote
5160
const remotes = repository.state.remotes
5261
.filter(remote => remote.fetchUrl && getRepositoryFromUrl(remote.fetchUrl));
5362

5463
if (remotes.length === 0) {
55-
vscode.window.showErrorMessage(vscode.l10n.t('Could not find a GitHub remote for this repository.'));
56-
return;
64+
if (showErrors) {
65+
vscode.window.showErrorMessage(vscode.l10n.t('Could not find a GitHub remote for this repository.'));
66+
}
67+
return undefined;
5768
}
5869

59-
// Prefer upstream -> origin -> first
6070
const gitRemote = remotes.find(r => r.name === 'upstream')
6171
?? remotes.find(r => r.name === 'origin')
6272
?? remotes[0];
6373

6474
const remoteInfo = getRepositoryFromUrl(gitRemote.fetchUrl!);
6575
if (!remoteInfo) {
66-
vscode.window.showErrorMessage(vscode.l10n.t('Could not parse GitHub remote URL.'));
67-
return;
76+
if (showErrors) {
77+
vscode.window.showErrorMessage(vscode.l10n.t('Could not parse GitHub remote URL.'));
78+
}
79+
return undefined;
6880
}
6981

70-
// Get the current branch (the worktree branch)
7182
const head = repository.state.HEAD;
7283
if (!head?.name) {
73-
vscode.window.showErrorMessage(vscode.l10n.t('Could not determine the current branch.'));
84+
if (showErrors) {
85+
vscode.window.showErrorMessage(vscode.l10n.t('Could not determine the current branch.'));
86+
}
87+
return undefined;
88+
}
89+
90+
return { repository, remoteInfo, gitRemote: { name: gitRemote.name, fetchUrl: gitRemote.fetchUrl! }, head: head as ResolvedSessionRepo['head'] };
91+
}
92+
93+
async function checkOpenPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise<void> {
94+
const resolved = resolveSessionRepo(gitAPI, sessionMetadata, false);
95+
if (!resolved) {
96+
vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false);
97+
return;
98+
}
99+
100+
try {
101+
const octokit = await getOctokit();
102+
const { data: openPRs } = await octokit.pulls.list({
103+
owner: resolved.remoteInfo.owner,
104+
repo: resolved.remoteInfo.repo,
105+
head: `${resolved.remoteInfo.owner}:${resolved.head.name}`,
106+
state: 'all',
107+
});
108+
109+
vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', openPRs.length > 0);
110+
} catch {
111+
vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false);
112+
}
113+
}
114+
115+
async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise<void> {
116+
if (!sessionResource) {
117+
return;
118+
}
119+
120+
const resolved = resolveSessionRepo(gitAPI, sessionMetadata, true);
121+
if (!resolved) {
74122
return;
75123
}
76124

125+
const { repository, remoteInfo, gitRemote, head } = resolved;
126+
77127
// Ensure the branch is published to the remote
78128
if (!head.upstream) {
79129
try {
@@ -96,6 +146,34 @@ async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | u
96146
vscode.env.openExternal(vscode.Uri.parse(prUrl));
97147
}
98148

149+
async function openPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise<void> {
150+
const resolved = resolveSessionRepo(gitAPI, sessionMetadata, true);
151+
if (!resolved) {
152+
return;
153+
}
154+
155+
try {
156+
const octokit = await getOctokit();
157+
const { data: pullRequests } = await octokit.pulls.list({
158+
owner: resolved.remoteInfo.owner,
159+
repo: resolved.remoteInfo.repo,
160+
head: `${resolved.remoteInfo.owner}:${resolved.head.name}`,
161+
state: 'all',
162+
});
163+
164+
if (pullRequests.length > 0) {
165+
vscode.env.openExternal(vscode.Uri.parse(pullRequests[0].html_url));
166+
return;
167+
}
168+
} catch {
169+
// If the API call fails, fall through to open the repo page
170+
}
171+
172+
// Fallback: open the repository page
173+
const { remoteInfo } = resolved;
174+
vscode.env.openExternal(vscode.Uri.parse(`https://github.com/${remoteInfo.owner}/${remoteInfo.repo}`));
175+
}
176+
99177
async function openOnGitHub(repository: Repository, commit: string): Promise<void> {
100178
// Get the unique remotes that contain the commit
101179
const branches = await repository.getBranches({ contains: commit, remote: true });
@@ -181,5 +259,13 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
181259
return createPullRequest(gitAPI, sessionResource, sessionMetadata);
182260
}));
183261

262+
disposables.add(vscode.commands.registerCommand('github.openPullRequest', async (sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined) => {
263+
return openPullRequest(gitAPI, sessionResource, sessionMetadata);
264+
}));
265+
266+
disposables.add(vscode.commands.registerCommand('github.checkOpenPullRequest', async (sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined) => {
267+
return checkOpenPullRequest(gitAPI, sessionResource, sessionMetadata);
268+
}));
269+
184270
return disposables;
185271
}

src/vs/sessions/contrib/changesView/browser/changesView.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import { createFileIconThemableTreeContainerScope } from '../../../../workbench/
5454
import { IActivityService, NumberBadge } from '../../../../workbench/services/activity/common/activity.js';
5555
import { IEditorService, MODAL_GROUP, SIDE_GROUP } from '../../../../workbench/services/editor/common/editorService.js';
5656
import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js';
57+
import { ICommandService } from '../../../../platform/commands/common/commands.js';
5758
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
5859
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
5960

@@ -236,6 +237,7 @@ export class ChangesViewPane extends ViewPane {
236237
@ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService,
237238
@ILabelService private readonly labelService: ILabelService,
238239
@IStorageService private readonly storageService: IStorageService,
240+
@ICommandService private readonly commandService: ICommandService,
239241
) {
240242
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
241243

@@ -542,13 +544,24 @@ export class ChangesViewPane extends ViewPane {
542544
return files > 0;
543545
}));
544546

547+
// Check if a PR exists when the active session changes
548+
this.renderDisposables.add(autorun(reader => {
549+
const sessionResource = activeSessionResource.read(reader);
550+
if (sessionResource) {
551+
const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata;
552+
this.commandService.executeCommand('github.checkOpenPullRequest', sessionResource, metadata).catch(() => { /* ignore */ });
553+
}
554+
}));
555+
545556
this.renderDisposables.add(autorun(reader => {
546557
const { isSessionMenu, added, removed } = topLevelStats.read(reader);
547558
const sessionResource = activeSessionResource.read(reader);
559+
const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar;
560+
548561
reader.store.add(scopedInstantiationService.createInstance(
549562
MenuWorkbenchButtonBar,
550563
this.actionsContainer!,
551-
isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar,
564+
menuId,
552565
{
553566
telemetrySource: 'changesView',
554567
menuOptions: isSessionMenu && sessionResource
@@ -562,7 +575,7 @@ export class ChangesViewPane extends ViewPane {
562575
);
563576
return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel };
564577
}
565-
if (action.id === 'github.createPullRequest') {
578+
if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') {
566579
return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow' };
567580
}
568581
if (action.id === 'chatEditing.applyToParentRepo') {

0 commit comments

Comments
 (0)