From 1a0f3bea8ea7858a02a5299c09bc885a5e8e29a0 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Fri, 8 May 2026 16:59:31 +0300 Subject: [PATCH] fix: allow PR worktrees from existing local branches Use existing local PR branches instead of blocking worktree creation Only block PR branches already checked out in another worktree Clarify local branch validation messages across locales --- .../session/GitHubIntegrationDialog.tsx | 12 +++++---- .../components/session/NewWorktreeDialog.tsx | 27 ++++++++++++++++--- packages/ui/src/lib/i18n/messages/en.ts | 1 + packages/ui/src/lib/i18n/messages/es.ts | 1 + packages/ui/src/lib/i18n/messages/ko.ts | 1 + packages/ui/src/lib/i18n/messages/pl.ts | 1 + packages/ui/src/lib/i18n/messages/pt-BR.ts | 1 + packages/ui/src/lib/i18n/messages/uk.ts | 1 + packages/ui/src/lib/i18n/messages/zh-CN.ts | 1 + 9 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/session/GitHubIntegrationDialog.tsx b/packages/ui/src/components/session/GitHubIntegrationDialog.tsx index 0deacae2..e80b537a 100644 --- a/packages/ui/src/components/session/GitHubIntegrationDialog.tsx +++ b/packages/ui/src/components/session/GitHubIntegrationDialog.tsx @@ -195,13 +195,15 @@ export function GitHubIntegrationDialog({ worktreeName: branchName, }); - const isBlocked = result.errors.some( - (entry) => entry.code === 'branch_in_use' || entry.code === 'branch_exists' - ); + const blockingError = result.errors.find((entry) => entry.code === 'branch_in_use'); setValidations(prev => new Map(prev).set(branchName, { - isValid: !isBlocked, - error: isBlocked ? t('session.githubIntegration.validation.branchAlreadyCheckedOut') : null, + isValid: !blockingError, + error: blockingError + ? t(blockingError.code === 'branch_exists' + ? 'session.githubIntegration.validation.branchAlreadyExists' + : 'session.githubIntegration.validation.branchAlreadyCheckedOut') + : null, })); } catch { setValidations(prev => new Map(prev).set(branchName, { diff --git a/packages/ui/src/components/session/NewWorktreeDialog.tsx b/packages/ui/src/components/session/NewWorktreeDialog.tsx index 90f271d5..15bff4f4 100644 --- a/packages/ui/src/components/session/NewWorktreeDialog.tsx +++ b/packages/ui/src/components/session/NewWorktreeDialog.tsx @@ -129,12 +129,24 @@ const sanitizeRemoteName = (value: string): string => { return normalized || 'pr-head'; }; -const resolvePrWorktreeConfig = (pr: GitHubPullRequestSummary, remoteBranches: string[]) => { +const resolvePrWorktreeConfig = (pr: GitHubPullRequestSummary, localBranches: string[], remoteBranches: string[]) => { const headBranch = normalizeBranchName(pr.head || ''); if (!headBranch) { throw new Error('PR head branch is missing'); } + if (localBranches.includes(headBranch)) { + return { + existingBranch: headBranch, + setUpstream: undefined, + upstreamRemote: undefined, + upstreamBranch: undefined, + ensureRemoteName: undefined, + ensureRemoteUrl: undefined, + sourceLabel: headBranch, + }; + } + const availableRemoteBranch = remoteBranches.find((remoteBranch) => { const slashIndex = remoteBranch.indexOf('/'); if (slashIndex <= 0 || slashIndex >= remoteBranch.length - 1) { @@ -734,11 +746,15 @@ export function NewWorktreeDialog({ // Only run server validation if we have values if (normalizedBranch && normalizedWorktree) { + const linkedPr = mode === 'new-branch' ? newBranchState.linkedPr : null; + const prConfig = linkedPr ? resolvePrWorktreeConfig(linkedPr, localBranches, remoteBranches) : null; const result = await validateWorktreeCreate(projectRef, { - mode: mode === 'existing-branch' ? 'existing' : 'new', + mode: mode === 'existing-branch' || prConfig ? 'existing' : 'new', branchName: normalizedBranch, worktreeName: normalizedWorktree, - existingBranch: mode === 'existing-branch' ? normalizedBranch : undefined, + existingBranch: prConfig?.existingBranch ?? (mode === 'existing-branch' ? normalizedBranch : undefined), + ...(prConfig?.ensureRemoteName ? { ensureRemoteName: prConfig.ensureRemoteName } : {}), + ...(prConfig?.ensureRemoteUrl ? { ensureRemoteUrl: prConfig.ensureRemoteUrl } : {}), }); if (abortController.signal.aborted) return; @@ -777,8 +793,11 @@ export function NewWorktreeDialog({ projectRef, mode, newBranchState.branchName, + newBranchState.linkedPr, existingBranchState.selectedBranch, currentState.worktreeName, + localBranches, + remoteBranches, validation.touched, validationAbortController, isCreating, @@ -846,7 +865,7 @@ export function NewWorktreeDialog({ let sourceLabel = ''; const args = (() => { if (linkedPr) { - const prConfig = resolvePrWorktreeConfig(linkedPr, remoteBranches); + const prConfig = resolvePrWorktreeConfig(linkedPr, localBranches, remoteBranches); sourceLabel = prConfig.sourceLabel; return { preferredName: normalizedBranch || normalizedWorktree, diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index c353d00c..d37cf241 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -1323,6 +1323,7 @@ export const dict = { 'session.githubIntegration.error.notConnected': 'GitHub not connected', 'session.githubIntegration.error.loadDataFailed': 'Failed to load data', 'session.githubIntegration.validation.branchAlreadyCheckedOut': 'Branch is already checked out in a worktree', + 'session.githubIntegration.validation.branchAlreadyExists': 'Branch already exists locally', 'session.githubIntegration.validation.failed': 'Validation failed', 'chat.fileAttachment.toast.attachFailed': 'Failed to attach file', 'chat.fileAttachment.toast.someFilesSkipped': 'Some files were skipped:\n{summary}', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 4ae14555..e975342f 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -1289,6 +1289,7 @@ export const dict: Record = { "session.githubIntegration.error.notConnected": "GitHub no está conectado", "session.githubIntegration.error.loadDataFailed": "No se pudieron cargar los datos", "session.githubIntegration.validation.branchAlreadyCheckedOut": "La rama ya está en uso en un worktree", + "session.githubIntegration.validation.branchAlreadyExists": "La rama ya existe localmente", "session.githubIntegration.validation.failed": "No se pudo validar", "chat.fileAttachment.toast.attachFailed": "No se pudo adjuntar el archivo", "chat.fileAttachment.toast.someFilesSkipped": "Algunos archivos se omitieron:\n{summary}", diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index dee46a18..5dc7c9e7 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -1325,6 +1325,7 @@ export const dict: Record = { 'session.githubIntegration.error.notConnected': 'GitHub에 연결되어 있지 않습니다', 'session.githubIntegration.error.loadDataFailed': '데이터를 불러오지 못했습니다', 'session.githubIntegration.validation.branchAlreadyCheckedOut': '브랜치가 이미 워크트리에 체크아웃되어 있습니다', + 'session.githubIntegration.validation.branchAlreadyExists': '브랜치가 이미 로컬에 있습니다', 'session.githubIntegration.validation.failed': '유효성 검사에 실패했습니다', 'chat.fileAttachment.toast.attachFailed': '첨부 파일 실패', 'chat.fileAttachment.toast.someFilesSkipped': '일부 파일을 건너뛰었습니다:\n{summary}', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 9ac6c423..f542017e 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -390,6 +390,7 @@ export const dict: Record = { 'session.githubIntegration.error.notConnected': 'GitHub nie jest połączony', 'session.githubIntegration.error.loadDataFailed': 'Nie udało się załadować danych', 'session.githubIntegration.validation.branchAlreadyCheckedOut': 'Gałąź jest już wyewidencjonowana w drzewie pracy', + 'session.githubIntegration.validation.branchAlreadyExists': 'Gałąź już istnieje lokalnie', 'session.githubIntegration.validation.failed': 'Walidacja nie powiodła się', 'chat.fileAttachment.toast.attachFailed': 'Nie udało się dołączyć pliku', 'chat.fileAttachment.toast.someFilesSkipped': 'Niektóre pliki zostały pominięte:\n{summary}', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 366a1262..5a4a5da0 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -1289,6 +1289,7 @@ export const dict: Record = { "session.githubIntegration.error.notConnected": "GitHub não está conectado", "session.githubIntegration.error.loadDataFailed": "Não foi possível carregar os dados", "session.githubIntegration.validation.branchAlreadyCheckedOut": "A branch já está em uso em um worktree", + "session.githubIntegration.validation.branchAlreadyExists": "A branch já existe localmente", "session.githubIntegration.validation.failed": "Não foi possível validar", "chat.fileAttachment.toast.attachFailed": "Não foi possível anexar o arquivo", "chat.fileAttachment.toast.someFilesSkipped": "Alguns arquivos foram omitidos:\n{summary}", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 23bd7f04..d2258504 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -1289,6 +1289,7 @@ export const dict: Record = { "session.githubIntegration.error.notConnected": "GitHub не підключено", "session.githubIntegration.error.loadDataFailed": "Не вдалося завантажити дані", "session.githubIntegration.validation.branchAlreadyCheckedOut": "Гілку вже відкрито в worktree", + "session.githubIntegration.validation.branchAlreadyExists": "Гілка вже існує локально", "session.githubIntegration.validation.failed": "Помилка перевірки", "chat.fileAttachment.toast.attachFailed": "Не вдалося прикріпити файл", "chat.fileAttachment.toast.someFilesSkipped": "Деякі файли були пропущені:\n{summary}", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index e121369b..9772183b 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -1289,6 +1289,7 @@ export const dict: Record = { 'session.githubIntegration.error.notConnected': 'GitHub 未连接', 'session.githubIntegration.error.loadDataFailed': '加载数据失败', 'session.githubIntegration.validation.branchAlreadyCheckedOut': '该分支已在某个 worktree 中检出', + 'session.githubIntegration.validation.branchAlreadyExists': '该分支已在本地存在', 'session.githubIntegration.validation.failed': '校验失败', 'chat.fileAttachment.toast.attachFailed': '附加文件失败', 'chat.fileAttachment.toast.someFilesSkipped': '以下文件被跳过:\n{summary}',