fix: improve delete worktree failure toast messages (#319)

- Show user-friendly guidance instead of raw git stderr when worktree
  deletion fails due to changed files, directing users to Force Delete.
- Extract toast message logic to reusable getDeleteWorktreeToastCopy
  function with comprehensive test coverage.
This commit is contained in:
Jinjing
2026-04-05 17:59:09 -07:00
committed by GitHub
parent a4ac7db972
commit b82a8a62dc
3 changed files with 60 additions and 3 deletions
@@ -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 (
@@ -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
})
})
})
@@ -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
}
}