From c4f26872d1350e7bd6f7a7fc87c7d9cddf4bef5b Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega Date: Thu, 26 Feb 2026 15:08:16 -0800 Subject: [PATCH] Add command to check for open pull requests and refactor session repository resolution --- extensions/github/src/commands.ts | 80 +++++++++++++++---- extensions/github/src/extension.ts | 6 -- .../changesView/browser/changesView.ts | 11 ++- 3 files changed, 75 insertions(+), 22 deletions(-) diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 1ab189feae7..bbdec6c59a6 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -35,46 +35,94 @@ async function openVscodeDevLink(gitAPI: GitAPI): Promise { - 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 { + const resolved = resolveSessionRepo(gitAPI, sessionMetadata, false); + if (!resolved) { 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: pullRequests } = await octokit.pulls.list({ + owner: resolved.remoteInfo.owner, + repo: resolved.remoteInfo.repo, + head: `${resolved.remoteInfo.owner}:${resolved.head.name}`, + state: 'open', + }); + + vscode.commands.executeCommand('setContext', 'github.hasOpenPullRequest', pullRequests.length > 0); + } catch { + // Silently fail — leave context key unchanged + } +} + +async function createPullRequest(gitAPI: GitAPI, sessionResource: vscode.Uri | undefined, sessionMetadata: { worktreePath?: string } | undefined): Promise { + 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 { @@ -205,5 +253,9 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return createPullRequest(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; } diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts index 90b382d5f3d..17906c57d44 100644 --- a/extensions/github/src/extension.ts +++ b/extensions/github/src/extension.ts @@ -20,7 +20,6 @@ import { GitHubSourceControlHistoryItemDetailsProvider } from './historyItemDeta import { OctokitService } from './auth.js'; export function activate(context: ExtensionContext): void { - console.log('[github ext] activate() called'); const disposables: Disposable[] = []; context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose())); @@ -97,12 +96,9 @@ function initializeGitExtension(context: ExtensionContext, octokitService: Octok const initialize = () => { gitExtension!.activate() .then(extension => { - console.log('[github ext] git extension activated, enabled:', extension.enabled); const onDidChangeGitExtensionEnablement = (enabled: boolean) => { - console.log('[github ext] onDidChangeGitExtensionEnablement:', enabled); if (enabled) { const gitAPI = extension.getAPI(1); - console.log('[github ext] got gitAPI, repositories:', gitAPI.repositories.length); disposables.add(registerCommands(gitAPI)); disposables.add(new GithubCredentialProviderManager(gitAPI)); @@ -126,10 +122,8 @@ function initializeGitExtension(context: ExtensionContext, octokitService: Octok }; if (gitExtension) { - console.log('[github ext] vscode.git extension found, initializing'); initialize(); } else { - console.log('[github ext] vscode.git extension NOT found, waiting...'); const listener = extensions.onDidChange(() => { if (!gitExtension && extensions.getExtension('vscode.git')) { gitExtension = extensions.getExtension('vscode.git'); diff --git a/src/vs/sessions/contrib/changesView/browser/changesView.ts b/src/vs/sessions/contrib/changesView/browser/changesView.ts index fd6f40a2d6a..702acb7808c 100644 --- a/src/vs/sessions/contrib/changesView/browser/changesView.ts +++ b/src/vs/sessions/contrib/changesView/browser/changesView.ts @@ -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); @@ -546,7 +548,13 @@ export class ChangesViewPane extends ViewPane { const { isSessionMenu, added, removed } = topLevelStats.read(reader); const sessionResource = activeSessionResource.read(reader); const menuId = isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar; - console.log('[changesView] isSessionMenu:', isSessionMenu, 'menuId:', menuId.id, 'sessionResource:', sessionResource?.toString()); + + // Proactively check if a PR exists for the current session branch + if (isSessionMenu && sessionResource) { + const metadata = this.agentSessionsService.getSession(sessionResource)?.metadata; + this.commandService.executeCommand('github.checkOpenPullRequest', sessionResource, metadata).catch(() => { /* ignore */ }); + } + reader.store.add(scopedInstantiationService.createInstance( MenuWorkbenchButtonBar, this.actionsContainer!, @@ -557,7 +565,6 @@ export class ChangesViewPane extends ViewPane { ? { args: [sessionResource, this.agentSessionsService.getSession(sessionResource)?.metadata] } : { shouldForwardArgs: true }, buttonConfigProvider: (action) => { - console.log('[changesView] buttonConfigProvider action:', action.id); if (action.id === 'chatEditing.viewChanges' || action.id === 'chatEditing.viewPreviousEdits' || action.id === 'chatEditing.viewAllSessionChanges' || action.id === 'chat.openSessionWorktreeInVSCode') { const diffStatsLabel = new MarkdownString( `+${added} -${removed}`,