Skip to content

Commit 1e59550

Browse files
Copilotalexr00
andcommitted
Add "Request review from Copilot" feature
- Added addReviewerCopilot backend handler in pullRequestOverview.ts - Added canRequestCopilotReview property to PullRequest interface - Added Copilot review button in Reviewers section UI - Button shows only when Copilot is available and not already a reviewer - Follows same pattern as existing "Assign Copilot" feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 1357576 commit 1e59550

File tree

6 files changed

+74
-6
lines changed

6 files changed

+74
-6
lines changed

src/github/issueOverview.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
233233
isEnterprise: issue.githubRepository.remote.isEnterprise,
234234
isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark,
235235
canAssignCopilot: assignableUsers.find(user => COPILOT_ACCOUNTS[user.login]) !== undefined,
236+
canRequestCopilotReview: false,
236237
reactions: issue.item.reactions,
237238
isAuthor: issue.author.login === currentUser.login,
238239
};

src/github/pullRequestOverview.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommo
2626
import { pickEmail, reviewersQuickPick } from './quickPicks';
2727
import { parseReviewers } from './utils';
2828
import { CancelCodingAgentReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType } from './views';
29-
import { IComment } from '../common/comment';
29+
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
3030
import { COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
3131
import { commands, contexts } from '../common/executeCommands';
3232
import { disposeAll } from '../common/lifecycle';
@@ -257,7 +257,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
257257
mergeability,
258258
emailForCommit,
259259
coAuthors,
260-
hasReviewDraft
260+
hasReviewDraft,
261+
assignableUsers
261262
] = await Promise.all([
262263
this._folderRepositoryManager.resolvePullRequest(
263264
pullRequestModel.remote.owner,
@@ -279,6 +280,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
279280
this._folderRepositoryManager.getPreferredEmail(pullRequestModel),
280281
pullRequestModel.getCoAuthors(),
281282
pullRequestModel.validateDraftMode(),
283+
this._folderRepositoryManager.getAssignableUsers()
282284
]);
283285
if (!pullRequest) {
284286
throw new Error(
@@ -302,12 +304,16 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
302304
const reviewState = this.getCurrentUserReviewState(this._existingReviewers, currentUser);
303305

304306
Logger.debug('pr.initialize', PullRequestOverviewPanel.ID);
305-
const baseContext = this.getInitializeContext(currentUser, pullRequest, timelineEvents, repositoryAccess, viewerCanEdit, []);
307+
const users = assignableUsers[pullRequestModel.remote.remoteName] ?? [];
308+
const copilotUser = users.find(user => COPILOT_ACCOUNTS[user.login]);
309+
const isCopilotAlreadyReviewer = this._existingReviewers.some(reviewer => !isITeam(reviewer.reviewer) && COPILOT_ACCOUNTS[reviewer.reviewer.login]);
310+
const baseContext = this.getInitializeContext(currentUser, pullRequest, timelineEvents, repositoryAccess, viewerCanEdit, users);
306311

307312
this.preLoadInfoNotRequiredForOverview(pullRequest);
308313

309314
const context: Partial<PullRequest> = {
310315
...baseContext,
316+
canRequestCopilotReview: copilotUser !== undefined && !isCopilotAlreadyReviewer,
311317
isCurrentlyCheckedOut: isCurrentlyCheckedOut,
312318
isRemoteBaseDeleted: pullRequest.isRemoteBaseDeleted,
313319
base: pullRequest.base.label,
@@ -415,6 +421,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
415421
return this.gotoChangesSinceReview(message);
416422
case 'pr.re-request-review':
417423
return this.reRequestReview(message);
424+
case 'pr.add-reviewer-copilot':
425+
return this.addReviewerCopilot(message);
418426
case 'pr.revert':
419427
return this.revert(message);
420428
case 'pr.open-session-log':
@@ -743,6 +751,26 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
743751
return PullRequestReviewCommon.reRequestReview(this.getReviewContext(), message);
744752
}
745753

754+
private async addReviewerCopilot(message: IRequestMessage<void>): Promise<void> {
755+
try {
756+
const copilotUser = (await this._folderRepositoryManager.getAssignableUsers())[this._item.remote.remoteName].find(user => COPILOT_ACCOUNTS[user.login]);
757+
if (copilotUser) {
758+
await this._item.requestReview([copilotUser], []);
759+
const newReviewers = await this._item.getReviewRequests();
760+
this._existingReviewers = parseReviewers(newReviewers!, await this._item.getTimelineEvents(), this._item.author);
761+
const reply = {
762+
reviewers: this._existingReviewers
763+
};
764+
this._replyMessage(message, reply);
765+
} else {
766+
this._throwError(message, 'Copilot reviewer not found.');
767+
}
768+
} catch (e) {
769+
vscode.window.showErrorMessage(formatError(e));
770+
this._throwError(message, formatError(e));
771+
}
772+
}
773+
746774
private async revert(message: IRequestMessage<string>): Promise<void> {
747775
await this._folderRepositoryManager.createPullRequestHelper.revert(this._telemetry, this._extensionUri, this._folderRepositoryManager, this._item, async (pullRequest) => {
748776
const result: Partial<PullRequest> = { revertable: !pullRequest };

src/github/views.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface Issue {
6767
isDarkTheme: boolean;
6868
isEnterprise: boolean;
6969
canAssignCopilot: boolean;
70+
canRequestCopilotReview: boolean;
7071
reactions: Reaction[];
7172
busy?: boolean;
7273
}

webviews/common/context.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export class PRContext {
9292
public readyForReviewAndMerge = (args: { mergeMethod: MergeMethod }): Promise<ReadyForReview> => this.postMessage({ command: 'pr.readyForReviewAndMerge', args });
9393

9494
public addReviewers = () => this.postMessage({ command: 'pr.change-reviewers' });
95+
public addReviewerCopilot = () => this.postMessage({ command: 'pr.add-reviewer-copilot' });
9596
public changeProjects = (): Promise<ProjectItemsReply> => this.postMessage({ command: 'pr.change-projects' });
9697
public removeProject = (project: IProjectItem) => this.postMessage({ command: 'pr.remove-project', args: project });
9798
public addMilestone = () => this.postMessage({ command: 'pr.add-milestone' });

webviews/components/sidebar.tsx

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { closeIcon, copilotIcon, settingsIcon } from './icon';
88
import { Reviewer } from './reviewer';
99
import { COPILOT_LOGINS } from '../../src/common/copilot';
1010
import { gitHubLabelColor } from '../../src/common/utils';
11-
import { IAccount, IMilestone, IProjectItem, reviewerId, reviewerLabel, ReviewState } from '../../src/github/interface';
11+
import { IAccount, IMilestone, IProjectItem, isITeam, reviewerId, reviewerLabel, ReviewState } from '../../src/github/interface';
1212
import { PullRequest } from '../../src/github/views';
1313
import PullRequestContext from '../common/context';
1414
import { Label } from '../common/label';
@@ -53,9 +53,10 @@ function Section({
5353
);
5454
}
5555

56-
export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot }: PullRequest) {
56+
export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot, canRequestCopilotReview }: PullRequest) {
5757
const {
5858
addReviewers,
59+
addReviewerCopilot,
5960
addAssignees,
6061
addAssigneeYourself,
6162
addAssigneeCopilot,
@@ -68,8 +69,10 @@ export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue
6869
} = useContext(PullRequestContext);
6970

7071
const [assigningCopilot, setAssigningCopilot] = useState(false);
72+
const [requestingCopilotReview, setRequestingCopilotReview] = useState(false);
7173

7274
const shouldShowCopilotButton = canAssignCopilot && assignees.every(assignee => !COPILOT_LOGINS.includes(assignee.login));
75+
const shouldShowCopilotReviewButton = canRequestCopilotReview && reviewers.every(reviewer => !isITeam(reviewer.reviewer) && !COPILOT_LOGINS.includes(reviewer.reviewer.login));
7376

7477
const updateProjects = async () => {
7578
const newProjects = await changeProjects();
@@ -83,10 +86,43 @@ export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue
8386
id="reviewers"
8487
title="Reviewers"
8588
hasWritePermission={hasWritePermission}
86-
onHeaderClick={async () => {
89+
onHeaderClick={async (e) => {
90+
const target = e?.target as HTMLElement;
91+
if (target?.closest && target.closest('#request-copilot-review-btn')) {
92+
return;
93+
}
8794
const newReviewers = await addReviewers();
8895
updatePR({ reviewers: newReviewers.reviewers });
8996
}}
97+
iconButtonGroup={hasWritePermission && (
98+
<div className="icon-button-group">
99+
{shouldShowCopilotReviewButton ? (
100+
<button
101+
id="request-copilot-review-btn"
102+
className="icon-button"
103+
title="Request review from Copilot"
104+
disabled={requestingCopilotReview}
105+
onClick={async (e) => {
106+
e.stopPropagation();
107+
setRequestingCopilotReview(true);
108+
try {
109+
const newReviewers = await addReviewerCopilot();
110+
updatePR({ reviewers: newReviewers.reviewers });
111+
} finally {
112+
setRequestingCopilotReview(false);
113+
}
114+
}}>
115+
{copilotIcon}
116+
</button>
117+
) : null}
118+
<button
119+
className="icon-button"
120+
title="Add Reviewers"
121+
>
122+
{settingsIcon}
123+
</button>
124+
</div>
125+
)}
90126
>
91127
{reviewers && reviewers.length ? (
92128
reviewers.map(state => (

webviews/editorWebview/test/builder/pullRequest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const PullRequestBuilder = createBuilderClass<PullRequest>()({
6161
busy: { default: undefined },
6262
lastReviewType: { default: undefined },
6363
canAssignCopilot: { default: false },
64+
canRequestCopilotReview: { default: false },
6465
isCopilotOnMyBehalf: { default: false },
6566
reactions: { default: [] },
6667
});

0 commit comments

Comments
 (0)