feat: add branch picker dialog for creating worktree sessions (#160)
- Add BranchPickerDialog component to browse branches across all projects - Filter out branches that already have worktrees - Show commit hash and ahead/behind indicators for each branch - Add createWorktreeSessionForBranch() for specific project/branch - Add worktree and branch picker buttons in sidebar project header Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
033e370c55
commit
7aa4d79b1d
@@ -0,0 +1,299 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
RiGitBranchLine,
|
||||
RiSearchLine,
|
||||
RiFolderLine,
|
||||
RiAddLine,
|
||||
RiArrowRightSLine,
|
||||
RiLoader4Line,
|
||||
} from '@remixicon/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getGitBranches, listGitWorktrees } from '@/lib/gitApi';
|
||||
import type { GitBranch, GitWorktreeInfo } from '@/lib/api/types';
|
||||
import { createWorktreeSessionForBranch } from '@/lib/worktreeSessionCreator';
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
path: string;
|
||||
normalizedPath: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface BranchPickerDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
projects: Project[];
|
||||
activeProjectId: string | null;
|
||||
}
|
||||
|
||||
interface ProjectBranchData {
|
||||
branches: GitBranch | null;
|
||||
worktrees: GitWorktreeInfo[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function BranchPickerDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
projects,
|
||||
activeProjectId,
|
||||
}: BranchPickerDialogProps) {
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [projectData, setProjectData] = React.useState<Map<string, ProjectBranchData>>(new Map());
|
||||
const [expandedProjects, setExpandedProjects] = React.useState<Set<string>>(new Set());
|
||||
const [creatingWorktree, setCreatingWorktree] = React.useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setSearchQuery('');
|
||||
return;
|
||||
}
|
||||
|
||||
const activeProject = projects.find(p => p.id === activeProjectId);
|
||||
if (activeProject) {
|
||||
setExpandedProjects(new Set([activeProject.id]));
|
||||
}
|
||||
|
||||
projects.forEach(async (project) => {
|
||||
setProjectData(prev => {
|
||||
const next = new Map(prev);
|
||||
next.set(project.id, { branches: null, worktrees: [], loading: true, error: null });
|
||||
return next;
|
||||
});
|
||||
|
||||
try {
|
||||
const [branches, worktrees] = await Promise.all([
|
||||
getGitBranches(project.path),
|
||||
listGitWorktrees(project.path),
|
||||
]);
|
||||
|
||||
setProjectData(prev => {
|
||||
const next = new Map(prev);
|
||||
next.set(project.id, { branches, worktrees, loading: false, error: null });
|
||||
return next;
|
||||
});
|
||||
} catch (err) {
|
||||
setProjectData(prev => {
|
||||
const next = new Map(prev);
|
||||
next.set(project.id, {
|
||||
branches: null,
|
||||
worktrees: [],
|
||||
loading: false,
|
||||
error: err instanceof Error ? err.message : 'Failed to load',
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [open, projects, activeProjectId]);
|
||||
|
||||
const toggleProject = (projectId: string) => {
|
||||
setExpandedProjects(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(projectId)) {
|
||||
next.delete(projectId);
|
||||
} else {
|
||||
next.add(projectId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateWorktree = async (project: Project, branchName: string) => {
|
||||
const key = `${project.id}:${branchName}`;
|
||||
setCreatingWorktree(key);
|
||||
|
||||
try {
|
||||
await createWorktreeSessionForBranch(project.path, branchName);
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
console.error('Failed to create worktree:', err);
|
||||
} finally {
|
||||
setCreatingWorktree(null);
|
||||
}
|
||||
};
|
||||
|
||||
const filterBranches = (branches: string[], query: string): string[] => {
|
||||
if (!query.trim()) return branches;
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return branches.filter(b => b.toLowerCase().includes(lowerQuery));
|
||||
};
|
||||
|
||||
const gitRepoProjects = projects.filter(p => {
|
||||
const data = projectData.get(p.id);
|
||||
return data && !data.error && (data.loading || data.branches);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-xl max-h-[80vh] flex flex-col overflow-hidden gap-3">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>Branches & Worktrees</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="relative flex-shrink-0">
|
||||
<RiSearchLine className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search branches..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
{gitRepoProjects.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No git repositories found
|
||||
</div>
|
||||
) : (
|
||||
gitRepoProjects.map((project) => {
|
||||
const data = projectData.get(project.id);
|
||||
const isExpanded = expandedProjects.has(project.id);
|
||||
const branches = data?.branches;
|
||||
const worktrees = data?.worktrees || [];
|
||||
const worktreeBranches = new Set(worktrees.map(w => w.branch).filter(Boolean));
|
||||
|
||||
const allBranches = branches?.all || [];
|
||||
const filteredBranches = filterBranches(allBranches, searchQuery);
|
||||
const localBranches = filteredBranches
|
||||
.filter(b => !b.startsWith('remotes/'))
|
||||
.filter(b => !worktreeBranches.has(b));
|
||||
|
||||
return (
|
||||
<div key={project.id} className="rounded-lg border">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleProject(project.id)}
|
||||
className="w-full flex items-center gap-2 p-3 hover:bg-muted/50 transition-colors rounded-t-lg"
|
||||
>
|
||||
<RiArrowRightSLine
|
||||
className={cn(
|
||||
'h-4 w-4 text-muted-foreground transition-transform',
|
||||
isExpanded && 'rotate-90'
|
||||
)}
|
||||
/>
|
||||
<RiFolderLine className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium text-sm truncate flex-1 text-left">
|
||||
{project.label || project.normalizedPath.split('/').pop() || project.normalizedPath}
|
||||
</span>
|
||||
{data?.loading && (
|
||||
<RiLoader4Line className="h-4 w-4 text-muted-foreground animate-spin" />
|
||||
)}
|
||||
{branches && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{localBranches.length} branches
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="border-t">
|
||||
{data?.loading ? (
|
||||
<div className="p-4 text-center text-muted-foreground text-sm">
|
||||
Loading branches...
|
||||
</div>
|
||||
) : data?.error ? (
|
||||
<div className="p-4 text-center text-destructive text-sm">
|
||||
{data.error}
|
||||
</div>
|
||||
) : localBranches.length === 0 ? (
|
||||
<div className="p-4 text-center text-muted-foreground text-sm">
|
||||
{searchQuery ? 'No matching branches' : 'No branches found'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y overflow-hidden">
|
||||
{localBranches.map((branchName) => {
|
||||
const branchDetails = branches?.branches[branchName];
|
||||
const isCurrent = branchDetails?.current;
|
||||
const isCreating = creatingWorktree === `${project.id}:${branchName}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={branchName}
|
||||
className="flex items-center gap-2 px-3 py-2 hover:bg-muted/30 overflow-hidden"
|
||||
>
|
||||
<RiGitBranchLine className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span className={cn(
|
||||
'text-sm truncate',
|
||||
isCurrent && 'font-medium text-primary'
|
||||
)}>
|
||||
{branchName}
|
||||
</span>
|
||||
{isCurrent && (
|
||||
<span className="text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded flex-shrink-0 whitespace-nowrap">
|
||||
current
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{branchDetails?.commit && (
|
||||
<span className="font-mono">
|
||||
{branchDetails.commit.slice(0, 7)}
|
||||
</span>
|
||||
)}
|
||||
{branchDetails?.ahead !== undefined && branchDetails.ahead > 0 && (
|
||||
<span className="text-[color:var(--status-success)]">
|
||||
↑{branchDetails.ahead}
|
||||
</span>
|
||||
)}
|
||||
{branchDetails?.behind !== undefined && branchDetails.behind > 0 && (
|
||||
<span className="text-[color:var(--status-warning)]">
|
||||
↓{branchDetails.behind}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCreateWorktree(project, branchName)}
|
||||
disabled={isCreating}
|
||||
className="inline-flex h-7 px-2 items-center justify-center text-xs rounded-md bg-primary/10 hover:bg-primary/20 text-primary transition-colors disabled:opacity-50 flex-shrink-0"
|
||||
>
|
||||
{isCreating ? (
|
||||
<RiLoader4Line className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<RiAddLine className="h-3.5 w-3.5 mr-1" />
|
||||
Worktree
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
Create worktree for this branch
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -39,7 +39,9 @@ import {
|
||||
RiFileCopyLine,
|
||||
RiFolderAddLine,
|
||||
RiGitBranchLine,
|
||||
RiGitRepositoryLine,
|
||||
RiLinkUnlinkM,
|
||||
|
||||
RiMore2Line,
|
||||
RiPencilAiLine,
|
||||
RiShare2Line,
|
||||
@@ -59,6 +61,7 @@ import { checkIsGitRepository } from '@/lib/gitApi';
|
||||
import { getSafeStorage } from '@/stores/utils/safeStorage';
|
||||
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { BranchPickerDialog } from './BranchPickerDialog';
|
||||
|
||||
const PROJECT_COLLAPSE_STORAGE_KEY = 'oc.sessions.projectCollapse';
|
||||
const SESSION_EXPANDED_STORAGE_KEY = 'oc.sessions.expandedParents';
|
||||
@@ -135,6 +138,7 @@ interface SortableProjectItemProps {
|
||||
onHoverChange: (hovered: boolean) => void;
|
||||
onNewSession: () => void;
|
||||
onNewWorktreeSession?: () => void;
|
||||
onOpenBranchPicker?: () => void;
|
||||
onOpenMultiRunLauncher: () => void;
|
||||
onClose: () => void;
|
||||
sentinelRef: (el: HTMLDivElement | null) => void;
|
||||
@@ -158,6 +162,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
onHoverChange,
|
||||
onNewSession,
|
||||
onNewWorktreeSession,
|
||||
onOpenBranchPicker,
|
||||
onOpenMultiRunLauncher,
|
||||
onClose,
|
||||
sentinelRef,
|
||||
@@ -257,7 +262,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{isRepo && onNewWorktreeSession && !settingsAutoCreateWorktree && (
|
||||
{isRepo && !hideDirectoryControls && onNewWorktreeSession && !settingsAutoCreateWorktree && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
@@ -267,7 +272,7 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
onNewWorktreeSession();
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground',
|
||||
'inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground flex-shrink-0',
|
||||
mobileVariant ? 'opacity-70' : 'opacity-0 group-hover/project:opacity-100',
|
||||
)}
|
||||
aria-label="New session in worktree"
|
||||
@@ -280,6 +285,29 @@ const SortableProjectItem: React.FC<SortableProjectItemProps> = ({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isRepo && !hideDirectoryControls && onOpenBranchPicker && (
|
||||
<Tooltip delayDuration={700}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenBranchPicker();
|
||||
}}
|
||||
className={cn(
|
||||
'inline-flex h-6 w-6 items-center justify-center text-muted-foreground hover:text-foreground flex-shrink-0 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-opacity',
|
||||
mobileVariant ? 'opacity-70' : 'opacity-0 group-hover/project:opacity-100',
|
||||
)}
|
||||
aria-label="Browse branches"
|
||||
>
|
||||
<RiGitRepositoryLine className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={4}>
|
||||
<p>Browse branches</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
@@ -367,6 +395,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
const [projectRepoStatus, setProjectRepoStatus] = React.useState<Map<string, boolean | null>>(new Map());
|
||||
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
|
||||
const [hoveredProjectId, setHoveredProjectId] = React.useState<string | null>(null);
|
||||
const [branchPickerOpen, setBranchPickerOpen] = React.useState(false);
|
||||
const [activeDragId, setActiveDragId] = React.useState<string | null>(null);
|
||||
const [stuckProjectHeaders, setStuckProjectHeaders] = React.useState<Set<string>>(new Set());
|
||||
const [openMenuSessionId, setOpenMenuSessionId] = React.useState<string | null>(null);
|
||||
@@ -1592,6 +1621,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
}
|
||||
createWorktreeSession();
|
||||
}}
|
||||
onOpenBranchPicker={() => setBranchPickerOpen(true)}
|
||||
onOpenMultiRunLauncher={() => {
|
||||
if (projectKey !== activeProjectId) {
|
||||
setActiveProject(projectKey);
|
||||
@@ -1627,6 +1657,12 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
)}
|
||||
</ScrollableOverlay>
|
||||
|
||||
<BranchPickerDialog
|
||||
open={branchPickerOpen}
|
||||
onOpenChange={setBranchPickerOpen}
|
||||
projects={normalizedProjects}
|
||||
activeProjectId={activeProjectId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -249,3 +249,192 @@ export async function createWorktreeSession(): Promise<{ id: string } | null> {
|
||||
export function isCreatingWorktree(): boolean {
|
||||
return isCreatingWorktreeSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new session with a worktree for a specific branch.
|
||||
* Unlike createWorktreeSession(), this allows specifying the project and branch explicitly.
|
||||
*
|
||||
* @param projectDirectory - The root directory of the git repository
|
||||
* @param branchName - The name of the branch to create a worktree for
|
||||
* @returns The created session, or null if creation failed
|
||||
*/
|
||||
export async function createWorktreeSessionForBranch(
|
||||
projectDirectory: string,
|
||||
branchName: string
|
||||
): Promise<{ id: string } | null> {
|
||||
if (isCreatingWorktreeSession) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if it's a git repo
|
||||
let isGitRepo = false;
|
||||
try {
|
||||
isGitRepo = await checkIsGitRepository(projectDirectory);
|
||||
} catch {
|
||||
// Ignore errors, treat as not a git repo
|
||||
}
|
||||
|
||||
if (!isGitRepo) {
|
||||
toast.error('Not a Git repository', {
|
||||
description: 'Worktrees can only be created in Git repositories.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
isCreatingWorktreeSession = true;
|
||||
startConfigUpdate("Creating worktree session...");
|
||||
|
||||
try {
|
||||
// Use the branch name as the worktree slug (sanitized)
|
||||
const worktreeSlug = sanitizeWorktreeSlug(branchName);
|
||||
|
||||
// Create the worktree - don't create a new branch, use existing one
|
||||
const metadata = await createWorktree({
|
||||
projectDirectory,
|
||||
worktreeSlug,
|
||||
branch: branchName,
|
||||
createBranch: false, // Use existing branch
|
||||
});
|
||||
|
||||
// Get worktree status
|
||||
const status = await getWorktreeStatus(metadata.path).catch(() => undefined);
|
||||
const createdMetadata = status ? { ...metadata, status } : metadata;
|
||||
|
||||
// Create the session
|
||||
const sessionStore = useSessionStore.getState();
|
||||
const session = await sessionStore.createSession(undefined, metadata.path);
|
||||
if (!session) {
|
||||
// Clean up the worktree if session creation failed
|
||||
await removeWorktree({ projectDirectory, path: metadata.path, force: true }).catch(() => undefined);
|
||||
toast.error('Failed to create session', {
|
||||
description: 'Could not create a session for the worktree.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Initialize the session
|
||||
const configState = useConfigStore.getState();
|
||||
const agents = configState.agents;
|
||||
sessionStore.initializeNewOpenChamberSession(session.id, agents);
|
||||
sessionStore.setSessionDirectory(session.id, metadata.path);
|
||||
sessionStore.setWorktreeMetadata(session.id, createdMetadata);
|
||||
|
||||
// Apply default agent and model settings
|
||||
try {
|
||||
const visibleAgents = configState.getVisibleAgents();
|
||||
let agentName: string | undefined;
|
||||
|
||||
// Priority: settingsDefaultAgent → build → first visible
|
||||
if (configState.settingsDefaultAgent) {
|
||||
const settingsAgent = visibleAgents.find((a) => a.name === configState.settingsDefaultAgent);
|
||||
if (settingsAgent) {
|
||||
agentName = settingsAgent.name;
|
||||
}
|
||||
}
|
||||
if (!agentName) {
|
||||
agentName =
|
||||
visibleAgents.find((agent) => agent.name === 'build')?.name ||
|
||||
visibleAgents[0]?.name;
|
||||
}
|
||||
|
||||
if (agentName) {
|
||||
// 1. Update global UI state
|
||||
configState.setAgent(agentName);
|
||||
|
||||
// 2. Persist to session context so it sticks after reload/switch
|
||||
useContextStore.getState().saveSessionAgentSelection(session.id, agentName);
|
||||
|
||||
// 3. Handle default model for the agent if set in global settings
|
||||
const settingsDefaultModel = configState.settingsDefaultModel;
|
||||
if (settingsDefaultModel) {
|
||||
const parts = settingsDefaultModel.split('/');
|
||||
if (parts.length === 2) {
|
||||
const [providerId, modelId] = parts;
|
||||
// Validate model exists (optional, but good practice)
|
||||
const modelMetadata = configState.getModelMetadata(providerId, modelId);
|
||||
if (modelMetadata) {
|
||||
useContextStore.getState().saveSessionModelSelection(session.id, providerId, modelId);
|
||||
// Also save the specific agent's model preference for this session
|
||||
useContextStore.getState().saveAgentModelForSession(session.id, agentName, providerId, modelId);
|
||||
|
||||
// Seed default variant into session context so ModelControls restore logic
|
||||
// doesn't wipe it on first switch to the new session.
|
||||
const settingsDefaultVariant = configState.settingsDefaultVariant;
|
||||
if (settingsDefaultVariant) {
|
||||
const provider = configState.providers.find((p) => p.id === providerId);
|
||||
const model = provider?.models.find((m: Record<string, unknown>) => (m as { id?: string }).id === modelId) as
|
||||
| { variants?: Record<string, unknown> }
|
||||
| undefined;
|
||||
const variants = model?.variants;
|
||||
|
||||
if (variants && Object.prototype.hasOwnProperty.call(variants, settingsDefaultVariant)) {
|
||||
configState.setCurrentVariant(settingsDefaultVariant);
|
||||
useContextStore
|
||||
.getState()
|
||||
.saveAgentModelVariantForSession(session.id, agentName, providerId, modelId, settingsDefaultVariant);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors setting default agent
|
||||
}
|
||||
|
||||
// Update directory
|
||||
useDirectoryStore.getState().setDirectory(metadata.path, { showOverlay: false });
|
||||
|
||||
// Refresh sessions list
|
||||
try {
|
||||
await sessionStore.loadSessions();
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Get and run setup commands
|
||||
const setupCommands = await getWorktreeSetupCommands(projectDirectory);
|
||||
const commandsToRun = setupCommands.filter(cmd => cmd.trim().length > 0);
|
||||
|
||||
if (commandsToRun.length > 0) {
|
||||
toast.success('Worktree created', {
|
||||
description: `Branch: ${branchName}. Running ${commandsToRun.length} setup command${commandsToRun.length === 1 ? '' : 's'}...`,
|
||||
});
|
||||
|
||||
// Run setup commands in background
|
||||
runWorktreeSetupCommands(metadata.path, projectDirectory, commandsToRun).then((result) => {
|
||||
if (result.success) {
|
||||
toast.success('Setup commands completed', {
|
||||
description: `All ${result.results.length} command${result.results.length === 1 ? '' : 's'} succeeded.`,
|
||||
});
|
||||
} else {
|
||||
const failed = result.results.filter(r => !r.success);
|
||||
const succeeded = result.results.filter(r => r.success);
|
||||
toast.error('Setup commands failed', {
|
||||
description: `${failed.length} of ${result.results.length} command${result.results.length === 1 ? '' : 's'} failed.` +
|
||||
(succeeded.length > 0 ? ` ${succeeded.length} succeeded.` : ''),
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
toast.error('Setup commands failed', {
|
||||
description: 'Could not execute setup commands.',
|
||||
});
|
||||
});
|
||||
} else {
|
||||
toast.success('Worktree created', {
|
||||
description: `Branch: ${branchName}`,
|
||||
});
|
||||
}
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create worktree session';
|
||||
toast.error('Failed to create worktree', {
|
||||
description: message,
|
||||
});
|
||||
return null;
|
||||
} finally {
|
||||
finishConfigUpdate();
|
||||
isCreatingWorktreeSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user