Skip to content

Commit 2f6fda9

Browse files
Copilotalexr00
andauthored
Add checkout pull request in worktree option (#8513)
* Initial plan * initial plan for checkout PR in worktree feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * feat: add checkout pull request in worktree option Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * fix: use VS Code Tasks API for reliable cross-platform worktree creation Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * fix: address code review feedback - improve error handling and branch naming Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * argument clean up and keep branch name * Update git API * refactor: use git extension API for worktree creation instead of shell execution Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * fix: run fetch and worktree selection in parallel, move info message after progress Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix createWorktree call * fix: start progress before fetch operation, include fetch in progress scope Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * feat: add 'Checkout in Worktree' option to PR Description checkout button Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/113a0bba-a0a7-47d9-baca-dd4dd5fda645 * fix: handle existing local branch when creating worktree Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/c7f23b9d-14de-42d2-960a-36aa53e2d704 Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * feat: use modal dialog with new window and current window options for worktree Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/b2dbdafb-3aa3-4d09-8555-21772c09c7e9 Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * feat: add pr.pickInWorktreeFromDescription command for PR overview checkout menu Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/32630df4-6da8-4847-8013-e2fb1dabc1bb Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * refactor: extract worktree checkout logic into standalone utility function Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/ddf40003-0929-42c3-abfb-5abf98150f00 Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix strings * fix strings * fix: stop if fetch fails and use commands.openFolder helper Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/6af4fefc-4eff-4f7e-902b-52be43a94c15 Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * CCR cleanup --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 22504bd commit 2f6fda9

7 files changed

Lines changed: 282 additions & 13 deletions

File tree

package.json

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,18 @@
10131013
"category": "%command.pull.request.category%",
10141014
"icon": "$(cloud)"
10151015
},
1016+
{
1017+
"command": "pr.pickInWorktree",
1018+
"title": "%command.pr.pickInWorktreeFromDescription.title%",
1019+
"category": "%command.pull.request.category%",
1020+
"icon": "$(folder-library)"
1021+
},
1022+
{
1023+
"command": "pr.pickInWorktreeFromDescription",
1024+
"title": "%command.pr.pickInWorktree.title%",
1025+
"category": "%command.pull.request.category%",
1026+
"icon": "$(folder-library)"
1027+
},
10161028
{
10171029
"command": "pr.exit",
10181030
"title": "%command.pr.exit.title%",
@@ -2084,6 +2096,14 @@
20842096
"command": "pr.pickOnCodespaces",
20852097
"when": "false"
20862098
},
2099+
{
2100+
"command": "pr.pickInWorktree",
2101+
"when": "false"
2102+
},
2103+
{
2104+
"command": "pr.pickInWorktreeFromDescription",
2105+
"when": "false"
2106+
},
20872107
{
20882108
"command": "pr.exit",
20892109
"when": "github:inReviewMode"
@@ -2882,6 +2902,11 @@
28822902
"when": "view == pr:github && viewItem =~ /pullrequest(:local)?:nonactive/ && (!isWeb || remoteName != codespaces && virtualWorkspace != vscode-vfs)",
28832903
"group": "1_pullrequest@3"
28842904
},
2905+
{
2906+
"command": "pr.pickInWorktree",
2907+
"when": "view == pr:github && viewItem =~ /pullrequest(:local)?:nonactive/ && !isWeb",
2908+
"group": "1_pullrequest@4"
2909+
},
28852910
{
28862911
"command": "pr.openChanges",
28872912
"when": "view =~ /(pr|prStatus):github/ && viewItem =~ /(pullrequest|description)/",
@@ -3641,13 +3666,18 @@
36413666
"when": "webviewId == PullRequestOverview && github:checkoutMenu"
36423667
},
36433668
{
3644-
"command": "pr.checkoutOnVscodeDevFromDescription",
3669+
"command": "pr.pickInWorktreeFromDescription",
36453670
"group": "checkout@1",
3671+
"when": "webviewId == PullRequestOverview && github:checkoutMenu && !isWeb"
3672+
},
3673+
{
3674+
"command": "pr.checkoutOnVscodeDevFromDescription",
3675+
"group": "checkout@2",
36463676
"when": "webviewId == PullRequestOverview && github:checkoutMenu"
36473677
},
36483678
{
36493679
"command": "pr.checkoutOnCodespacesFromDescription",
3650-
"group": "checkout@2",
3680+
"group": "checkout@3",
36513681
"when": "webviewId == PullRequestOverview && github:checkoutMenu"
36523682
},
36533683
{

package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@
215215
"command.pr.openChanges.title": "Open Changes",
216216
"command.pr.pickOnVscodeDev.title": "Checkout Pull Request on vscode.dev",
217217
"command.pr.pickOnCodespaces.title": "Checkout Pull Request on Codespaces",
218+
"command.pr.pickInWorktree.title": "Checkout in Worktree",
219+
"command.pr.pickInWorktreeFromDescription.title": "Checkout Pull Request in Worktree",
218220
"command.pr.exit.title": "Checkout Default Branch",
219221
"command.pr.dismissNotification.title": "Dismiss Notification",
220222
"command.pr.markAllCopilotNotificationsAsRead.title": "Dismiss All Copilot Notifications",

src/@types/vscode.proposed.chatParticipantAdditions.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,11 @@ declare module 'vscode' {
453453
constructor(value: string | MarkdownString);
454454
}
455455

456+
export class ChatResponseInfoPart {
457+
value: MarkdownString;
458+
constructor(value: string | MarkdownString);
459+
}
460+
456461
export class ChatResponseProgressPart2 extends ChatResponseProgressPart {
457462
value: string;
458463
task?: (progress: Progress<ChatResponseWarningPart | ChatResponseReferencePart>) => Thenable<string | void>;
@@ -633,6 +638,15 @@ declare module 'vscode' {
633638
*/
634639
warning(message: string | MarkdownString): void;
635640

641+
/**
642+
* Push an info banner to this stream. Short-hand for
643+
* `push(new ChatResponseInfoPart(message))`.
644+
*
645+
* @param message An informational message
646+
* @returns This stream.
647+
*/
648+
info(message: string | MarkdownString): void;
649+
636650
reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void;
637651

638652
reference2(value: Uri | Location | string | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }, options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }): void;

src/@types/vscode.proposed.chatSessionsProvider.d.ts

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,23 @@ declare module 'vscode' {
7676
// TODO: Do we need a flag to try auth if needed?
7777
provideChatSessionItems(token: CancellationToken): ProviderResult<ChatSessionItem[]>;
7878

79+
/**
80+
* @deprecated Use {@linkcode ChatSessionItemController.resolveChatSessionItem} instead.
81+
*
82+
* Given a chat session item fill in more data, like {@link ChatSessionItem.timing timing},
83+
* {@link ChatSessionItem.changes changes}, or {@link ChatSessionItem.badge badge}.
84+
*
85+
* The editor will call this when a chat session item becomes visible in the UI, for example
86+
* when the user scrolls to it or when it is first rendered.
87+
*
88+
* @param item A chat session item currently visible in the UI. Treat this as read-only.
89+
* @param token A cancellation token.
90+
* @returns A new {@link ChatSessionItem} instance (or a thenable that resolves to one) with the
91+
* same `resource` as `item` and any additional properties filled in. When no result is returned,
92+
* the given `item` is left unchanged.
93+
*/
94+
resolveChatSessionItem?: (item: ChatSessionItem, token: CancellationToken) => ProviderResult<ChatSessionItem>;
95+
7996
// #region Unstable parts of API
8097

8198
/**
@@ -100,11 +117,6 @@ declare module 'vscode' {
100117
readonly command?: string;
101118
};
102119

103-
/**
104-
* @deprecated Use `inputState` instead
105-
*/
106-
readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>;
107-
108120
readonly inputState: ChatSessionInputState;
109121
}
110122

@@ -199,6 +211,26 @@ declare module 'vscode' {
199211
*/
200212
getChatSessionInputState?: ChatSessionControllerGetInputState;
201213

214+
/**
215+
* Called to fill in more data on a chat session item, like {@link ChatSessionItem.timing timing},
216+
* {@link ChatSessionItem.changes changes}, or {@link ChatSessionItem.badge badge}.
217+
*
218+
* The editor will call this when a chat session item becomes visible in the UI, for example
219+
* when the user scrolls to it or when it is first rendered.
220+
*
221+
* The editor will only resolve a chat session item once, unless the item is updated via
222+
* {@link ChatSessionItemCollection.add add} or {@link ChatSessionItemCollection.replace replace},
223+
* which invalidates the resolve cache.
224+
*
225+
* The handler should update the item in the {@link ChatSessionItemController.items items collection} via
226+
* {@link ChatSessionItemCollection.add add}. The editor picks up the updated item from
227+
* the collection after the returned thenable resolves.
228+
*
229+
* @param item A chat session item currently visible in the UI.
230+
* @param token A cancellation token.
231+
*/
232+
resolveChatSessionItem?: (item: ChatSessionItem, token: CancellationToken) => Thenable<void>;
233+
202234
/**
203235
* Create a new managed ChatSessionInputState object.
204236
*/
@@ -505,11 +537,6 @@ declare module 'vscode' {
505537
*/
506538
provideChatSessionContent(resource: Uri, token: CancellationToken, context: {
507539
readonly inputState: ChatSessionInputState;
508-
509-
/**
510-
* @deprecated Use `inputState` instead
511-
*/
512-
readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>;
513540
}): Thenable<ChatSession> | ChatSession;
514541

515542
/**
@@ -621,6 +648,16 @@ declare module 'vscode' {
621648
* Only one item per option group should be marked as default.
622649
*/
623650
readonly default?: boolean;
651+
652+
/**
653+
* Optional slash-command alias (without leading `/`) that selects this option
654+
* when the user submits `/<slashCommand>`. Does not send a chat request; only
655+
* updates the selection so the next prompt runs with this option active.
656+
*
657+
* Scoped to chat sessions owned by the contributing provider. Names must be
658+
* unique across the provider's groups; on conflict, the first declared wins.
659+
*/
660+
readonly slashCommand?: string;
624661
}
625662

626663
/**
@@ -678,6 +715,22 @@ declare module 'vscode' {
678715
* `{ inputState: ChatSessionInputState; sessionResource: Uri | undefined }` that they can use to determine which session and options they are being invoked for.
679716
*/
680717
readonly commands?: Command[];
718+
719+
/**
720+
* Optional kind that hints how this option group should be presented in the UI.
721+
*
722+
* - `'permissions'`: The group represents tool-approval permissions for the session.
723+
* The editor will not render this group as its own picker. Instead, its items
724+
* replace the built-in items in the chat permission picker for the session,
725+
* and the user's selection is reported back through the standard
726+
* {@link ChatSessionContentProvider.handleChatSessionOptionsChange} flow.
727+
* At most one option group per provider may use this kind; if more than one is
728+
* declared, the first one (in declaration order) is used. The group is invisible
729+
* if the chat permission picker itself is hidden by other `when` clauses.
730+
*
731+
* When omitted, the group is rendered as a standalone picker as usual.
732+
*/
733+
readonly kind?: 'permissions';
681734
}
682735

683736
export interface ChatSessionProviderOptions {

src/commands.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ 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';
3434
import { BaseContext, OverviewContext } from './github/views';
35+
import { checkoutPRInWorktree } from './github/worktree';
3536
import { IssueChatContextItem } from './lm/issueContextProvider';
3637
import { PRChatContextItem } from './lm/pullRequestContextProvider';
3738
import { isNotificationTreeItem, NotificationTreeItem } from './notifications/notificationItem';
@@ -825,6 +826,47 @@ export function registerCommands(
825826
),
826827
);
827828

829+
context.subscriptions.push(
830+
vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | PullRequestModel | unknown) => {
831+
if (pr === undefined) {
832+
Logger.error('Unexpectedly received undefined when picking a PR for worktree checkout.', logId);
833+
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));
834+
}
835+
836+
let pullRequestModel: PullRequestModel;
837+
let repository: Repository | undefined;
838+
839+
if (pr instanceof PRNode) {
840+
pullRequestModel = pr.pullRequestModel;
841+
repository = pr.repository;
842+
} else if (pr instanceof PullRequestModel) {
843+
pullRequestModel = pr;
844+
} else {
845+
Logger.error('Unexpectedly received unknown type when picking a PR for worktree checkout.', logId);
846+
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));
847+
}
848+
849+
// Get the folder manager to access the repository
850+
const folderManager = reposManager.getManagerForIssueModel(pullRequestModel);
851+
if (!folderManager) {
852+
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find repository for this pull request.'));
853+
}
854+
855+
return checkoutPRInWorktree(telemetry, folderManager, pullRequestModel, repository);
856+
}),
857+
);
858+
859+
context.subscriptions.push(vscode.commands.registerCommand('pr.pickInWorktreeFromDescription', async (ctx: BaseContext | undefined) => {
860+
if (!ctx) {
861+
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.'));
862+
}
863+
const resolved = await resolvePr(ctx);
864+
if (!resolved) {
865+
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to resolve pull request for checkout.'));
866+
}
867+
return checkoutPRInWorktree(telemetry, resolved.folderManager, resolved.pr, undefined);
868+
}));
869+
828870
context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnVscodeDevFromDescription', async (context: BaseContext | undefined) => {
829871
if (!context) {
830872
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.'));

0 commit comments

Comments
 (0)