diff --git a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts
index ff8b7b0b..8c712299 100644
--- a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts
+++ b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts
@@ -12,6 +12,7 @@ import {
Brain,
Network,
Bell,
+ Settings,
} from 'lucide-react';
import type { NavSection, NavItem } from '../types';
import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts';
@@ -33,6 +34,7 @@ interface UseNavigationProps {
agent: string;
terminal: string;
settings: string;
+ projectSettings: string;
ideation: string;
githubIssues: string;
githubPrs: string;
@@ -218,6 +220,19 @@ export function useNavigation({
],
});
+ // Add Project Settings as a standalone section (no label for visual separation)
+ sections.push({
+ label: '',
+ items: [
+ {
+ id: 'project-settings',
+ label: 'Project Settings',
+ icon: Settings,
+ shortcut: shortcuts.projectSettings,
+ },
+ ],
+ });
+
return sections;
}, [
shortcuts,
@@ -277,11 +292,11 @@ export function useNavigation({
});
});
- // Add settings shortcut
+ // Add global settings shortcut
shortcutsList.push({
key: shortcuts.settings,
action: () => navigate({ to: '/settings' }),
- description: 'Navigate to Settings',
+ description: 'Navigate to Global Settings',
});
}
diff --git a/apps/ui/src/components/ui/shell-syntax-editor.tsx b/apps/ui/src/components/ui/shell-syntax-editor.tsx
index 159123c4..c405309a 100644
--- a/apps/ui/src/components/ui/shell-syntax-editor.tsx
+++ b/apps/ui/src/components/ui/shell-syntax-editor.tsx
@@ -70,8 +70,7 @@ const editorTheme = EditorView.theme({
backgroundColor: 'oklch(0.55 0.25 265 / 0.3)',
},
'.cm-activeLine': {
- backgroundColor: 'var(--accent)',
- opacity: '0.3',
+ backgroundColor: 'transparent',
},
'.cm-line': {
padding: '0 0.25rem',
@@ -114,7 +113,7 @@ export function ShellSyntaxEditor({
}: ShellSyntaxEditorProps) {
return (
diff --git a/apps/ui/src/components/views/project-settings-view/components/project-settings-navigation.tsx b/apps/ui/src/components/views/project-settings-view/components/project-settings-navigation.tsx
new file mode 100644
index 00000000..1c06dad3
--- /dev/null
+++ b/apps/ui/src/components/views/project-settings-view/components/project-settings-navigation.tsx
@@ -0,0 +1,122 @@
+import { X } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { PROJECT_SETTINGS_NAV_ITEMS } from '../config/navigation';
+import type { ProjectSettingsViewId } from '../hooks/use-project-settings-view';
+
+interface ProjectSettingsNavigationProps {
+ activeSection: ProjectSettingsViewId;
+ onNavigate: (sectionId: ProjectSettingsViewId) => void;
+ isOpen?: boolean;
+ onClose?: () => void;
+}
+
+export function ProjectSettingsNavigation({
+ activeSection,
+ onNavigate,
+ isOpen = true,
+ onClose,
+}: ProjectSettingsNavigationProps) {
+ return (
+ <>
+ {/* Mobile backdrop overlay - only shown when isOpen is true on mobile */}
+ {isOpen && (
+
+ )}
+
+ {/* Navigation sidebar */}
+
+ {/* Mobile close button */}
+
+ Navigation
+
+
+
+
+
+
+ {PROJECT_SETTINGS_NAV_ITEMS.map((item) => {
+ const Icon = item.icon;
+ const isActive = activeSection === item.id;
+ const isDanger = item.id === 'danger';
+
+ return (
+
onNavigate(item.id)}
+ className={cn(
+ 'group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden',
+ isActive
+ ? [
+ isDanger
+ ? 'bg-gradient-to-r from-red-500/15 via-red-500/10 to-red-600/5'
+ : 'bg-gradient-to-r from-brand-500/15 via-brand-500/10 to-brand-600/5',
+ 'text-foreground',
+ isDanger ? 'border border-red-500/25' : 'border border-brand-500/25',
+ isDanger ? 'shadow-sm shadow-red-500/5' : 'shadow-sm shadow-brand-500/5',
+ ]
+ : [
+ 'text-muted-foreground hover:text-foreground',
+ 'hover:bg-accent/50',
+ 'border border-transparent hover:border-border/40',
+ ],
+ 'hover:scale-[1.01] active:scale-[0.98]'
+ )}
+ >
+ {/* Active indicator bar */}
+ {isActive && (
+
+ )}
+
+ {item.label}
+
+ );
+ })}
+
+
+ >
+ );
+}
diff --git a/apps/ui/src/components/views/project-settings-view/config/navigation.ts b/apps/ui/src/components/views/project-settings-view/config/navigation.ts
new file mode 100644
index 00000000..7f052ef5
--- /dev/null
+++ b/apps/ui/src/components/views/project-settings-view/config/navigation.ts
@@ -0,0 +1,16 @@
+import type { LucideIcon } from 'lucide-react';
+import { User, GitBranch, Palette, AlertTriangle } from 'lucide-react';
+import type { ProjectSettingsViewId } from '../hooks/use-project-settings-view';
+
+export interface ProjectNavigationItem {
+ id: ProjectSettingsViewId;
+ label: string;
+ icon: LucideIcon;
+}
+
+export const PROJECT_SETTINGS_NAV_ITEMS: ProjectNavigationItem[] = [
+ { id: 'identity', label: 'Identity', icon: User },
+ { id: 'worktrees', label: 'Worktrees', icon: GitBranch },
+ { id: 'theme', label: 'Theme', icon: Palette },
+ { id: 'danger', label: 'Danger Zone', icon: AlertTriangle },
+];
diff --git a/apps/ui/src/components/views/project-settings-view/hooks/index.ts b/apps/ui/src/components/views/project-settings-view/hooks/index.ts
new file mode 100644
index 00000000..023eca9e
--- /dev/null
+++ b/apps/ui/src/components/views/project-settings-view/hooks/index.ts
@@ -0,0 +1 @@
+export { useProjectSettingsView, type ProjectSettingsViewId } from './use-project-settings-view';
diff --git a/apps/ui/src/components/views/project-settings-view/hooks/use-project-settings-view.ts b/apps/ui/src/components/views/project-settings-view/hooks/use-project-settings-view.ts
new file mode 100644
index 00000000..19faf5e3
--- /dev/null
+++ b/apps/ui/src/components/views/project-settings-view/hooks/use-project-settings-view.ts
@@ -0,0 +1,22 @@
+import { useState, useCallback } from 'react';
+
+export type ProjectSettingsViewId = 'identity' | 'theme' | 'worktrees' | 'danger';
+
+interface UseProjectSettingsViewOptions {
+ initialView?: ProjectSettingsViewId;
+}
+
+export function useProjectSettingsView({
+ initialView = 'identity',
+}: UseProjectSettingsViewOptions = {}) {
+ const [activeView, setActiveView] = useState
(initialView);
+
+ const navigateTo = useCallback((viewId: ProjectSettingsViewId) => {
+ setActiveView(viewId);
+ }, []);
+
+ return {
+ activeView,
+ navigateTo,
+ };
+}
diff --git a/apps/ui/src/components/views/project-settings-view/index.ts b/apps/ui/src/components/views/project-settings-view/index.ts
new file mode 100644
index 00000000..bc16ffaf
--- /dev/null
+++ b/apps/ui/src/components/views/project-settings-view/index.ts
@@ -0,0 +1,6 @@
+export { ProjectSettingsView } from './project-settings-view';
+export { ProjectIdentitySection } from './project-identity-section';
+export { ProjectThemeSection } from './project-theme-section';
+export { WorktreePreferencesSection } from './worktree-preferences-section';
+export { useProjectSettingsView, type ProjectSettingsViewId } from './hooks';
+export { ProjectSettingsNavigation } from './components/project-settings-navigation';
diff --git a/apps/ui/src/components/views/project-settings-view/project-identity-section.tsx b/apps/ui/src/components/views/project-settings-view/project-identity-section.tsx
new file mode 100644
index 00000000..669b7879
--- /dev/null
+++ b/apps/ui/src/components/views/project-settings-view/project-identity-section.tsx
@@ -0,0 +1,225 @@
+import { useState, useRef, useEffect } from 'react';
+import { Label } from '@/components/ui/label';
+import { Input } from '@/components/ui/input';
+import { Button } from '@/components/ui/button';
+import { Palette, Upload, X, ImageIcon } from 'lucide-react';
+import { cn } from '@/lib/utils';
+import { useAppStore } from '@/store/app-store';
+import { IconPicker } from '@/components/layout/project-switcher/components/icon-picker';
+import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
+import { getHttpApiClient } from '@/lib/http-api-client';
+import { toast } from 'sonner';
+import type { Project } from '@/lib/electron';
+
+interface ProjectIdentitySectionProps {
+ project: Project;
+}
+
+export function ProjectIdentitySection({ project }: ProjectIdentitySectionProps) {
+ const { setProjectIcon, setProjectName, setProjectCustomIcon } = useAppStore();
+ const [projectName, setProjectNameLocal] = useState(project.name || '');
+ const [projectIcon, setProjectIconLocal] = useState(project.icon || null);
+ const [customIconPath, setCustomIconPathLocal] = useState(
+ project.customIconPath || null
+ );
+ const [isUploadingIcon, setIsUploadingIcon] = useState(false);
+ const fileInputRef = useRef(null);
+
+ // Sync local state when project changes
+ useEffect(() => {
+ setProjectNameLocal(project.name || '');
+ setProjectIconLocal(project.icon || null);
+ setCustomIconPathLocal(project.customIconPath || null);
+ }, [project]);
+
+ // Auto-save when values change
+ const handleNameChange = (name: string) => {
+ setProjectNameLocal(name);
+ if (name.trim() && name.trim() !== project.name) {
+ setProjectName(project.id, name.trim());
+ }
+ };
+
+ const handleIconChange = (icon: string | null) => {
+ setProjectIconLocal(icon);
+ setProjectIcon(project.id, icon);
+ };
+
+ const handleCustomIconChange = (path: string | null) => {
+ setCustomIconPathLocal(path);
+ setProjectCustomIcon(project.id, path);
+ // Clear Lucide icon when custom icon is set
+ if (path) {
+ setProjectIconLocal(null);
+ setProjectIcon(project.id, null);
+ }
+ };
+
+ const handleCustomIconUpload = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ // Validate file type
+ const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+ if (!validTypes.includes(file.type)) {
+ toast.error('Invalid file type', {
+ description: 'Please upload a PNG, JPG, GIF, or WebP image.',
+ });
+ return;
+ }
+
+ // Validate file size (max 2MB for icons)
+ if (file.size > 2 * 1024 * 1024) {
+ toast.error('File too large', {
+ description: 'Please upload an image smaller than 2MB.',
+ });
+ return;
+ }
+
+ setIsUploadingIcon(true);
+ try {
+ // Convert to base64
+ const reader = new FileReader();
+ reader.onload = async () => {
+ try {
+ const base64Data = reader.result as string;
+ const result = await getHttpApiClient().saveImageToTemp(
+ base64Data,
+ `project-icon-${file.name}`,
+ file.type,
+ project.path
+ );
+ if (result.success && result.path) {
+ handleCustomIconChange(result.path);
+ toast.success('Icon uploaded successfully');
+ } else {
+ toast.error('Failed to upload icon', {
+ description: result.error || 'Please try again.',
+ });
+ }
+ } catch (error) {
+ toast.error('Failed to upload icon', {
+ description: 'Network error. Please try again.',
+ });
+ } finally {
+ setIsUploadingIcon(false);
+ }
+ };
+ reader.onerror = () => {
+ toast.error('Failed to read file', {
+ description: 'Please try again with a different file.',
+ });
+ setIsUploadingIcon(false);
+ };
+ reader.readAsDataURL(file);
+ } catch {
+ toast.error('Failed to upload icon');
+ setIsUploadingIcon(false);
+ }
+ };
+
+ const handleRemoveCustomIcon = () => {
+ handleCustomIconChange(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ return (
+
+
+
+
+ Customize how your project appears in the sidebar and project switcher.
+
+
+
+ {/* Project Name */}
+
+ Project Name
+ handleNameChange(e.target.value)}
+ placeholder="Enter project name"
+ />
+
+
+ {/* Project Icon */}
+
+
Project Icon
+
+ Choose a preset icon or upload a custom image
+
+
+ {/* Custom Icon Upload */}
+
+
+ {customIconPath ? (
+
+
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
fileInputRef.current?.click()}
+ disabled={isUploadingIcon}
+ className="gap-1.5"
+ >
+
+ {isUploadingIcon ? 'Uploading...' : 'Upload Custom Icon'}
+
+
+ PNG, JPG, GIF or WebP. Max 2MB.
+
+
+
+
+
+ {/* Preset Icon Picker - only show if no custom icon */}
+ {!customIconPath && (
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/ui/src/components/views/project-settings-view/project-settings-view.tsx b/apps/ui/src/components/views/project-settings-view/project-settings-view.tsx
new file mode 100644
index 00000000..f441cc72
--- /dev/null
+++ b/apps/ui/src/components/views/project-settings-view/project-settings-view.tsx
@@ -0,0 +1,174 @@
+import { useState, useEffect } from 'react';
+import { useAppStore } from '@/store/app-store';
+import { Settings, FolderOpen, Menu } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { ProjectIdentitySection } from './project-identity-section';
+import { ProjectThemeSection } from './project-theme-section';
+import { WorktreePreferencesSection } from './worktree-preferences-section';
+import { DangerZoneSection } from '../settings-view/danger-zone/danger-zone-section';
+import { DeleteProjectDialog } from '../settings-view/components/delete-project-dialog';
+import { ProjectSettingsNavigation } from './components/project-settings-navigation';
+import { useProjectSettingsView } from './hooks/use-project-settings-view';
+import type { Project as ElectronProject } from '@/lib/electron';
+
+// Breakpoint constant for mobile (matches Tailwind lg breakpoint)
+const LG_BREAKPOINT = 1024;
+
+// Convert to the shared types used by components
+interface SettingsProject {
+ id: string;
+ name: string;
+ path: string;
+ theme?: string;
+ icon?: string | null;
+ customIconPath?: string | null;
+}
+
+export function ProjectSettingsView() {
+ const { currentProject, moveProjectToTrash } = useAppStore();
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+
+ // Use project settings view navigation hook
+ const { activeView, navigateTo } = useProjectSettingsView();
+
+ // Mobile navigation state - default to showing on desktop, hidden on mobile
+ const [showNavigation, setShowNavigation] = useState(() => {
+ if (typeof window !== 'undefined') {
+ return window.innerWidth >= LG_BREAKPOINT;
+ }
+ return true;
+ });
+
+ // Auto-close navigation on mobile when a section is selected
+ useEffect(() => {
+ if (typeof window !== 'undefined' && window.innerWidth < LG_BREAKPOINT) {
+ setShowNavigation(false);
+ }
+ }, [activeView]);
+
+ // Handle window resize to show/hide navigation appropriately
+ useEffect(() => {
+ const handleResize = () => {
+ if (window.innerWidth >= LG_BREAKPOINT) {
+ setShowNavigation(true);
+ }
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ // Convert electron Project to settings-view Project type
+ const convertProject = (project: ElectronProject | null): SettingsProject | null => {
+ if (!project) return null;
+ return {
+ id: project.id,
+ name: project.name,
+ path: project.path,
+ theme: project.theme,
+ icon: project.icon,
+ customIconPath: project.customIconPath,
+ };
+ };
+
+ const settingsProject = convertProject(currentProject);
+
+ // Render the active section based on current view
+ const renderActiveSection = () => {
+ if (!currentProject) return null;
+
+ switch (activeView) {
+ case 'identity':
+ return ;
+ case 'theme':
+ return ;
+ case 'worktrees':
+ return ;
+ case 'danger':
+ return (
+ setShowDeleteDialog(true)}
+ />
+ );
+ default:
+ return ;
+ }
+ };
+
+ // Show message if no project is selected
+ if (!currentProject) {
+ return (
+
+
+
+
+
+
+
No Project Selected
+
+ Select a project from the sidebar to configure project-specific settings.
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Mobile menu button */}
+
setShowNavigation(!showNavigation)}
+ className="lg:hidden h-8 w-8 p-0"
+ aria-label="Toggle navigation menu"
+ >
+
+
+
+
+
Project Settings
+
+ Configure settings for {currentProject.name}
+
+
+
+
+
+ {/* Content Area with Sidebar */}
+
+ {/* Side Navigation */}
+
setShowNavigation(false)}
+ />
+
+ {/* Content Panel - Shows only the active section */}
+
+
{renderActiveSection()}
+
+
+
+ {/* Delete Project Confirmation Dialog */}
+
+
+ );
+}
diff --git a/apps/ui/src/components/views/project-settings-view/project-theme-section.tsx b/apps/ui/src/components/views/project-settings-view/project-theme-section.tsx
new file mode 100644
index 00000000..d9293df2
--- /dev/null
+++ b/apps/ui/src/components/views/project-settings-view/project-theme-section.tsx
@@ -0,0 +1,164 @@
+import { useState } from 'react';
+import { Label } from '@/components/ui/label';
+import { Checkbox } from '@/components/ui/checkbox';
+import { Palette, Moon, Sun } from 'lucide-react';
+import { darkThemes, lightThemes, type Theme } from '@/config/theme-options';
+import { cn } from '@/lib/utils';
+import { useAppStore } from '@/store/app-store';
+import type { Project } from '@/lib/electron';
+
+interface ProjectThemeSectionProps {
+ project: Project;
+}
+
+export function ProjectThemeSection({ project }: ProjectThemeSectionProps) {
+ const { theme: globalTheme, setProjectTheme } = useAppStore();
+ const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
+
+ const projectTheme = project.theme as Theme | undefined;
+ const hasCustomTheme = projectTheme !== undefined;
+ const effectiveTheme = projectTheme || globalTheme;
+
+ const themesToShow = activeTab === 'dark' ? darkThemes : lightThemes;
+
+ const handleThemeChange = (theme: Theme) => {
+ setProjectTheme(project.id, theme);
+ };
+
+ const handleUseGlobalTheme = (checked: boolean) => {
+ if (checked) {
+ // Clear project theme to use global
+ setProjectTheme(project.id, null);
+ } else {
+ // Set project theme to current global theme
+ setProjectTheme(project.id, globalTheme);
+ }
+ };
+
+ return (
+
+
+
+
+ Customize the theme for this project.
+
+
+
+ {/* Use Global Theme Toggle */}
+
+
+
+
+
+ Use Global Theme
+
+
+ When enabled, this project will use the global theme setting. Disable to set a
+ project-specific theme.
+
+
+
+
+ {/* Theme Selection - only show if not using global theme */}
+ {hasCustomTheme && (
+
+
+
Project Theme
+ {/* Dark/Light Tabs */}
+
+ setActiveTab('dark')}
+ className={cn(
+ 'flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200',
+ activeTab === 'dark'
+ ? 'bg-brand-500 text-white shadow-sm'
+ : 'text-muted-foreground hover:text-foreground'
+ )}
+ >
+
+ Dark
+
+ setActiveTab('light')}
+ className={cn(
+ 'flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200',
+ activeTab === 'light'
+ ? 'bg-brand-500 text-white shadow-sm'
+ : 'text-muted-foreground hover:text-foreground'
+ )}
+ >
+
+ Light
+
+
+
+
+ {themesToShow.map(({ value, label, Icon, testId, color }) => {
+ const isActive = effectiveTheme === value;
+ return (
+ handleThemeChange(value)}
+ className={cn(
+ 'group flex items-center justify-center gap-2.5 px-4 py-3.5 rounded-xl',
+ 'text-sm font-medium transition-all duration-200 ease-out',
+ isActive
+ ? [
+ 'bg-gradient-to-br from-brand-500/15 to-brand-600/10',
+ 'border-2 border-brand-500/40',
+ 'text-foreground',
+ 'shadow-md shadow-brand-500/10',
+ ]
+ : [
+ 'bg-accent/30 hover:bg-accent/50',
+ 'border border-border/50 hover:border-border',
+ 'text-muted-foreground hover:text-foreground',
+ 'hover:shadow-sm',
+ ],
+ 'hover:scale-[1.02] active:scale-[0.98]'
+ )}
+ data-testid={`project-${testId}`}
+ >
+
+ {label}
+
+ );
+ })}
+
+
+ )}
+
+ {/* Info when using global theme */}
+ {!hasCustomTheme && (
+
+
+ This project is using the global theme:{' '}
+ {globalTheme}
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx b/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx
new file mode 100644
index 00000000..c289d382
--- /dev/null
+++ b/apps/ui/src/components/views/project-settings-view/worktree-preferences-section.tsx
@@ -0,0 +1,478 @@
+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,
+ Loader2,
+ PanelBottomClose,
+} from 'lucide-react';
+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"
+ />
+
+
+
+ Enable Git Worktree Isolation
+
+
+ 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"
+ />
+
+
+
+ Show Init Script Indicator
+
+
+ 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"
+ />
+
+
+ Auto-dismiss After Completion
+
+
+ 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"
+ />
+
+
+
+ Delete Branch by Default
+
+
+ When deleting a worktree, automatically check the "Also delete the branch" option.
+
+
+
+
+ {/* Separator */}
+
+
+ {/* Init Script Section */}
+
+
+
+
+ Initialization Script
+
+
+
+ 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 */}
+
+
+
+ Reset
+
+
+ {isDeleting ? (
+
+ ) : (
+
+ )}
+ Delete
+
+
+ {isSaving ? (
+
+ ) : (
+
+ )}
+ Save
+
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/apps/ui/src/components/views/settings-view.tsx b/apps/ui/src/components/views/settings-view.tsx
index 1ddf0a39..3bcec3bb 100644
--- a/apps/ui/src/components/views/settings-view.tsx
+++ b/apps/ui/src/components/views/settings-view.tsx
@@ -6,7 +6,6 @@ import { useSettingsView, type SettingsViewId } from './settings-view/hooks';
import { NAV_ITEMS } from './settings-view/config/navigation';
import { SettingsHeader } from './settings-view/components/settings-header';
import { KeyboardMapDialog } from './settings-view/components/keyboard-map-dialog';
-import { DeleteProjectDialog } from './settings-view/components/delete-project-dialog';
import { SettingsNavigation } from './settings-view/components/settings-navigation';
import { ApiKeysSection } from './settings-view/api-keys/api-keys-section';
import { ModelDefaultsSection } from './settings-view/model-defaults';
@@ -16,7 +15,6 @@ import { AudioSection } from './settings-view/audio/audio-section';
import { KeyboardShortcutsSection } from './settings-view/keyboard-shortcuts/keyboard-shortcuts-section';
import { FeatureDefaultsSection } from './settings-view/feature-defaults/feature-defaults-section';
import { WorktreesSection } from './settings-view/worktrees';
-import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section';
import { AccountSection } from './settings-view/account';
import { SecuritySection } from './settings-view/security';
import { DeveloperSection } from './settings-view/developer/developer-section';
@@ -30,8 +28,7 @@ import { MCPServersSection } from './settings-view/mcp-servers';
import { PromptCustomizationSection } from './settings-view/prompts';
import { EventHooksSection } from './settings-view/event-hooks';
import { ImportExportDialog } from './settings-view/components/import-export-dialog';
-import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
-import type { Project as ElectronProject } from '@/lib/electron';
+import type { Theme } from './settings-view/shared/types';
// Breakpoint constant for mobile (matches Tailwind lg breakpoint)
const LG_BREAKPOINT = 1024;
@@ -40,7 +37,6 @@ export function SettingsView() {
const {
theme,
setTheme,
- setProjectTheme,
defaultSkipTests,
setDefaultSkipTests,
enableDependencyBlocking,
@@ -54,7 +50,6 @@ export function SettingsView() {
muteDoneSound,
setMuteDoneSound,
currentProject,
- moveProjectToTrash,
defaultPlanningMode,
setDefaultPlanningMode,
defaultRequirePlanApproval,
@@ -69,34 +64,8 @@ export function SettingsView() {
setSkipSandboxWarning,
} = useAppStore();
- // Convert electron Project to settings-view Project type
- const convertProject = (project: ElectronProject | null): SettingsProject | null => {
- if (!project) return null;
- return {
- id: project.id,
- name: project.name,
- path: project.path,
- theme: project.theme as Theme | undefined,
- icon: project.icon,
- customIconPath: project.customIconPath,
- };
- };
-
- const settingsProject = convertProject(currentProject);
-
- // Compute the effective theme for the current project
- const effectiveTheme = (settingsProject?.theme || theme) as Theme;
-
- // Handler to set theme - always updates global theme (user's preference),
- // and also sets per-project theme if a project is selected
- const handleSetTheme = (newTheme: typeof theme) => {
- // Always update global theme so user's preference persists across all projects
- setTheme(newTheme);
- // Also set per-project theme if a project is selected
- if (currentProject) {
- setProjectTheme(currentProject.id, newTheme);
- }
- };
+ // Global theme (project-specific themes are managed in Project Settings)
+ const globalTheme = theme as Theme;
// Get initial view from URL search params
const { view: initialView } = useSearch({ from: '/settings' });
@@ -113,7 +82,6 @@ export function SettingsView() {
}
};
- const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);
const [showImportExportDialog, setShowImportExportDialog] = useState(false);
@@ -172,9 +140,8 @@ export function SettingsView() {
case 'appearance':
return (
handleSetTheme(theme as any)}
+ effectiveTheme={globalTheme}
+ onThemeChange={(newTheme) => setTheme(newTheme as typeof theme)}
/>
);
case 'terminal':
@@ -223,13 +190,6 @@ export function SettingsView() {
);
case 'developer':
return ;
- case 'danger':
- return (
- setShowDeleteDialog(true)}
- />
- );
default:
return ;
}
@@ -265,14 +225,6 @@ export function SettingsView() {
{/* Keyboard Map Dialog */}
- {/* Delete Project Confirmation Dialog */}
-
-
{/* Import/Export Settings Dialog */}
diff --git a/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx b/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx
index 003501f9..47646287 100644
--- a/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx
+++ b/apps/ui/src/components/views/settings-view/appearance/appearance-section.tsx
@@ -1,118 +1,20 @@
-import { useState, useRef, useEffect } from 'react';
+import { useState } from 'react';
import { Label } from '@/components/ui/label';
-import { Input } from '@/components/ui/input';
-import { Button } from '@/components/ui/button';
-import { Palette, Moon, Sun, Upload, X, ImageIcon } from 'lucide-react';
+import { Palette, Moon, Sun } from 'lucide-react';
import { darkThemes, lightThemes } from '@/config/theme-options';
import { cn } from '@/lib/utils';
-import { useAppStore } from '@/store/app-store';
-import { IconPicker } from '@/components/layout/project-switcher/components/icon-picker';
-import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
-import { getHttpApiClient } from '@/lib/http-api-client';
-import type { Theme, Project } from '../shared/types';
+import type { Theme } from '../shared/types';
interface AppearanceSectionProps {
effectiveTheme: Theme;
- currentProject: Project | null;
onThemeChange: (theme: Theme) => void;
}
-export function AppearanceSection({
- effectiveTheme,
- currentProject,
- onThemeChange,
-}: AppearanceSectionProps) {
- const { setProjectIcon, setProjectName, setProjectCustomIcon } = useAppStore();
+export function AppearanceSection({ effectiveTheme, onThemeChange }: AppearanceSectionProps) {
const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
- const [projectName, setProjectNameLocal] = useState(currentProject?.name || '');
- const [projectIcon, setProjectIconLocal] = useState
(currentProject?.icon || null);
- const [customIconPath, setCustomIconPathLocal] = useState(
- currentProject?.customIconPath || null
- );
- const [isUploadingIcon, setIsUploadingIcon] = useState(false);
- const fileInputRef = useRef(null);
-
- // Sync local state when currentProject changes
- useEffect(() => {
- setProjectNameLocal(currentProject?.name || '');
- setProjectIconLocal(currentProject?.icon || null);
- setCustomIconPathLocal(currentProject?.customIconPath || null);
- }, [currentProject]);
const themesToShow = activeTab === 'dark' ? darkThemes : lightThemes;
- // Auto-save when values change
- const handleNameChange = (name: string) => {
- setProjectNameLocal(name);
- if (currentProject && name.trim() && name.trim() !== currentProject.name) {
- setProjectName(currentProject.id, name.trim());
- }
- };
-
- const handleIconChange = (icon: string | null) => {
- setProjectIconLocal(icon);
- if (currentProject) {
- setProjectIcon(currentProject.id, icon);
- }
- };
-
- const handleCustomIconChange = (path: string | null) => {
- setCustomIconPathLocal(path);
- if (currentProject) {
- setProjectCustomIcon(currentProject.id, path);
- // Clear Lucide icon when custom icon is set
- if (path) {
- setProjectIconLocal(null);
- setProjectIcon(currentProject.id, null);
- }
- }
- };
-
- const handleCustomIconUpload = async (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file || !currentProject) return;
-
- // Validate file type
- const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
- if (!validTypes.includes(file.type)) {
- return;
- }
-
- // Validate file size (max 2MB for icons)
- if (file.size > 2 * 1024 * 1024) {
- return;
- }
-
- setIsUploadingIcon(true);
- try {
- // Convert to base64
- const reader = new FileReader();
- reader.onload = async () => {
- const base64Data = reader.result as string;
- const result = await getHttpApiClient().saveImageToTemp(
- base64Data,
- `project-icon-${file.name}`,
- file.type,
- currentProject.path
- );
- if (result.success && result.path) {
- handleCustomIconChange(result.path);
- }
- setIsUploadingIcon(false);
- };
- reader.readAsDataURL(file);
- } catch {
- setIsUploadingIcon(false);
- }
- };
-
- const handleRemoveCustomIcon = () => {
- handleCustomIconChange(null);
- if (fileInputRef.current) {
- fileInputRef.current.value = '';
- }
- };
-
return (
- {/* Project Details Section */}
- {currentProject && (
-
-
-
- Project Name
- handleNameChange(e.target.value)}
- placeholder="Enter project name"
- />
-
-
-
-
Project Icon
-
- Choose a preset icon or upload a custom image
-
-
- {/* Custom Icon Upload */}
-
-
- {customIconPath ? (
-
-
-
-
-
-
- ) : (
-
-
-
- )}
-
-
-
fileInputRef.current?.click()}
- disabled={isUploadingIcon}
- className="gap-1.5"
- >
-
- {isUploadingIcon ? 'Uploading...' : 'Upload Custom Icon'}
-
-
- PNG, JPG, GIF or WebP. Max 2MB.
-
-
-
-
-
- {/* Preset Icon Picker - only show if no custom icon */}
- {!customIconPath && (
-
- )}
-
-
-
- )}
-
{/* Theme Section */}
-
- Theme{' '}
-
- {currentProject ? `(for ${currentProject.name})` : '(Global)'}
-
-
+
Theme
{/* Dark/Light Tabs */}
))}
-
- {/* Project Settings - only show when a project is selected */}
- {currentProject && (
- <>
- {/* Divider */}
-
-
- {/* Project Settings Label */}
-
- Project Settings
-
-
- {/* Project Settings Items */}
-
- {PROJECT_NAV_ITEMS.map((item) => (
-
- ))}
-
- >
- )}
>
diff --git a/apps/ui/src/components/views/settings-view/config/navigation.ts b/apps/ui/src/components/views/settings-view/config/navigation.ts
index c5d5d362..107d8678 100644
--- a/apps/ui/src/components/views/settings-view/config/navigation.ts
+++ b/apps/ui/src/components/views/settings-view/config/navigation.ts
@@ -8,13 +8,11 @@ import {
Settings2,
Volume2,
FlaskConical,
- Trash2,
Workflow,
Plug,
MessageSquareText,
User,
Shield,
- Cpu,
GitBranch,
Code2,
Webhook,
@@ -84,10 +82,5 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [
// Flat list of all global nav items for backwards compatibility
export const GLOBAL_NAV_ITEMS: NavigationItem[] = GLOBAL_NAV_GROUPS.flatMap((group) => group.items);
-// Project-specific settings - only visible when a project is selected
-export const PROJECT_NAV_ITEMS: NavigationItem[] = [
- { id: 'danger', label: 'Danger Zone', icon: Trash2 },
-];
-
// Legacy export for backwards compatibility
-export const NAV_ITEMS: NavigationItem[] = [...GLOBAL_NAV_ITEMS, ...PROJECT_NAV_ITEMS];
+export const NAV_ITEMS: NavigationItem[] = GLOBAL_NAV_ITEMS;
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 2d232a65..062d2d0d 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,172 +1,14 @@
-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,
- Loader2,
- PanelBottomClose,
-} from 'lucide-react';
+import { GitBranch } from 'lucide-react';
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';
interface WorktreesSectionProps {
useWorktrees: boolean;
onUseWorktreesChange: (value: boolean) => void;
}
-interface InitScriptResponse {
- success: boolean;
- exists: boolean;
- content: string;
- path: string;
- error?: string;
-}
-
export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: WorktreesSectionProps) {
- const currentProject = useAppStore((s) => s.currentProject);
- 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);
- 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 show indicator setting
- const showIndicator = currentProject?.path
- ? getShowInitScriptIndicator(currentProject.path)
- : true;
-
- // Get the default delete branch setting
- const defaultDeleteBranch = currentProject?.path
- ? getDefaultDeleteBranch(currentProject.path)
- : false;
-
- // Get the auto-dismiss setting
- const autoDismiss = currentProject?.path
- ? getAutoDismissInitScriptIndicator(currentProject.path)
- : true;
-
- // Check if there are unsaved changes
- const hasChanges = scriptContent !== originalContent;
-
- // Load init script content when project changes
- useEffect(() => {
- if (!currentProject?.path) {
- setScriptContent('');
- setOriginalContent('');
- setScriptExists(false);
- setIsLoading(false);
- return;
- }
-
- const loadInitScript = async () => {
- setIsLoading(true);
- try {
- const response = await apiGet
(
- `/api/worktree/init-script?projectPath=${encodeURIComponent(currentProject.path)}`
- );
- if (response.success) {
- const content = response.content || '';
- setScriptContent(content);
- setOriginalContent(content);
- setScriptExists(response.exists);
- }
- } catch (error) {
- console.error('Failed to load init script:', error);
- } finally {
- setIsLoading(false);
- }
- };
-
- loadInitScript();
- }, [currentProject?.path]);
-
- // 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: 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);
- }
- }, [currentProject?.path, scriptContent]);
-
- // Reset to original content
- const handleReset = useCallback(() => {
- setScriptContent(originalContent);
- }, [originalContent]);
-
- // 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]);
-
- // Handle content change (no auto-save)
- const handleContentChange = useCallback((value: string) => {
- setScriptContent(value);
- }, []);
-
return (
Worktrees
- Configure git worktree isolation and initialization scripts.
+ Configure git worktree isolation for feature development.
@@ -212,217 +54,12 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre
- {/* Show Init Script Indicator Toggle */}
- {currentProject && (
-
-
{
- if (currentProject?.path) {
- const value = checked === true;
- setShowInitScriptIndicator(currentProject.path, value);
- // Persist to server
- try {
- const httpClient = getHttpApiClient();
- await httpClient.settings.updateProject(currentProject.path, {
- showInitScriptIndicator: value,
- });
- } catch (error) {
- console.error('Failed to persist showInitScriptIndicator:', error);
- }
- }
- }}
- className="mt-1"
- />
-
-
-
- Show Init Script Indicator
-
-
- 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 */}
- {currentProject && showIndicator && (
-
-
{
- if (currentProject?.path) {
- const value = checked === true;
- setAutoDismissInitScriptIndicator(currentProject.path, value);
- // Persist to server
- try {
- const httpClient = getHttpApiClient();
- await httpClient.settings.updateProject(currentProject.path, {
- autoDismissInitScriptIndicator: value,
- });
- } catch (error) {
- console.error('Failed to persist autoDismissInitScriptIndicator:', error);
- }
- }
- }}
- className="mt-1"
- />
-
-
- Auto-dismiss After Completion
-
-
- Automatically hide the indicator 5 seconds after the script completes.
-
-
-
- )}
-
- {/* Default Delete Branch Toggle */}
- {currentProject && (
-
-
{
- if (currentProject?.path) {
- const value = checked === true;
- setDefaultDeleteBranch(currentProject.path, value);
- // Persist to server
- try {
- const httpClient = getHttpApiClient();
- await httpClient.settings.updateProject(currentProject.path, {
- defaultDeleteBranch: value,
- });
- } catch (error) {
- console.error('Failed to persist defaultDeleteBranch:', error);
- }
- }
- }}
- className="mt-1"
- />
-
-
-
- Delete Branch by Default
-
-
- When deleting a worktree, automatically check the "Also delete the branch" option.
-
-
-
- )}
-
- {/* Separator */}
-
-
- {/* Init Script Section */}
-
-
-
-
- Initialization Script
-
-
-
- Shell commands to run after a worktree is created. Runs once per worktree. Uses Git Bash
- on Windows for cross-platform compatibility.
+ {/* Info about project-specific settings */}
+
+
+ Project-specific worktree preferences (init script, delete branch behavior) can be
+ configured in each project's settings via the sidebar.
-
- {currentProject ? (
- <>
- {/* File path indicator */}
-
-
- .automaker/worktree-init.sh
- {hasChanges && (
- (unsaved changes)
- )}
-
-
- {isLoading ? (
-
-
-
- ) : (
- <>
-
-
- {/* Action buttons */}
-
-
-
- Reset
-
-
- {isDeleting ? (
-
- ) : (
-
- )}
- Delete
-
-
- {isSaving ? (
-
- ) : (
-
- )}
- Save
-
-
- >
- )}
- >
- ) : (
-
- Select a project to configure the init script.
-
- )}
diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts
index f8a12c14..3d40883e 100644
--- a/apps/ui/src/lib/http-api-client.ts
+++ b/apps/ui/src/lib/http-api-client.ts
@@ -2175,6 +2175,9 @@ export class HttpApiClient implements ElectronAPI {
hideScrollbar: boolean;
};
worktreePanelVisible?: boolean;
+ showInitScriptIndicator?: boolean;
+ defaultDeleteBranchWithWorktree?: boolean;
+ autoDismissInitScriptIndicator?: boolean;
lastSelectedSessionId?: string;
};
error?: string;
diff --git a/apps/ui/src/routes/project-settings.tsx b/apps/ui/src/routes/project-settings.tsx
new file mode 100644
index 00000000..e933d58d
--- /dev/null
+++ b/apps/ui/src/routes/project-settings.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { ProjectSettingsView } from '@/components/views/project-settings-view';
+
+export const Route = createFileRoute('/project-settings')({
+ component: ProjectSettingsView,
+});
diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts
index 8fcbd203..b05e6697 100644
--- a/apps/ui/src/store/app-store.ts
+++ b/apps/ui/src/store/app-store.ts
@@ -231,6 +231,7 @@ export interface KeyboardShortcuts {
context: string;
memory: string;
settings: string;
+ projectSettings: string;
terminal: string;
ideation: string;
notifications: string;
@@ -267,6 +268,7 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
context: 'C',
memory: 'Y',
settings: 'S',
+ projectSettings: 'Shift+S',
terminal: 'T',
ideation: 'I',
notifications: 'X',
@@ -732,6 +734,10 @@ export interface AppState {
// Whether to auto-dismiss the indicator after completion (default: true)
autoDismissInitScriptIndicatorByProject: Record