diff --git a/apps/ui/src/components/ui/shell-syntax-editor.tsx b/apps/ui/src/components/ui/shell-syntax-editor.tsx index 03675539..159123c4 100644 --- a/apps/ui/src/components/ui/shell-syntax-editor.tsx +++ b/apps/ui/src/components/ui/shell-syntax-editor.tsx @@ -13,6 +13,7 @@ interface ShellSyntaxEditorProps { placeholder?: string; className?: string; minHeight?: string; + maxHeight?: string; 'data-testid'?: string; } @@ -108,14 +109,12 @@ export function ShellSyntaxEditor({ placeholder, className, minHeight = '200px', + maxHeight, 'data-testid': testId, }: ShellSyntaxEditorProps) { return (
@@ -125,7 +124,9 @@ export function ShellSyntaxEditor({ extensions={extensions} theme="none" placeholder={placeholder} - className="h-full [&_.cm-editor]:h-full [&_.cm-editor]:min-h-[inherit]" + height={maxHeight} + minHeight={minHeight} + className="[&_.cm-editor]:min-h-[inherit]" basicSetup={{ lineNumbers: true, foldGutter: false, diff --git a/apps/ui/src/components/views/settings-view/worktrees/worktrees-section.tsx b/apps/ui/src/components/views/settings-view/worktrees/worktrees-section.tsx index 1af4c755..8a615c58 100644 --- a/apps/ui/src/components/views/settings-view/worktrees/worktrees-section.tsx +++ b/apps/ui/src/components/views/settings-view/worktrees/worktrees-section.tsx @@ -1,10 +1,11 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; +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, Check, Loader2 } from 'lucide-react'; +import { GitBranch, Terminal, FileCode, Save, RotateCcw, Trash2, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { apiPost, apiPut } from '@/lib/api-fetch'; +import { apiPost, apiPut, apiDelete } from '@/lib/api-fetch'; import { toast } from 'sonner'; import { useAppStore } from '@/store/app-store'; @@ -24,18 +25,21 @@ interface InitScriptResponse { export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: WorktreesSectionProps) { const currentProject = useAppStore((s) => s.currentProject); const [scriptContent, setScriptContent] = useState(''); - const [scriptPath, setScriptPath] = useState(''); + const [originalContent, setOriginalContent] = useState(''); + const [scriptExists, setScriptExists] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); - const [showSaved, setShowSaved] = useState(false); - const saveTimeoutRef = useRef | null>(null); - const savedTimeoutRef = useRef | null>(null); + const [isDeleting, setIsDeleting] = useState(false); + + // Check if there are unsaved changes + const hasChanges = scriptContent !== originalContent; // Load init script content when project changes useEffect(() => { if (!currentProject?.path) { setScriptContent(''); - setScriptPath(''); + setOriginalContent(''); + setScriptExists(false); setIsLoading(false); return; } @@ -47,8 +51,10 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre projectPath: currentProject.path, }); if (response.success) { - setScriptContent(response.content || ''); - setScriptPath(response.path || ''); + const content = response.content || ''; + setScriptContent(content); + setOriginalContent(content); + setScriptExists(response.exists); } } catch (error) { console.error('Failed to load init script:', error); @@ -60,66 +66,74 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre loadInitScript(); }, [currentProject?.path]); - // Debounced save function - const saveScript = useCallback( - async (content: string) => { - if (!currentProject?.path) return; + // Save script + const handleSave = useCallback(async () => { + if (!currentProject?.path) return; - setIsSaving(true); - try { - const response = await apiPut<{ success: boolean; error?: string }>( - '/api/worktree/init-script', - { - projectPath: currentProject.path, - content, - } - ); - if (response.success) { - setShowSaved(true); - savedTimeoutRef.current = setTimeout(() => setShowSaved(false), 2000); - } else { - toast.error('Failed to save init script', { - description: response.error, - }); + setIsSaving(true); + try { + const response = await apiPut<{ success: boolean; error?: string }>( + '/api/worktree/init-script', + { + projectPath: currentProject.path, + content: scriptContent, } - } catch (error) { - console.error('Failed to save init script:', error); - toast.error('Failed to save init script'); - } finally { - setIsSaving(false); + ); + if (response.success) { + setOriginalContent(scriptContent); + setScriptExists(true); + toast.success('Init script saved'); + } else { + toast.error('Failed to save init script', { + description: response.error, + }); } - }, - [currentProject?.path] - ); + } catch (error) { + console.error('Failed to save init script:', error); + toast.error('Failed to save init script'); + } finally { + setIsSaving(false); + } + }, [currentProject?.path, scriptContent]); - // Handle content change with debounce - const handleContentChange = useCallback( - (value: string) => { - setScriptContent(value); - setShowSaved(false); + // Reset to original content + const handleReset = useCallback(() => { + setScriptContent(originalContent); + }, [originalContent]); - // Clear existing timeouts - if (saveTimeoutRef.current) { - clearTimeout(saveTimeoutRef.current); - } - if (savedTimeoutRef.current) { - clearTimeout(savedTimeoutRef.current); + // Delete script + const handleDelete = useCallback(async () => { + if (!currentProject?.path) return; + + setIsDeleting(true); + try { + const response = await apiDelete<{ success: boolean; error?: string }>( + '/api/worktree/init-script', + { + body: { projectPath: currentProject.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); + } + }, [currentProject?.path]); - // Debounce save - saveTimeoutRef.current = setTimeout(() => { - saveScript(value); - }, 1000); - }, - [saveScript] - ); - - // Cleanup timeouts - useEffect(() => { - return () => { - if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); - if (savedTimeoutRef.current) clearTimeout(savedTimeoutRef.current); - }; + // Handle content change (no auto-save) + const handleContentChange = useCallback((value: string) => { + setScriptContent(value); }, []); return ( @@ -177,24 +191,10 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
-
- {isSaving && ( - - - Saving... - - )} - {showSaved && !isSaving && ( - - - Saved - - )} -

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

{currentProject ? ( @@ -203,6 +203,9 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
.automaker/worktree-init.sh + {hasChanges && ( + (unsaved changes) + )}
{isLoading ? ( @@ -210,10 +213,11 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre ) : ( - + + minHeight="200px" + maxHeight="500px" + data-testid="init-script-editor" + /> + + {/* Action buttons */} +
+ + + +
+ )} ) : (