From 624f4a666c4e2abf2eb0f10b82c906ec589d1fe9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 08:34:03 +0000 Subject: [PATCH 1/3] Initial plan From e493b79251360a97a2fda62dc6d917a9db186db9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 08:47:05 +0000 Subject: [PATCH 2/3] Use custom InputBox UI for checkout-in-worktree, matching built-in Git: Create Worktree Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/a494f95c-24ab-4aca-b78a-cd2ee4cca5a3 Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/worktree.ts | 121 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 10 deletions(-) diff --git a/src/github/worktree.ts b/src/github/worktree.ts index ca466a8e8a..247f6a2972 100644 --- a/src/github/worktree.ts +++ b/src/github/worktree.ts @@ -44,22 +44,21 @@ export async function checkoutPRInWorktree( // Prepare for operations const repoRootPath = repositoryToUse.rootUri.fsPath; const parentDir = path.dirname(repoRootPath); - const defaultWorktreePath = path.join(parentDir, `pr-${pullRequestModel.number}`); + const worktreeName = `pr-${pullRequestModel.number}`; + // Match the default location convention used by VS Code's built-in `Git: Create Worktree...` command: + // `/.worktrees/`. + const defaultWorktreePath = path.join(parentDir, `${path.basename(repoRootPath)}.worktrees`, worktreeName); const branchName = prHead.ref; const remoteName = pullRequestModel.remote.remoteName; - // 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) { + // Ask user for worktree location using a custom InputBox UI (matches the built-in + // `Git: Create Worktree...` experience instead of showing the OS save dialog). + const worktreePath = await promptForWorktreePath(repositoryToUse, worktreeName, defaultWorktreePath); + if (!worktreePath) { return; // User cancelled } - const worktreePath = worktreeUri.fsPath; + const worktreeUri = vscode.Uri.file(worktreePath); const trackedBranchName = `${remoteName}/${branchName}`; try { @@ -126,3 +125,105 @@ export async function checkoutPRInWorktree( vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage)); } } + +/** + * Prompts the user for a worktree path using an `InputBox` that mirrors VS Code's + * built-in `Git: Create Worktree...` UI: the path is pre-filled and editable, the + * worktree-name segment is pre-selected, and an inline folder-picker button lets + * the user browse to a parent directory. + * + * @param repository The repository the worktree will be created from (used to detect + * conflicts with existing worktrees). + * @param worktreeName The default leaf folder name for the new worktree (e.g. `pr-123`). + * @param defaultWorktreePath The default full path to suggest in the input box. + * @returns The chosen absolute path, or `undefined` if the user cancelled. + */ +async function promptForWorktreePath( + repository: Repository, + worktreeName: string, + defaultWorktreePath: string +): Promise { + const getValueSelection = (value: string): [number, number] | undefined => { + if (!value || !worktreeName || !value.endsWith(worktreeName)) { + return undefined; + } + const start = value.length - worktreeName.length; + return [start, value.length]; + }; + + const getValidationMessage = (value: string): vscode.InputBoxValidationMessage | undefined => { + const normalized = path.normalize(value); + const conflict = repository.state.worktrees?.find(w => path.normalize(w.path) === normalized); + return conflict ? { + message: vscode.l10n.t('A worktree already exists at "{0}".', value), + severity: vscode.InputBoxValidationSeverity.Warning + } : undefined; + }; + + const browseForParent = async (): Promise => { + const currentValue = inputBox.value; + const defaultUri = currentValue + ? vscode.Uri.file(path.dirname(currentValue)) + : vscode.Uri.file(path.dirname(defaultWorktreePath)); + + const uris = await vscode.window.showOpenDialog({ + defaultUri, + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: vscode.l10n.t('Select as Worktree Destination'), + }); + + if (!uris || uris.length === 0) { + return undefined; + } + return path.join(uris[0].fsPath, worktreeName); + }; + + const disposables: vscode.Disposable[] = []; + const inputBox = vscode.window.createInputBox(); + disposables.push(inputBox); + + inputBox.title = vscode.l10n.t('Create Worktree'); + inputBox.placeholder = vscode.l10n.t('Worktree path'); + inputBox.prompt = vscode.l10n.t('Please provide a worktree path'); + inputBox.value = defaultWorktreePath; + inputBox.valueSelection = getValueSelection(inputBox.value); + inputBox.validationMessage = getValidationMessage(inputBox.value); + inputBox.ignoreFocusOut = true; + inputBox.buttons = [ + { + iconPath: new vscode.ThemeIcon('folder'), + tooltip: vscode.l10n.t('Select Worktree Destination'), + location: vscode.QuickInputButtonLocation.Inline + } + ]; + + try { + inputBox.show(); + + return await new Promise((resolve) => { + disposables.push(inputBox.onDidHide(() => resolve(undefined))); + disposables.push(inputBox.onDidAccept(() => { + if (!inputBox.value) { + return; + } + resolve(inputBox.value); + inputBox.hide(); + })); + disposables.push(inputBox.onDidChangeValue(value => { + inputBox.validationMessage = getValidationMessage(value); + })); + disposables.push(inputBox.onDidTriggerButton(async () => { + const chosen = await browseForParent(); + if (chosen) { + inputBox.value = chosen; + inputBox.valueSelection = getValueSelection(inputBox.value); + inputBox.validationMessage = getValidationMessage(inputBox.value); + } + })); + }); + } finally { + disposables.forEach(d => d.dispose()); + } +} From ee8fb42781401caa2732fc9bf465684d519df1bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:28:44 +0000 Subject: [PATCH 3/3] Delegate worktree path selection to Repository.createWorktree API Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/worktree.ts | 139 ++++------------------------------------- 1 file changed, 12 insertions(+), 127 deletions(-) diff --git a/src/github/worktree.ts b/src/github/worktree.ts index 247f6a2972..d95543a2fa 100644 --- a/src/github/worktree.ts +++ b/src/github/worktree.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; import * as vscode from 'vscode'; import { FolderRepositoryManager } from './folderRepositoryManager'; import { PullRequestModel } from './pullRequestModel'; @@ -41,24 +40,8 @@ export async function checkoutPRInWorktree( */ telemetry.sendTelemetryEvent('pr.checkoutInWorktree'); - // Prepare for operations - const repoRootPath = repositoryToUse.rootUri.fsPath; - const parentDir = path.dirname(repoRootPath); - const worktreeName = `pr-${pullRequestModel.number}`; - // Match the default location convention used by VS Code's built-in `Git: Create Worktree...` command: - // `/.worktrees/`. - const defaultWorktreePath = path.join(parentDir, `${path.basename(repoRootPath)}.worktrees`, worktreeName); const branchName = prHead.ref; const remoteName = pullRequestModel.remote.remoteName; - - // Ask user for worktree location using a custom InputBox UI (matches the built-in - // `Git: Create Worktree...` experience instead of showing the OS save dialog). - const worktreePath = await promptForWorktreePath(repositoryToUse, worktreeName, defaultWorktreePath); - if (!worktreePath) { - return; // User cancelled - } - - const worktreeUri = vscode.Uri.file(worktreePath); const trackedBranchName = `${remoteName}/${branchName}`; try { @@ -67,8 +50,12 @@ export async function checkoutPRInWorktree( throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); } - // Start progress for fetch and worktree creation - await vscode.window.withProgress( + // Fetch the PR branch and create the worktree. The Git extension's + // createWorktree API picks the worktree path itself (using the user's + // saved worktree root or the default `.worktrees/` + // convention) and returns the resulting path, so we don't need any + // custom picker UI here. + const worktreePath = await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Creating worktree for Pull Request #{0}...', pullRequestModel.number), @@ -87,16 +74,14 @@ export async function checkoutPRInWorktree( branchExists = false; } - // Use the git extension's createWorktree API - // If branch already exists, don't specify the branch parameter to avoid "branch already exists" error + // Use the git extension's createWorktree API. If branch already exists, + // don't specify the branch parameter to avoid "branch already exists" error. if (branchExists) { - await repositoryToUse.createWorktree!({ - path: worktreePath, + return await repositoryToUse.createWorktree!({ commitish: branchName }); } else { - await repositoryToUse.createWorktree!({ - path: worktreePath, + return await repositoryToUse.createWorktree!({ commitish: trackedBranchName, branch: branchName }); @@ -104,6 +89,8 @@ export async function checkoutPRInWorktree( } ); + const worktreeUri = vscode.Uri.file(worktreePath); + // Ask user how they want to open the worktree (modal dialog) const openInNewWindow = vscode.l10n.t('New Window'); const openInCurrentWindow = vscode.l10n.t('Current Window'); @@ -125,105 +112,3 @@ export async function checkoutPRInWorktree( vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage)); } } - -/** - * Prompts the user for a worktree path using an `InputBox` that mirrors VS Code's - * built-in `Git: Create Worktree...` UI: the path is pre-filled and editable, the - * worktree-name segment is pre-selected, and an inline folder-picker button lets - * the user browse to a parent directory. - * - * @param repository The repository the worktree will be created from (used to detect - * conflicts with existing worktrees). - * @param worktreeName The default leaf folder name for the new worktree (e.g. `pr-123`). - * @param defaultWorktreePath The default full path to suggest in the input box. - * @returns The chosen absolute path, or `undefined` if the user cancelled. - */ -async function promptForWorktreePath( - repository: Repository, - worktreeName: string, - defaultWorktreePath: string -): Promise { - const getValueSelection = (value: string): [number, number] | undefined => { - if (!value || !worktreeName || !value.endsWith(worktreeName)) { - return undefined; - } - const start = value.length - worktreeName.length; - return [start, value.length]; - }; - - const getValidationMessage = (value: string): vscode.InputBoxValidationMessage | undefined => { - const normalized = path.normalize(value); - const conflict = repository.state.worktrees?.find(w => path.normalize(w.path) === normalized); - return conflict ? { - message: vscode.l10n.t('A worktree already exists at "{0}".', value), - severity: vscode.InputBoxValidationSeverity.Warning - } : undefined; - }; - - const browseForParent = async (): Promise => { - const currentValue = inputBox.value; - const defaultUri = currentValue - ? vscode.Uri.file(path.dirname(currentValue)) - : vscode.Uri.file(path.dirname(defaultWorktreePath)); - - const uris = await vscode.window.showOpenDialog({ - defaultUri, - canSelectFiles: false, - canSelectFolders: true, - canSelectMany: false, - openLabel: vscode.l10n.t('Select as Worktree Destination'), - }); - - if (!uris || uris.length === 0) { - return undefined; - } - return path.join(uris[0].fsPath, worktreeName); - }; - - const disposables: vscode.Disposable[] = []; - const inputBox = vscode.window.createInputBox(); - disposables.push(inputBox); - - inputBox.title = vscode.l10n.t('Create Worktree'); - inputBox.placeholder = vscode.l10n.t('Worktree path'); - inputBox.prompt = vscode.l10n.t('Please provide a worktree path'); - inputBox.value = defaultWorktreePath; - inputBox.valueSelection = getValueSelection(inputBox.value); - inputBox.validationMessage = getValidationMessage(inputBox.value); - inputBox.ignoreFocusOut = true; - inputBox.buttons = [ - { - iconPath: new vscode.ThemeIcon('folder'), - tooltip: vscode.l10n.t('Select Worktree Destination'), - location: vscode.QuickInputButtonLocation.Inline - } - ]; - - try { - inputBox.show(); - - return await new Promise((resolve) => { - disposables.push(inputBox.onDidHide(() => resolve(undefined))); - disposables.push(inputBox.onDidAccept(() => { - if (!inputBox.value) { - return; - } - resolve(inputBox.value); - inputBox.hide(); - })); - disposables.push(inputBox.onDidChangeValue(value => { - inputBox.validationMessage = getValidationMessage(value); - })); - disposables.push(inputBox.onDidTriggerButton(async () => { - const chosen = await browseForParent(); - if (chosen) { - inputBox.value = chosen; - inputBox.valueSelection = getValueSelection(inputBox.value); - inputBox.validationMessage = getValidationMessage(inputBox.value); - } - })); - }); - } finally { - disposables.forEach(d => d.dispose()); - } -}