diff --git a/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx index 97fd236568e..1e84f7532d9 100644 --- a/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx +++ b/src/renderer/src/components/sidebar/DeleteWorktreeDialog.tsx @@ -11,6 +11,7 @@ import { Button } from '@/components/ui/button' import { AlertTriangle, LoaderCircle, Trash2 } from 'lucide-react' import { useAppStore } from '@/store' import { toast } from 'sonner' +import { getDeleteWorktreeToastCopy } from './delete-worktree-toast' const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { const activeModal = useAppStore((s) => s.activeModal) @@ -32,6 +33,7 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { const isDeleting = deleteState?.isDeleting ?? false const deleteError = deleteState?.error ?? null const canForceDelete = deleteState?.canForceDelete ?? false + const worktreeName = worktree?.displayName ?? 'unknown' useEffect(() => { if (isOpen && worktreeId && !worktree && !isDeleting) { @@ -66,8 +68,14 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { .then((result) => { if (!result.ok) { const state = useAppStore.getState().deleteStateByWorktreeId[targetWorktreeId] - toast.error('Failed to delete worktree', { - description: result.error, + const toastCopy = getDeleteWorktreeToastCopy( + worktreeName, + state?.canForceDelete ?? false, + result.error + ) + const showToast = toastCopy.isDestructive ? toast.error : toast.info + showToast(toastCopy.title, { + description: toastCopy.description, duration: 10000, action: state?.canForceDelete ? { @@ -97,7 +105,7 @@ const DeleteWorktreeDialog = React.memo(function DeleteWorktreeDialog() { }) closeModal() }, - [closeModal, removeWorktree, worktreeId] + [closeModal, removeWorktree, worktreeId, worktreeName] ) return ( diff --git a/src/renderer/src/components/sidebar/delete-worktree-toast.test.ts b/src/renderer/src/components/sidebar/delete-worktree-toast.test.ts new file mode 100644 index 00000000000..24a3ac9b508 --- /dev/null +++ b/src/renderer/src/components/sidebar/delete-worktree-toast.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest' +import { getDeleteWorktreeToastCopy } from './delete-worktree-toast' + +describe('getDeleteWorktreeToastCopy', () => { + it('uses direct guidance when force delete is available', () => { + expect(getDeleteWorktreeToastCopy('feature/foo', true, 'branch has changes')).toEqual({ + title: 'Failed to delete worktree feature/foo', + description: 'It has changed files. Use Force Delete to delete it anyway.', + isDestructive: false + }) + }) + + it('preserves the raw error when force delete is unavailable', () => { + expect(getDeleteWorktreeToastCopy('feature/foo', false, 'permission denied')).toEqual({ + title: 'Failed to delete worktree feature/foo', + description: 'permission denied', + isDestructive: true + }) + }) +}) diff --git a/src/renderer/src/components/sidebar/delete-worktree-toast.ts b/src/renderer/src/components/sidebar/delete-worktree-toast.ts new file mode 100644 index 00000000000..cb0899df0d4 --- /dev/null +++ b/src/renderer/src/components/sidebar/delete-worktree-toast.ts @@ -0,0 +1,29 @@ +export type DeleteWorktreeToastCopy = { + title: string + description?: string + isDestructive: boolean +} + +export function getDeleteWorktreeToastCopy( + worktreeName: string, + canForceDelete: boolean, + error: string +): DeleteWorktreeToastCopy { + if (canForceDelete) { + return { + title: `Failed to delete worktree ${worktreeName}`, + description: 'It has changed files. Use Force Delete to delete it anyway.', + // Why: git commonly refuses the first delete when the worktree still has + // modified or untracked files. Showing raw stderr in a destructive toast + // made a normal cleanup step look like an Orca bug, so this common case + // gets a concise explanation plus the force-delete path instead. + isDestructive: false + } + } + + return { + title: `Failed to delete worktree ${worktreeName}`, + description: error, + isDestructive: true + } +}