feat(worktree): add a "create multiple" toggle to the worktree composer

Add a checkbox to the New Workspace Composer ("Create worktree") modal.
When enabled, creating a worktree keeps the modal open and clears the
identity fields (name, prompt, note, attachments, linked GitHub/GitLab
issues and PRs, branch override, and the Start-from-derived compare base
and push target) while retaining context (repository, base branch, agent,
project group). This lets users spin up several worktrees from the same
repo in a row without reopening the dialog.

The toggle defaults off and, because the modal unmounts on close, always
reopens unchecked. It is shown only for git (worktree) targets;
folder-workspace creation keeps its create-and-close behavior.
This commit is contained in:
AJ Meireles
2026-06-19 15:23:21 -03:00
parent ae77156ab2
commit d151ac9feb
7 changed files with 111 additions and 7 deletions
@@ -88,6 +88,10 @@ type NewWorkspaceComposerCardProps = {
canReuseSelectedBranch: boolean
reuseSelectedBranch: boolean
onReuseSelectedBranchChange: (next: boolean) => void
/** Shows the footer "create another after this" toggle — worktree targets only. */
showCreateMultiple?: boolean
createMultiple?: boolean
onCreateMultipleChange?: (next: boolean) => void
smartNameGitHubSourceContext?: TaskSourceContext | null
/** Advisory shown under the name field when a fork PR can't accept maintainer pushes. */
forkPushWarning: string | null
@@ -324,6 +328,9 @@ export default function NewWorkspaceComposerCard({
canReuseSelectedBranch,
reuseSelectedBranch,
onReuseSelectedBranchChange,
showCreateMultiple = false,
createMultiple = false,
onCreateMultipleChange,
smartNameGitHubSourceContext,
forkPushWarning,
detectedAgentIds,
@@ -1002,7 +1009,43 @@ export default function NewWorkspaceComposerCard({
</div>
) : null}
<div className="flex justify-end">
<div
className={cn(
'flex items-center gap-3',
showCreateMultiple ? 'justify-between' : 'justify-end'
)}
>
{showCreateMultiple ? (
<label className="group flex w-fit cursor-pointer items-center gap-2 text-xs text-foreground">
<span
className={cn(
'flex size-4 items-center justify-center rounded-[3px] border shadow-sm transition',
createMultiple
? 'border-emerald-500/60 bg-emerald-500 text-white'
: 'border-foreground/20 bg-background dark:border-white/20 dark:bg-muted/10'
)}
>
<Check
className={cn(
'size-3 transition-opacity',
createMultiple ? 'opacity-100' : 'opacity-0'
)}
/>
</span>
<input
type="checkbox"
checked={createMultiple}
onChange={(event) => onCreateMultipleChange?.(event.target.checked)}
className="sr-only"
/>
<span>
{translate(
'auto.components.NewWorkspaceComposerCard.createMultiple',
'Create another after this'
)}
</span>
</label>
) : null}
<Button
onClick={() => void onCreate()}
disabled={createDisabled}
+58 -2
View File
@@ -252,6 +252,13 @@ export type ComposerCardProps = {
* rather than used as the base for a new branch. */
reuseSelectedBranch: boolean
onReuseSelectedBranchChange: (next: boolean) => void
/** Whether the "create multiple" toggle is shown — worktree (git) targets
* only; folder-workspace targets create-and-close as before. */
showCreateMultiple: boolean
/** When on, the modal stays open after each create and resets identity fields
* so the user can create several worktrees in a row. */
createMultiple: boolean
onCreateMultipleChange: (next: boolean) => void
agentPrompt: string
onAgentPromptChange: (value: string) => void
/** Rendered issueCommand template to preview inside the empty prompt
@@ -890,6 +897,11 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
const [setupDecision, setSetupDecision] = useState<'run' | 'skip' | null>(null)
const [creating, setCreating] = useState(false)
const [createError, setCreateError] = useState<WorkspaceCreateErrorDisplay | null>(null)
// Why: when checked, a successful worktree create keeps the modal open and
// resets identity fields so the user can queue another worktree without
// reopening. Defaults off; the modal unmounts on close, so reopening always
// starts unchecked.
const [createMultiple, setCreateMultiple] = useState(false)
const [advancedOpen, setAdvancedOpen] = useState(
persistDraft ? Boolean((newWorkspaceDraft?.note ?? '').trim()) : false
)
@@ -3205,6 +3217,37 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
submitFolderTarget
])
const resetForNextCreate = useCallback(() => {
// Why: with "create multiple" on, clear identity fields after each create so
// the next worktree starts clean. Context (repo, base branch, agent, project
// group) is intentionally retained for fast sequential creation. The
// PR-pick-derived refs (compare base, push target, branch override) are
// identity, not durable context, so they reset too — leaving them while the
// linked item is cleared would carry a half-set Start-from state (e.g. a
// silent fork push target) into the next worktree.
setName('')
lastAutoNameRef.current = ''
setAgentPrompt('')
setNote('')
setAttachmentPaths([])
setLinkedWorkItem(null)
setLinkedIssue('')
setLinkedPR(null)
setLinkedGitLabIssue(null)
setLinkedGitLabMR(null)
setBranchNameOverride(undefined)
setBranchNameOverridePreservesNameEdits(false)
setCompareBaseRef(undefined)
setPushTarget(undefined)
setReuseSelectedBranch(false)
setStartFromResetHint(null)
setForkPushWarning(null)
setCreateError(null)
// Refocus the name field on the next frame (after the reset re-render) so the
// user can immediately type the next worktree name.
requestAnimationFrame(() => nameInputRef.current?.focus())
}, [])
const submitQuick = useCallback(
async (requestedAgent: TuiAgent | null): Promise<void> => {
if (isProjectGroupTarget) {
@@ -3507,8 +3550,14 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
if (persistDraft) {
clearNewWorkspaceDraft()
}
onCreated?.()
runBackgroundWorktreeCreation(request)
if (createMultiple) {
// Why: keep the modal open and reset identity so the user can queue
// another worktree right away; the creation above runs in the background.
resetForNextCreate()
} else {
onCreated?.()
}
} catch (error) {
const formattedError = formatWorkspaceCreateError(error)
setCreateError(formattedError)
@@ -3566,7 +3615,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
setupConfig,
setupPolicy,
isProjectGroupTarget,
submitFolderTarget
submitFolderTarget,
createMultiple,
resetForNextCreate
]
)
@@ -3616,6 +3667,11 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
smartNameSelection?.kind === 'branch',
reuseSelectedBranch,
onReuseSelectedBranchChange: handleReuseSelectedBranchChange,
// Why: the "create multiple" toggle only applies to worktree (git) targets;
// folder-workspace targets keep the create-and-close behavior.
showCreateMultiple: !isProjectGroupTarget,
createMultiple,
onCreateMultipleChange: setCreateMultiple,
agentPrompt,
onAgentPromptChange: setAgentPrompt,
linkedOnlyTemplatePreview: shouldApplyLinkedOnlyTemplate ? linkedOnlyTemplatePrompt : null,
+1
View File
@@ -1026,6 +1026,7 @@
"NewWorkspaceComposerCard": {
"reuseExistingBranch": "Reuse branch",
"reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it.",
"createMultiple": "Create another after this",
"cbb47ee0dc": "Only available for local Git projects.",
"d861de981b": "Sparse checkout",
"090cfedeb4": "Write a note",
+2 -1
View File
@@ -1078,7 +1078,8 @@
"importingHostSetup": "Importing...",
"importHostSetup": "Import",
"reuseExistingBranch": "Reuse branch",
"reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it."
"reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it.",
"createMultiple": "Create another after this"
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "Elija el proyecto, el nombre del espacio de trabajo y el agente antes de crear el espacio de trabajo."
+2 -1
View File
@@ -1078,7 +1078,8 @@
"importingHostSetup": "Importing...",
"importHostSetup": "インポート",
"reuseExistingBranch": "Reuse branch",
"reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it."
"reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it.",
"createMultiple": "Create another after this"
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "ワークスペースを作成する前に、プロジェクト、ワークスペース名、および agent を選択します。"
+2 -1
View File
@@ -1078,7 +1078,8 @@
"importingHostSetup": "Importing...",
"importHostSetup": "가져오기",
"reuseExistingBranch": "Reuse branch",
"reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it."
"reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it.",
"createMultiple": "Create another after this"
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "워크스페이스를 생성하기 전에 프로젝트, 워크스페이스 이름, agent를 선택하세요."
+2 -1
View File
@@ -1078,7 +1078,8 @@
"importingHostSetup": "导入中...",
"importHostSetup": "导入",
"reuseExistingBranch": "Reuse branch",
"reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it."
"reuseExistingBranchHint": "Check out the existing branch instead of creating a new one from it.",
"createMultiple": "Create another after this"
},
"NewWorkspaceComposerModal": {
"fa90f739a5": "创建工作区之前选择项目、工作区名称和 Agent。"