From b44c1ed8d482cf39e7e6cedcce2334dc6406097a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:31:21 +0000 Subject: [PATCH 01/11] Initial plan From cf080272872015150b899c6853642c4c14575fc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:33:58 +0000 Subject: [PATCH 02/11] initial plan for checkout PR in worktree feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- .../vscode.proposed.chatContextProvider.d.ts | 2 - ...ode.proposed.chatParticipantAdditions.d.ts | 231 +++++++++++++++++- ...scode.proposed.chatParticipantPrivate.d.ts | 60 ++++- .../vscode.proposed.chatSessionsProvider.d.ts | 43 ++-- 4 files changed, 297 insertions(+), 39 deletions(-) diff --git a/src/@types/vscode.proposed.chatContextProvider.d.ts b/src/@types/vscode.proposed.chatContextProvider.d.ts index cf0f1744a4..e7cd493ae5 100644 --- a/src/@types/vscode.proposed.chatContextProvider.d.ts +++ b/src/@types/vscode.proposed.chatContextProvider.d.ts @@ -93,7 +93,6 @@ declare module 'vscode' { /** * An optional command that is executed when the context item is clicked. * The original context item will be passed as the first argument to the command. - * The original context item will be passed as the first argument to the command. */ command?: Command; } @@ -158,7 +157,6 @@ declare module 'vscode' { * `resolveChatContext` is only called for items that do not have a `value`. * * Called when the resource is a webview or a text editor. - * Called when the resource is a webview or a text editor. * * @param options Options include the resource for which to provide context. * @param token A cancellation token. diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index df0f1045c8..603f8a6fcb 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// version: 3 + declare module 'vscode' { export interface ChatParticipant { @@ -96,6 +98,108 @@ declare module 'vscode' { constructor(title: string, message: string | MarkdownString, data: any, buttons?: string[]); } + /** + * An option for a question in a carousel. + */ + export interface ChatQuestionOption { + /** + * Unique identifier for the option. + */ + id: string; + /** + * The display label for the option. + */ + label: string; + /** + * The value returned when this option is selected. + */ + value: unknown; + } + + /** + * The type of question for a chat question carousel. + */ + export enum ChatQuestionType { + /** + * A free-form text input question. + */ + Text = 1, + /** + * A single-select question with radio buttons. + */ + SingleSelect = 2, + /** + * A multi-select question with checkboxes. + */ + MultiSelect = 3 + } + + /** + * A question to be displayed in a question carousel. + */ + export class ChatQuestion { + /** + * Unique identifier for the question. + */ + id: string; + /** + * The type of question: Text for free-form input, SingleSelect for radio buttons, MultiSelect for checkboxes. + */ + type: ChatQuestionType; + /** + * The title/header of the question. + */ + title: string; + /** + * Optional detailed message or description for the question. + */ + message?: string | MarkdownString; + /** + * Options for singleSelect or multiSelect questions. + */ + options?: ChatQuestionOption[]; + /** + * The id(s) of the default selected option(s). + * For SingleSelect, this should be a single option id. + * For MultiSelect, this can be an array of option ids. + */ + defaultValue?: string | string[]; + /** + * Whether to allow free-form text input in addition to predefined options. + * When true, users can provide their own text answer even for SingleSelect or MultiSelect questions. + */ + allowFreeformInput?: boolean; + + constructor( + id: string, + type: ChatQuestionType, + title: string, + options?: { + message?: string | MarkdownString; + options?: ChatQuestionOption[]; + defaultValue?: string | string[]; + allowFreeformInput?: boolean; + } + ); + } + + /** + * A carousel view for presenting multiple questions inline in the chat. + * The UI is displayed but does not block the chat input. + */ + export class ChatResponseQuestionCarouselPart { + /** + * The questions to display in the carousel. + */ + questions: ChatQuestion[]; + /** + * Whether users can skip answering the questions. + */ + allowSkip: boolean; + + constructor(questions: ChatQuestion[], allowSkip?: boolean); + } + export class ChatResponseCodeCitationPart { value: Uri; license: string; @@ -162,6 +266,66 @@ declare module 'vscode' { output: McpToolInvocationContentData[]; } + export enum ChatTodoStatus { + NotStarted = 1, + InProgress = 2, + Completed = 3 + } + + export interface ChatTodoToolInvocationData { + todoList: Array<{ + id: number; + title: string; + status: ChatTodoStatus; + }>; + } + + /** + * Generic tool result data that displays input and output in collapsible sections. + */ + export interface ChatSimpleToolResultData { + /** + * The input to display. + */ + input: string; + /** + * The output to display. + */ + output: string; + } + + + export interface ChatToolResourcesInvocationData { + /** + * Array of file URIs or locations to display as a collapsible list + */ + values: Array; + } + + export class ChatSubagentToolInvocationData { + /** + * A description of the subagent's purpose or task. + */ + description?: string; + + /** + * The name of the subagent being invoked. + */ + agentName?: string; + + /** + * The prompt given to the subagent. + */ + prompt?: string; + + /** + * The result text from the subagent after completion. + */ + result?: string; + + constructor(description?: string, agentName?: string, prompt?: string, result?: string); + } + export class ChatToolInvocationPart { toolName: string; toolCallId: string; @@ -171,11 +335,16 @@ declare module 'vscode' { pastTenseMessage?: string | MarkdownString; isConfirmed?: boolean; isComplete?: boolean; - toolSpecificData?: ChatTerminalToolInvocationData; - fromSubAgent?: boolean; + toolSpecificData?: ChatTerminalToolInvocationData | ChatMcpToolInvocationData | ChatTodoToolInvocationData | ChatSimpleToolResultData | ChatToolResourcesInvocationData | ChatSubagentToolInvocationData; + subAgentInvocationId?: string; presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; - constructor(toolName: string, toolCallId: string, isError?: boolean); + /** + * If this flag is set, this will be treated as an update to any previous tool call with the same id. + */ + enablePartialUpdate?: boolean; + + constructor(toolName: string, toolCallId: string, errorMessage?: string); } /** @@ -244,7 +413,31 @@ declare module 'vscode' { constructor(uris: Uri[], callback: () => Thenable); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatResponsePullRequestPart | ChatPrepareToolInvocationPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart | ChatResponseExternalEditPart; + /** + * Internal type that lists all the proposed chat response parts. This is used to generate `ExtendedChatResponsePart` + * which is the actual type used in this API. This is done so that other proposals can easily add their own response parts + * without having to modify this file. + */ + export interface ExtendedChatResponseParts { + ChatResponsePart: ChatResponsePart; + ChatResponseTextEditPart: ChatResponseTextEditPart; + ChatResponseNotebookEditPart: ChatResponseNotebookEditPart; + ChatResponseWorkspaceEditPart: ChatResponseWorkspaceEditPart; + ChatResponseConfirmationPart: ChatResponseConfirmationPart; + ChatResponseCodeCitationPart: ChatResponseCodeCitationPart; + ChatResponseReferencePart2: ChatResponseReferencePart2; + ChatResponseMovePart: ChatResponseMovePart; + ChatResponseExtensionsPart: ChatResponseExtensionsPart; + ChatResponsePullRequestPart: ChatResponsePullRequestPart; + ChatToolInvocationPart: ChatToolInvocationPart; + ChatResponseMultiDiffPart: ChatResponseMultiDiffPart; + ChatResponseThinkingProgressPart: ChatResponseThinkingProgressPart; + ChatResponseExternalEditPart: ChatResponseExternalEditPart; + ChatResponseQuestionCarouselPart: ChatResponseQuestionCarouselPart; + } + + export type ExtendedChatResponsePart = ExtendedChatResponseParts[keyof ExtendedChatResponseParts]; + export class ChatResponseWarningPart { value: MarkdownString; constructor(value: string | MarkdownString); @@ -348,12 +541,16 @@ declare module 'vscode' { } export class ChatResponsePullRequestPart { - readonly uri: Uri; + /** + * @deprecated use `command` instead + */ + readonly uri?: Uri; + readonly command: Command; readonly linkTag: string; readonly title: string; readonly description: string; readonly author: string; - constructor(uri: Uri, title: string, description: string, author: string, linkTag: string); + constructor(uriOrCommand: Uri | Command, title: string, description: string, author: string, linkTag: string); } export interface ChatResponseStream { @@ -408,6 +605,15 @@ declare module 'vscode' { */ confirmation(title: string, message: string | MarkdownString, data: any, buttons?: string[]): void; + /** + * Show an inline carousel of questions to gather information from the user. + * This is a blocking call that waits for the user to submit or skip the questions. + * @param questions Array of questions to display to the user + * @param allowSkip Whether the user can skip questions without answering + * @returns A promise that resolves with the user's answers, or undefined if skipped + */ + questionCarousel(questions: ChatQuestion[], allowSkip?: boolean): Thenable | undefined>; + /** * Push a warning to this stream. Short-hand for * `push(new ChatResponseWarningPart(message))`. @@ -442,6 +648,13 @@ declare module 'vscode' { push(part: ExtendedChatResponsePart): void; clearToPreviousToolInvocation(reason: ChatResponseClearToPreviousToolInvocationReason): void; + + /** + * Report token usage information for this request. + * This is typically called when the underlying language model provides usage statistics. + * @param usage Token usage information including prompt and completion tokens + */ + usage(usage: ChatResultUsage): void; } export enum ChatResponseReferencePartStatusKind { @@ -633,12 +846,6 @@ declare module 'vscode' { * An optional detail string that will be rendered at the end of the response in certain UI contexts. */ details?: string; - - /** - * Token usage information for this request, if available. - * This is typically provided by the underlying language model. - */ - readonly usage?: ChatResultUsage; } export namespace chat { diff --git a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts index 4ab722c122..63f9ec2488 100644 --- a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 11 +// version: 14 declare module 'vscode' { @@ -110,6 +110,16 @@ declare module 'vscode' { * Display name of the subagent that is invoking this request. */ readonly subAgentName?: string; + + /** + * The request ID of the parent request that invoked this subagent. + */ + readonly parentRequestId?: string; + + /** + * Whether any hooks are enabled for this request. + */ + readonly hasHooksEnabled: boolean; } export enum ChatRequestEditedFileEventKind { @@ -127,6 +137,10 @@ declare module 'vscode' { * ChatRequestTurn + private additions. Note- at runtime this is the SAME as ChatRequestTurn and instanceof is safe. */ export class ChatRequestTurn2 { + /** + * The id of the chat request. Used to identity an interaction with any of the chat surfaces. + */ + readonly id?: string; /** * The prompt as entered by the user. * @@ -165,7 +179,7 @@ declare module 'vscode' { /** * @hidden */ - constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined); + constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined, id: string | undefined); } export class ChatResponseTurn2 { @@ -244,6 +258,8 @@ declare module 'vscode' { provideFileIgnored(uri: Uri, token: CancellationToken): ProviderResult; } + export type PreToolUsePermissionDecision = 'allow' | 'deny' | 'ask'; + export interface LanguageModelToolInvocationOptions { chatRequestId?: string; /** @deprecated Use {@link chatSessionResource} instead */ @@ -255,6 +271,16 @@ declare module 'vscode' { * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. */ subAgentInvocationId?: string; + /** + * Pre-tool-use hook result, if the hook was already executed by the caller. + * When provided, the tools service will skip executing its own preToolUse hook + * and use this result for permission decisions and input modifications instead. + */ + preToolUseResult?: { + permissionDecision?: PreToolUsePermissionDecision; + permissionDecisionReason?: string; + updatedInput?: object; + }; } export interface LanguageModelToolInvocationPrepareOptions { @@ -267,6 +293,10 @@ declare module 'vscode' { chatSessionId?: string; chatSessionResource?: Uri; chatInteractionId?: string; + /** + * If set, tells the tool that it should include confirmation messages. + */ + forceConfirmationReason?: string; } export interface PreparedToolInvocation { @@ -305,6 +335,19 @@ declare module 'vscode' { export const onDidDisposeChatSession: Event; } + export namespace window { + /** + * The resource URI of the currently active chat panel session, + * or `undefined` if there is no active chat panel session. + */ + export const activeChatPanelSessionResource: Uri | undefined; + + /** + * An event that fires when the active chat panel session resource changes. + */ + export const onDidChangeActiveChatPanelSessionResource: Event; + } + // #endregion // #region ChatErrorDetailsWithConfirmation @@ -339,4 +382,17 @@ declare module 'vscode' { } // #endregion + + // #region Steering + + export interface ChatContext { + /** + * Set to `true` by the editor to request the language model gracefully + * stop after its next opportunity. When set, it's likely that the editor + * will immediately follow up with a new request in the same conversation. + */ + readonly yieldRequested: boolean; + } + + // #endregion } diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index 84cd547599..c3641a3706 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -23,7 +23,12 @@ declare module 'vscode' { /** * The chat session is currently in progress. */ - InProgress = 2 + InProgress = 2, + + /** + * The chat session needs user input (e.g. an unresolved confirmation). + */ + NeedsInput = 3 } export namespace chat { @@ -42,7 +47,7 @@ declare module 'vscode' { /** * Creates a new {@link ChatSessionItemController chat session item controller} with the given unique identifier. */ - export function createChatSessionItemController(id: string, refreshHandler: () => Thenable): ChatSessionItemController; + export function createChatSessionItemController(id: string, refreshHandler: (token: CancellationToken) => Thenable): ChatSessionItemController; } /** @@ -97,7 +102,7 @@ declare module 'vscode' { * * This is also called on first load to get the initial set of items. */ - refreshHandler: () => Thenable; + readonly refreshHandler: (token: CancellationToken) => Thenable; /** * Fired when an item's archived state changes. @@ -199,54 +204,46 @@ declare module 'vscode' { /** * Timestamp when the session was created in milliseconds elapsed since January 1, 1970 00:00:00 UTC. */ - created: number; + readonly created: number; /** * Timestamp when the most recent request started in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * * Should be undefined if no requests have been made yet. */ - lastRequestStarted?: number; + readonly lastRequestStarted?: number; /** * Timestamp when the most recent request completed in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * * Should be undefined if the most recent request is still in progress or if no requests have been made yet. */ - lastRequestEnded?: number; + readonly lastRequestEnded?: number; /** * Session start timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * @deprecated Use `created` and `lastRequestStarted` instead. */ - startTime?: number; + readonly startTime?: number; /** * Session end timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * @deprecated Use `lastRequestEnded` instead. */ - endTime?: number; + readonly endTime?: number; }; /** * Statistics about the chat session. */ - changes?: readonly ChatSessionChangedFile[] | readonly ChatSessionChangedFile2[] | { - /** - * Number of files edited during the session. - */ - files: number; - - /** - * Number of insertions made during the session. - */ - insertions: number; + changes?: readonly ChatSessionChangedFile[] | readonly ChatSessionChangedFile2[]; - /** - * Number of deletions made during the session. - */ - deletions: number; - }; + /** + * Arbitrary metadata for the chat session. Can be anything, but must be JSON-stringifyable. + * + * To update the metadata you must re-set this property. + */ + metadata?: { readonly [key: string]: any }; } export class ChatSessionChangedFile { From 40d6d90d12eae54bcbd49e505e20603fb31a2663 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:38:21 +0000 Subject: [PATCH 03/11] feat: add checkout pull request in worktree option Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- package.json | 15 +++++++ package.nls.json | 1 + src/commands.ts | 113 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) diff --git a/package.json b/package.json index 76d03fb65d..c74f3341e7 100644 --- a/package.json +++ b/package.json @@ -988,6 +988,12 @@ "category": "%command.pull.request.category%", "icon": "$(cloud)" }, + { + "command": "pr.pickInWorktree", + "title": "%command.pr.pickInWorktree.title%", + "category": "%command.pull.request.category%", + "icon": "$(folder-library)" + }, { "command": "pr.exit", "title": "%command.pr.exit.title%", @@ -2059,6 +2065,10 @@ "command": "pr.pickOnCodespaces", "when": "false" }, + { + "command": "pr.pickInWorktree", + "when": "false" + }, { "command": "pr.exit", "when": "github:inReviewMode" @@ -2857,6 +2867,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)/", diff --git a/package.nls.json b/package.nls.json index 47bceaa46c..e4ec98e12b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -208,6 +208,7 @@ "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 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", diff --git a/src/commands.ts b/src/commands.ts index 88e3784b06..334a198d32 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -825,6 +825,119 @@ export function registerCommands( ), ); + context.subscriptions.push( + vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | RepositoryChangesNode | PullRequestModel) => { + 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 || pr instanceof RepositoryChangesNode) { + pullRequestModel = pr.pullRequestModel; + repository = pr.repository; + } else { + pullRequestModel = pr; + } + + // Validate that the PR has a valid head branch + if (!pullRequestModel.head) { + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to checkout pull request: missing head branch information.')); + } + + // 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.')); + } + + const repositoryToUse = repository || folderManager.repository; + + /* __GDPR__ + "pr.checkoutInWorktree" : {} + */ + telemetry.sendTelemetryEvent('pr.checkoutInWorktree'); + + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Checking out Pull Request #{0} in worktree', pullRequestModel.number), + }, + async (progress) => { + // Generate a branch name for the worktree + const branchName = pullRequestModel.head!.ref; + const remoteName = pullRequestModel.remote.remoteName; + + // Fetch the PR branch first + progress.report({ message: vscode.l10n.t('Fetching branch {0}...', branchName) }); + try { + await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); + } catch (e) { + Logger.appendLine(`Failed to fetch branch ${branchName}: ${e}`, logId); + // Continue even if fetch fails - the branch might already be available locally + } + + // Ask user for worktree location + const repoRootPath = repositoryToUse.rootUri.fsPath; + const parentDir = pathLib.dirname(repoRootPath); + const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`); + + const worktreeUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(defaultWorktreePath), + title: vscode.l10n.t('Select Worktree Location'), + saveLabel: vscode.l10n.t('Create Worktree'), + }); + + if (!worktreeUri) { + return; // User cancelled + } + + const worktreePath = worktreeUri.fsPath; + + // Create the worktree using git command + progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); + + const trackedBranchName = `${remoteName}/${branchName}`; + const localBranchName = `pr-${pullRequestModel.number}/${branchName}`; + + try { + // Execute git worktree add command + const terminal = vscode.window.createTerminal({ + name: vscode.l10n.t('Git Worktree'), + cwd: repoRootPath, + hideFromUser: true, + }); + + // Create worktree with a new local branch tracking the remote + terminal.sendText(`git worktree add -b "${localBranchName}" "${worktreePath}" "${trackedBranchName}" && exit`); + + // Wait a bit for the command to complete + await new Promise(resolve => setTimeout(resolve, 2000)); + + terminal.dispose(); + + // Ask user if they want to open the worktree + const openAction = vscode.l10n.t('Open in New Window'); + const result = await vscode.window.showInformationMessage( + vscode.l10n.t('Worktree created for Pull Request #{0}', pullRequestModel.number), + openAction + ); + + if (result === openAction) { + await commands.openFolder(worktreeUri, { forceNewWindow: true }); + } + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + Logger.error(`Failed to create worktree: ${errorMessage}`, logId); + return vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage)); + } + } + ); + }), + ); + 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.')); From ecc650ddd3d527c459648bde54051e0892350576 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:42:25 +0000 Subject: [PATCH 04/11] fix: use VS Code Tasks API for reliable cross-platform worktree creation Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/commands.ts | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 334a198d32..6162a4d18f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -896,27 +896,51 @@ export function registerCommands( const worktreePath = worktreeUri.fsPath; - // Create the worktree using git command + // Create the worktree using a VS Code task progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); const trackedBranchName = `${remoteName}/${branchName}`; const localBranchName = `pr-${pullRequestModel.number}/${branchName}`; try { - // Execute git worktree add command - const terminal = vscode.window.createTerminal({ - name: vscode.l10n.t('Git Worktree'), - cwd: repoRootPath, - hideFromUser: true, + // Create a VS Code task to execute the git worktree command + const taskDefinition: vscode.TaskDefinition = { + type: 'shell' + }; + + const shellExecution = new vscode.ShellExecution('git', [ + 'worktree', 'add', + '-b', { value: localBranchName, quoting: vscode.ShellQuoting.Strong }, + { value: worktreePath, quoting: vscode.ShellQuoting.Strong }, + { value: trackedBranchName, quoting: vscode.ShellQuoting.Strong } + ], { + cwd: repoRootPath }); - // Create worktree with a new local branch tracking the remote - terminal.sendText(`git worktree add -b "${localBranchName}" "${worktreePath}" "${trackedBranchName}" && exit`); - - // Wait a bit for the command to complete - await new Promise(resolve => setTimeout(resolve, 2000)); + const task = new vscode.Task( + taskDefinition, + vscode.TaskScope.Workspace, + vscode.l10n.t('Create Worktree for Pull Request #{0}', pullRequestModel.number), + 'git', + shellExecution + ); - terminal.dispose(); + // Execute the task and wait for completion + const taskExecution = await vscode.tasks.executeTask(task); + + // Wait for task to complete + await new Promise((resolve, reject) => { + const disposable = vscode.tasks.onDidEndTaskProcess(e => { + if (e.execution === taskExecution) { + disposable.dispose(); + if (e.exitCode === 0) { + resolve(); + } else { + reject(new Error(vscode.l10n.t('Git worktree command failed with exit code {0}', e.exitCode?.toString() ?? 'unknown'))); + } + } + }); + }); // Ask user if they want to open the worktree const openAction = vscode.l10n.t('Open in New Window'); From fab1204c8d71649c812651b970736e1777f15070 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:44:10 +0000 Subject: [PATCH 05/11] fix: address code review feedback - improve error handling and branch naming Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/commands.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 6162a4d18f..6fee5bf5e6 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -847,6 +847,9 @@ export function registerCommands( return vscode.window.showErrorMessage(vscode.l10n.t('Unable to checkout pull request: missing head branch information.')); } + // Store validated head to avoid non-null assertions later + const prHead = pullRequestModel.head; + // Get the folder manager to access the repository const folderManager = reposManager.getManagerForIssueModel(pullRequestModel); if (!folderManager) { @@ -867,7 +870,7 @@ export function registerCommands( }, async (progress) => { // Generate a branch name for the worktree - const branchName = pullRequestModel.head!.ref; + const branchName = prHead.ref; const remoteName = pullRequestModel.remote.remoteName; // Fetch the PR branch first @@ -875,7 +878,8 @@ export function registerCommands( try { await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); } catch (e) { - Logger.appendLine(`Failed to fetch branch ${branchName}: ${e}`, logId); + const errorMessage = e instanceof Error ? e.message : String(e); + Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId); // Continue even if fetch fails - the branch might already be available locally } @@ -900,7 +904,7 @@ export function registerCommands( progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); const trackedBranchName = `${remoteName}/${branchName}`; - const localBranchName = `pr-${pullRequestModel.number}/${branchName}`; + const localBranchName = `pr-${pullRequestModel.number}-${branchName}`; try { // Create a VS Code task to execute the git worktree command From 80e46a2571c09fe667d7c01d740a68f4a326a089 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:40:23 +0100 Subject: [PATCH 06/11] argument clean up and keep branch name --- src/commands.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 6fee5bf5e6..ccf90c2f75 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -826,8 +826,8 @@ export function registerCommands( ); context.subscriptions.push( - vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | RepositoryChangesNode | PullRequestModel) => { - if (pr === undefined) { + vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | unknown) => { + if (pr === undefined || !(pr instanceof PRNode)) { 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.')); } @@ -835,12 +835,8 @@ export function registerCommands( let pullRequestModel: PullRequestModel; let repository: Repository | undefined; - if (pr instanceof PRNode || pr instanceof RepositoryChangesNode) { - pullRequestModel = pr.pullRequestModel; - repository = pr.repository; - } else { - pullRequestModel = pr; - } + pullRequestModel = pr.pullRequestModel; + repository = pr.repository; // Validate that the PR has a valid head branch if (!pullRequestModel.head) { @@ -904,7 +900,7 @@ export function registerCommands( progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); const trackedBranchName = `${remoteName}/${branchName}`; - const localBranchName = `pr-${pullRequestModel.number}-${branchName}`; + const localBranchName = branchName; try { // Create a VS Code task to execute the git worktree command From 103da71cff1d312beddadcdc7aca5223a827f15d Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:45:55 +0100 Subject: [PATCH 07/11] Update git API --- src/@types/git.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/@types/git.d.ts b/src/@types/git.d.ts index 718d8f4b6b..7e4aebbced 100644 --- a/src/@types/git.d.ts +++ b/src/@types/git.d.ts @@ -287,6 +287,9 @@ export interface Repository { applyStash(index?: number): Promise; popStash(index?: number): Promise; dropStash(index?: number): Promise; + + createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise; + deleteWorktree(path: string, options?: { force?: boolean }): Promise; } export interface RemoteSource { From 04c416a09a3a4ebd8a1a877820df582130eb6f5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:51:45 +0000 Subject: [PATCH 08/11] refactor: use git extension API for worktree creation instead of shell execution Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/api/api.d.ts | 3 +++ src/commands.ts | 48 ++++++++++-------------------------------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/api/api.d.ts b/src/api/api.d.ts index 011cbd9cfa..68ed7b79eb 100644 --- a/src/api/api.d.ts +++ b/src/api/api.d.ts @@ -209,6 +209,9 @@ export interface Repository { add(paths: string[]): Promise; merge(ref: string): Promise; mergeAbort(): Promise; + + createWorktree?(options?: { path?: string; commitish?: string; branch?: string }): Promise; + deleteWorktree?(path: string, options?: { force?: boolean }): Promise; } /** diff --git a/src/commands.ts b/src/commands.ts index ccf90c2f75..196377a1a3 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -896,50 +896,22 @@ export function registerCommands( const worktreePath = worktreeUri.fsPath; - // Create the worktree using a VS Code task + // Create the worktree using the git extension API progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); const trackedBranchName = `${remoteName}/${branchName}`; - const localBranchName = branchName; try { - // Create a VS Code task to execute the git worktree command - const taskDefinition: vscode.TaskDefinition = { - type: 'shell' - }; - - const shellExecution = new vscode.ShellExecution('git', [ - 'worktree', 'add', - '-b', { value: localBranchName, quoting: vscode.ShellQuoting.Strong }, - { value: worktreePath, quoting: vscode.ShellQuoting.Strong }, - { value: trackedBranchName, quoting: vscode.ShellQuoting.Strong } - ], { - cwd: repoRootPath - }); - - const task = new vscode.Task( - taskDefinition, - vscode.TaskScope.Workspace, - vscode.l10n.t('Create Worktree for Pull Request #{0}', pullRequestModel.number), - 'git', - shellExecution - ); + // Check if the createWorktree API is available + if (!repositoryToUse.createWorktree) { + throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); + } - // Execute the task and wait for completion - const taskExecution = await vscode.tasks.executeTask(task); - - // Wait for task to complete - await new Promise((resolve, reject) => { - const disposable = vscode.tasks.onDidEndTaskProcess(e => { - if (e.execution === taskExecution) { - disposable.dispose(); - if (e.exitCode === 0) { - resolve(); - } else { - reject(new Error(vscode.l10n.t('Git worktree command failed with exit code {0}', e.exitCode?.toString() ?? 'unknown'))); - } - } - }); + // Use the git extension's createWorktree API + await repositoryToUse.createWorktree({ + path: worktreePath, + commitish: trackedBranchName, + branch: branchName }); // Ask user if they want to open the worktree From ad7444526b565c573a102a398956f982c9013b1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:06:27 +0000 Subject: [PATCH 09/11] fix: run fetch and worktree selection in parallel, move info message after progress Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/commands.ts | 106 ++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 196377a1a3..c51df0dd7e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -859,18 +859,17 @@ export function registerCommands( */ telemetry.sendTelemetryEvent('pr.checkoutInWorktree'); - return vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: vscode.l10n.t('Checking out Pull Request #{0} in worktree', pullRequestModel.number), - }, - async (progress) => { - // Generate a branch name for the worktree - const branchName = prHead.ref; - const remoteName = pullRequestModel.remote.remoteName; - - // Fetch the PR branch first - progress.report({ message: vscode.l10n.t('Fetching branch {0}...', branchName) }); + // Prepare for parallel operations + const repoRootPath = repositoryToUse.rootUri.fsPath; + const parentDir = pathLib.dirname(repoRootPath); + const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`); + const branchName = prHead.ref; + const remoteName = pullRequestModel.remote.remoteName; + + // Run fetch and worktree location selection in parallel + const [, worktreeUri] = await Promise.all([ + // Fetch the PR branch + (async () => { try { await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); } catch (e) { @@ -878,59 +877,62 @@ export function registerCommands( Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId); // Continue even if fetch fails - the branch might already be available locally } + })(), + // Ask user for worktree location + vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(defaultWorktreePath), + title: vscode.l10n.t('Select Worktree Location'), + saveLabel: vscode.l10n.t('Create Worktree'), + }) + ]); - // Ask user for worktree location - const repoRootPath = repositoryToUse.rootUri.fsPath; - const parentDir = pathLib.dirname(repoRootPath); - const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`); - - const worktreeUri = await vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file(defaultWorktreePath), - title: vscode.l10n.t('Select Worktree Location'), - saveLabel: vscode.l10n.t('Create Worktree'), - }); - - if (!worktreeUri) { - return; // User cancelled - } + if (!worktreeUri) { + return; // User cancelled + } - const worktreePath = worktreeUri.fsPath; + const worktreePath = worktreeUri.fsPath; + const trackedBranchName = `${remoteName}/${branchName}`; - // Create the worktree using the git extension API - progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); + try { + // Check if the createWorktree API is available + if (!repositoryToUse.createWorktree) { + throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); + } - const trackedBranchName = `${remoteName}/${branchName}`; - - try { - // Check if the createWorktree API is available - if (!repositoryToUse.createWorktree) { - throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); - } + // Store reference to ensure type narrowing + const createWorktree = repositoryToUse.createWorktree; + // Create the worktree with progress + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Creating worktree for Pull Request #{0}...', pullRequestModel.number), + }, + async () => { // Use the git extension's createWorktree API - await repositoryToUse.createWorktree({ + await createWorktree({ path: worktreePath, commitish: trackedBranchName, branch: branchName }); + } + ); - // Ask user if they want to open the worktree - const openAction = vscode.l10n.t('Open in New Window'); - const result = await vscode.window.showInformationMessage( - vscode.l10n.t('Worktree created for Pull Request #{0}', pullRequestModel.number), - openAction - ); + // Ask user if they want to open the worktree (after progress is finished) + const openAction = vscode.l10n.t('Open in New Window'); + const result = await vscode.window.showInformationMessage( + vscode.l10n.t('Worktree created for Pull Request #{0}', pullRequestModel.number), + openAction + ); - if (result === openAction) { - await commands.openFolder(worktreeUri, { forceNewWindow: true }); - } - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - Logger.error(`Failed to create worktree: ${errorMessage}`, logId); - return vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage)); - } + if (result === openAction) { + await commands.openFolder(worktreeUri, { forceNewWindow: true }); } - ); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + Logger.error(`Failed to create worktree: ${errorMessage}`, logId); + return vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage)); + } }), ); From 3e1c44ebc26887d64ed04edde0feeb057f975d73 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:07:26 +0100 Subject: [PATCH 10/11] Fix createWorktree call --- src/commands.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index c51df0dd7e..67b3d21503 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -899,9 +899,6 @@ export function registerCommands( throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); } - // Store reference to ensure type narrowing - const createWorktree = repositoryToUse.createWorktree; - // Create the worktree with progress await vscode.window.withProgress( { @@ -910,7 +907,7 @@ export function registerCommands( }, async () => { // Use the git extension's createWorktree API - await createWorktree({ + await repositoryToUse.createWorktree!({ path: worktreePath, commitish: trackedBranchName, branch: branchName From 766d8a006fac77f74c9e6eeea20b3e81ba206a06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:12:37 +0000 Subject: [PATCH 11/11] fix: start progress before fetch operation, include fetch in progress scope Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/commands.ts | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 67b3d21503..8999e6ed1f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -859,32 +859,19 @@ export function registerCommands( */ telemetry.sendTelemetryEvent('pr.checkoutInWorktree'); - // Prepare for parallel operations + // Prepare for operations const repoRootPath = repositoryToUse.rootUri.fsPath; const parentDir = pathLib.dirname(repoRootPath); const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`); const branchName = prHead.ref; const remoteName = pullRequestModel.remote.remoteName; - // Run fetch and worktree location selection in parallel - const [, worktreeUri] = await Promise.all([ - // Fetch the PR branch - (async () => { - try { - await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId); - // Continue even if fetch fails - the branch might already be available locally - } - })(), - // Ask user for worktree location - vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file(defaultWorktreePath), - title: vscode.l10n.t('Select Worktree Location'), - saveLabel: vscode.l10n.t('Create Worktree'), - }) - ]); + // Ask user for worktree location first (not in progress) + const worktreeUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(defaultWorktreePath), + title: vscode.l10n.t('Select Worktree Location'), + saveLabel: vscode.l10n.t('Create Worktree'), + }); if (!worktreeUri) { return; // User cancelled @@ -899,13 +886,22 @@ export function registerCommands( throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); } - // Create the worktree with progress + // Start progress for fetch and worktree creation await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Creating worktree for Pull Request #{0}...', pullRequestModel.number), }, async () => { + // Fetch the PR branch first + try { + await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId); + // Continue even if fetch fails - the branch might already be available locally + } + // Use the git extension's createWorktree API await repositoryToUse.createWorktree!({ path: worktreePath,