feat(worktree): add start point option for new branch creation

Allow selecting a base branch when creating new worktree branches
Add dropdown to choose start point from existing branches
Extend worktree creation flow to support custom branch origins
This commit is contained in:
Bohdan Triapitsyn
2026-01-01 13:39:55 +02:00
parent 39b9f985ce
commit 5a71e050a5
8 changed files with 326 additions and 48 deletions
@@ -1366,6 +1366,7 @@ pub async fn add_git_worktree(
path_str: String, path_str: String,
branch: String, branch: String,
create_branch: Option<bool>, create_branch: Option<bool>,
start_point: Option<String>,
state: State<'_, DesktopRuntime>, state: State<'_, DesktopRuntime>,
) -> Result<(), String> { ) -> Result<(), String> {
let root = validate_git_path(&directory, state.settings()) let root = validate_git_path(&directory, state.settings())
@@ -1381,6 +1382,11 @@ pub async fn add_git_worktree(
if !create_branch.unwrap_or(false) { if !create_branch.unwrap_or(false) {
args.push(&branch); args.push(&branch);
} else if let Some(start_point) = start_point.as_deref() {
let start_point = start_point.trim();
if !start_point.is_empty() {
args.push(start_point);
}
} }
run_git(&args, &root).await.map_err(|e| e.to_string())?; run_git(&args, &root).await.map_err(|e| e.to_string())?;
+2 -1
View File
@@ -116,7 +116,8 @@ export const createDesktopGitAPI = (): GitAPI => ({
directory, directory,
pathStr: payload.path, pathStr: payload.path,
branch: payload.branch, branch: payload.branch,
createBranch: payload.createBranch createBranch: payload.createBranch,
startPoint: payload.startPoint,
}); });
return { success: true, path: payload.path, branch: payload.branch }; return { success: true, path: payload.path, branch: payload.branch };
}, },
@@ -1,7 +1,18 @@
import React from 'react'; import React from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { AnimatedTabs } from '@/components/ui/animated-tabs';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -23,7 +34,7 @@ import {
mapWorktreeToMetadata, mapWorktreeToMetadata,
removeWorktree, removeWorktree,
} from '@/lib/git/worktreeService'; } from '@/lib/git/worktreeService';
import { checkIsGitRepository, ensureOpenChamberIgnored } from '@/lib/gitApi'; import { checkIsGitRepository, ensureOpenChamberIgnored, getGitBranches } from '@/lib/gitApi';
import { useSessionStore } from '@/stores/useSessionStore'; import { useSessionStore } from '@/stores/useSessionStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useConfigStore } from '@/stores/useConfigStore'; import { useConfigStore } from '@/stores/useConfigStore';
@@ -78,6 +89,14 @@ const joinWorktreePath = (projectDirectory: string, slug: string): string => {
return cleanSlug ? `${base}/${cleanSlug}` : base; return cleanSlug ? `${base}/${cleanSlug}` : base;
}; };
type WorktreeBaseOption = {
value: string;
label: string;
group: 'special' | 'local' | 'remote';
};
type WorktreeCreateMode = 'new' | 'existing';
type DeleteDialogState = { type DeleteDialogState = {
sessions: Session[]; sessions: Session[];
dateLabel?: string; dateLabel?: string;
@@ -88,7 +107,14 @@ type DeleteDialogState = {
export const SessionDialogs: React.FC = () => { export const SessionDialogs: React.FC = () => {
const [isDirectoryDialogOpen, setIsDirectoryDialogOpen] = React.useState(false); const [isDirectoryDialogOpen, setIsDirectoryDialogOpen] = React.useState(false);
const [hasShownInitialDirectoryPrompt, setHasShownInitialDirectoryPrompt] = React.useState(false); const [hasShownInitialDirectoryPrompt, setHasShownInitialDirectoryPrompt] = React.useState(false);
const [worktreeCreateMode, setWorktreeCreateMode] = React.useState<WorktreeCreateMode>('new');
const [branchName, setBranchName] = React.useState<string>(''); const [branchName, setBranchName] = React.useState<string>('');
const [existingWorktreeBranch, setExistingWorktreeBranch] = React.useState<string>('');
const [worktreeBaseBranch, setWorktreeBaseBranch] = React.useState<string>('HEAD');
const [availableWorktreeBaseBranches, setAvailableWorktreeBaseBranches] = React.useState<WorktreeBaseOption[]>([
{ value: 'HEAD', label: 'Current (HEAD)', group: 'special' },
]);
const [isLoadingWorktreeBaseBranches, setIsLoadingWorktreeBaseBranches] = React.useState(false);
const [availableWorktrees, setAvailableWorktrees] = React.useState<WorktreeMetadata[]>([]); const [availableWorktrees, setAvailableWorktrees] = React.useState<WorktreeMetadata[]>([]);
const [isLoadingWorktrees, setIsLoadingWorktrees] = React.useState(false); const [isLoadingWorktrees, setIsLoadingWorktrees] = React.useState(false);
const [worktreeError, setWorktreeError] = React.useState<string | null>(null); const [worktreeError, setWorktreeError] = React.useState<string | null>(null);
@@ -120,8 +146,12 @@ export const SessionDialogs: React.FC = () => {
const useMobileOverlay = isMobile || isTablet || hasTouchInput; const useMobileOverlay = isMobile || isTablet || hasTouchInput;
const projectDirectory = React.useMemo(() => normalizeProjectDirectory(currentDirectory), [currentDirectory]); const projectDirectory = React.useMemo(() => normalizeProjectDirectory(currentDirectory), [currentDirectory]);
const sanitizedBranchName = React.useMemo(() => sanitizeBranchNameInput(branchName), [branchName]); const sanitizedNewBranchName = React.useMemo(() => sanitizeBranchNameInput(branchName), [branchName]);
const sanitizedWorktreeSlug = React.useMemo(() => sanitizeWorktreeSlug(sanitizedBranchName), [sanitizedBranchName]); const worktreeTargetBranch = React.useMemo(
() => (worktreeCreateMode === 'existing' ? existingWorktreeBranch.trim() : sanitizedNewBranchName),
[existingWorktreeBranch, sanitizedNewBranchName, worktreeCreateMode],
);
const sanitizedWorktreeSlug = React.useMemo(() => sanitizeWorktreeSlug(worktreeTargetBranch), [worktreeTargetBranch]);
const worktreePreviewPath = React.useMemo(() => { const worktreePreviewPath = React.useMemo(() => {
if (!projectDirectory || !sanitizedWorktreeSlug) { if (!projectDirectory || !sanitizedWorktreeSlug) {
return ''; return '';
@@ -129,6 +159,16 @@ export const SessionDialogs: React.FC = () => {
return joinWorktreePath(projectDirectory, sanitizedWorktreeSlug); return joinWorktreePath(projectDirectory, sanitizedWorktreeSlug);
}, [projectDirectory, sanitizedWorktreeSlug]); }, [projectDirectory, sanitizedWorktreeSlug]);
const isGitRepo = isGitRepository === true; const isGitRepo = isGitRepository === true;
const selectedWorktreeBaseLabel = React.useMemo(() => {
const match = availableWorktreeBaseBranches.find((option) => option.value === worktreeBaseBranch);
if (match) {
return match.label;
}
if (worktreeBaseBranch === 'HEAD') {
return 'Current (HEAD)';
}
return worktreeBaseBranch;
}, [availableWorktreeBaseBranches, worktreeBaseBranch]);
const hasDirtyWorktrees = React.useMemo( const hasDirtyWorktrees = React.useMemo(
() => () =>
(deleteDialog?.worktree?.status?.isDirty ?? false) || (deleteDialog?.worktree?.status?.isDirty ?? false) ||
@@ -181,7 +221,12 @@ export const SessionDialogs: React.FC = () => {
React.useEffect(() => { React.useEffect(() => {
if (!isSessionCreateDialogOpen) { if (!isSessionCreateDialogOpen) {
setWorktreeCreateMode('new');
setBranchName(''); setBranchName('');
setExistingWorktreeBranch('');
setWorktreeBaseBranch('HEAD');
setAvailableWorktreeBaseBranches([{ value: 'HEAD', label: 'Current (HEAD)', group: 'special' }]);
setIsLoadingWorktreeBaseBranches(false);
setAvailableWorktrees([]); setAvailableWorktrees([]);
setWorktreeError(null); setWorktreeError(null);
setIsLoadingWorktrees(false); setIsLoadingWorktrees(false);
@@ -192,6 +237,10 @@ export const SessionDialogs: React.FC = () => {
} }
if (!projectDirectory) { if (!projectDirectory) {
setWorktreeCreateMode('new');
setExistingWorktreeBranch('');
setAvailableWorktreeBaseBranches([{ value: 'HEAD', label: 'Current (HEAD)', group: 'special' }]);
setWorktreeBaseBranch('HEAD');
setAvailableWorktrees([]); setAvailableWorktrees([]);
setIsGitRepository(null); setIsGitRepository(null);
setIsCheckingGitRepository(false); setIsCheckingGitRepository(false);
@@ -200,6 +249,7 @@ export const SessionDialogs: React.FC = () => {
let cancelled = false; let cancelled = false;
setIsLoadingWorktrees(true); setIsLoadingWorktrees(true);
setIsLoadingWorktreeBaseBranches(true);
setIsCheckingGitRepository(true); setIsCheckingGitRepository(true);
setWorktreeError(null); setWorktreeError(null);
@@ -212,13 +262,60 @@ export const SessionDialogs: React.FC = () => {
setIsGitRepository(repoStatus); setIsGitRepository(repoStatus);
if (!repoStatus) { if (!repoStatus) {
setWorktreeCreateMode('new');
setExistingWorktreeBranch('');
setAvailableWorktreeBaseBranches([{ value: 'HEAD', label: 'Current (HEAD)', group: 'special' }]);
setWorktreeBaseBranch('HEAD');
setAvailableWorktrees([]); setAvailableWorktrees([]);
setWorktreeError(null); setWorktreeError(null);
} else { } else {
const worktrees = await listGitWorktrees(projectDirectory); const [worktrees, branches] = await Promise.all([
listGitWorktrees(projectDirectory),
getGitBranches(projectDirectory).catch(() => null),
]);
if (cancelled) { if (cancelled) {
return; return;
} }
const worktreeBaseOptions: WorktreeBaseOption[] = [];
const headLabel = branches?.current
? `Current (HEAD: ${branches.current})`
: 'Current (HEAD)';
worktreeBaseOptions.push({ value: 'HEAD', label: headLabel, group: 'special' });
if (branches) {
const localBranches = branches.all
.filter((name) => !name.startsWith('remotes/'))
.sort((a, b) => a.localeCompare(b));
const defaultExistingBranch = branches.current && !branches.current.startsWith('remotes/')
? branches.current
: (localBranches[0] ?? '');
setExistingWorktreeBranch((previous) => {
const trimmed = previous.trim();
if (trimmed && localBranches.includes(trimmed)) {
return trimmed;
}
return defaultExistingBranch;
});
localBranches.forEach((name) => {
worktreeBaseOptions.push({ value: name, label: name, group: 'local' });
});
const remoteBranches = branches.all
.filter((name) => name.startsWith('remotes/'))
.map((name) => name.replace(/^remotes\//, ''))
.sort((a, b) => a.localeCompare(b));
remoteBranches.forEach((name) => {
worktreeBaseOptions.push({ value: name, label: name, group: 'remote' });
});
}
setAvailableWorktreeBaseBranches(worktreeBaseOptions);
setWorktreeBaseBranch((previous) =>
worktreeBaseOptions.some((option) => option.value === previous) ? previous : 'HEAD'
);
const mapped = worktrees.map((info) => mapWorktreeToMetadata(projectDirectory, info)); const mapped = worktrees.map((info) => mapWorktreeToMetadata(projectDirectory, info));
const worktreeRoot = joinWorktreePath(projectDirectory, ''); const worktreeRoot = joinWorktreePath(projectDirectory, '');
const worktreePrefix = `${worktreeRoot}/`; const worktreePrefix = `${worktreeRoot}/`;
@@ -234,6 +331,7 @@ export const SessionDialogs: React.FC = () => {
} finally { } finally {
if (!cancelled) { if (!cancelled) {
setIsLoadingWorktrees(false); setIsLoadingWorktrees(false);
setIsLoadingWorktreeBaseBranches(false);
setIsCheckingGitRepository(false); setIsCheckingGitRepository(false);
} }
} }
@@ -274,7 +372,11 @@ export const SessionDialogs: React.FC = () => {
React.useEffect(() => { React.useEffect(() => {
return sessionEvents.onCreateRequest(() => { return sessionEvents.onCreateRequest(() => {
setWorktreeCreateMode('new');
setBranchName(''); setBranchName('');
setExistingWorktreeBranch('');
setWorktreeBaseBranch('HEAD');
setWorktreeError(null);
setSessionCreateDialogOpen(true); setSessionCreateDialogOpen(true);
}); });
}, [setSessionCreateDialogOpen]); }, [setSessionCreateDialogOpen]);
@@ -378,10 +480,13 @@ export const SessionDialogs: React.FC = () => {
return false; return false;
} }
const normalizedBranch = sanitizedBranchName; const normalizedBranch = worktreeTargetBranch;
const slugValue = sanitizedWorktreeSlug; const slugValue = sanitizedWorktreeSlug;
if (!normalizedBranch) { if (!normalizedBranch) {
const message = 'Provide a branch name for the new worktree.'; const message =
worktreeCreateMode === 'existing'
? 'Select an existing branch for the new worktree.'
: 'Provide a branch name for the new worktree.';
setWorktreeError(message); setWorktreeError(message);
toast.error(message); toast.error(message);
return false; return false;
@@ -402,7 +507,7 @@ export const SessionDialogs: React.FC = () => {
setWorktreeError(null); setWorktreeError(null);
return true; return true;
}, [projectDirectory, sanitizedBranchName, sanitizedWorktreeSlug, availableWorktrees]); }, [projectDirectory, worktreeCreateMode, worktreeTargetBranch, sanitizedWorktreeSlug, availableWorktrees]);
const handleCreateWorktree = async () => { const handleCreateWorktree = async () => {
if (isCreatingWorktree || isLoading) { if (isCreatingWorktree || isLoading) {
@@ -419,13 +524,18 @@ export const SessionDialogs: React.FC = () => {
let cleanupMetadata: WorktreeMetadata | null = null; let cleanupMetadata: WorktreeMetadata | null = null;
try { try {
const normalizedBranch = sanitizedBranchName; const normalizedBranch = worktreeTargetBranch;
const slugValue = sanitizedWorktreeSlug; const slugValue = sanitizedWorktreeSlug;
const shouldCreateBranch = worktreeCreateMode === 'new';
const startPoint = shouldCreateBranch && worktreeBaseBranch && worktreeBaseBranch !== 'HEAD'
? worktreeBaseBranch
: undefined;
const metadata = await createWorktree({ const metadata = await createWorktree({
projectDirectory, projectDirectory,
worktreeSlug: slugValue, worktreeSlug: slugValue,
branch: normalizedBranch, branch: normalizedBranch,
createBranch: true, createBranch: shouldCreateBranch,
startPoint,
}); });
cleanupMetadata = metadata; cleanupMetadata = metadata;
const status = await getWorktreeStatus(metadata.path).catch(() => undefined); const status = await getWorktreeStatus(metadata.path).catch(() => undefined);
@@ -446,6 +556,7 @@ export const SessionDialogs: React.FC = () => {
await refreshWorktrees(); await refreshWorktrees();
setBranchName(''); setBranchName('');
setExistingWorktreeBranch('');
toast.success('Worktree created'); toast.success('Worktree created');
} catch (error) { } catch (error) {
if (cleanupMetadata) { if (cleanupMetadata) {
@@ -556,41 +667,195 @@ export const SessionDialogs: React.FC = () => {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="typography-meta font-medium text-foreground" htmlFor="worktree-branch-input"> <AnimatedTabs
Branch name tabs={[
</label> { value: 'new', label: 'New branch' },
<div className="flex gap-2"> { value: 'existing', label: 'Existing branch' },
<Input ]}
id="worktree-branch-input" value={worktreeCreateMode}
value={branchName} onValueChange={(value) => {
onChange={(e) => handleBranchInputChange(e.target.value)} setWorktreeCreateMode(value);
placeholder="feature/new-branch" setWorktreeError(null);
className="h-8 flex-1 typography-meta text-foreground placeholder:text-muted-foreground/70"
disabled={!isGitRepo || isCheckingGitRepository} if (value === 'existing' && !existingWorktreeBranch) {
onKeyDown={(e) => { const firstLocal = availableWorktreeBaseBranches.find((option) => option.group === 'local')?.value ?? '';
if (e.key === 'Enter' && !isCreatingWorktree) { if (firstLocal) {
handleCreateWorktree(); setExistingWorktreeBranch(firstLocal);
} }
}} }
/> }}
<Button animate={false}
onClick={handleCreateWorktree} />
disabled={isCreatingWorktree || isLoading || !isGitRepo || !sanitizedBranchName}
className="h-8" {worktreeCreateMode === 'new' ? (
> <>
{isCreatingWorktree ? 'Creating…' : 'Create'} <label className="typography-meta font-medium text-foreground" htmlFor="worktree-base-branch-select">
</Button> Base branch
</div> </label>
{sanitizedBranchName && ( <Select
<p className="typography-micro text-muted-foreground/70"> value={worktreeBaseBranch}
Creates branch{' '} onValueChange={setWorktreeBaseBranch}
<code className="font-mono text-xs text-muted-foreground">{sanitizedBranchName}</code> disabled={!isGitRepo || isCheckingGitRepository || isLoadingWorktreeBaseBranches}
{' '}at{' '} >
<code className="font-mono text-xs text-muted-foreground break-all"> <SelectTrigger
{formatPathForDisplay(worktreePreviewPath, homeDirectory)} id="worktree-base-branch-select"
</code> size="lg"
</p> className="w-full typography-meta text-foreground"
>
<SelectValue placeholder={isLoadingWorktreeBaseBranches ? 'Loading branches…' : 'Select a branch'} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Default</SelectLabel>
{availableWorktreeBaseBranches
.filter((option) => option.group === 'special')
.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
{availableWorktreeBaseBranches.some((option) => option.group === 'local') && (
<>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Local branches</SelectLabel>
{availableWorktreeBaseBranches
.filter((option) => option.group === 'local')
.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</>
)}
{availableWorktreeBaseBranches.some((option) => option.group === 'remote') && (
<>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Remote branches</SelectLabel>
{availableWorktreeBaseBranches
.filter((option) => option.group === 'remote')
.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</>
)}
</SelectContent>
</Select>
<label className="typography-meta font-medium text-foreground" htmlFor="worktree-branch-input">
New branch name
</label>
<div className="flex gap-2">
<Input
id="worktree-branch-input"
value={branchName}
onChange={(e) => handleBranchInputChange(e.target.value)}
placeholder="feature/new-branch"
className="h-8 flex-1 typography-meta text-foreground placeholder:text-muted-foreground/70"
disabled={!isGitRepo || isCheckingGitRepository}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isCreatingWorktree) {
handleCreateWorktree();
}
}}
/>
<Button
onClick={handleCreateWorktree}
disabled={isCreatingWorktree || isLoading || !isGitRepo || !worktreeTargetBranch}
className="h-8"
>
{isCreatingWorktree ? 'Creating…' : 'Create'}
</Button>
</div>
</>
) : (
<>
<label className="typography-meta font-medium text-foreground" htmlFor="worktree-existing-branch-select">
Existing branch
</label>
<div className="flex gap-2">
<Select
value={existingWorktreeBranch}
onValueChange={(value) => {
setExistingWorktreeBranch(value);
setWorktreeError(null);
}}
disabled={
!isGitRepo
|| isCheckingGitRepository
|| isLoadingWorktreeBaseBranches
|| !availableWorktreeBaseBranches.some((option) => option.group === 'local')
}
>
<SelectTrigger
id="worktree-existing-branch-select"
size="lg"
className="flex-1 typography-meta text-foreground"
>
<SelectValue placeholder={isLoadingWorktreeBaseBranches ? 'Loading branches…' : 'Select a branch'} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Local branches</SelectLabel>
{availableWorktreeBaseBranches
.filter((option) => option.group === 'local')
.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Button
onClick={handleCreateWorktree}
disabled={isCreatingWorktree || isLoading || !isGitRepo || !worktreeTargetBranch}
className="h-8"
>
{isCreatingWorktree ? 'Creating…' : 'Create'}
</Button>
</div>
{!isLoadingWorktreeBaseBranches && !availableWorktreeBaseBranches.some((option) => option.group === 'local') ? (
<p className="typography-micro text-muted-foreground/70">
No local branches found. Fetch or create a branch first.
</p>
) : null}
</>
)} )}
{worktreeTargetBranch ? (
<p className="typography-micro text-muted-foreground/70">
{worktreeCreateMode === 'existing' ? (
<>
Uses branch{' '}
<code className="font-mono text-xs text-muted-foreground">{worktreeTargetBranch}</code>
{' '}at{' '}
<code className="font-mono text-xs text-muted-foreground break-all">
{formatPathForDisplay(worktreePreviewPath, homeDirectory)}
</code>
</>
) : (
<>
Creates branch{' '}
<code className="font-mono text-xs text-muted-foreground">{worktreeTargetBranch}</code>
{' '}from{' '}
<code className="font-mono text-xs text-muted-foreground">{selectedWorktreeBaseLabel}</code>
{' '}at{' '}
<code className="font-mono text-xs text-muted-foreground break-all">
{formatPathForDisplay(worktreePreviewPath, homeDirectory)}
</code>
</>
)}
</p>
) : null}
</div> </div>
{worktreeError && <p className="typography-meta text-destructive">{worktreeError}</p>} {worktreeError && <p className="typography-meta text-destructive">{worktreeError}</p>}
+2 -2
View File
@@ -31,14 +31,14 @@ function SelectTrigger({
children, children,
...props ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default" size?: "sm" | "default" | "lg"
}) { }) {
return ( return (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none focus-visible:outline-none hover:bg-muted data-[state=open]:bg-muted focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-6 data-[size=sm]:h-6 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-lg border bg-transparent px-2 py-2 typography-ui-label whitespace-nowrap shadow-none outline-none focus-visible:outline-none hover:bg-muted data-[state=open]:bg-muted focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-6 data-[size=sm]:h-6 data-[size=lg]:h-8 data-[size=lg]:py-1.5 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...props} {...props}
+1
View File
@@ -222,6 +222,7 @@ export interface GitAddWorktreePayload {
path: string; path: string;
branch: string; branch: string;
createBranch?: boolean; createBranch?: boolean;
startPoint?: string;
} }
export interface GitRemoveWorktreePayload { export interface GitRemoveWorktreePayload {
+3 -1
View File
@@ -59,6 +59,7 @@ export interface CreateWorktreeOptions {
worktreeSlug: string; worktreeSlug: string;
branch: string; branch: string;
createBranch?: boolean; createBranch?: boolean;
startPoint?: string;
} }
export interface RemoveWorktreeOptions { export interface RemoveWorktreeOptions {
@@ -84,7 +85,7 @@ export async function resolveWorktreePath(projectDirectory: string, worktreeSlug
} }
export async function createWorktree(options: CreateWorktreeOptions): Promise<WorktreeMetadata> { export async function createWorktree(options: CreateWorktreeOptions): Promise<WorktreeMetadata> {
const { projectDirectory, worktreeSlug, branch, createBranch } = options; const { projectDirectory, worktreeSlug, branch, createBranch, startPoint } = options;
const normalizedProject = normalize(projectDirectory); const normalizedProject = normalize(projectDirectory);
const worktreePath = await resolveWorktreePath(normalizedProject, worktreeSlug); const worktreePath = await resolveWorktreePath(normalizedProject, worktreeSlug);
@@ -92,6 +93,7 @@ export async function createWorktree(options: CreateWorktreeOptions): Promise<Wo
path: worktreePath, path: worktreePath,
branch, branch,
createBranch: Boolean(createBranch), createBranch: Boolean(createBranch),
startPoint: startPoint?.trim() || undefined,
}; };
await addGitWorktree(normalizedProject, payload); await addGitWorktree(normalizedProject, payload);
+2 -2
View File
@@ -3400,12 +3400,12 @@ async function main(options = {}) {
return res.status(400).json({ error: 'directory parameter is required' }); return res.status(400).json({ error: 'directory parameter is required' });
} }
const { path, branch, createBranch } = req.body; const { path, branch, createBranch, startPoint } = req.body;
if (!path || !branch) { if (!path || !branch) {
return res.status(400).json({ error: 'path and branch are required' }); return res.status(400).json({ error: 'path and branch are required' });
} }
const result = await addWorktree(directory, path, branch, { createBranch }); const result = await addWorktree(directory, path, branch, { createBranch, startPoint });
res.json(result); res.json(result);
} catch (error) { } catch (error) {
console.error('Failed to add worktree:', error); console.error('Failed to add worktree:', error);
+3
View File
@@ -830,6 +830,7 @@ export async function addWorktree(directory, worktreePath, branch, options = {})
try { try {
const args = ['worktree', 'add']; const args = ['worktree', 'add'];
const startPoint = typeof options.startPoint === 'string' ? options.startPoint.trim() : '';
if (options.createBranch) { if (options.createBranch) {
args.push('-b', branch); args.push('-b', branch);
@@ -839,6 +840,8 @@ export async function addWorktree(directory, worktreePath, branch, options = {})
if (!options.createBranch) { if (!options.createBranch) {
args.push(branch); args.push(branch);
} else if (startPoint) {
args.push(startPoint);
} }
await git.raw(args); await git.raw(args);