Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b44c1ed
Initial plan
Copilot Feb 13, 2026
cf08027
initial plan for checkout PR in worktree feature
Copilot Feb 13, 2026
40d6d90
feat: add checkout pull request in worktree option
Copilot Feb 13, 2026
ecc650d
fix: use VS Code Tasks API for reliable cross-platform worktree creation
Copilot Feb 13, 2026
fab1204
fix: address code review feedback - improve error handling and branch…
Copilot Feb 13, 2026
80e46a2
argument clean up and keep branch name
alexr00 Feb 13, 2026
103da71
Update git API
alexr00 Feb 13, 2026
04c416a
refactor: use git extension API for worktree creation instead of shel…
Copilot Feb 13, 2026
ad74445
fix: run fetch and worktree selection in parallel, move info message …
Copilot Feb 13, 2026
3e1c44e
Fix createWorktree call
alexr00 Feb 13, 2026
766d8a0
fix: start progress before fetch operation, include fetch in progress…
Copilot Feb 13, 2026
fa08fb7
feat: add 'Checkout in Worktree' option to PR Description checkout bu…
Copilot Apr 7, 2026
8ea92b4
Merge branch 'main' into copilot/add-worktree-checkout-option
alexr00 Apr 17, 2026
db793c6
fix: handle existing local branch when creating worktree
Copilot Apr 17, 2026
5f65fda
feat: use modal dialog with new window and current window options for…
Copilot Apr 17, 2026
af07a44
feat: add pr.pickInWorktreeFromDescription command for PR overview ch…
Copilot Apr 17, 2026
ed22158
refactor: extract worktree checkout logic into standalone utility fun…
Copilot Apr 17, 2026
b79ac67
Fix strings
alexr00 Apr 24, 2026
4fc3bc1
fix strings
alexr00 Apr 24, 2026
897e23d
fix: stop if fetch fails and use commands.openFolder helper
Copilot Apr 24, 2026
290ab97
CCR cleanup
alexr00 Apr 24, 2026
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
34 changes: 32 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,18 @@
"category": "%command.pull.request.category%",
"icon": "$(cloud)"
},
{
"command": "pr.pickInWorktree",
"title": "%command.pr.pickInWorktreeFromDescription.title%",
"category": "%command.pull.request.category%",
"icon": "$(folder-library)"
},
{
"command": "pr.pickInWorktreeFromDescription",
"title": "%command.pr.pickInWorktree.title%",
Comment thread
alexr00 marked this conversation as resolved.
"category": "%command.pull.request.category%",
"icon": "$(folder-library)"
},
{
"command": "pr.exit",
"title": "%command.pr.exit.title%",
Expand Down Expand Up @@ -2084,6 +2096,14 @@
"command": "pr.pickOnCodespaces",
"when": "false"
},
{
"command": "pr.pickInWorktree",
"when": "false"
},
{
"command": "pr.pickInWorktreeFromDescription",
"when": "false"
},
{
"command": "pr.exit",
"when": "github:inReviewMode"
Expand Down Expand Up @@ -2882,6 +2902,11 @@
"when": "view == pr:github && viewItem =~ /pullrequest(:local)?:nonactive/ && (!isWeb || remoteName != codespaces && virtualWorkspace != vscode-vfs)",
"group": "1_pullrequest@3"
},
{
"command": "pr.pickInWorktree",
"when": "view == pr:github && viewItem =~ /pullrequest(:local)?:nonactive/ && !isWeb",
"group": "1_pullrequest@4"
},
{
"command": "pr.openChanges",
"when": "view =~ /(pr|prStatus):github/ && viewItem =~ /(pullrequest|description)/",
Expand Down Expand Up @@ -3641,13 +3666,18 @@
"when": "webviewId == PullRequestOverview && github:checkoutMenu"
},
{
"command": "pr.checkoutOnVscodeDevFromDescription",
"command": "pr.pickInWorktreeFromDescription",
"group": "checkout@1",
"when": "webviewId == PullRequestOverview && github:checkoutMenu && !isWeb"
},
{
"command": "pr.checkoutOnVscodeDevFromDescription",
"group": "checkout@2",
"when": "webviewId == PullRequestOverview && github:checkoutMenu"
},
{
"command": "pr.checkoutOnCodespacesFromDescription",
"group": "checkout@2",
"group": "checkout@3",
"when": "webviewId == PullRequestOverview && github:checkoutMenu"
},
{
Expand Down
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@
"command.pr.openChanges.title": "Open Changes",
"command.pr.pickOnVscodeDev.title": "Checkout Pull Request on vscode.dev",
"command.pr.pickOnCodespaces.title": "Checkout Pull Request on Codespaces",
"command.pr.pickInWorktree.title": "Checkout in Worktree",
"command.pr.pickInWorktreeFromDescription.title": "Checkout Pull Request in Worktree",
"command.pr.exit.title": "Checkout Default Branch",
"command.pr.dismissNotification.title": "Dismiss Notification",
"command.pr.markAllCopilotNotificationsAsRead.title": "Dismiss All Copilot Notifications",
Expand Down
14 changes: 14 additions & 0 deletions src/@types/vscode.proposed.chatParticipantAdditions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ declare module 'vscode' {
constructor(value: string | MarkdownString);
}

export class ChatResponseInfoPart {
value: MarkdownString;
constructor(value: string | MarkdownString);
}

export class ChatResponseProgressPart2 extends ChatResponseProgressPart {
value: string;
task?: (progress: Progress<ChatResponseWarningPart | ChatResponseReferencePart>) => Thenable<string | void>;
Expand Down Expand Up @@ -633,6 +638,15 @@ declare module 'vscode' {
*/
warning(message: string | MarkdownString): void;

/**
* Push an info banner to this stream. Short-hand for
* `push(new ChatResponseInfoPart(message))`.
*
* @param message An informational message
* @returns This stream.
*/
info(message: string | MarkdownString): void;

reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void;

reference2(value: Uri | Location | string | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }, options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }): void;
Expand Down
73 changes: 63 additions & 10 deletions src/@types/vscode.proposed.chatSessionsProvider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ declare module 'vscode' {
// TODO: Do we need a flag to try auth if needed?
provideChatSessionItems(token: CancellationToken): ProviderResult<ChatSessionItem[]>;

/**
* @deprecated Use {@linkcode ChatSessionItemController.resolveChatSessionItem} instead.
*
* Given a chat session item fill in more data, like {@link ChatSessionItem.timing timing},
* {@link ChatSessionItem.changes changes}, or {@link ChatSessionItem.badge badge}.
*
* The editor will call this when a chat session item becomes visible in the UI, for example
* when the user scrolls to it or when it is first rendered.
*
* @param item A chat session item currently visible in the UI. Treat this as read-only.
* @param token A cancellation token.
* @returns A new {@link ChatSessionItem} instance (or a thenable that resolves to one) with the
* same `resource` as `item` and any additional properties filled in. When no result is returned,
* the given `item` is left unchanged.
*/
resolveChatSessionItem?: (item: ChatSessionItem, token: CancellationToken) => ProviderResult<ChatSessionItem>;

// #region Unstable parts of API

/**
Expand All @@ -100,11 +117,6 @@ declare module 'vscode' {
readonly command?: string;
};

/**
* @deprecated Use `inputState` instead
*/
readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>;

readonly inputState: ChatSessionInputState;
}

Expand Down Expand Up @@ -199,6 +211,26 @@ declare module 'vscode' {
*/
getChatSessionInputState?: ChatSessionControllerGetInputState;

/**
* Called to fill in more data on a chat session item, like {@link ChatSessionItem.timing timing},
* {@link ChatSessionItem.changes changes}, or {@link ChatSessionItem.badge badge}.
*
* The editor will call this when a chat session item becomes visible in the UI, for example
* when the user scrolls to it or when it is first rendered.
*
* The editor will only resolve a chat session item once, unless the item is updated via
* {@link ChatSessionItemCollection.add add} or {@link ChatSessionItemCollection.replace replace},
* which invalidates the resolve cache.
*
* The handler should update the item in the {@link ChatSessionItemController.items items collection} via
* {@link ChatSessionItemCollection.add add}. The editor picks up the updated item from
* the collection after the returned thenable resolves.
*
* @param item A chat session item currently visible in the UI.
* @param token A cancellation token.
*/
resolveChatSessionItem?: (item: ChatSessionItem, token: CancellationToken) => Thenable<void>;

/**
* Create a new managed ChatSessionInputState object.
*/
Expand Down Expand Up @@ -505,11 +537,6 @@ declare module 'vscode' {
*/
provideChatSessionContent(resource: Uri, token: CancellationToken, context: {
readonly inputState: ChatSessionInputState;

/**
* @deprecated Use `inputState` instead
*/
readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>;
}): Thenable<ChatSession> | ChatSession;

/**
Expand Down Expand Up @@ -621,6 +648,16 @@ declare module 'vscode' {
* Only one item per option group should be marked as default.
*/
readonly default?: boolean;

/**
* Optional slash-command alias (without leading `/`) that selects this option
* when the user submits `/<slashCommand>`. Does not send a chat request; only
* updates the selection so the next prompt runs with this option active.
*
* Scoped to chat sessions owned by the contributing provider. Names must be
* unique across the provider's groups; on conflict, the first declared wins.
*/
readonly slashCommand?: string;
}

/**
Expand Down Expand Up @@ -678,6 +715,22 @@ declare module 'vscode' {
* `{ inputState: ChatSessionInputState; sessionResource: Uri | undefined }` that they can use to determine which session and options they are being invoked for.
*/
readonly commands?: Command[];

/**
* Optional kind that hints how this option group should be presented in the UI.
*
* - `'permissions'`: The group represents tool-approval permissions for the session.
* The editor will not render this group as its own picker. Instead, its items
* replace the built-in items in the chat permission picker for the session,
* and the user's selection is reported back through the standard
* {@link ChatSessionContentProvider.handleChatSessionOptionsChange} flow.
* At most one option group per provider may use this kind; if more than one is
* declared, the first one (in declaration order) is used. The group is invisible
* if the chat permission picker itself is hidden by other `when` clauses.
*
* When omitted, the group is rendered as a standalone picker as usual.
*/
readonly kind?: 'permissions';
}

export interface ChatSessionProviderOptions {
Expand Down
42 changes: 42 additions & 0 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { chooseItem } from './github/quickPicks';
import { RepositoriesManager } from './github/repositoriesManager';
import { codespacesPrLink, getIssuesUrl, getPullsUrl, isInCodespaces, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, vscodeDevPrLink } from './github/utils';
import { BaseContext, OverviewContext } from './github/views';
import { checkoutPRInWorktree } from './github/worktree';
import { IssueChatContextItem } from './lm/issueContextProvider';
import { PRChatContextItem } from './lm/pullRequestContextProvider';
import { isNotificationTreeItem, NotificationTreeItem } from './notifications/notificationItem';
Expand Down Expand Up @@ -825,6 +826,47 @@ export function registerCommands(
),
);

context.subscriptions.push(
vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | PullRequestModel | unknown) => {
if (pr === undefined) {
Logger.error('Unexpectedly received undefined when picking a PR for worktree checkout.', logId);
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));
}

let pullRequestModel: PullRequestModel;
let repository: Repository | undefined;

if (pr instanceof PRNode) {
pullRequestModel = pr.pullRequestModel;
repository = pr.repository;
} else if (pr instanceof PullRequestModel) {
pullRequestModel = pr;
} else {
Logger.error('Unexpectedly received unknown type when picking a PR for worktree checkout.', logId);
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));
}

// Get the folder manager to access the repository
const folderManager = reposManager.getManagerForIssueModel(pullRequestModel);
if (!folderManager) {
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find repository for this pull request.'));
}

return checkoutPRInWorktree(telemetry, folderManager, pullRequestModel, repository);
}),
);

context.subscriptions.push(vscode.commands.registerCommand('pr.pickInWorktreeFromDescription', async (ctx: BaseContext | undefined) => {
if (!ctx) {
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.'));
}
const resolved = await resolvePr(ctx);
if (!resolved) {
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to resolve pull request for checkout.'));
}
return checkoutPRInWorktree(telemetry, resolved.folderManager, resolved.pr, undefined);
}));

context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnVscodeDevFromDescription', async (context: BaseContext | undefined) => {
if (!context) {
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.'));
Expand Down
14 changes: 14 additions & 0 deletions src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommo
import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks';
import { parseReviewers, processDiffLinks, processPermalinks } from './utils';
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReadyForReviewAndMergeContext, ReadyForReviewContext, ReviewCommentContext, ReviewType, UnresolvedIdentity } from './views';
import { checkoutPRInWorktree } from './worktree';
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 @@ -507,6 +508,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
switch (message.command) {
case 'pr.checkout':
return this.checkoutPullRequest(message);
case 'pr.checkout-in-worktree':
return this.checkoutPullRequestInWorktree(message);
Comment thread
alexr00 marked this conversation as resolved.
Outdated
case 'pr.merge':
return this.mergePullRequest(message);
case 'pr.change-email':
Expand Down Expand Up @@ -827,6 +830,17 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
);
}

private checkoutPullRequestInWorktree(message: IRequestMessage<any>): void {
checkoutPRInWorktree(this._telemetry, this._folderRepositoryManager, this._item, undefined).then(
() => {
this._replyMessage(message, {});
},
() => {
this._replyMessage(message, {});
},
);
}

private async mergePullRequest(
message: IRequestMessage<MergeArguments>,
): Promise<void> {
Expand Down
Loading
Loading