mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-24 00:05:46 +01:00
Merge pull request #298110 from microsoft/copilot-worktree-2026-02-26T19-05-36
Sessions window: open pull request if created
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
"main": "./out/extension.js",
|
||||
"type": "module",
|
||||
"capabilities": {
|
||||
"virtualWorkspaces": false,
|
||||
"virtualWorkspaces": true,
|
||||
"untrustedWorkspaces": {
|
||||
"supported": true
|
||||
}
|
||||
@@ -74,6 +74,11 @@
|
||||
"command": "github.createPullRequest",
|
||||
"title": "%command.createPullRequest%",
|
||||
"icon": "$(git-pull-request)"
|
||||
},
|
||||
{
|
||||
"command": "github.openPullRequest",
|
||||
"title": "%command.openPullRequest%",
|
||||
"icon": "$(git-pull-request)"
|
||||
}
|
||||
],
|
||||
"continueEditSession": [
|
||||
@@ -95,6 +100,10 @@
|
||||
"command": "github.createPullRequest",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "github.openPullRequest",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "github.graph.openOnGitHub",
|
||||
"when": "false"
|
||||
@@ -179,7 +188,13 @@
|
||||
"command": "github.createPullRequest",
|
||||
"group": "navigation",
|
||||
"order": 1,
|
||||
"when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli"
|
||||
"when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli && !github.hasOpenPullRequest"
|
||||
},
|
||||
{
|
||||
"command": "github.openPullRequest",
|
||||
"group": "navigation",
|
||||
"order": 1,
|
||||
"when": "isSessionsWindow && agentSessionHasChanges && chatSessionType == copilotcli && github.hasOpenPullRequest"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"command.openOnGitHub": "Open on GitHub",
|
||||
"command.openOnVscodeDev": "Open in vscode.dev",
|
||||
"command.createPullRequest": "Create Pull Request",
|
||||
"command.openPullRequest": "Open Pull Request",
|
||||
"config.branchProtection": "Controls whether to query repository rules for GitHub repositories",
|
||||
"config.gitAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.",
|
||||
"config.gitProtocol": "Controls which protocol is used to clone a GitHub repository",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { API as GitAPI, RefType, Repository } from './typings/git.js';
|
||||
import { publishRepository } from './publish.js';
|
||||
import { DisposableStore, getRepositoryFromUrl } from './util.js';
|
||||
import { LinkContext, getCommitLink, getLink, getVscodeDevHost } from './links.js';
|
||||
import { getOctokit } from './auth.js';
|
||||
|
||||
async function copyVscodeDevLink(gitAPI: GitAPI, useSelection: boolean, context: LinkContext, includeRange = true) {
|
||||
try {
|
||||
@@ -34,46 +35,95 @@ async function openVscodeDevLink(gitAPI: GitAPI): Promise<vscode.Uri | undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise<void> {
|
||||
if (!sessionResource || !sessionMetadata?.worktreePath) {
|
||||
return;
|
||||
interface ResolvedSessionRepo {
|
||||
repository: Repository;
|
||||
remoteInfo: { owner: string; repo: string };
|
||||
gitRemote: { name: string; fetchUrl: string };
|
||||
head: { name: string; upstream?: { name: string; remote: string; commit: string } };
|
||||
}
|
||||
|
||||
function resolveSessionRepo(gitAPI: GitAPI, sessionMetadata: { worktreePath?: string } | undefined, showErrors: boolean): ResolvedSessionRepo | undefined {
|
||||
if (!sessionMetadata?.worktreePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const worktreeUri = vscode.Uri.file(sessionMetadata.worktreePath);
|
||||
const repository = gitAPI.getRepository(worktreeUri);
|
||||
|
||||
if (!repository) {
|
||||
vscode.window.showErrorMessage(vscode.l10n.t('Could not find a git repository for the session worktree.'));
|
||||
return;
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(vscode.l10n.t('Could not find a git repository for the session worktree.'));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the GitHub remote
|
||||
const remotes = repository.state.remotes
|
||||
.filter(remote => remote.fetchUrl && getRepositoryFromUrl(remote.fetchUrl));
|
||||
|
||||
if (remotes.length === 0) {
|
||||
vscode.window.showErrorMessage(vscode.l10n.t('Could not find a GitHub remote for this repository.'));
|
||||
return;
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(vscode.l10n.t('Could not find a GitHub remote for this repository.'));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Prefer upstream -> origin -> first
|
||||
const gitRemote = remotes.find(r => r.name === 'upstream')
|
||||
?? remotes.find(r => r.name === 'origin')
|
||||
?? remotes[0];
|
||||
|
||||
const remoteInfo = getRepositoryFromUrl(gitRemote.fetchUrl!);
|
||||
if (!remoteInfo) {
|
||||
vscode.window.showErrorMessage(vscode.l10n.t('Could not parse GitHub remote URL.'));
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(vscode.l10n.t('Could not parse GitHub remote URL.'));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const head = repository.state.HEAD;
|
||||
if (!head?.name) {
|
||||
if (showErrors) {
|
||||
vscode.window.showErrorMessage(vscode.l10n.t('Could not determine the current branch.'));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { repository, remoteInfo, gitRemote: { name: gitRemote.name, fetchUrl: gitRemote.fetchUrl! }, head: head as ResolvedSessionRepo['head'] };
|
||||
}
|
||||
|
||||
async function checkOpenPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise<void> {
|
||||
const resolved = resolveSessionRepo(gitAPI, sessionMetadata, false);
|
||||
if (!resolved) {
|
||||
vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current branch (the worktree branch)
|
||||
const head = repository.state.HEAD;
|
||||
if (!head?.name) {
|
||||
vscode.window.showErrorMessage(vscode.l10n.t('Could not determine the current branch.'));
|
||||
try {
|
||||
const octokit = await getOctokit();
|
||||
const { data: openPRs } = await octokit.pulls.list({
|
||||
owner: resolved.remoteInfo.owner,
|
||||
repo: resolved.remoteInfo.repo,
|
||||
head: `${resolved.remoteInfo.owner}:${resolved.head.name}`,
|
||||
state: 'all',
|
||||
});
|
||||
|
||||
vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', openPRs.length > 0);
|
||||
} catch {
|
||||
vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise<void> {
|
||||
if (!sessionResource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = resolveSessionRepo(gitAPI, sessionMetadata, true);
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { repository, remoteInfo, gitRemote, head } = resolved;
|
||||
|
||||
// Ensure the branch is published to the remote
|
||||
if (!head.upstream) {
|
||||
try {
|
||||
@@ -96,6 +146,34 @@ async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | u
|
||||
vscode.env.openExternal(vscode.Uri.parse(prUrl));
|
||||
}
|
||||
|
||||
async function openPullRequest(gitAPI: GitAPI, _sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise<void> {
|
||||
const resolved = resolveSessionRepo(gitAPI, sessionMetadata, true);
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const octokit = await getOctokit();
|
||||
const { data: pullRequests } = await octokit.pulls.list({
|
||||
owner: resolved.remoteInfo.owner,
|
||||
repo: resolved.remoteInfo.repo,
|
||||
head: `${resolved.remoteInfo.owner}:${resolved.head.name}`,
|
||||
state: 'all',
|
||||
});
|
||||
|
||||
if (pullRequests.length > 0) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(pullRequests[0].html_url));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// If the API call fails, fall through to open the repo page
|
||||
}
|
||||
|
||||
// Fallback: open the repository page
|
||||
const { remoteInfo } = resolved;
|
||||
vscode.env.openExternal(vscode.Uri.parse(`https://github.com/${remoteInfo.owner}/${remoteInfo.repo}`));
|
||||
}
|
||||
|
||||
async function openOnGitHub(repository: Repository, commit: string): Promise<void> {
|
||||
// Get the unique remotes that contain the commit
|
||||
const branches = await repository.getBranches({ contains: commit, remote: true });
|
||||
@@ -181,5 +259,13 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
|
||||
return createPullRequest(gitAPI, sessionResource, sessionMetadata);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.openPullRequest', async (sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined) => {
|
||||
return openPullRequest(gitAPI, sessionResource, sessionMetadata);
|
||||
}));
|
||||
|
||||
disposables.add(vscode.commands.registerCommand('github.checkOpenPullRequest', async (sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined) => {
|
||||
return checkOpenPullRequest(gitAPI, sessionResource, sessionMetadata);
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import { createFileIconThemableTreeContainerScope } from '../../../../workbench/
|
||||
import { IActivityService, NumberBadge } from '../../../../workbench/services/activity/common/activity.js';
|
||||
import { IEditorService, MODAL_GROUP, SIDE_GROUP } from '../../../../workbench/services/editor/common/editorService.js';
|
||||
import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
|
||||
import { ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
|
||||
|
||||
@@ -236,6 +237,7 @@ export class ChangesViewPane extends ViewPane {
|
||||
@ISessionsManagementService private readonly sessionManagementService: ISessionsManagementService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
|
||||
|
||||
@@ -542,13 +544,24 @@ export class ChangesViewPane extends ViewPane {
|
||||
return files > 0;
|
||||
}));
|
||||
|
||||
// Check if a PR exists when the active session changes
|
||||
this.renderDisposables.add(autorun(reader => {
|
||||
const sessionResource = activeSessionResource.read(reader);
|
||||
if (sessionResource) {
|
||||
const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata;
|
||||
this.commandService.executeCommand('github.checkOpenPullRequest', sessionResource, metadata).catch(() => { /* ignore */ });
|
||||
}
|
||||
}));
|
||||
|
||||
this.renderDisposables.add(autorun(reader => {
|
||||
const { isSessionMenu, added, removed } = topLevelStats.read(reader);
|
||||
const sessionResource = activeSessionResource.read(reader);
|
||||
const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar;
|
||||
|
||||
reader.store.add(scopedInstantiationService.createInstance(
|
||||
MenuWorkbenchButtonBar,
|
||||
this.actionsContainer!,
|
||||
isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar,
|
||||
menuId,
|
||||
{
|
||||
telemetrySource: 'changesView',
|
||||
menuOptions: isSessionMenu && sessionResource
|
||||
@@ -562,7 +575,7 @@ export class ChangesViewPane extends ViewPane {
|
||||
);
|
||||
return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'working-set-diff-stats', customLabel: diffStatsLabel };
|
||||
}
|
||||
if (action.id === 'github.createPullRequest') {
|
||||
if (action.id === 'github.createPullRequest' || action.id === 'github.openPullRequest') {
|
||||
return { showIcon: true, showLabel: true, isSecondary: true, customClass: 'flex-grow' };
|
||||
}
|
||||
if (action.id === 'chatEditing.applyToParentRepo') {
|
||||
|
||||
Reference in New Issue
Block a user