From 892928658d291591d14e888c89a23de260e32f2f Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:00:14 +0200 Subject: [PATCH] Fix PR webview stuck loading when switching between sessions in agents window --- src/commands.ts | 14 ++-- src/extension.ts | 9 +-- src/github/folderRepositoryManagerResolver.ts | 3 +- src/github/issueOverview.ts | 4 ++ src/github/overviewRestorer.ts | 60 +++------------- src/github/pullRequestOverview.ts | 4 ++ src/test/github/overviewRestorer.test.ts | 70 +++++++++++++++++++ src/test/github/pullRequestOverview.test.ts | 21 ++++++ src/test/uriHandler.test.ts | 25 +++++++ 9 files changed, 146 insertions(+), 64 deletions(-) create mode 100644 src/test/github/overviewRestorer.test.ts diff --git a/src/commands.ts b/src/commands.ts index e67fe8a32b..af24bb8206 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -24,6 +24,7 @@ import { CrossChatSessionWithPR } from './github/copilotApi'; import { CopilotRemoteAgentManager, SessionIdForPr } from './github/copilotRemoteAgent'; import { guessExtensionFromMime, pickFilesForUpload, placeholdersForNames, runFileUploads, runPendingUploads } from './github/fileUpload'; import { FolderRepositoryManager } from './github/folderRepositoryManager'; +import { FolderRepositoryManagerResolver } from './github/folderRepositoryManagerResolver'; import { GitHubRepository } from './github/githubRepository'; import { Issue } from './github/interface'; import { IssueModel } from './github/issueModel'; @@ -192,7 +193,8 @@ export function registerCommands( copilotRemoteAgentManager: CopilotRemoteAgentManager, notificationManager: NotificationsManager, prsTreeModel: PrsTreeModel, - tree: PullRequestsTreeDataProvider + tree: PullRequestsTreeDataProvider, + folderRepositoryManagerResolver: FolderRepositoryManagerResolver, ) { const logId = 'RegisterCommands'; @@ -573,10 +575,7 @@ export function registerCommands( return undefined; } - const folderManager = reposManager.getManagerForRepository(context.owner, context.repo) ?? reposManager.folderManagers[0]; - if (!folderManager) { - return undefined; - } + const folderManager = folderRepositoryManagerResolver.getManagerForRepository(context.owner, context.repo); const pr = await folderManager.resolvePullRequest(context.owner, context.repo, context.number, true); if (!pr) { @@ -1123,7 +1122,10 @@ export function registerCommands( return; } - const folderManager = reposManager.getManagerForIssueModel(issueModel) ?? reposManager.folderManagers[0]; + const folderManager = folderRepositoryManagerResolver.getManagerForRepository( + issueModel.remote.owner, + issueModel.remote.repositoryName, + ); let descriptionNode: PRNode | RepositoryChangesNode | undefined; if (argument instanceof PRNode) { diff --git a/src/extension.ts b/src/extension.ts index 24396f5d0b..42f4534a0d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -259,7 +259,10 @@ async function init( tree.initialize(reviewsManager.reviewManagers.map(manager => manager.reviewModel), notificationsManager); - registerCommands(context, reposManager, reviewsManager, telemetry, copilotRemoteAgentManager, notificationsManager, prsTreeModel, tree); + const folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, reposManager, telemetry); + context.subscriptions.push(folderRepositoryManagerResolver); + + registerCommands(context, reposManager, reviewsManager, telemetry, copilotRemoteAgentManager, notificationsManager, prsTreeModel, tree, folderRepositoryManagerResolver); const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(FILE_LIST_LAYOUT); await vscode.commands.executeCommand('setContext', 'fileListLayout:flat', layout === 'flat'); @@ -293,9 +296,7 @@ async function init( context.subscriptions.push(new GitLensIntegration()); - const folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, reposManager, telemetry); - context.subscriptions.push(folderRepositoryManagerResolver); - context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context, credentialStore, folderRepositoryManagerResolver)); + context.subscriptions.push(new OverviewRestorer(telemetry, context, credentialStore, folderRepositoryManagerResolver)); await vscode.commands.executeCommand('setContext', 'github:initialized', true); diff --git a/src/github/folderRepositoryManagerResolver.ts b/src/github/folderRepositoryManagerResolver.ts index c89a44fae2..a9cf80e313 100644 --- a/src/github/folderRepositoryManagerResolver.ts +++ b/src/github/folderRepositoryManagerResolver.ts @@ -26,8 +26,7 @@ export class FolderRepositoryManagerResolver extends Disposable { } getManagerForRepository(owner: string, repo: string): FolderRepositoryManager { - const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo) - ?? this._repositoriesManager.folderManagers[0]; + const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo); if (existingManager) { return existingManager; } diff --git a/src/github/issueOverview.ts b/src/github/issueOverview.ts index 69c702bb17..ade86e4ca9 100644 --- a/src/github/issueOverview.ts +++ b/src/github/issueOverview.ts @@ -65,6 +65,10 @@ export class IssueOverviewPanel extends W const key = panelKey(identity.owner, identity.repo, identity.number); let panel = this._panels.get(key); + if (existingPanel && panel && panel._panel !== existingPanel) { + panel.dispose(); + panel = undefined; + } const activeColumn = IssueOverviewPanel._getViewColumn(toTheSide, panel); if (panel) { diff --git a/src/github/overviewRestorer.ts b/src/github/overviewRestorer.ts index a9df4941ee..95ce7416eb 100644 --- a/src/github/overviewRestorer.ts +++ b/src/github/overviewRestorer.ts @@ -6,72 +6,44 @@ import * as vscode from 'vscode'; import { CredentialStore } from './credentials'; import { registerGitHubIssueOrPullRequestExternalUriOpener } from './externalUriOpener'; -import { FolderRepositoryManager } from './folderRepositoryManager'; import { FolderRepositoryManagerResolver } from './folderRepositoryManagerResolver'; -import { GitHubRepository } from './githubRepository'; import { IssueOverviewPanel } from './issueOverview'; import { PullRequestOverviewPanel } from './pullRequestOverview'; -import { RepositoriesManager } from './repositoriesManager'; import { PullRequest } from './views'; import { Disposable } from '../common/lifecycle'; -import Logger from '../common/logger'; import { ITelemetry } from '../common/telemetry'; export class OverviewRestorer extends Disposable implements vscode.WebviewPanelSerializer { - private static ID = 'OverviewRestorer'; - - constructor(private readonly _repositoriesManager: RepositoriesManager, - private readonly _telemetry: ITelemetry, + constructor(private readonly _telemetry: ITelemetry, private readonly _context: vscode.ExtensionContext, private readonly _credentialStore: CredentialStore, - folderRepositoryManagerResolver: FolderRepositoryManagerResolver, + private readonly _folderRepositoryManagerResolver: FolderRepositoryManagerResolver, ) { super(); this._register(vscode.window.registerWebviewPanelSerializer(IssueOverviewPanel.viewType, this)); this._register(vscode.window.registerWebviewPanelSerializer(PullRequestOverviewPanel.viewType, this)); - this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, folderRepositoryManagerResolver, _telemetry)); + this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, _folderRepositoryManagerResolver, _telemetry)); } async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: PullRequest): Promise { - await this.waitForAuth(); - await this.waitForAnyGitHubRepos(this._repositoriesManager); - - if (!state || !state.number || this._repositoriesManager.folderManagers.length === 0) { + if (!state || !state.number) { webviewPanel.dispose(); return; } - let repo: GitHubRepository | undefined; - let folderManager: FolderRepositoryManager | undefined; - for (const manager of this._repositoriesManager.folderManagers) { - const githubRepository = manager.findExistingGitHubRepository({ owner: state.owner, repositoryName: state.repo }); - if (githubRepository) { - repo = githubRepository; - folderManager = manager; - break; - } - } - - if (!repo || !folderManager) { - folderManager = this._repositoriesManager.folderManagers[0]; - repo = await folderManager.createGitHubRepositoryFromOwnerName(state.owner, state.repo); - } - - if (!repo || !folderManager) { - webviewPanel.dispose(); - return; - } + await this.waitForAuth(); + const folderManager = this._folderRepositoryManagerResolver.getManagerForRepository(state.owner, state.repo); const identity = { owner: state.owner, repo: state.repo, number: state.number }; if (state.isIssue) { - const issueModel = await repo.getIssue(state.number, true); + const issueModel = await folderManager.resolveIssue(state.owner, state.repo, state.number, true, true); if (!issueModel) { webviewPanel.dispose(); return; } return IssueOverviewPanel.createOrShow(this._telemetry, this._context.extensionUri, folderManager, identity, issueModel, undefined, true, webviewPanel); } else { - const pullRequestModel = await repo.getPullRequest(state.number, 'OverviewRestorer.deserializeWebviewPanel', true); + const pullRequestModel = await folderManager.resolvePullRequest(state.owner, state.repo, state.number, true); if (!pullRequestModel) { webviewPanel.dispose(); return; @@ -86,20 +58,4 @@ export class OverviewRestorer extends Disposable implements vscode.WebviewPanelS } return new Promise(resolve => this._credentialStore.onDidGetSession(() => resolve())); } - - protected async waitForAnyGitHubRepos(reposManager: RepositoriesManager): Promise { - // Check if any folder manager already has GitHub repositories - if (reposManager.folderManagers.some(manager => manager.gitHubRepositories.length > 0)) { - return; - } - - Logger.appendLine('Waiting for GitHub repositories.', OverviewRestorer.ID); - return new Promise(resolve => { - const disposable = reposManager.onDidChangeAnyGitHubRepository(() => { - Logger.appendLine('Found GitHub repositories.', OverviewRestorer.ID); - disposable.dispose(); - resolve(); - }); - }); - } } \ No newline at end of file diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 5c12e7ab8f..0530c23b8d 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -93,6 +93,10 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel { builder.pullRequest(response => { @@ -160,6 +173,14 @@ describe('PullRequestOverview', function () { assert.strictEqual(panel0, PullRequestOverviewPanel.findPanel(identity0.owner, identity0.repo, identity0.number)); assert.strictEqual(createWebviewPanel.callCount, 1); + + const restoredWebviewPanel = vscode.window.createWebviewPanel(PullRequestOverviewPanel.viewType, '#1000', vscode.ViewColumn.One, {}); + await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity0, prModel0, false, true, restoredWebviewPanel); + + const restoredPanel = PullRequestOverviewPanel.findPanel(identity0.owner, identity0.repo, identity0.number); + assert.notStrictEqual(restoredPanel, panel0); + assert.strictEqual((restoredPanel as any)._panel, restoredWebviewPanel); + restoredWebviewPanel.dispose(); }); it('coalesces an update requested during initialization', async function () { diff --git a/src/test/uriHandler.test.ts b/src/test/uriHandler.test.ts index 39c88ad11b..055725edd5 100644 --- a/src/test/uriHandler.test.ts +++ b/src/test/uriHandler.test.ts @@ -9,11 +9,14 @@ import * as vscode from 'vscode'; import { GitApiImpl } from '../api/api1'; import { RemoteOnlyRepository } from '../api/remoteOnlyRepository'; import { CredentialStore } from '../github/credentials'; +import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { FolderRepositoryManagerResolver } from '../github/folderRepositoryManagerResolver'; import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; import { RepositoriesManager } from '../github/repositoriesManager'; import { UriHandler } from '../uriHandler'; +import { CreatePullRequestHelper } from '../view/createPullRequestHelper'; import { ReviewsManager } from '../view/reviewsManager'; +import { ThemeData } from '../view/theme'; import { MockExtensionContext } from './mocks/mockExtensionContext'; import { MockTelemetry } from './mocks/mockTelemetry'; @@ -63,4 +66,26 @@ describe('UriHandler', function () { assert.ok(createOrShow.firstCall.args[2].repository instanceof RemoteOnlyRepository); assert.strictEqual(repositoriesManager.folderManagers.length, 0); }); + + it('uses a remote-only manager when no workspace manager matches the repository', function () { + const repository = new RemoteOnlyRepository(); + const onDidChangeTheme = new vscode.EventEmitter(); + context.subscriptions.push(onDidChangeTheme); + const existingManager = new FolderRepositoryManager( + 0, + context, + repository, + telemetry, + git, + credentialStore, + new CreatePullRequestHelper(), + { onDidChangeTheme: onDidChangeTheme.event, themeData: undefined }, + ); + repositoriesManager.insertFolderManager(existingManager); + + const resolvedManager = folderRepositoryManagerResolver.getManagerForRepository('microsoft', 'vscode'); + + assert.notStrictEqual(resolvedManager, existingManager); + assert.ok(resolvedManager.repository instanceof RemoteOnlyRepository); + }); });