diff --git a/src/main/ipc/worktrees.test.ts b/src/main/ipc/worktrees.test.ts
index 3f342ef26..ca2df50e7 100644
--- a/src/main/ipc/worktrees.test.ts
+++ b/src/main/ipc/worktrees.test.ts
@@ -1027,6 +1027,66 @@ describe('registerWorktreeHandlers', () => {
})
})
+ it('reuses an existing local branch when the worktree folder is renamed (#5181)', async () => {
+ // Why: the reuse checkbox keeps branchNameOverride pinned to the selected
+ // branch while the worktree folder is named independently. The backend must
+ // still check out that exact branch (no -b) into the renamed folder.
+ listWorktreesMock
+ .mockResolvedValueOnce([
+ {
+ path: '/workspace/repo',
+ head: 'main',
+ branch: 'refs/heads/main',
+ isBare: false,
+ isMainWorktree: true
+ }
+ ])
+ .mockResolvedValueOnce([
+ {
+ path: '/workspace/repo',
+ head: 'main',
+ branch: 'refs/heads/main',
+ isBare: false,
+ isMainWorktree: true
+ },
+ {
+ path: '/workspace/my-folder',
+ head: 'abc123',
+ branch: 'refs/heads/fix/bug-0',
+ isBare: false,
+ isMainWorktree: false
+ }
+ ])
+
+ const result = await handlers['worktrees:create'](null, {
+ repoId: 'repo-1',
+ name: 'my-folder',
+ baseBranch: 'fix/bug-0',
+ branchNameOverride: 'fix/bug-0'
+ })
+
+ expect(getBranchConflictKindMock).not.toHaveBeenCalled()
+ expect(addWorktreeMock).toHaveBeenCalledWith(
+ '/workspace/repo',
+ '/workspace/my-folder',
+ 'fix/bug-0',
+ 'fix/bug-0',
+ false,
+ false,
+ { checkoutExistingBranch: true }
+ )
+ expect(store.setWorktreeMeta).toHaveBeenCalledWith(
+ 'repo-1::/workspace/my-folder',
+ expect.objectContaining({ preserveBranchOnDelete: true })
+ )
+ expect(result).toMatchObject({
+ worktree: expect.objectContaining({
+ path: '/workspace/my-folder',
+ branch: 'refs/heads/fix/bug-0'
+ })
+ })
+ })
+
it('suffixes only the path when an existing local branch checkout path already exists', async () => {
const mainWorktree = {
path: '/workspace/repo',
diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx
index 2916ca625..899524a11 100644
--- a/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx
+++ b/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx
@@ -132,6 +132,9 @@ function renderCard(
onSmartLinearIssueSelect={() => {}}
smartNameSelection={null}
onClearSmartNameSelection={() => {}}
+ canReuseSelectedBranch={false}
+ reuseSelectedBranch={false}
+ onReuseSelectedBranchChange={() => {}}
forkPushWarning={null}
detectedAgentIds={null}
onOpenAgentSettings={() => {}}
@@ -205,6 +208,64 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
expect(current.container.querySelectorAll('[data-testid="project-combobox"]')).toHaveLength(1)
})
+ it('keeps the reuse-branch row collapsed until a local branch is reusable', () => {
+ // Why: the row stays mounted (for the smooth height transition) but is
+ // collapsed + aria-hidden when reuse isn't possible.
+ current = renderCard({ canReuseSelectedBranch: false })
+ const collapsedReuse = [...current.container.querySelectorAll('[aria-hidden="true"]')].find(
+ (el) => el.textContent?.includes('Reuse branch')
+ )
+ expect(collapsedReuse).toBeTruthy()
+
+ act(() => current?.root.unmount())
+ current?.container.remove()
+
+ current = renderCard({ canReuseSelectedBranch: true, reuseSelectedBranch: true })
+ const reuseLabel = [...current.container.querySelectorAll('label')].find((label) =>
+ label.textContent?.includes('Reuse branch')
+ )
+ expect(reuseLabel).toBeTruthy()
+ // Visible: not inside an aria-hidden (collapsed) wrapper.
+ expect(reuseLabel?.closest('[aria-hidden="true"]')).toBeNull()
+ expect(current.container.textContent).toContain(
+ 'Check out the existing branch instead of creating a new one from it.'
+ )
+ })
+
+ it('emits the toggled value from the reuse checkbox in both directions', () => {
+ const clickReuseCheckbox = (): void => {
+ const reuseLabel = [...(current?.container.querySelectorAll('label') ?? [])].find((label) =>
+ label.textContent?.includes('Reuse branch')
+ )
+ const checkbox = reuseLabel?.querySelector('input[type="checkbox"]')
+ expect(checkbox).toBeTruthy()
+ act(() => checkbox?.click())
+ }
+
+ // Checked -> unchecked (opting out of reuse).
+ const offChanges: boolean[] = []
+ current = renderCard({
+ canReuseSelectedBranch: true,
+ reuseSelectedBranch: true,
+ onReuseSelectedBranchChange: (next) => offChanges.push(next)
+ })
+ clickReuseCheckbox()
+ expect(offChanges).toEqual([false])
+
+ act(() => current?.root.unmount())
+ current?.container.remove()
+
+ // Unchecked -> checked (opting into reuse — the action that pins the branch).
+ const onChanges: boolean[] = []
+ current = renderCard({
+ canReuseSelectedBranch: true,
+ reuseSelectedBranch: false,
+ onReuseSelectedBranchChange: (next) => onChanges.push(next)
+ })
+ clickReuseCheckbox()
+ expect(onChanges).toEqual([true])
+ })
+
it('does not disable folder workspace creation when only source lookup needs SSH', () => {
current = renderCard({
eligibleRepos: [
diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.tsx
index 5867b65d5..f4304dbe9 100644
--- a/src/renderer/src/components/NewWorkspaceComposerCard.tsx
+++ b/src/renderer/src/components/NewWorkspaceComposerCard.tsx
@@ -84,6 +84,10 @@ type NewWorkspaceComposerCardProps = {
onSmartLinearIssueSelect: (issue: LinearIssue) => void
smartNameSelection: SmartWorkspaceNameSelection | null
onClearSmartNameSelection: () => void
+ /** True when an existing local branch is selected and can be reused. */
+ canReuseSelectedBranch: boolean
+ reuseSelectedBranch: boolean
+ onReuseSelectedBranchChange: (next: boolean) => void
smartNameGitHubSourceContext?: TaskSourceContext | null
/** Advisory shown under the name field when a fork PR can't accept maintainer pushes. */
forkPushWarning: string | null
@@ -317,6 +321,9 @@ export default function NewWorkspaceComposerCard({
onSmartLinearIssueSelect,
smartNameSelection,
onClearSmartNameSelection,
+ canReuseSelectedBranch,
+ reuseSelectedBranch,
+ onReuseSelectedBranchChange,
smartNameGitHubSourceContext,
forkPushWarning,
detectedAgentIds,
@@ -658,6 +665,64 @@ export default function NewWorkspaceComposerCard({
{forkPushWarning}
) : null}
+ {/* Why (#5181): sits right under the branch selection (not the Name
+ field, which can differ from the branch) so reusing the picked
+ branch is an explicit, discoverable choice. Stays mounted and
+ collapses via a grid-rows transition (matching the Advanced
+ drawer) so the dialog grows/shrinks smoothly as the option
+ appears. Only offered when reuse is possible — an existing local
+ branch not already checked out in another worktree. */}
+
+
+
+
+
+ {translate(
+ 'auto.components.NewWorkspaceComposerCard.reuseExistingBranchHint',
+ 'Check out the existing branch instead of creating a new one from it.'
+ )}
+
+
+
+
diff --git a/src/renderer/src/hooks/composer-branch-selection.test.ts b/src/renderer/src/hooks/composer-branch-selection.test.ts
index a61c86fae..2d3799245 100644
--- a/src/renderer/src/hooks/composer-branch-selection.test.ts
+++ b/src/renderer/src/hooks/composer-branch-selection.test.ts
@@ -1,7 +1,10 @@
import { describe, expect, it } from 'vitest'
import {
+ isBranchCheckedOutInWorktrees,
resolveComposerBranchNameOverrideForCreate,
- resolveComposerBranchSelection
+ resolveComposerBranchReuse,
+ resolveComposerBranchSelection,
+ resolveComposerReuseOverride
} from './composer-branch-selection'
describe('resolveComposerBranchSelection', () => {
@@ -76,3 +79,112 @@ describe('resolveComposerBranchSelection', () => {
).toBeUndefined()
})
})
+
+describe('isBranchCheckedOutInWorktrees', () => {
+ it('matches a branch against both refs/heads-qualified and short worktree refs', () => {
+ expect(
+ isBranchCheckedOutInWorktrees('feature-x', ['refs/heads/main', 'refs/heads/feature-x'])
+ ).toBe(true)
+ expect(isBranchCheckedOutInWorktrees('feature-x', ['feature-x'])).toBe(true)
+ expect(isBranchCheckedOutInWorktrees('feature-x', ['refs/heads/main', ''])).toBe(false)
+ expect(isBranchCheckedOutInWorktrees('feature-x', [])).toBe(false)
+ })
+})
+
+describe('resolveComposerReuseOverride', () => {
+ it('keeps the selection override for a reusable (non-busy) local branch', () => {
+ expect(
+ resolveComposerReuseOverride({
+ refName: 'feature-x',
+ localBranchName: 'feature-x',
+ branchNameOverride: 'feature-x',
+ branchCheckedOutElsewhere: false
+ })
+ ).toBe('feature-x')
+ })
+
+ it('drops the override for a local branch checked out in another worktree', () => {
+ // Why: pinning a busy branch would collide and produce a suffixed branch.
+ expect(
+ resolveComposerReuseOverride({
+ refName: 'feature-x',
+ localBranchName: 'feature-x',
+ branchNameOverride: 'feature-x',
+ branchCheckedOutElsewhere: true
+ })
+ ).toBeUndefined()
+ })
+
+ it('keeps the override for a remote-only ref even if its local name is busy', () => {
+ // Why: a remote-only ref (ref !== local name) creates a fresh local tracking
+ // branch, so the busy check on the local name must not drop its override.
+ expect(
+ resolveComposerReuseOverride({
+ refName: 'origin/feature-x',
+ localBranchName: 'feature-x',
+ branchNameOverride: 'feature-x',
+ branchCheckedOutElsewhere: true
+ })
+ ).toBe('feature-x')
+ })
+})
+
+describe('resolveComposerBranchReuse', () => {
+ it('marks an existing local branch reusable and defaults reuse ON for an auto-derived name', () => {
+ expect(
+ resolveComposerBranchReuse({
+ refName: 'feature-x',
+ localBranchName: 'feature-x',
+ selectionProducedOverride: true,
+ branchCheckedOutElsewhere: false
+ })
+ ).toEqual({ reuseEligibleBranch: 'feature-x', defaultReuse: true })
+ })
+
+ it('treats a slash-containing local branch as reusable (ref equals local name)', () => {
+ expect(
+ resolveComposerBranchReuse({
+ refName: 'fix/bug-0',
+ localBranchName: 'fix/bug-0',
+ selectionProducedOverride: true,
+ branchCheckedOutElsewhere: false
+ })
+ ).toEqual({ reuseEligibleBranch: 'fix/bug-0', defaultReuse: true })
+ })
+
+ it('does not offer reuse for a remote-only ref (ref carries an origin/ prefix)', () => {
+ expect(
+ resolveComposerBranchReuse({
+ refName: 'origin/feature/something',
+ localBranchName: 'feature/something',
+ selectionProducedOverride: true,
+ branchCheckedOutElsewhere: false
+ })
+ ).toEqual({ reuseEligibleBranch: null, defaultReuse: false })
+ })
+
+ it('does not offer reuse when the branch is already checked out in another worktree', () => {
+ // Why: git refuses a branch in two worktrees, so reuse is impossible here.
+ expect(
+ resolveComposerBranchReuse({
+ refName: 'feature-x',
+ localBranchName: 'feature-x',
+ selectionProducedOverride: true,
+ branchCheckedOutElsewhere: true
+ })
+ ).toEqual({ reuseEligibleBranch: null, defaultReuse: false })
+ })
+
+ it('keeps a local branch reuse-eligible but defaults reuse OFF when the user typed a custom name', () => {
+ // Why: no override means the user is branching off the ref with a custom
+ // worktree name; reuse stays opt-in (checkbox still shown via eligibility).
+ expect(
+ resolveComposerBranchReuse({
+ refName: 'feature-x',
+ localBranchName: 'feature-x',
+ selectionProducedOverride: false,
+ branchCheckedOutElsewhere: false
+ })
+ ).toEqual({ reuseEligibleBranch: 'feature-x', defaultReuse: false })
+ })
+})
diff --git a/src/renderer/src/hooks/composer-branch-selection.ts b/src/renderer/src/hooks/composer-branch-selection.ts
index b88fe16af..e254ac6c9 100644
--- a/src/renderer/src/hooks/composer-branch-selection.ts
+++ b/src/renderer/src/hooks/composer-branch-selection.ts
@@ -1,5 +1,8 @@
export {
+ isBranchCheckedOutInWorktrees,
resolveComposerBranchNameOverrideForCreate,
+ resolveComposerBranchReuse,
resolveComposerBranchSelection,
+ resolveComposerReuseOverride,
type ComposerBranchSelection
} from '../../../shared/composer-branch-selection'
diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts
index ccb3201c7..f74a194d9 100644
--- a/src/renderer/src/hooks/useComposerState.ts
+++ b/src/renderer/src/hooks/useComposerState.ts
@@ -151,8 +151,11 @@ import {
} from '@/lib/workspace-create-error-format'
import type { SshConnectionStatus } from '../../../shared/ssh-types'
import {
+ isBranchCheckedOutInWorktrees,
resolveComposerBranchNameOverrideForCreate,
- resolveComposerBranchSelection
+ resolveComposerBranchReuse,
+ resolveComposerBranchSelection,
+ resolveComposerReuseOverride
} from './composer-branch-selection'
import { translate } from '@/i18n/i18n'
@@ -242,6 +245,13 @@ export type ComposerCardProps = {
) => void
smartNameSelection: SmartWorkspaceNameSelection | null
onClearSmartNameSelection: () => void
+ /** True when the selected source is an existing LOCAL branch that can be
+ * reused (checked out) instead of branched off — gates the reuse checkbox. */
+ canReuseSelectedBranch: boolean
+ /** Whether the selected existing local branch will be reused (checked out)
+ * rather than used as the base for a new branch. */
+ reuseSelectedBranch: boolean
+ onReuseSelectedBranchChange: (next: boolean) => void
agentPrompt: string
onAgentPromptChange: (value: string) => void
/** Rendered issueCommand template to preview inside the empty prompt
@@ -810,6 +820,13 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
const [branchNameOverride, setBranchNameOverride] = useState(undefined)
const [branchNameOverridePreservesNameEdits, setBranchNameOverridePreservesNameEdits] =
useState(false)
+ // Why (#5181): when the user picks an existing LOCAL branch, let them reuse it
+ // (check it out) instead of creating a new branch from it. `reuseEligibleBranch`
+ // is the local branch name eligible for reuse (null = not a reusable local
+ // branch, e.g. a remote-only ref or non-branch source); `reuseSelectedBranch`
+ // is the explicit checkbox value driving whether reuse actually happens.
+ const [reuseEligibleBranch, setReuseEligibleBranch] = useState(null)
+ const [reuseSelectedBranch, setReuseSelectedBranch] = useState(false)
const [pushTarget, setPushTarget] = useState(undefined)
// Why: when a repo switch wipes a prior Start-from selection, surface the
// reset inline (e.g. "was PR #8778") so the change is recoverable visually
@@ -2143,6 +2160,11 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
setCompareBaseRef(undefined)
setPushTarget(undefined)
setBranchNameOverride(undefined)
+ // Why (#5181): reuse state is branch-scoped, so a repo switch must clear
+ // it alongside the branch override (matches the other reset paths).
+ setBranchNameOverridePreservesNameEdits(false)
+ setReuseEligibleBranch(null)
+ setReuseSelectedBranch(false)
setForkPushWarning(null)
setStartFromResetHint(hint)
}
@@ -2214,6 +2236,10 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
setBaseBranch(undefined)
setPushTarget(undefined)
setBranchNameOverride(undefined)
+ // Why (#5181): clear branch-scoped reuse state on a project switch too.
+ setBranchNameOverridePreservesNameEdits(false)
+ setReuseEligibleBranch(null)
+ setReuseSelectedBranch(false)
setForkPushWarning(null)
setStartFromResetHint(null)
return
@@ -2277,6 +2303,12 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
setCompareBaseRef(undefined)
setPushTarget(undefined)
setBranchNameOverride(undefined)
+ // Why (#5181): the Start-from picker means "create a new branch from this
+ // base", so it never offers branch reuse — clear any reuse state left over
+ // from a prior smart-field branch pick.
+ setBranchNameOverridePreservesNameEdits(false)
+ setReuseEligibleBranch(null)
+ setReuseSelectedBranch(false)
setForkPushWarning(null)
branchAutoNameRef.current = ''
setStartFromResetHint(null)
@@ -2531,18 +2563,65 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
setPushTarget(undefined)
setStartFromResetHint(null)
setForkPushWarning(null)
- setBranchNameOverridePreservesNameEdits(false)
+ // Why (#5181): reuse an existing local branch (check it out) instead of
+ // branching off it. Default reuse ON when the worktree name was
+ // auto-derived from the branch, and preserve name edits so reuse survives
+ // renaming the worktree folder. Reuse is impossible when the branch is
+ // already checked out in another worktree (git allows it in only one), so
+ // gate eligibility on that and don't pin the override to a busy branch.
+ // Note: worktreesByRepo only covers visible worktrees; a branch busy only
+ // in a hidden external worktree falls through to the backend conflict
+ // check, which rejects it with a clear "already exists locally" error.
+ const branchCheckedOutElsewhere = isBranchCheckedOutInWorktrees(
+ localBranchName,
+ (worktreesByRepo[repoId] ?? []).map((worktree) => worktree.branch)
+ )
+ const { reuseEligibleBranch: nextReuseEligibleBranch, defaultReuse } =
+ resolveComposerBranchReuse({
+ refName,
+ localBranchName,
+ selectionProducedOverride: selection.branchNameOverride !== undefined,
+ branchCheckedOutElsewhere
+ })
+ setReuseEligibleBranch(nextReuseEligibleBranch)
+ setReuseSelectedBranch(defaultReuse)
+ setBranchNameOverridePreservesNameEdits(defaultReuse)
+ const effectiveOverride = resolveComposerReuseOverride({
+ refName,
+ localBranchName,
+ branchNameOverride: selection.branchNameOverride,
+ branchCheckedOutElsewhere
+ })
if (selection.name !== undefined && selection.lastAutoName !== undefined) {
setName(selection.name)
lastAutoNameRef.current = selection.lastAutoName
- branchAutoNameRef.current = selection.branchAutoName
- setBranchNameOverride(selection.branchNameOverride)
+ branchAutoNameRef.current = effectiveOverride ? selection.branchAutoName : ''
+ setBranchNameOverride(effectiveOverride)
} else {
- setBranchNameOverride(selection.branchNameOverride)
- branchAutoNameRef.current = selection.branchAutoName
+ setBranchNameOverride(effectiveOverride)
+ branchAutoNameRef.current = effectiveOverride ? selection.branchAutoName : ''
}
},
- [name]
+ [name, worktreesByRepo, repoId]
+ )
+
+ const handleReuseSelectedBranchChange = useCallback(
+ (next: boolean): void => {
+ if (!reuseEligibleBranch) {
+ return
+ }
+ setReuseSelectedBranch(next)
+ // Why (#5181): reuse pins the exact existing branch as the override and
+ // preserves it across worktree-name edits, so the folder can be named
+ // independently while the branch is checked out. Opting out drops the
+ // override so a fresh branch is created from the selected ref as base.
+ setBranchNameOverridePreservesNameEdits(next)
+ setBranchNameOverride(next ? reuseEligibleBranch : undefined)
+ if (next) {
+ branchAutoNameRef.current = reuseEligibleBranch
+ }
+ },
+ [reuseEligibleBranch]
)
const handleSmartLinearIssueSelect = useCallback(
@@ -2604,6 +2683,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
setCompareBaseRef(undefined)
setPushTarget(undefined)
setBranchNameOverride(undefined)
+ setBranchNameOverridePreservesNameEdits(false)
+ setReuseEligibleBranch(null)
+ setReuseSelectedBranch(false)
setForkPushWarning(null)
branchAutoNameRef.current = ''
setStartFromResetHint(null)
@@ -3528,6 +3610,12 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
smartNameGitHubSourceContext: selectedRepoGitHubSourceContext,
smartNameSelection,
onClearSmartNameSelection: handleClearSmartNameSelection,
+ canReuseSelectedBranch:
+ !isProjectGroupTarget &&
+ reuseEligibleBranch !== null &&
+ smartNameSelection?.kind === 'branch',
+ reuseSelectedBranch,
+ onReuseSelectedBranchChange: handleReuseSelectedBranchChange,
agentPrompt,
onAgentPromptChange: setAgentPrompt,
linkedOnlyTemplatePreview: shouldApplyLinkedOnlyTemplate ? linkedOnlyTemplatePrompt : null,
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index c8f66aade..25c6577d3 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -1024,6 +1024,8 @@
"d71cd3003e": "+ Assignee"
},
"NewWorkspaceComposerCard": {
+ "reuseExistingBranch": "Reuse branch",
+ "reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it.",
"cbb47ee0dc": "Only available for local Git projects.",
"d861de981b": "Sparse checkout",
"090cfedeb4": "Write a note",
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index c3a59eff6..0866938eb 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -1076,7 +1076,9 @@
"setupKindFolder": "Folder",
"setupHostExistingFolderHelp": "Link a checkout that already exists there, then create this workspace on that host.",
"importingHostSetup": "Importing...",
- "importHostSetup": "Import"
+ "importHostSetup": "Import",
+ "reuseExistingBranch": "Reuse branch",
+ "reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it."
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "Elija el proyecto, el nombre del espacio de trabajo y el agente antes de crear el espacio de trabajo."
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 7b59e624a..50f47f047 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -1076,7 +1076,9 @@
"setupKindFolder": "Folder",
"setupHostExistingFolderHelp": "Link a checkout that already exists there, then create this workspace on that host.",
"importingHostSetup": "Importing...",
- "importHostSetup": "インポート"
+ "importHostSetup": "インポート",
+ "reuseExistingBranch": "Reuse branch",
+ "reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it."
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "ワークスペースを作成する前に、プロジェクト、ワークスペース名、および agent を選択します。"
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index 147e503d8..919609547 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -1076,7 +1076,9 @@
"setupKindFolder": "Folder",
"setupHostExistingFolderHelp": "Link a checkout that already exists there, then create this workspace on that host.",
"importingHostSetup": "Importing...",
- "importHostSetup": "가져오기"
+ "importHostSetup": "가져오기",
+ "reuseExistingBranch": "Reuse branch",
+ "reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it."
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "워크스페이스를 생성하기 전에 프로젝트, 워크스페이스 이름, agent를 선택하세요."
@@ -4376,10 +4378,10 @@
"5f5142a62a": "이전 아이콘"
},
"AppearancePane": {
+ "3057983501": "할당되지 않음",
"872af9556e": "시스템 트레이",
"2edf606c46": "닫을 때 트레이로 최소화",
"b707773a0d": "활성화하면 창을 닫아도 Orca가 종료되지 않고 시스템 트레이에서 계속 실행됩니다.",
- "3057983501": "할당되지 않음",
"0cd9b8228f": "Dock 및 창 전환기에 표시된 앱 아이콘을 선택합니다.",
"ca1590d42f": "앱 아이콘",
"61d842eca0": "사이드바에 Orca Mobile 바로가기를 표시합니다. 도구 상자에서는 계속 사용할 수 있습니다.",
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 43edbc648..1b1569758 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -1076,7 +1076,9 @@
"setupKindFolder": "文件夹",
"setupHostExistingFolderHelp": "链接已存在的检出目录,然后在该主机上创建此工作区。",
"importingHostSetup": "导入中...",
- "importHostSetup": "导入"
+ "importHostSetup": "导入",
+ "reuseExistingBranch": "Reuse branch",
+ "reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it."
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "创建工作区之前选择项目、工作区名称和 Agent。"
diff --git a/src/shared/composer-branch-selection.ts b/src/shared/composer-branch-selection.ts
index a868fd66f..4b8a0dca9 100644
--- a/src/shared/composer-branch-selection.ts
+++ b/src/shared/composer-branch-selection.ts
@@ -36,6 +36,67 @@ export function resolveComposerBranchSelection(args: {
}
}
+/**
+ * True when `branchName` is already checked out in one of the given worktree
+ * branch refs (which may be `refs/heads/foo` or short `foo`). Git refuses to
+ * check out a branch in two worktrees, so such a branch cannot be reused.
+ */
+export function isBranchCheckedOutInWorktrees(
+ branchName: string,
+ worktreeBranches: readonly string[]
+): boolean {
+ return worktreeBranches.some((ref) => ref.replace(/^refs\/heads\//, '') === branchName)
+}
+
+/**
+ * Issue #5181: decide whether a picked branch row is an existing LOCAL branch
+ * that can be reused (checked out) instead of branched off, and whether reuse
+ * should default ON.
+ *
+ * Reuse is only possible for a LOCAL branch (ref === local name; remote-only
+ * refs carry an `origin/`-style prefix) that is NOT already checked out in
+ * another worktree — git allows a branch in only one worktree at a time. Reuse
+ * defaults ON only when the worktree name was auto-derived from the branch (the
+ * selection produced a branch-name override); a user who typed a custom
+ * worktree name first is branching off the ref, so reuse stays OFF unless they
+ * opt in.
+ */
+export function resolveComposerBranchReuse(args: {
+ refName: string
+ localBranchName: string
+ selectionProducedOverride: boolean
+ branchCheckedOutElsewhere: boolean
+}): { reuseEligibleBranch: string | null; defaultReuse: boolean } {
+ const reuseEligibleBranch =
+ args.refName === args.localBranchName && !args.branchCheckedOutElsewhere
+ ? args.localBranchName
+ : null
+ return {
+ reuseEligibleBranch,
+ defaultReuse: reuseEligibleBranch !== null && args.selectionProducedOverride
+ }
+}
+
+/**
+ * Issue #5181: the branch-name override to apply for a picked branch. A local
+ * branch already checked out in another worktree can't be reused, so it must
+ * NOT be pinned as the override — pinning it would collide and silently produce
+ * a suffixed branch. In that case fall back to letting the worktree name derive
+ * a fresh branch from the selected ref as base; otherwise use the selection's
+ * override unchanged.
+ */
+export function resolveComposerReuseOverride(args: {
+ refName: string
+ localBranchName: string
+ branchNameOverride: string | undefined
+ branchCheckedOutElsewhere: boolean
+}): string | undefined {
+ if (args.branchCheckedOutElsewhere && args.refName === args.localBranchName) {
+ return undefined
+ }
+ return args.branchNameOverride
+}
+
export function resolveComposerBranchNameOverrideForCreate(args: {
branchNameOverride: string | undefined
branchAutoName: string