diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 028f7bd1..683c46c5 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -119,6 +119,7 @@ export function BoardView() { (state) => state.showInitScriptIndicatorByProject ); const getShowInitScriptIndicator = useAppStore((state) => state.getShowInitScriptIndicator); + const getDefaultDeleteBranch = useAppStore((state) => state.getDefaultDeleteBranch); const shortcuts = useKeyboardShortcutsConfig(); const { features: hookFeatures, @@ -1513,6 +1514,7 @@ export function BoardView() { ? hookFeatures.filter((f) => f.branchName === selectedWorktreeForAction.branch).length : 0 } + defaultDeleteBranch={getDefaultDeleteBranch(currentProject.path)} onDeleted={(deletedWorktree, _deletedBranch) => { // Reset features that were assigned to the deleted worktree (by branch) hookFeatures.forEach((feature) => { diff --git a/apps/ui/src/components/views/board-view/dialogs/delete-worktree-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/delete-worktree-dialog.tsx index 3c45c014..718bef0c 100644 --- a/apps/ui/src/components/views/board-view/dialogs/delete-worktree-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/delete-worktree-dialog.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Dialog, DialogContent, @@ -30,6 +30,8 @@ interface DeleteWorktreeDialogProps { onDeleted: (deletedWorktree: WorktreeInfo, deletedBranch: boolean) => void; /** Number of features assigned to this worktree's branch */ affectedFeatureCount?: number; + /** Default value for the "delete branch" checkbox */ + defaultDeleteBranch?: boolean; } export function DeleteWorktreeDialog({ @@ -39,10 +41,18 @@ export function DeleteWorktreeDialog({ worktree, onDeleted, affectedFeatureCount = 0, + defaultDeleteBranch = false, }: DeleteWorktreeDialogProps) { - const [deleteBranch, setDeleteBranch] = useState(false); + const [deleteBranch, setDeleteBranch] = useState(defaultDeleteBranch); const [isLoading, setIsLoading] = useState(false); + // Reset deleteBranch to default when dialog opens + useEffect(() => { + if (open) { + setDeleteBranch(defaultDeleteBranch); + } + }, [open, defaultDeleteBranch]); + const handleDelete = async () => { if (!worktree) return; diff --git a/apps/ui/src/components/views/board-view/init-script-indicator.tsx b/apps/ui/src/components/views/board-view/init-script-indicator.tsx index ca108dbd..a039f58b 100644 --- a/apps/ui/src/components/views/board-view/init-script-indicator.tsx +++ b/apps/ui/src/components/views/board-view/init-script-indicator.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; import { Terminal, Check, X, Loader2, ChevronDown, ChevronUp } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useAppStore, type InitScriptState } from '@/store/app-store'; @@ -8,45 +8,38 @@ interface InitScriptIndicatorProps { projectPath: string; } -export function InitScriptIndicator({ projectPath }: InitScriptIndicatorProps) { - const initScriptState = useAppStore((s) => s.initScriptState[projectPath]); - const clearInitScriptState = useAppStore((s) => s.clearInitScriptState); +interface SingleIndicatorProps { + stateKey: string; + state: InitScriptState; + onDismiss: (key: string) => void; + isOnlyOne: boolean; // Whether this is the only indicator shown +} + +function SingleIndicator({ stateKey, state, onDismiss, isOnlyOne }: SingleIndicatorProps) { const [showLogs, setShowLogs] = useState(false); - const [dismissed, setDismissed] = useState(false); const logsEndRef = useRef(null); + const { status, output, branch, error } = state; + // Auto-scroll to bottom when new output arrives useEffect(() => { if (showLogs && logsEndRef.current) { logsEndRef.current.scrollIntoView({ behavior: 'smooth' }); } - }, [initScriptState?.output, showLogs]); + }, [output, showLogs]); - // Reset dismissed state when a new script starts + // Auto-expand logs when script starts (only if it's the only one or running) useEffect(() => { - if (initScriptState?.status === 'running') { - setDismissed(false); + if (status === 'running' && isOnlyOne) { setShowLogs(true); } - }, [initScriptState?.status]); + }, [status, isOnlyOne]); - if (!initScriptState || dismissed) return null; - if (initScriptState.status === 'idle') return null; - - const { status, output, branch, error } = initScriptState; - - const handleDismiss = () => { - setDismissed(true); - // Clear state after a delay to allow for future scripts - setTimeout(() => { - clearInitScriptState(projectPath); - }, 100); - }; + if (status === 'idle') return null; return (
- {status === 'running' && ( - - )} + {status === 'running' && } {status === 'success' && } {status === 'failed' && } Init Script{' '} - {status === 'running' - ? 'Running' - : status === 'success' - ? 'Completed' - : 'Failed'} + {status === 'running' ? 'Running' : status === 'success' ? 'Completed' : 'Failed'}
@@ -83,7 +70,7 @@ export function InitScriptIndicator({ projectPath }: InitScriptIndicatorProps) { {status !== 'running' && (
)} - {error && ( -
- Error: {error} -
- )} + {error &&
Error: {error}
}
@@ -125,9 +108,7 @@ export function InitScriptIndicator({ projectPath }: InitScriptIndicatorProps) {
{status === 'success' @@ -138,3 +119,69 @@ export function InitScriptIndicator({ projectPath }: InitScriptIndicatorProps) {
); } + +export function InitScriptIndicator({ projectPath }: InitScriptIndicatorProps) { + const getInitScriptStatesForProject = useAppStore((s) => s.getInitScriptStatesForProject); + const clearInitScriptState = useAppStore((s) => s.clearInitScriptState); + const [dismissedKeys, setDismissedKeys] = useState>(new Set()); + + // Get all init script states for this project + const allStates = getInitScriptStatesForProject(projectPath); + + // Filter out dismissed and idle states + const activeStates = allStates.filter( + ({ key, state }) => !dismissedKeys.has(key) && state.status !== 'idle' + ); + + // Reset dismissed keys when a new script starts for a branch + useEffect(() => { + const runningKeys = allStates + .filter(({ state }) => state.status === 'running') + .map(({ key }) => key); + + if (runningKeys.length > 0) { + setDismissedKeys((prev) => { + const newSet = new Set(prev); + runningKeys.forEach((key) => newSet.delete(key)); + return newSet; + }); + } + }, [allStates]); + + const handleDismiss = useCallback( + (key: string) => { + setDismissedKeys((prev) => new Set(prev).add(key)); + // Extract branch from key (format: "projectPath::branch") + const branch = key.split('::')[1]; + if (branch) { + // Clear state after a delay to allow for future scripts + setTimeout(() => { + clearInitScriptState(projectPath, branch); + }, 100); + } + }, + [projectPath, clearInitScriptState] + ); + + if (activeStates.length === 0) return null; + + return ( +
+ {activeStates.map(({ key, state }) => ( + + ))} +
+ ); +} 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 01ff77c6..4a281995 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 @@ -35,6 +35,8 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre 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 [scriptContent, setScriptContent] = useState(''); const [originalContent, setOriginalContent] = useState(''); const [scriptExists, setScriptExists] = useState(false); @@ -47,6 +49,11 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre ? getShowInitScriptIndicator(currentProject.path) : true; + // Get the default delete branch setting + const defaultDeleteBranch = currentProject?.path + ? getDefaultDeleteBranch(currentProject.path) + : false; + // Check if there are unsaved changes const hasChanges = scriptContent !== originalContent; @@ -226,6 +233,34 @@ export function WorktreesSection({ useWorktrees, onUseWorktreesChange }: Worktre )} + {/* Default Delete Branch Toggle */} + {currentProject && ( +
+ { + if (currentProject?.path) { + setDefaultDeleteBranch(currentProject.path, checked === true); + } + }} + className="mt-1" + /> +
+ +

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

+
+
+ )} + {/* Separator */}
diff --git a/apps/ui/src/hooks/use-init-script-events.ts b/apps/ui/src/hooks/use-init-script-events.ts index b95b485d..aa51409f 100644 --- a/apps/ui/src/hooks/use-init-script-events.ts +++ b/apps/ui/src/hooks/use-init-script-events.ts @@ -50,7 +50,7 @@ export function useInitScriptEvents(projectPath: string | null) { switch (event.type) { case 'worktree:init-started': { const startPayload = payload as InitScriptStartedPayload; - setInitScriptState(projectPath, { + setInitScriptState(projectPath, startPayload.branch, { status: 'running', branch: startPayload.branch, output: [], @@ -60,12 +60,12 @@ export function useInitScriptEvents(projectPath: string | null) { } case 'worktree:init-output': { const outputPayload = payload as InitScriptOutputPayload; - appendInitScriptOutput(projectPath, outputPayload.content); + appendInitScriptOutput(projectPath, outputPayload.branch, outputPayload.content); break; } case 'worktree:init-completed': { const completePayload = payload as InitScriptCompletedPayload; - setInitScriptState(projectPath, { + setInitScriptState(projectPath, completePayload.branch, { status: completePayload.success ? 'success' : 'failed', error: completePayload.error, }); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 974ffe87..2d9f42da 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -669,6 +669,10 @@ export interface AppState { // Whether to show the floating init script indicator panel (default: true) showInitScriptIndicatorByProject: Record; + // Default Delete Branch With Worktree (per-project, keyed by project path) + // Whether to default the "delete branch" checkbox when deleting a worktree (default: false) + defaultDeleteBranchByProject: Record; + // UI State (previously in localStorage, now synced via API) /** Whether worktree panel is collapsed in board view */ worktreePanelCollapsed: boolean; @@ -677,7 +681,7 @@ export interface AppState { /** Recently accessed folders for quick access */ recentFolders: string[]; - // Init Script State (per-project, keyed by project path) + // Init Script State (keyed by "projectPath::branch" to support concurrent scripts) initScriptState: Record; } @@ -1086,6 +1090,10 @@ export interface AppActions { setShowInitScriptIndicator: (projectPath: string, visible: boolean) => void; getShowInitScriptIndicator: (projectPath: string) => boolean; + // Default Delete Branch actions (per-project) + setDefaultDeleteBranch: (projectPath: string, deleteBranch: boolean) => void; + getDefaultDeleteBranch: (projectPath: string) => boolean; + // UI State actions (previously in localStorage, now synced via API) setWorktreePanelCollapsed: (collapsed: boolean) => void; setLastProjectDir: (dir: string) => void; @@ -1114,11 +1122,18 @@ export interface AppActions { }> ) => void; - // Init Script State actions - setInitScriptState: (projectPath: string, state: Partial) => void; - appendInitScriptOutput: (projectPath: string, content: string) => void; - clearInitScriptState: (projectPath: string) => void; - getInitScriptState: (projectPath: string) => InitScriptState | null; + // Init Script State actions (keyed by projectPath::branch to support concurrent scripts) + setInitScriptState: ( + projectPath: string, + branch: string, + state: Partial + ) => void; + appendInitScriptOutput: (projectPath: string, branch: string, content: string) => void; + clearInitScriptState: (projectPath: string, branch: string) => void; + getInitScriptState: (projectPath: string, branch: string) => InitScriptState | null; + getInitScriptStatesForProject: ( + projectPath: string + ) => Array<{ key: string; state: InitScriptState }>; // Reset reset: () => void; @@ -1217,6 +1232,7 @@ const initialState: AppState = { pipelineConfigByProject: {}, worktreePanelVisibleByProject: {}, showInitScriptIndicatorByProject: {}, + defaultDeleteBranchByProject: {}, // UI State (previously in localStorage, now synced via API) worktreePanelCollapsed: false, lastProjectDir: '', @@ -3148,6 +3164,21 @@ export const useAppStore = create()((set, get) => ({ return get().showInitScriptIndicatorByProject[projectPath] ?? true; }, + // Default Delete Branch actions (per-project) + setDefaultDeleteBranch: (projectPath, deleteBranch) => { + set({ + defaultDeleteBranchByProject: { + ...get().defaultDeleteBranchByProject, + [projectPath]: deleteBranch, + }, + }); + }, + + getDefaultDeleteBranch: (projectPath) => { + // Default to false (don't delete branch) if not set + return get().defaultDeleteBranchByProject[projectPath] ?? false; + }, + // UI State actions (previously in localStorage, now synced via API) setWorktreePanelCollapsed: (collapsed) => set({ worktreePanelCollapsed: collapsed }), setLastProjectDir: (dir) => set({ lastProjectDir: dir }), @@ -3161,28 +3192,30 @@ export const useAppStore = create()((set, get) => ({ set({ recentFolders: updated }); }, - // Init Script State actions - setInitScriptState: (projectPath, state) => { - const current = get().initScriptState[projectPath] || { + // Init Script State actions (keyed by "projectPath::branch") + setInitScriptState: (projectPath, branch, state) => { + const key = `${projectPath}::${branch}`; + const current = get().initScriptState[key] || { status: 'idle', - branch: '', + branch, output: [], }; set({ initScriptState: { ...get().initScriptState, - [projectPath]: { ...current, ...state }, + [key]: { ...current, ...state }, }, }); }, - appendInitScriptOutput: (projectPath, content) => { - const current = get().initScriptState[projectPath]; + appendInitScriptOutput: (projectPath, branch, content) => { + const key = `${projectPath}::${branch}`; + const current = get().initScriptState[key]; if (!current) return; set({ initScriptState: { ...get().initScriptState, - [projectPath]: { + [key]: { ...current, output: [...current.output, content], }, @@ -3190,13 +3223,23 @@ export const useAppStore = create()((set, get) => ({ }); }, - clearInitScriptState: (projectPath) => { - const { [projectPath]: _, ...rest } = get().initScriptState; + clearInitScriptState: (projectPath, branch) => { + const key = `${projectPath}::${branch}`; + const { [key]: _, ...rest } = get().initScriptState; set({ initScriptState: rest }); }, - getInitScriptState: (projectPath) => { - return get().initScriptState[projectPath] || null; + getInitScriptState: (projectPath, branch) => { + const key = `${projectPath}::${branch}`; + return get().initScriptState[key] || null; + }, + + getInitScriptStatesForProject: (projectPath) => { + const prefix = `${projectPath}::`; + const states = get().initScriptState; + return Object.entries(states) + .filter(([key]) => key.startsWith(prefix)) + .map(([key, state]) => ({ key, state })); }, // Reset diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index 7a46d4aa..1b8f37d4 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -598,6 +598,10 @@ export interface ProjectSettings { /** Whether to show the init script indicator panel (default: true) */ showInitScriptIndicator?: boolean; + // Worktree Behavior + /** Default value for "delete branch" checkbox when deleting a worktree (default: false) */ + defaultDeleteBranchWithWorktree?: boolean; + // Session Tracking /** Last chat session selected in this project */ lastSelectedSessionId?: string;