Skip to content

Commit f602ec3

Browse files
committed
Add PR identity
1 parent 0a966de commit f602ec3

File tree

9 files changed

+160
-45
lines changed

9 files changed

+160
-45
lines changed

src/commands.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,16 @@ export async function openDescription(
7979
if (revealNode) {
8080
descriptionNode?.reveal(descriptionNode, { select: true, focus: true });
8181
}
82+
const identity = {
83+
owner: issue.remote.owner,
84+
repo: issue.remote.repositoryName,
85+
number: issue.number
86+
};
8287
// Create and show a new webview
8388
if (issue instanceof PullRequestModel) {
84-
await PullRequestOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, issue, undefined, preserveFocus);
89+
await PullRequestOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, identity, issue, undefined, preserveFocus);
8590
} else {
86-
await IssueOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, issue);
91+
await IssueOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, identity, issue);
8792
/* __GDPR__
8893
"issue.openDescription" : {}
8994
*/
@@ -1016,8 +1021,13 @@ export function registerCommands(
10161021
const pr = descriptionNode.pullRequestModel;
10171022
const pullRequest = ensurePR(folderManager, pr);
10181023
descriptionNode.reveal(descriptionNode, { select: true, focus: true });
1024+
const identity = {
1025+
owner: pullRequest.remote.owner,
1026+
repo: pullRequest.remote.repositoryName,
1027+
number: pullRequest.number
1028+
};
10191029
// Create and show a new webview
1020-
PullRequestOverviewPanel.createOrShow(telemetry, context.extensionUri, folderManager, pullRequest, true);
1030+
PullRequestOverviewPanel.createOrShow(telemetry, context.extensionUri, folderManager, identity, pullRequest, true);
10211031

10221032
/* __GDPR__
10231033
"pr.openDescriptionToTheSide" : {}

src/github/issueOverview.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { GithubItemStateEnum, IAccount, IMilestone, IProject, IProjectItem, Repo
1212
import { IssueModel } from './issueModel';
1313
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick } from './quickPicks';
1414
import { isInCodespaces, vscodeDevPrLink } from './utils';
15-
import { ChangeAssigneesReply, DisplayLabel, Issue, ProjectItemsReply, SubmitReviewReply } from './views';
15+
import { ChangeAssigneesReply, DisplayLabel, Issue, ProjectItemsReply, SubmitReviewReply, UnresolvedIdentity } from './views';
1616
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
1717
import { emojify, ensureEmojis } from '../common/emoji';
1818
import Logger from '../common/logger';
@@ -34,14 +34,16 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
3434

3535
protected readonly _panel: vscode.WebviewPanel;
3636
protected _item: TItem;
37+
protected _identity: UnresolvedIdentity;
3738
protected _folderRepositoryManager: FolderRepositoryManager;
3839
protected _scrollPosition = { x: 0, y: 0 };
3940

4041
public static async createOrShow(
4142
telemetry: ITelemetry,
4243
extensionUri: vscode.Uri,
4344
folderRepositoryManager: FolderRepositoryManager,
44-
issue: IssueModel,
45+
identity: UnresolvedIdentity,
46+
issue?: IssueModel,
4547
toTheSide: Boolean = false,
4648
_preserveFocus: boolean = true,
4749
existingPanel?: vscode.WebviewPanel
@@ -58,7 +60,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
5860
if (IssueOverviewPanel.currentPanel) {
5961
IssueOverviewPanel.currentPanel._panel.reveal(activeColumn, true);
6062
} else {
61-
const title = `Issue #${issue.number.toString()}`;
63+
const title = `Issue #${identity.number.toString()}`;
6264
IssueOverviewPanel.currentPanel = new IssueOverviewPanel(
6365
telemetry,
6466
extensionUri,
@@ -71,7 +73,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
7173
);
7274
}
7375

74-
await IssueOverviewPanel.currentPanel!.update(folderRepositoryManager, issue);
76+
await IssueOverviewPanel.currentPanel!.updateWithIdentity(folderRepositoryManager, identity, issue);
7577
}
7678

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

290-
public async update(foldersManager: FolderRepositoryManager, issueModel: TItem, progressLocation?: string): Promise<void> {
292+
/**
293+
* Resolve a model from an unresolved identity.
294+
* Subclasses can override to resolve different types (e.g., pull requests vs issues).
295+
*/
296+
protected async resolveModel(identity: UnresolvedIdentity): Promise<TItem | undefined> {
297+
return this._folderRepositoryManager.resolveIssue(
298+
identity.owner,
299+
identity.repo,
300+
identity.number
301+
) as Promise<TItem | undefined>;
302+
}
303+
304+
/**
305+
* Get the display name for the item type (for error messages).
306+
*/
307+
protected getItemTypeName(): string {
308+
return 'issue';
309+
}
310+
311+
/**
312+
* Update the panel with an unresolved identity and optional model.
313+
* If no model is provided, it will be resolved from the identity.
314+
*/
315+
public async updateWithIdentity(foldersManager: FolderRepositoryManager, identity: UnresolvedIdentity, issueModel?: TItem, progressLocation?: string): Promise<void> {
316+
this._identity = identity;
317+
291318
if (this._folderRepositoryManager !== foldersManager) {
292319
this._folderRepositoryManager = foldersManager;
293320
this.registerPrListeners();
@@ -298,19 +325,39 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
298325
scrollPosition: this._scrollPosition,
299326
});
300327

301-
if (!this._item || (this._item.number !== issueModel.number) || !this._panel.webview.html) {
328+
const isNewItem = !this._item || (this._item.number !== identity.number);
329+
if (isNewItem || !this._panel.webview.html) {
302330
this._panel.webview.html = this.getHtmlForWebview();
303331
this._postMessage({ command: 'pr.clear' });
332+
}
304333

334+
// If no model provided, resolve it from the identity
335+
if (!issueModel) {
336+
const resolvedModel = await this.resolveModel(identity);
337+
if (!resolvedModel) {
338+
throw new Error(
339+
`Failed to resolve ${this.getItemTypeName()} #${identity.number} in ${identity.owner}/${identity.repo}`,
340+
);
341+
}
342+
issueModel = resolvedModel;
305343
}
306344

307345
if (progressLocation) {
308-
return vscode.window.withProgress({ location: { viewId: progressLocation } }, () => this.updateItem(issueModel));
346+
return vscode.window.withProgress({ location: { viewId: progressLocation } }, () => this.updateItem(issueModel!));
309347
} else {
310348
return this.updateItem(issueModel);
311349
}
312350
}
313351

352+
public async update(foldersManager: FolderRepositoryManager, issueModel: TItem, progressLocation?: string): Promise<void> {
353+
const identity: UnresolvedIdentity = {
354+
owner: issueModel.remote.owner,
355+
repo: issueModel.remote.repositoryName,
356+
number: issueModel.number
357+
};
358+
return this.updateWithIdentity(foldersManager, identity, issueModel, progressLocation);
359+
}
360+
314361
protected override async _onDidReceiveMessage(message: IRequestMessage<any>) {
315362
const result = await super._onDidReceiveMessage(message);
316363
if (result !== this.MESSAGE_UNHANDLED) {

src/github/overviewRestorer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,21 @@ export class OverviewRestorer extends Disposable implements vscode.WebviewPanelS
5353
repo = await folderManager.createGitHubRepositoryFromOwnerName(state.owner, state.repo);
5454
}
5555

56+
const identity = { owner: state.owner, repo: state.repo, number: state.number };
5657
if (state.isIssue) {
5758
const issueModel = await repo.getIssue(state.number, true);
5859
if (!issueModel) {
5960
webviewPanel.dispose();
6061
return;
6162
}
62-
return IssueOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, issueModel, undefined, true, webviewPanel);
63+
return IssueOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, identity, issueModel, undefined, true, webviewPanel);
6364
} else {
6465
const pullRequestModel = await repo.getPullRequest(state.number, true);
6566
if (!pullRequestModel) {
6667
webviewPanel.dispose();
6768
return;
6869
}
69-
return PullRequestOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, pullRequestModel, undefined, true, webviewPanel);
70+
return PullRequestOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, identity, pullRequestModel, undefined, true, webviewPanel);
7071
}
7172
}
7273

src/github/pullRequestOverview.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel';
2525
import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon';
2626
import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks';
2727
import { parseReviewers } from './utils';
28-
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType } from './views';
28+
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType, UnresolvedIdentity } from './views';
2929
import { debounce } from '../common/async';
3030
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
3131
import { COPILOT_REVIEWER, COPILOT_REVIEWER_ACCOUNT, COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
@@ -64,7 +64,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
6464
telemetry: ITelemetry,
6565
extensionUri: vscode.Uri,
6666
folderRepositoryManager: FolderRepositoryManager,
67-
issue: PullRequestModel,
67+
identity: UnresolvedIdentity,
68+
issue?: PullRequestModel,
6869
toTheSide: boolean = false,
6970
preserveFocus: boolean = true,
7071
existingPanel?: vscode.WebviewPanel
@@ -75,7 +76,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
7576
"isCopilot" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
7677
}
7778
*/
78-
telemetry.sendTelemetryEvent('pr.openDescription', { isCopilot: (issue.author.login === COPILOT_SWE_AGENT) ? 'true' : 'false' });
79+
telemetry.sendTelemetryEvent('pr.openDescription', { isCopilot: (issue?.author.login === COPILOT_SWE_AGENT) ? 'true' : 'false' });
7980

8081
const activeColumn = toTheSide
8182
? vscode.ViewColumn.Beside
@@ -88,7 +89,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
8889
if (PullRequestOverviewPanel.currentPanel) {
8990
PullRequestOverviewPanel.currentPanel._panel.reveal(activeColumn, preserveFocus);
9091
} else {
91-
const title = `Pull Request #${issue.number.toString()}`;
92+
const title = `Pull Request #${identity.number.toString()}`;
9293
PullRequestOverviewPanel.currentPanel = new PullRequestOverviewPanel(
9394
telemetry,
9495
extensionUri,
@@ -99,7 +100,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
99100
);
100101
}
101102

102-
await PullRequestOverviewPanel.currentPanel!.update(folderRepositoryManager, issue);
103+
await PullRequestOverviewPanel.currentPanel!.updateWithIdentity(folderRepositoryManager, identity, issue);
103104
}
104105

105106
protected override set _currentPanel(panel: PullRequestOverviewPanel | undefined) {
@@ -379,20 +380,43 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
379380
}
380381
}
381382

382-
public override async update(
383+
/**
384+
* Override to resolve pull requests instead of issues.
385+
*/
386+
protected override async resolveModel(identity: UnresolvedIdentity): Promise<PullRequestModel | undefined> {
387+
return this._folderRepositoryManager.resolvePullRequest(
388+
identity.owner,
389+
identity.repo,
390+
identity.number
391+
);
392+
}
393+
394+
protected override getItemTypeName(): string {
395+
return 'Pull Request';
396+
}
397+
398+
public override async updateWithIdentity(
383399
folderRepositoryManager: FolderRepositoryManager,
384-
pullRequestModel: PullRequestModel,
400+
identity: UnresolvedIdentity,
401+
pullRequestModel?: PullRequestModel,
402+
progressLocation?: string
385403
): Promise<void> {
386-
const result = super.update(folderRepositoryManager, pullRequestModel, 'pr:github');
387-
if (this._folderRepositoryManager !== folderRepositoryManager) {
388-
this.registerPrListeners();
389-
}
404+
await super.updateWithIdentity(folderRepositoryManager, identity, pullRequestModel, progressLocation);
390405

391-
await result;
392406
// Notify that this PR overview is now active
393-
PullRequestOverviewPanel._onVisible.fire(pullRequestModel);
407+
PullRequestOverviewPanel._onVisible.fire(this._item);
408+
}
394409

395-
return result;
410+
public override async update(
411+
folderRepositoryManager: FolderRepositoryManager,
412+
pullRequestModel: PullRequestModel,
413+
): Promise<void> {
414+
const identity: UnresolvedIdentity = {
415+
owner: pullRequestModel.remote.owner,
416+
repo: pullRequestModel.remote.repositoryName,
417+
number: pullRequestModel.number
418+
};
419+
return this.updateWithIdentity(folderRepositoryManager, identity, pullRequestModel, 'pr:github');
396420
}
397421

398422
protected override async _onDidReceiveMessage(message: IRequestMessage<any>) {

src/github/views.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,14 @@ export interface CodingAgentContext extends SessionLinkInfo {
193193
export interface ChangeBaseReply {
194194
base: string;
195195
events: TimelineEvent[];
196+
}
197+
198+
/**
199+
* Represents an unresolved PR or issue identity - just enough info to show the overview
200+
* panel before the full model is loaded.
201+
*/
202+
export interface UnresolvedIdentity {
203+
owner: string;
204+
repo: string;
205+
number: number;
196206
}

src/issues/issueFeatureRegistrar.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1528,7 +1528,12 @@ ${options?.body ?? ''}\n
15281528
await vscode.env.clipboard.writeText(issue.html_url);
15291529
break;
15301530
case openIssue:
1531-
await IssueOverviewPanel.createOrShow(this.telemetry, this.context.extensionUri, constFolderManager, issue);
1531+
const identity = {
1532+
owner: issue.remote.owner,
1533+
repo: issue.remote.repositoryName,
1534+
number: issue.number
1535+
};
1536+
await IssueOverviewPanel.createOrShow(this.telemetry, this.context.extensionUri, constFolderManager, identity, issue);
15321537
break;
15331538
}
15341539
});

src/test/github/pullRequestOverview.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ describe('PullRequestOverview', function () {
8282

8383
const prItem = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().number(1000).build(), repo);
8484
const prModel = new PullRequestModel(credentialStore, telemetry, repo, remote, prItem);
85+
const identity = { owner: prModel.remote.owner, repo: prModel.remote.repositoryName, number: prModel.number };
8586

86-
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel);
87+
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity, prModel);
8788

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

117118
const prItem0 = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().number(1000).build(), repo);
118119
const prModel0 = new PullRequestModel(credentialStore, telemetry, repo, remote, prItem0);
120+
const identity0 = { owner: prModel0.remote.owner, repo: prModel0.remote.repositoryName, number: prModel0.number };
119121
const resolveStub = sinon.stub(pullRequestManager, 'resolvePullRequest').resolves(prModel0);
120122
sinon.stub(prModel0, 'getReviewRequests').resolves([]);
121123
sinon.stub(prModel0, 'getTimelineEvents').resolves([]);
122124
sinon.stub(prModel0, 'validateDraftMode').resolves(true);
123125
sinon.stub(prModel0, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]);
124-
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel0);
126+
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity0, prModel0);
125127

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

131133
const prItem1 = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().number(2000).build(), repo);
132134
const prModel1 = new PullRequestModel(credentialStore, telemetry, repo, remote, prItem1);
135+
const identity1 = { owner: prModel1.remote.owner, repo: prModel1.remote.repositoryName, number: prModel1.number };
133136
resolveStub.resolves(prModel1);
134137
sinon.stub(prModel1, 'getReviewRequests').resolves([]);
135138
sinon.stub(prModel1, 'getTimelineEvents').resolves([]);
136139
sinon.stub(prModel1, 'validateDraftMode').resolves(true);
137140
sinon.stub(prModel1, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]);
138-
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel1);
141+
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity1, prModel1);
139142

140143
assert.strictEqual(panel0, PullRequestOverviewPanel.currentPanel);
141144
assert.strictEqual(createWebviewPanel.callCount, 1);

0 commit comments

Comments
 (0)