Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,16 @@ export async function openDescription(
if (revealNode) {
descriptionNode?.reveal(descriptionNode, { select: true, focus: true });
}
const identity = {
owner: issue.remote.owner,
repo: issue.remote.repositoryName,
number: issue.number
};
// Create and show a new webview
if (issue instanceof PullRequestModel) {
await PullRequestOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, issue, undefined, preserveFocus);
await PullRequestOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, identity, issue, undefined, preserveFocus);
} else {
await IssueOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, issue);
await IssueOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, identity, issue);
/* __GDPR__
"issue.openDescription" : {}
*/
Expand Down Expand Up @@ -1016,8 +1021,13 @@ export function registerCommands(
const pr = descriptionNode.pullRequestModel;
const pullRequest = ensurePR(folderManager, pr);
descriptionNode.reveal(descriptionNode, { select: true, focus: true });
const identity = {
owner: pullRequest.remote.owner,
repo: pullRequest.remote.repositoryName,
number: pullRequest.number
};
// Create and show a new webview
PullRequestOverviewPanel.createOrShow(telemetry, context.extensionUri, folderManager, pullRequest, true);
PullRequestOverviewPanel.createOrShow(telemetry, context.extensionUri, folderManager, identity, pullRequest, true);

/* __GDPR__
"pr.openDescriptionToTheSide" : {}
Expand Down
61 changes: 54 additions & 7 deletions src/github/issueOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { GithubItemStateEnum, IAccount, IMilestone, IProject, IProjectItem, Repo
import { IssueModel } from './issueModel';
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick } from './quickPicks';
import { isInCodespaces, vscodeDevPrLink } from './utils';
import { ChangeAssigneesReply, DisplayLabel, Issue, ProjectItemsReply, SubmitReviewReply } from './views';
import { ChangeAssigneesReply, DisplayLabel, Issue, ProjectItemsReply, SubmitReviewReply, UnresolvedIdentity } from './views';
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
import { emojify, ensureEmojis } from '../common/emoji';
import Logger from '../common/logger';
Expand All @@ -34,14 +34,16 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W

protected readonly _panel: vscode.WebviewPanel;
protected _item: TItem;
protected _identity: UnresolvedIdentity;
protected _folderRepositoryManager: FolderRepositoryManager;
protected _scrollPosition = { x: 0, y: 0 };

public static async createOrShow(
telemetry: ITelemetry,
extensionUri: vscode.Uri,
folderRepositoryManager: FolderRepositoryManager,
issue: IssueModel,
identity: UnresolvedIdentity,
issue?: IssueModel,
toTheSide: Boolean = false,
_preserveFocus: boolean = true,
existingPanel?: vscode.WebviewPanel
Expand All @@ -58,7 +60,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
if (IssueOverviewPanel.currentPanel) {
IssueOverviewPanel.currentPanel._panel.reveal(activeColumn, true);
} else {
const title = `Issue #${issue.number.toString()}`;
const title = `Issue #${identity.number.toString()}`;
IssueOverviewPanel.currentPanel = new IssueOverviewPanel(
telemetry,
extensionUri,
Expand All @@ -71,7 +73,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
);
}

await IssueOverviewPanel.currentPanel!.update(folderRepositoryManager, issue);
await IssueOverviewPanel.currentPanel!.updateWithIdentity(folderRepositoryManager, identity, issue);
}

public static refresh(): void {
Expand Down Expand Up @@ -287,7 +289,32 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
// none for issues
}

public async update(foldersManager: FolderRepositoryManager, issueModel: TItem, progressLocation?: string): Promise<void> {
/**
* Resolve a model from an unresolved identity.
* Subclasses can override to resolve different types (e.g., pull requests vs issues).
*/
protected async resolveModel(identity: UnresolvedIdentity): Promise<TItem | undefined> {
return this._folderRepositoryManager.resolveIssue(
identity.owner,
identity.repo,
identity.number
) as Promise<TItem | undefined>;
}

/**
* Get the display name for the item type (for error messages).
*/
protected getItemTypeName(): string {
return 'issue';
}

/**
* Update the panel with an unresolved identity and optional model.
* If no model is provided, it will be resolved from the identity.
*/
public async updateWithIdentity(foldersManager: FolderRepositoryManager, identity: UnresolvedIdentity, issueModel?: TItem, progressLocation?: string): Promise<void> {
this._identity = identity;

if (this._folderRepositoryManager !== foldersManager) {
this._folderRepositoryManager = foldersManager;
this.registerPrListeners();
Expand All @@ -298,19 +325,39 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
scrollPosition: this._scrollPosition,
});

if (!this._item || (this._item.number !== issueModel.number) || !this._panel.webview.html) {
const isNewItem = !this._item || (this._item.number !== identity.number);
if (isNewItem || !this._panel.webview.html) {
this._panel.webview.html = this.getHtmlForWebview();
this._postMessage({ command: 'pr.clear' });
}

// If no model provided, resolve it from the identity
if (!issueModel) {
const resolvedModel = await this.resolveModel(identity);
if (!resolvedModel) {
throw new Error(
`Failed to resolve ${this.getItemTypeName()} #${identity.number} in ${identity.owner}/${identity.repo}`,
);
}
issueModel = resolvedModel;
}

if (progressLocation) {
return vscode.window.withProgress({ location: { viewId: progressLocation } }, () => this.updateItem(issueModel));
return vscode.window.withProgress({ location: { viewId: progressLocation } }, () => this.updateItem(issueModel!));
} else {
return this.updateItem(issueModel);
}
}

public async update(foldersManager: FolderRepositoryManager, issueModel: TItem, progressLocation?: string): Promise<void> {
const identity: UnresolvedIdentity = {
owner: issueModel.remote.owner,
repo: issueModel.remote.repositoryName,
number: issueModel.number
};
return this.updateWithIdentity(foldersManager, identity, issueModel, progressLocation);
}

protected override async _onDidReceiveMessage(message: IRequestMessage<any>) {
const result = await super._onDidReceiveMessage(message);
if (result !== this.MESSAGE_UNHANDLED) {
Expand Down
5 changes: 3 additions & 2 deletions src/github/overviewRestorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,21 @@ export class OverviewRestorer extends Disposable implements vscode.WebviewPanelS
repo = await folderManager.createGitHubRepositoryFromOwnerName(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);
if (!issueModel) {
webviewPanel.dispose();
return;
}
return IssueOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, issueModel, undefined, true, webviewPanel);
return IssueOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, identity, issueModel, undefined, true, webviewPanel);
} else {
const pullRequestModel = await repo.getPullRequest(state.number, true);
if (!pullRequestModel) {
webviewPanel.dispose();
return;
}
return PullRequestOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, pullRequestModel, undefined, true, webviewPanel);
return PullRequestOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, identity, pullRequestModel, undefined, true, webviewPanel);
}
Comment on lines +56 to 71
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overview restorer still resolves the issue/PR model before showing the webview (lines 58 and 65). If the resolution fails, the panel is disposed without showing anything to the user (lines 59-61, 66-68). Consider updating this to match the pattern used in UriHandler._openIssueWebview, where the webview is shown immediately with the identity and the model is resolved asynchronously. This would provide better user feedback by showing the webview while the model is being loaded.

See below for a potential fix:

			return IssueOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, identity, undefined, undefined, true, webviewPanel);
		} else {
			return PullRequestOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, identity, undefined, undefined, true, webviewPanel);

Copilot uses AI. Check for mistakes.
}

Expand Down
52 changes: 38 additions & 14 deletions src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel';
import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon';
import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks';
import { parseReviewers } from './utils';
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType } from './views';
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType, UnresolvedIdentity } from './views';
import { debounce } from '../common/async';
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
import { COPILOT_REVIEWER, COPILOT_REVIEWER_ACCOUNT, COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
Expand Down Expand Up @@ -64,7 +64,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
telemetry: ITelemetry,
extensionUri: vscode.Uri,
folderRepositoryManager: FolderRepositoryManager,
issue: PullRequestModel,
identity: UnresolvedIdentity,
issue?: PullRequestModel,
toTheSide: boolean = false,
preserveFocus: boolean = true,
existingPanel?: vscode.WebviewPanel
Expand All @@ -75,7 +76,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
"isCopilot" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
telemetry.sendTelemetryEvent('pr.openDescription', { isCopilot: (issue.author.login === COPILOT_SWE_AGENT) ? 'true' : 'false' });
telemetry.sendTelemetryEvent('pr.openDescription', { isCopilot: (issue?.author.login === COPILOT_SWE_AGENT) ? 'true' : 'false' });

const activeColumn = toTheSide
? vscode.ViewColumn.Beside
Expand All @@ -88,7 +89,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
if (PullRequestOverviewPanel.currentPanel) {
PullRequestOverviewPanel.currentPanel._panel.reveal(activeColumn, preserveFocus);
} else {
const title = `Pull Request #${issue.number.toString()}`;
const title = `Pull Request #${identity.number.toString()}`;
PullRequestOverviewPanel.currentPanel = new PullRequestOverviewPanel(
telemetry,
extensionUri,
Expand All @@ -99,7 +100,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
);
}

await PullRequestOverviewPanel.currentPanel!.update(folderRepositoryManager, issue);
await PullRequestOverviewPanel.currentPanel!.updateWithIdentity(folderRepositoryManager, identity, issue);
}

protected override set _currentPanel(panel: PullRequestOverviewPanel | undefined) {
Expand Down Expand Up @@ -379,20 +380,43 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
}
}

public override async update(
/**
* Override to resolve pull requests instead of issues.
*/
protected override async resolveModel(identity: UnresolvedIdentity): Promise<PullRequestModel | undefined> {
return this._folderRepositoryManager.resolvePullRequest(
identity.owner,
identity.repo,
identity.number
);
}

protected override getItemTypeName(): string {
return 'Pull Request';
}

public override async updateWithIdentity(
folderRepositoryManager: FolderRepositoryManager,
pullRequestModel: PullRequestModel,
identity: UnresolvedIdentity,
pullRequestModel?: PullRequestModel,
progressLocation?: string
): Promise<void> {
const result = super.update(folderRepositoryManager, pullRequestModel, 'pr:github');
if (this._folderRepositoryManager !== folderRepositoryManager) {
this.registerPrListeners();
}
await super.updateWithIdentity(folderRepositoryManager, identity, pullRequestModel, progressLocation);

await result;
// Notify that this PR overview is now active
PullRequestOverviewPanel._onVisible.fire(pullRequestModel);
PullRequestOverviewPanel._onVisible.fire(this._item);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _onVisible event is fired unconditionally with this._item, but this._item could potentially be undefined if an error occurred during updateItem. The existing code at line 189-191 shows that the pattern elsewhere is to check if (this._item) before firing this event. Consider adding a similar guard here to ensure the event is only fired when a valid pull request model is available.

Suggested change
PullRequestOverviewPanel._onVisible.fire(this._item);
if (this._item) {
PullRequestOverviewPanel._onVisible.fire(this._item);
}

Copilot uses AI. Check for mistakes.
}

return result;
public override async update(
folderRepositoryManager: FolderRepositoryManager,
pullRequestModel: PullRequestModel,
): Promise<void> {
const identity: UnresolvedIdentity = {
owner: pullRequestModel.remote.owner,
repo: pullRequestModel.remote.repositoryName,
number: pullRequestModel.number
};
return this.updateWithIdentity(folderRepositoryManager, identity, pullRequestModel, 'pr:github');
}

protected override async _onDidReceiveMessage(message: IRequestMessage<any>) {
Expand Down
10 changes: 10 additions & 0 deletions src/github/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,14 @@ export interface CodingAgentContext extends SessionLinkInfo {
export interface ChangeBaseReply {
base: string;
events: TimelineEvent[];
}

/**
* Represents an unresolved PR or issue identity - just enough info to show the overview
* panel before the full model is loaded.
*/
export interface UnresolvedIdentity {
owner: string;
repo: string;
number: number;
}
7 changes: 6 additions & 1 deletion src/issues/issueFeatureRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1528,7 +1528,12 @@ ${options?.body ?? ''}\n
await vscode.env.clipboard.writeText(issue.html_url);
break;
case openIssue:
await IssueOverviewPanel.createOrShow(this.telemetry, this.context.extensionUri, constFolderManager, issue);
const identity = {
owner: issue.remote.owner,
repo: issue.remote.repositoryName,
number: issue.number
};
await IssueOverviewPanel.createOrShow(this.telemetry, this.context.extensionUri, constFolderManager, identity, issue);
break;
}
});
Expand Down
9 changes: 6 additions & 3 deletions src/test/github/pullRequestOverview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ describe('PullRequestOverview', function () {

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, prModel);
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity, prModel);

assert(
createWebviewPanel.calledWith(sinonMatch.string, 'Pull Request #1000', vscode.ViewColumn.One, {
Expand Down Expand Up @@ -116,12 +117,13 @@ describe('PullRequestOverview', function () {

const prItem0 = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().number(1000).build(), repo);
const prModel0 = new PullRequestModel(credentialStore, telemetry, repo, remote, prItem0);
const identity0 = { owner: prModel0.remote.owner, repo: prModel0.remote.repositoryName, number: prModel0.number };
const resolveStub = sinon.stub(pullRequestManager, 'resolvePullRequest').resolves(prModel0);
sinon.stub(prModel0, 'getReviewRequests').resolves([]);
sinon.stub(prModel0, 'getTimelineEvents').resolves([]);
sinon.stub(prModel0, 'validateDraftMode').resolves(true);
sinon.stub(prModel0, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]);
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel0);
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity0, prModel0);

const panel0 = PullRequestOverviewPanel.currentPanel;
assert.notStrictEqual(panel0, undefined);
Expand All @@ -130,12 +132,13 @@ describe('PullRequestOverview', function () {

const prItem1 = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().number(2000).build(), repo);
const prModel1 = new PullRequestModel(credentialStore, telemetry, repo, remote, prItem1);
const identity1 = { owner: prModel1.remote.owner, repo: prModel1.remote.repositoryName, number: prModel1.number };
resolveStub.resolves(prModel1);
sinon.stub(prModel1, 'getReviewRequests').resolves([]);
sinon.stub(prModel1, 'getTimelineEvents').resolves([]);
sinon.stub(prModel1, 'validateDraftMode').resolves(true);
sinon.stub(prModel1, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]);
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel1);
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity1, prModel1);

assert.strictEqual(panel0, PullRequestOverviewPanel.currentPanel);
assert.strictEqual(createWebviewPanel.callCount, 1);
Expand Down
Loading
Loading