Skip to content

Commit 0033ec3

Browse files
committed
Get webview context menu commands ready for multi-webview
Part of #3058
1 parent 926e0f2 commit 0033ec3

6 files changed

Lines changed: 151 additions & 44 deletions

File tree

src/commands.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { PullRequestOverviewPanel } from './github/pullRequestOverview';
3131
import { chooseItem } from './github/quickPicks';
3232
import { RepositoriesManager } from './github/repositoriesManager';
3333
import { codespacesPrLink, getIssuesUrl, getPullsUrl, isInCodespaces, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, vscodeDevPrLink } from './github/utils';
34-
import { OverviewContext } from './github/views';
34+
import { BaseContext, OverviewContext } from './github/views';
3535
import { IssueChatContextItem } from './lm/issueContextProvider';
3636
import { PRChatContextItem } from './lm/pullRequestContextProvider';
3737
import { isNotificationTreeItem, NotificationTreeItem } from './notifications/notificationItem';
@@ -141,6 +141,9 @@ export function registerCommands(
141141
tree: PullRequestsTreeDataProvider
142142
) {
143143
const logId = 'RegisterCommands';
144+
145+
PullRequestOverviewPanel.registerGlobalCommands(context, telemetry);
146+
144147
context.subscriptions.push(
145148
vscode.commands.registerCommand(
146149
'pr.openPullRequestOnGitHub',
@@ -484,7 +487,7 @@ export function registerCommands(
484487

485488
}));
486489

487-
const resolvePr = async (context: OverviewContext | undefined): Promise<{ folderManager: FolderRepositoryManager, pr: PullRequestModel } | undefined> => {
490+
const resolvePr = async (context: BaseContext | undefined): Promise<{ folderManager: FolderRepositoryManager, pr: PullRequestModel } | undefined> => {
488491
if (!context) {
489492
return undefined;
490493
}
@@ -589,7 +592,7 @@ export function registerCommands(
589592
return { folderManager, githubRepo: folderManager.gitHubRepositories[0] };
590593
}
591594

592-
function contextHasPath(ctx: OverviewContext | { path: string } | undefined): ctx is { path: string } {
595+
function contextHasPath(ctx: BaseContext | { path: string } | undefined): ctx is { path: string } {
593596
const contextAsPath: Partial<{ path: string }> = (ctx as { path: string });
594597
return !!contextAsPath.path;
595598
}
@@ -646,7 +649,7 @@ export function registerCommands(
646649
return reviewsManager.switchToPr(resolved.folderManager, resolved.pullRequest, resolved.folderManager.repository, true);
647650
}));
648651

649-
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutFromDescription', async (ctx: OverviewContext | undefined) => {
652+
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutFromDescription', async (ctx: BaseContext | undefined) => {
650653
if (!ctx) {
651654
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.'));
652655
}
@@ -681,7 +684,7 @@ export function registerCommands(
681684
});
682685
}));
683686

684-
context.subscriptions.push(vscode.commands.registerCommand('pr.applyChangesFromDescription', async (ctx: OverviewContext | undefined) => {
687+
context.subscriptions.push(vscode.commands.registerCommand('pr.applyChangesFromDescription', async (ctx: BaseContext | undefined) => {
685688
if (!ctx) {
686689
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for applying changes.'));
687690
}
@@ -741,7 +744,7 @@ export function registerCommands(
741744
pullRequestModel = pullRequest;
742745
}
743746
else {
744-
const resolved = await resolvePr(pr as OverviewContext);
747+
const resolved = await resolvePr(pr as BaseContext);
745748
pullRequestModel = resolved?.pr;
746749
}
747750

@@ -821,7 +824,7 @@ export function registerCommands(
821824
),
822825
);
823826

824-
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnVscodeDevFromDescription', async (context: OverviewContext | undefined) => {
827+
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnVscodeDevFromDescription', async (context: BaseContext | undefined) => {
825828
if (!context) {
826829
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.'));
827830
}
@@ -832,7 +835,7 @@ export function registerCommands(
832835
return vscode.env.openExternal(vscode.Uri.parse(vscodeDevPrLink(resolved.pr)));
833836
}));
834837

835-
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnCodespacesFromDescription', async (context: OverviewContext | undefined) => {
838+
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnCodespacesFromDescription', async (context: BaseContext | undefined) => {
836839
if (!context) {
837840
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.'));
838841
}
@@ -1622,7 +1625,7 @@ ${contents}
16221625
}));
16231626

16241627
context.subscriptions.push(
1625-
vscode.commands.registerCommand('pr.copyVscodeDevPrLink', async (params: OverviewContext | undefined) => {
1628+
vscode.commands.registerCommand('pr.copyVscodeDevPrLink', async (params: BaseContext | undefined) => {
16261629
let pr: PullRequestModel | undefined;
16271630
if (params) {
16281631
pr = await reposManager.getManagerForRepository(params.owner, params.repo)?.resolvePullRequest(params.owner, params.repo, params.number, true);
@@ -1642,7 +1645,7 @@ ${contents}
16421645
}));
16431646

16441647
context.subscriptions.push(
1645-
vscode.commands.registerCommand('pr.copyPrLink', async (params: OverviewContext | undefined) => {
1648+
vscode.commands.registerCommand('pr.copyPrLink', async (params: BaseContext | undefined) => {
16461649
let item: PullRequestModel | IssueModel | undefined;
16471650
if (params) {
16481651
const folderManager = reposManager.getManagerForRepository(params.owner, params.repo);

src/github/pullRequestOverview.ts

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel';
2626
import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon';
2727
import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks';
2828
import { parseReviewers } from './utils';
29-
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType, UnresolvedIdentity } from './views';
29+
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReadyForReviewAndMergeContext, ReadyForReviewContext, ReviewCommentContext, ReviewType, UnresolvedIdentity } from './views';
3030
import { debounce } from '../common/async';
3131
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
3232
import { COPILOT_REVIEWER, COPILOT_REVIEWER_ACCOUNT, COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
@@ -127,6 +127,75 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
127127
return this.currentPanel?._item;
128128
}
129129

130+
/**
131+
* Find the panel showing a specific pull request.
132+
* Currently there is at most one panel, but this will support
133+
* multiple panels in the future.
134+
*/
135+
public static findPanel(owner: string, repo: string, number: number): PullRequestOverviewPanel | undefined {
136+
const panel = this.currentPanel;
137+
if (panel && panel._item &&
138+
panel._item.remote.owner === owner &&
139+
panel._item.remote.repositoryName === repo &&
140+
panel._item.number === number) {
141+
return panel;
142+
}
143+
return undefined;
144+
}
145+
146+
/**
147+
* Register the webview context-menu commands once globally,
148+
* rather than per panel instance. Each command receives the
149+
* PR identity (owner / repo / number) from the webview context
150+
* and looks up the matching panel.
151+
*/
152+
public static registerGlobalCommands(context: vscode.ExtensionContext, telemetry: ITelemetry): void {
153+
context.subscriptions.push(
154+
vscode.commands.registerCommand('pr.readyForReviewDescription', async (ctx: ReadyForReviewContext) => {
155+
const panel = PullRequestOverviewPanel.findPanel(ctx.owner, ctx.repo, ctx.number);
156+
if (panel) {
157+
return panel.readyForReviewCommand();
158+
}
159+
}),
160+
vscode.commands.registerCommand('pr.readyForReviewAndMergeDescription', async (ctx: ReadyForReviewAndMergeContext) => {
161+
const panel = PullRequestOverviewPanel.findPanel(ctx.owner, ctx.repo, ctx.number);
162+
if (panel) {
163+
return panel.readyForReviewAndMergeCommand(ctx);
164+
}
165+
}),
166+
vscode.commands.registerCommand('review.approveDescription', (ctx: ReviewCommentContext) => {
167+
const panel = PullRequestOverviewPanel.findPanel(ctx.owner, ctx.repo, ctx.number);
168+
if (panel) {
169+
return panel.approvePullRequestCommand(ctx);
170+
}
171+
}),
172+
vscode.commands.registerCommand('review.commentDescription', (ctx: ReviewCommentContext) => {
173+
const panel = PullRequestOverviewPanel.findPanel(ctx.owner, ctx.repo, ctx.number);
174+
if (panel) {
175+
return panel.submitReviewCommand(ctx);
176+
}
177+
}),
178+
vscode.commands.registerCommand('review.requestChangesDescription', (ctx: ReviewCommentContext) => {
179+
const panel = PullRequestOverviewPanel.findPanel(ctx.owner, ctx.repo, ctx.number);
180+
if (panel) {
181+
return panel.requestChangesCommand(ctx);
182+
}
183+
}),
184+
vscode.commands.registerCommand('review.approveOnDotComDescription', (ctx: ReviewCommentContext) => {
185+
const panel = PullRequestOverviewPanel.findPanel(ctx.owner, ctx.repo, ctx.number);
186+
if (panel) {
187+
return openPullRequestOnGitHub(panel._item, telemetry);
188+
}
189+
}),
190+
vscode.commands.registerCommand('review.requestChangesOnDotComDescription', (ctx: ReviewCommentContext) => {
191+
const panel = PullRequestOverviewPanel.findPanel(ctx.owner, ctx.repo, ctx.number);
192+
if (panel) {
193+
return openPullRequestOnGitHub(panel._item, telemetry);
194+
}
195+
}),
196+
);
197+
}
198+
130199
protected constructor(
131200
telemetry: ITelemetry,
132201
extensionUri: vscode.Uri,
@@ -143,22 +212,6 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
143212
this.registerPrListeners();
144213

145214
this.setVisibilityContext();
146-
147-
this._register(vscode.commands.registerCommand('pr.readyForReviewDescription', async () => {
148-
return this.readyForReviewCommand();
149-
}));
150-
this._register(vscode.commands.registerCommand('pr.readyForReviewAndMergeDescription', async (context: { mergeMethod: MergeMethod }) => {
151-
return this.readyForReviewAndMergeCommand(context);
152-
}));
153-
this._register(vscode.commands.registerCommand('review.approveDescription', (e) => this.approvePullRequestCommand(e)));
154-
this._register(vscode.commands.registerCommand('review.commentDescription', (e) => this.submitReviewCommand(e)));
155-
this._register(vscode.commands.registerCommand('review.requestChangesDescription', (e) => this.requestChangesCommand(e)));
156-
this._register(vscode.commands.registerCommand('review.approveOnDotComDescription', () => {
157-
return openPullRequestOnGitHub(this._item, this._telemetry);
158-
}));
159-
this._register(vscode.commands.registerCommand('review.requestChangesOnDotComDescription', () => {
160-
return openPullRequestOnGitHub(this._item, this._telemetry);
161-
}));
162215
}
163216

164217
protected override registerPrListeners() {

src/github/views.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,49 @@ export interface CancelCodingAgentReply {
177177
events: TimelineEvent[];
178178
}
179179

180-
export interface OverviewContext {
180+
export interface BaseContext {
181181
'preventDefaultContextMenuItems': true;
182182
owner: string;
183183
repo: string;
184184
number: number;
185185
[key: string]: boolean | string | number;
186186
}
187187

188+
export interface OverviewContext extends BaseContext {
189+
'github:checkoutMenu': true;
190+
}
191+
192+
export interface ReadyForReviewContext extends BaseContext {
193+
'github:readyForReviewMenu': true;
194+
}
195+
196+
export interface ReadyForReviewAndMergeContext extends ReadyForReviewContext {
197+
'github:readyForReviewMenuWithMerge': true;
198+
mergeMethod: MergeMethod;
199+
}
200+
188201
export interface CodingAgentContext extends SessionLinkInfo {
189202
'preventDefaultContextMenuItems': true;
203+
'github:codingAgentMenu': true;
190204
[key: string]: boolean | string | number | undefined;
191205
}
192206

207+
export interface ReviewCommentContext {
208+
'preventDefaultContextMenuItems': true;
209+
'github:reviewCommentMenu': true,
210+
owner: string;
211+
repo: string;
212+
number: number;
213+
body: string;
214+
'github:reviewCommentApprove'?: boolean;
215+
'github:reviewCommentApproveOnDotCom'?: boolean;
216+
'github:reviewCommentComment'?: boolean;
217+
'github:reviewCommentCommentEnabled'?: boolean;
218+
'github:reviewCommentRequestChanges'?: boolean;
219+
'github:reviewRequestChangesEnabled'?: boolean;
220+
'github:reviewCommentRequestChangesOnDotCom'?: boolean;
221+
}
222+
193223
export interface ChangeBaseReply {
194224
base: string;
195225
events: TimelineEvent[];

webviews/components/comment.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { AuthorLink, Avatar } from './user';
1212
import { IComment } from '../../src/common/comment';
1313
import { CommentEvent, EventType, ReviewEvent } from '../../src/common/timelineEvent';
1414
import { GithubItemStateEnum } from '../../src/github/interface';
15-
import { PullRequest, ReviewType } from '../../src/github/views';
15+
import { PullRequest, ReviewCommentContext, ReviewType } from '../../src/github/views';
1616
import { ariaAnnouncementForReview } from '../common/aria';
1717
import { COMMENT_TEXTAREA_ID } from '../common/constants';
1818
import PullRequestContext from '../common/context';
@@ -415,6 +415,9 @@ export function AddComment({
415415
lastReviewType,
416416
busy,
417417
hasReviewDraft,
418+
owner,
419+
repo,
420+
number
418421
}: PullRequest) {
419422
const { updatePR, requestChanges, approve, close, openOnGitHub, submit } = useContext(PullRequestContext);
420423
const [isBusy, setBusy] = useState(false);
@@ -516,7 +519,7 @@ export function AddComment({
516519

517520

518521
<ContextDropdown
519-
optionsContext={() => makeCommentMenuContext(availableActions, pendingCommentText, shouldDisableNonApproveButtons)}
522+
optionsContext={() => makeCommentMenuContext(owner, repo, number, availableActions, pendingCommentText, shouldDisableNonApproveButtons)}
520523
defaultAction={defaultSubmitAction}
521524
defaultOptionLabel={() => availableActions[currentSelection]!}
522525
defaultOptionValue={() => currentSelection}
@@ -558,10 +561,14 @@ const COMMENT_METHODS = {
558561
requestChanges: 'Request Changes',
559562
};
560563

561-
const makeCommentMenuContext = (availableActions: { comment?: string, approve?: string, requestChanges?: string }, pendingCommentText: string | undefined, shouldDisableNonApproveButtons: boolean) => {
562-
const createMenuContexts: Record<string, boolean | string> = {
564+
const makeCommentMenuContext = (owner: string, repo: string, number: number, availableActions: { comment?: string, approve?: string, requestChanges?: string }, pendingCommentText: string | undefined, shouldDisableNonApproveButtons: boolean) => {
565+
const createMenuContexts: ReviewCommentContext = {
563566
'preventDefaultContextMenuItems': true,
564567
'github:reviewCommentMenu': true,
568+
owner,
569+
repo,
570+
number,
571+
body: pendingCommentText ?? '',
565572
};
566573
if (availableActions.approve) {
567574
if (availableActions.approve === COMMENT_METHODS.approve) {
@@ -665,7 +672,7 @@ export const AddCommentSimple = (pr: PullRequest) => {
665672
/>
666673
<div className='comment-button'>
667674
<ContextDropdown
668-
optionsContext={() => makeCommentMenuContext(availableActions, pr.pendingCommentText, shouldDisableNonApproveButtons)}
675+
optionsContext={() => makeCommentMenuContext(pr.owner, pr.repo, pr.number, availableActions, pr.pendingCommentText, shouldDisableNonApproveButtons)}
669676
defaultAction={defaultSubmitAction}
670677
defaultOptionLabel={() => availableActions[currentSelection]!}
671678
defaultOptionValue={() => currentSelection}

webviews/components/header.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { AuthorLink, Avatar } from './user';
1010
import { copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../../src/common/copilot';
1111
import { CopilotStartedEvent, TimelineEvent } from '../../src/common/timelineEvent';
1212
import { GithubItemStateEnum, StateReason } from '../../src/github/interface';
13-
import { CodingAgentContext, OverviewContext, PullRequest } from '../../src/github/views';
13+
import { BaseContext, CodingAgentContext, OverviewContext, PullRequest } from '../../src/github/views';
1414
import { EDIT_TITLE_BUTTON_ID } from '../common/constants';
1515
import PullRequestContext from '../common/context';
1616
import { useStateProp } from '../common/hooks';
@@ -112,7 +112,7 @@ function Title({ title, titleHTML, number, url, inEditMode, setEditMode, setCurr
112112
</form>
113113
);
114114

115-
const context: OverviewContext = {
115+
const context: BaseContext = {
116116
'preventDefaultContextMenuItems': true,
117117
owner,
118118
repo,
@@ -197,10 +197,10 @@ function CancelCodingAgentButton({ canEdit, codingAgentEvent }: { canEdit: boole
197197

198198
const context: CodingAgentContext = {
199199
'preventDefaultContextMenuItems': true,
200+
'github:codingAgentMenu': true,
200201
...sessionLink
201202
};
202203

203-
context['github:codingAgentMenu'] = true;
204204
const actions: { label: string; value: string; action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void }[] = [];
205205

206206
if (sessionLink) {
@@ -337,12 +337,12 @@ const CheckoutButton: React.FC<CheckoutButtonProps> = ({ isCurrentlyCheckedOut,
337337

338338
const context: OverviewContext = {
339339
'preventDefaultContextMenuItems': true,
340+
'github:checkoutMenu': true,
340341
owner,
341342
repo,
342343
number
343344
};
344345

345-
context['github:checkoutMenu'] = true;
346346
const actions: { label: string; value: string; action: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void }[] = [];
347347

348348
if (isCurrentlyCheckedOut) {

webviews/components/merge.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
reviewerId,
3131
ReviewState,
3232
} from '../../src/github/interface';
33-
import { PullRequest } from '../../src/github/views';
33+
import { PullRequest, ReadyForReviewAndMergeContext, ReadyForReviewContext } from '../../src/github/views';
3434
import PullRequestContext from '../common/context';
3535
import { Reviewer } from '../components/reviewer';
3636

@@ -339,12 +339,26 @@ export const ReadyForReview = ({ isSimple, isCopilotOnMyBehalf, mergeMethod }: {
339339
</div>
340340
<div className='button-container'>
341341
<ContextDropdown
342-
optionsContext={() => JSON.stringify({
343-
'preventDefaultContextMenuItems': true,
344-
'github:readyForReviewMenu': true,
345-
'github:readyForReviewMenuWithMerge': isCopilotOnMyBehalf,
346-
'mergeMethod': mergeMethod
347-
})}
342+
optionsContext={() => {
343+
if (!pr) {
344+
throw new Error('PR context is required for ready for review options');
345+
}
346+
let ctx: ReadyForReviewContext | ReadyForReviewAndMergeContext = {
347+
'preventDefaultContextMenuItems': true,
348+
'github:readyForReviewMenu': true,
349+
owner: pr.owner,
350+
repo: pr.repo,
351+
number: pr.number,
352+
};
353+
if (isCopilotOnMyBehalf) {
354+
ctx = {
355+
...ctx,
356+
'github:readyForReviewMenuWithMerge': true,
357+
mergeMethod,
358+
};
359+
}
360+
return JSON.stringify(ctx);
361+
}}
348362
defaultAction={markReadyForReview}
349363
defaultOptionLabel={() => 'Ready for Review'}
350364
defaultOptionValue={() => 'ready'}

0 commit comments

Comments
 (0)