import { useState, useEffect, useCallback } from 'react'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { Button } from '@/components/ui/button'; import { ShellSyntaxEditor } from '@/components/ui/shell-syntax-editor'; import { GitBranch, Terminal, FileCode, Save, RotateCcw, Trash2, PanelBottomClose, } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { cn } from '@/lib/utils'; import { apiGet, apiPut, apiDelete } from '@/lib/api-fetch'; import { toast } from 'sonner'; import { useAppStore } from '@/store/app-store'; import { getHttpApiClient } from '@/lib/http-api-client'; import type { Project } from '@/lib/electron'; interface WorktreePreferencesSectionProps { project: Project; } interface InitScriptResponse { success: boolean; exists: boolean; content: string; path: string; error?: string; } export function WorktreePreferencesSection({ project }: WorktreePreferencesSectionProps) { const globalUseWorktrees = useAppStore((s) => s.useWorktrees); const getProjectUseWorktrees = useAppStore((s) => s.getProjectUseWorktrees); const setProjectUseWorktrees = useAppStore((s) => s.setProjectUseWorktrees); const getShowInitScriptIndicator = useAppStore((s) => s.getShowInitScriptIndicator); const setShowInitScriptIndicator = useAppStore((s) => s.setShowInitScriptIndicator); const getDefaultDeleteBranch = useAppStore((s) => s.getDefaultDeleteBranch); const setDefaultDeleteBranch = useAppStore((s) => s.setDefaultDeleteBranch); const getAutoDismissInitScriptIndicator = useAppStore((s) => s.getAutoDismissInitScriptIndicator); const setAutoDismissInitScriptIndicator = useAppStore((s) => s.setAutoDismissInitScriptIndicator); // Get effective worktrees setting (project override or global fallback) const projectUseWorktrees = getProjectUseWorktrees(project.path); const effectiveUseWorktrees = projectUseWorktrees ?? globalUseWorktrees; const [scriptContent, setScriptContent] = useState(''); const [originalContent, setOriginalContent] = useState(''); const [scriptExists, setScriptExists] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); // Get the current settings for this project const showIndicator = getShowInitScriptIndicator(project.path); const defaultDeleteBranch = getDefaultDeleteBranch(project.path); const autoDismiss = getAutoDismissInitScriptIndicator(project.path); // Check if there are unsaved changes const hasChanges = scriptContent !== originalContent; // Load project settings (including useWorktrees) when project changes useEffect(() => { let isCancelled = false; const currentPath = project.path; const loadProjectSettings = async () => { try { const httpClient = getHttpApiClient(); const response = await httpClient.settings.getProject(currentPath); // Avoid updating state if component unmounted or project changed if (isCancelled) return; if (response.success && response.settings) { // Sync useWorktrees to store if it has a value if (response.settings.useWorktrees !== undefined) { setProjectUseWorktrees(currentPath, response.settings.useWorktrees); } // Also sync other settings to store if (response.settings.showInitScriptIndicator !== undefined) { setShowInitScriptIndicator(currentPath, response.settings.showInitScriptIndicator); } if (response.settings.defaultDeleteBranchWithWorktree !== undefined) { setDefaultDeleteBranch(currentPath, response.settings.defaultDeleteBranchWithWorktree); } if (response.settings.autoDismissInitScriptIndicator !== undefined) { setAutoDismissInitScriptIndicator( currentPath, response.settings.autoDismissInitScriptIndicator ); } } } catch (error) { if (!isCancelled) { console.error('Failed to load project settings:', error); } } }; loadProjectSettings(); return () => { isCancelled = true; }; }, [ project.path, setProjectUseWorktrees, setShowInitScriptIndicator, setDefaultDeleteBranch, setAutoDismissInitScriptIndicator, ]); // Load init script content when project changes useEffect(() => { let isCancelled = false; const currentPath = project.path; const loadInitScript = async () => { setIsLoading(true); try { const response = await apiGet( `/api/worktree/init-script?projectPath=${encodeURIComponent(currentPath)}` ); // Avoid updating state if component unmounted or project changed if (isCancelled) return; if (response.success) { const content = response.content || ''; setScriptContent(content); setOriginalContent(content); setScriptExists(response.exists); } } catch (error) { if (!isCancelled) { console.error('Failed to load init script:', error); } } finally { if (!isCancelled) { setIsLoading(false); } } }; loadInitScript(); return () => { isCancelled = true; }; }, [project.path]); // Save script const handleSave = useCallback(async () => { setIsSaving(true); try { const response = await apiPut<{ success: boolean; error?: string }>( '/api/worktree/init-script', { projectPath: project.path, content: scriptContent, } ); if (response.success) { setOriginalContent(scriptContent); setScriptExists(true); toast.success('Init script saved'); } else { toast.error('Failed to save init script', { description: response.error, }); } } catch (error) { console.error('Failed to save init script:', error); toast.error('Failed to save init script'); } finally { setIsSaving(false); } }, [project.path, scriptContent]); // Reset to original content const handleReset = useCallback(() => { setScriptContent(originalContent); }, [originalContent]); // Delete script const handleDelete = useCallback(async () => { setIsDeleting(true); try { const response = await apiDelete<{ success: boolean; error?: string }>( '/api/worktree/init-script', { body: { projectPath: project.path }, } ); if (response.success) { setScriptContent(''); setOriginalContent(''); setScriptExists(false); toast.success('Init script deleted'); } else { toast.error('Failed to delete init script', { description: response.error, }); } } catch (error) { console.error('Failed to delete init script:', error); toast.error('Failed to delete init script'); } finally { setIsDeleting(false); } }, [project.path]); // Handle content change (no auto-save) const handleContentChange = useCallback((value: string) => { setScriptContent(value); }, []); return (

Worktree Preferences

Configure worktree behavior for this project.

{/* Enable Git Worktree Isolation Toggle */}
{ const value = checked === true; setProjectUseWorktrees(project.path, value); try { const httpClient = getHttpApiClient(); await httpClient.settings.updateProject(project.path, { useWorktrees: value, }); } catch (error) { console.error('Failed to persist useWorktrees:', error); } }} className="mt-1" data-testid="project-use-worktrees-checkbox" />

Creates isolated git branches for each feature in this project. When disabled, agents work directly in the main project directory.

{/* Separator */}
{/* Show Init Script Indicator Toggle */}
{ const value = checked === true; setShowInitScriptIndicator(project.path, value); // Persist to server try { const httpClient = getHttpApiClient(); await httpClient.settings.updateProject(project.path, { showInitScriptIndicator: value, }); } catch (error) { console.error('Failed to persist showInitScriptIndicator:', error); } }} className="mt-1" />

Display a floating panel in the bottom-right corner showing init script execution status and output when a worktree is created.

{/* Auto-dismiss Init Script Indicator Toggle */} {showIndicator && (
{ const value = checked === true; setAutoDismissInitScriptIndicator(project.path, value); // Persist to server try { const httpClient = getHttpApiClient(); await httpClient.settings.updateProject(project.path, { autoDismissInitScriptIndicator: value, }); } catch (error) { console.error('Failed to persist autoDismissInitScriptIndicator:', error); } }} className="mt-1" />

Automatically hide the indicator 5 seconds after the script completes.

)} {/* Default Delete Branch Toggle */}
{ const value = checked === true; setDefaultDeleteBranch(project.path, value); // Persist to server try { const httpClient = getHttpApiClient(); await httpClient.settings.updateProject(project.path, { defaultDeleteBranch: value, }); } catch (error) { console.error('Failed to persist defaultDeleteBranch:', error); } }} className="mt-1" />

When deleting a worktree, automatically check the "Also delete the branch" option.

{/* Separator */}
{/* Init Script Section */}

Shell commands to run after a worktree is created. Runs once per worktree. Uses Git Bash on Windows for cross-platform compatibility.

{/* File path indicator */}
.automaker/worktree-init.sh {hasChanges && (unsaved changes)}
{isLoading ? (
) : ( <> {/* Action buttons */}
)}
); }