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
Initialization Script
-
- {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 */}
+
+
+
+ Reset
+
+
+ {isDeleting ? (
+
+ ) : (
+
+ )}
+ Delete
+
+
+ {isSaving ? (
+
+ ) : (
+
+ )}
+ Save
+
+
+ >
)}
>
) : (