diff --git a/src/main/ipc/settings.test.ts b/src/main/ipc/settings.test.ts index 80757d55c92..5efe4e7d396 100644 --- a/src/main/ipc/settings.test.ts +++ b/src/main/ipc/settings.test.ts @@ -258,6 +258,23 @@ describe('registerSettingsHandlers', () => { ) }) + it('flushes the preserved-branch deletion preference before returning', async () => { + store.getSettings.mockReturnValue({ alwaysForceDeletePreservedBranches: false }) + store.updateSettingsAndFlush.mockRejectedValue(new Error('Disk full')) + registerSettingsHandlers(store as never) + const handler = handleMock.mock.calls.find((call) => call[0] === 'settings:set')?.[1] + if (typeof handler !== 'function') { + throw new Error('Missing settings:set handler') + } + await expect( + handler(settingsInvokeEvent, { alwaysForceDeletePreservedBranches: true }) + ).rejects.toThrow('Disk full') + expect(store.updateSettingsAndFlush).toHaveBeenCalledWith( + { alwaysForceDeletePreservedBranches: true }, + { notifyListeners: true, originWebContentsId: 1 } + ) + }) + it('persists Active Server only through the dedicated preference channel', () => { store.updateSettings.mockReturnValue({ activeRuntimeEnvironmentId: 'windows-2' }) registerSettingsHandlers(store as never) diff --git a/src/main/ipc/settings.ts b/src/main/ipc/settings.ts index 8bf7af7142f..23bf11fde21 100644 --- a/src/main/ipc/settings.ts +++ b/src/main/ipc/settings.ts @@ -205,7 +205,8 @@ export function registerSettingsHandlers( originWebContentsId: event.sender.id } const result = - 'alwaysForceDeleteWorktrees' in sanitizedArgs + 'alwaysForceDeleteWorktrees' in sanitizedArgs || + 'alwaysForceDeletePreservedBranches' in sanitizedArgs ? await store.updateSettingsAndFlush(sanitizedArgs, updateOptions) : store.updateSettings(sanitizedArgs, updateOptions) const proxySettingsChanged = diff --git a/src/renderer/src/components/sidebar/WorktreeForceDeleteButton.tsx b/src/renderer/src/components/sidebar/WorktreeForceDeleteButton.tsx index 2c7e28f0670..0ee9b649704 100644 --- a/src/renderer/src/components/sidebar/WorktreeForceDeleteButton.tsx +++ b/src/renderer/src/components/sidebar/WorktreeForceDeleteButton.tsx @@ -20,6 +20,11 @@ export function WorktreeForceDeleteButton({ size = 'sm', disabled = false, buttonRef, + fullWidth = false, + menuSide, + moreActionsLabel, + alwaysForceDeleteLabel, + alwaysForceDeleteTooltip, children }: { toastId?: string @@ -29,6 +34,11 @@ export function WorktreeForceDeleteButton({ size?: 'sm' | 'default' disabled?: boolean buttonRef?: Ref + fullWidth?: boolean + menuSide?: 'top' | 'bottom' + moreActionsLabel?: string + alwaysForceDeleteLabel?: string + alwaysForceDeleteTooltip?: string children?: ReactNode }): React.JSX.Element { const [isSaving, setIsSaving] = useState(false) @@ -62,6 +72,7 @@ export function WorktreeForceDeleteButton({ return ( { if ( event.relatedTarget instanceof HTMLElement && @@ -77,6 +88,7 @@ export function WorktreeForceDeleteButton({ variant="destructive" size={size} disabled={disabled || isSaving} + className={fullWidth ? 'min-w-0 flex-1' : undefined} onClick={forceDelete} > {children ?? @@ -89,7 +101,10 @@ export function WorktreeForceDeleteButton({ variant="destructive" size={size === 'sm' ? 'icon-sm' : 'icon'} disabled={disabled || isSaving} - aria-label={translate('workspaceDeletion.moreActions', 'More force delete options')} + aria-label={ + moreActionsLabel ?? + translate('workspaceDeletion.moreActions', 'More force delete options') + } > @@ -99,6 +114,7 @@ export function WorktreeForceDeleteButton({ {onAlwaysForceDelete ? ( event.stopPropagation()} onBlur={(event) => event.stopPropagation()} @@ -111,14 +127,16 @@ export function WorktreeForceDeleteButton({ void alwaysForceDelete() }} > - {translate('workspaceDeletion.alwaysForceDelete', 'Always force delete')} + {alwaysForceDeleteLabel ?? + translate('workspaceDeletion.alwaysForceDelete', 'Always force delete')} - {translate( - 'workspaceDeletion.alwaysForceDeleteTooltip', - 'Applies to all future workspace deletions.' - )} + {alwaysForceDeleteTooltip ?? + translate( + 'workspaceDeletion.alwaysForceDeleteTooltip', + 'Applies to all future workspace deletions.' + )} diff --git a/src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx b/src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx index 3424c372581..b02d5f0747f 100644 --- a/src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx +++ b/src/renderer/src/components/sidebar/preserved-branch-toast.test.tsx @@ -4,13 +4,15 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, describe, expect, it, vi } from 'vitest' import { toast } from 'sonner' +import { TooltipProvider } from '../ui/tooltip' import type { RemoveWorktreeResult } from '../../../../shared/worktree/create-types' import { showPreservedBranchToast } from './preserved-branch-toast' vi.mock('sonner', () => ({ toast: { warning: vi.fn(), - dismiss: vi.fn() + dismiss: vi.fn(), + error: vi.fn() } })) @@ -24,7 +26,7 @@ function renderToastBody(): HTMLElement { const root = createRoot(container) mountedRoots.push(root) act(() => { - root.render(description) + root.render({description}) }) return container } @@ -102,4 +104,55 @@ describe('showPreservedBranchToast', () => { expect.not.objectContaining({ duration: Infinity }) ) }) + + it('saves the future branch choice before deleting the current branch', async () => { + const savePreference = vi.fn().mockResolvedValue(undefined) + const onForceDelete = vi.fn() + showPreservedBranchToast( + { preservedBranch: { branchName: 'feature/test', head: 'abc123' } }, + undefined, + onForceDelete, + savePreference + ) + const body = renderToastBody() + const trigger = body.querySelector('button[aria-label="More branch delete options"]') + expect(trigger).not.toBeNull() + + await act(async () => { + trigger?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })) + }) + const option = document.querySelector('[role="menuitem"]') + expect(option?.textContent).toBe('Always delete preserved branches') + await act(async () => { + option?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(savePreference).toHaveBeenCalledOnce() + expect(onForceDelete).toHaveBeenCalledWith('feature/test', 'abc123') + }) + + it('keeps the branch and toast when saving the future choice fails', async () => { + const savePreference = vi.fn().mockRejectedValue(new Error('disk full')) + const onForceDelete = vi.fn() + showPreservedBranchToast( + { preservedBranch: { branchName: 'feature/test', head: 'abc123' } }, + undefined, + onForceDelete, + savePreference + ) + const body = renderToastBody() + await act(async () => { + body + .querySelector('button[aria-label="More branch delete options"]') + ?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })) + }) + await act(async () => { + document + .querySelector('[role="menuitem"]') + ?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(onForceDelete).not.toHaveBeenCalled() + expect(toast.dismiss).not.toHaveBeenCalled() + }) }) diff --git a/src/renderer/src/components/sidebar/preserved-branch-toast.tsx b/src/renderer/src/components/sidebar/preserved-branch-toast.tsx index 43b01513f53..54d4106988e 100644 --- a/src/renderer/src/components/sidebar/preserved-branch-toast.tsx +++ b/src/renderer/src/components/sidebar/preserved-branch-toast.tsx @@ -3,7 +3,7 @@ import { toast } from 'sonner' import type { RemoveWorktreeResult } from '../../../../shared/worktree/create-types' import type { Worktree } from '../../../../shared/worktree/types' import { translate } from '@/i18n/i18n' -import { Button } from '../ui/button' +import { WorktreeForceDeleteButton } from './WorktreeForceDeleteButton' type PreservedBranchWorktree = Pick @@ -11,6 +11,7 @@ type PreservedBranchToastBodyProps = { description: string forceDeleteLabel: string | undefined onForceDelete: (() => void) | undefined + onAlwaysForceDelete: (() => Promise) | undefined } function preservedBranchToastId(branchName: string, expectedHead: string | undefined): string { @@ -53,7 +54,8 @@ function getPreservedBranchDescription( function PreservedBranchToastBody({ description, forceDeleteLabel, - onForceDelete + onForceDelete, + onAlwaysForceDelete }: PreservedBranchToastBodyProps): React.JSX.Element { return (
@@ -61,17 +63,28 @@ function PreservedBranchToastBody({ {description}

{forceDeleteLabel && onForceDelete ? ( -
- +
) : null}
@@ -81,7 +94,8 @@ function PreservedBranchToastBody({ export function showPreservedBranchToast( result: RemoveWorktreeResult | undefined, worktree: PreservedBranchWorktree | undefined, - onForceDelete: (branchName: string, expectedHead: string) => void + onForceDelete: (branchName: string, expectedHead: string) => void, + onAlwaysForceDelete?: () => Promise ): void { const preservedBranch = result?.preservedBranch const branch = preservedBranch?.branchName @@ -111,6 +125,7 @@ export function showPreservedBranchToast( description={description} forceDeleteLabel={forceDeleteLabel} onForceDelete={forceDelete} + onAlwaysForceDelete={onAlwaysForceDelete} /> ), dismissible: true, diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index a174bca3ce6..26e157bc0cd 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -19925,7 +19925,10 @@ "preferenceSaveFailed": "Could not save deletion preference", "alwaysForceDelete": "Always force delete", "moreActions": "More force delete options", - "alwaysForceDeleteTooltip": "Applies to all future workspace deletions." + "alwaysForceDeleteTooltip": "Applies to all future workspace deletions.", + "moreBranchActions": "More branch delete options", + "alwaysForceDeleteBranches": "Always delete preserved branches", + "alwaysForceDeleteBranchesTooltip": "Applies to branches kept after future workspace deletions." }, "worktree": { "nestedRemoval": { diff --git a/src/renderer/src/store/slices/worktrees-always-force-delete.test.ts b/src/renderer/src/store/slices/worktrees-always-force-delete.test.ts index ebe52c37af8..2e552ca81ba 100644 --- a/src/renderer/src/store/slices/worktrees-always-force-delete.test.ts +++ b/src/renderer/src/store/slices/worktrees-always-force-delete.test.ts @@ -115,3 +115,90 @@ it('keeps forgetting a disconnected workspace metadata-only when the preference expect(mockApi.worktrees.remove).not.toHaveBeenCalled() expect(runtimeEnvironmentCall).not.toHaveBeenCalled() }) + +it('deletes a preserved branch with its saved head when the branch preference is enabled', async () => { + const store = createTestStore() + const wt = makeWorktree({ id: 'repo1::/path/wt1', repoId: 'repo1' }) + store.setState({ + settings: { + ...getDefaultSettings('/home/test'), + alwaysForceDeletePreservedBranches: true + }, + worktreesByRepo: { [wt.repoId]: [wt] } + }) + mockApi.worktrees.remove.mockResolvedValueOnce({ + preservedBranch: { branchName: 'feature/test', head: 'abc123' } + }) + + expect(await store.getState().removeWorktree({ id: wt.id, executionHostId: null })).toEqual({ + ok: true + }) + expect(mockApi.worktrees.forceDeletePreservedBranch).toHaveBeenCalledWith({ + worktreeId: wt.id, + branchName: 'feature/test', + expectedHead: 'abc123', + hostId: 'local' + }) +}) + +it('keeps a recoverable branch when automatic deletion fails', async () => { + const store = createTestStore() + const wt = makeWorktree({ id: 'repo1::/path/wt1', repoId: 'repo1' }) + store.setState({ + settings: { + ...getDefaultSettings('/home/test'), + alwaysForceDeletePreservedBranches: true + }, + worktreesByRepo: { [wt.repoId]: [wt] } + }) + mockApi.worktrees.remove.mockResolvedValueOnce({ + preservedBranch: { branchName: 'feature/test', head: 'abc123' } + }) + mockApi.worktrees.forceDeletePreservedBranch.mockRejectedValueOnce(new Error('branch moved')) + + expect(await store.getState().removeWorktree({ id: wt.id, executionHostId: null })).toEqual({ + ok: true, + preservedBranch: { branchName: 'feature/test', head: 'abc123', hostId: 'local' } + }) +}) + +it('routes automatic branch deletion through the owner runtime', async () => { + const store = createTestStore() + const wt = makeWorktree({ + id: 'repo1::/path/wt1', + repoId: 'repo1', + hostId: 'runtime:env-1', + runtimeOwnerEnvironmentId: 'env-1' + }) + store.setState({ + settings: { + ...getDefaultSettings('/home/test'), + alwaysForceDeletePreservedBranches: true + }, + worktreesByRepo: { [wt.repoId]: [wt] } + }) + runtimeEnvironmentCall.mockImplementation(({ method }) => ({ + id: 'rpc-removal', + ok: true, + result: + method === 'worktree.rm' + ? { preservedBranch: { branchName: 'feature/remote', head: 'abc123' } } + : { deleted: true }, + _meta: { runtimeId: 'runtime-remote' } + })) + + expect( + await store.getState().removeWorktree({ id: wt.id, executionHostId: 'runtime:env-1' }) + ).toEqual({ ok: true }) + expect(runtimeEnvironmentCall).toHaveBeenCalledWith( + expect.objectContaining({ + selector: 'env-1', + method: 'worktree.forceDeleteBranch', + params: expect.objectContaining({ + worktree: `id:${wt.id}`, + branchName: 'feature/remote', + expectedHead: 'abc123' + }) + }) + ) +}) diff --git a/src/renderer/src/store/slices/worktrees/teardown/complete-preserved-branch-cleanup.ts b/src/renderer/src/store/slices/worktrees/teardown/complete-preserved-branch-cleanup.ts new file mode 100644 index 00000000000..d378f25bc8d --- /dev/null +++ b/src/renderer/src/store/slices/worktrees/teardown/complete-preserved-branch-cleanup.ts @@ -0,0 +1,53 @@ +import type { RemoveWorktreeResult } from '../../../../../../shared/worktree/create-types' +import type { Worktree } from '../../../../../../shared/worktree/types' +import type { WorktreeSlice } from '../../worktree-helpers' +import type { WorktreeSliceGet } from '../listing/worktree-slice-types' +import { showPreservedBranchToast } from '@/components/sidebar/preserved-branch-toast' + +export async function completePreservedBranchCleanup({ + get, + worktreeId, + removalResult, + worktreeBeforeRemoval, + branchCleanupOptions, + suppressToast +}: { + get: WorktreeSliceGet + worktreeId: string + removalResult: RemoveWorktreeResult | undefined + worktreeBeforeRemoval: Pick | undefined + branchCleanupOptions: NonNullable[3]> + suppressToast: boolean +}): Promise { + const preservedBranch = removalResult?.preservedBranch + if (!preservedBranch) { + return false + } + const autoDeleted = + preservedBranch.head && get().settings?.alwaysForceDeletePreservedBranches === true + ? ( + await get().forceDeletePreservedBranch( + worktreeId, + preservedBranch.branchName, + preservedBranch.head, + { ...branchCleanupOptions, suppressToast: true } + ) + ).ok + : false + if (!autoDeleted && !suppressToast) { + showPreservedBranchToast( + removalResult, + worktreeBeforeRemoval, + (branch, expectedHead) => { + void get().forceDeletePreservedBranch( + worktreeId, + branch, + expectedHead, + branchCleanupOptions + ) + }, + () => get().updateSettingsOrThrow({ alwaysForceDeletePreservedBranches: true }) + ) + } + return autoDeleted +} diff --git a/src/renderer/src/store/slices/worktrees/teardown/host-qualified-worktree-removal.ts b/src/renderer/src/store/slices/worktrees/teardown/host-qualified-worktree-removal.ts index 5e396658d8e..ae782379de8 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/host-qualified-worktree-removal.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/host-qualified-worktree-removal.ts @@ -7,6 +7,7 @@ import type { WorktreeSliceGet, WorktreeSliceSet } from '../listing/worktree-slice-types' import type { RemoveWorktreeResult } from '../../../../../../shared/worktree/create-types' import type { WorktreeSlice } from '../../worktree-helpers' +import type { Worktree } from '../../../../../../shared/worktree/types' import type { RuntimeClientTarget } from '../../../../../../shared/runtime-client-target' import type { ExecutionHostId } from '../../../../../../shared/execution-host' import { @@ -25,7 +26,7 @@ import { import { translate } from '@/i18n/i18n' import { cleanupEphemeralVmRuntimesForDeleted } from '@/lib/ephemeral-vm-runtime-cleanup' import { purgeOrphanedRuntimeSshProjects } from './orphaned-runtime-ssh-project-purge' -import { showPreservedBranchToast } from '@/components/sidebar/preserved-branch-toast' +import { completePreservedBranchCleanup } from './complete-preserved-branch-cleanup' import { preservedBranchCleanupKey } from '../../../../../../shared/preserved-branch-cleanup' import { preservedBranchRuntimeTargetByCleanupKey } from './preserved-branch-cleanup-target' @@ -39,7 +40,7 @@ import { export { prepareHostScopedRemovalCompletion, preservesSameIdRendererState } -type PreservedBranchWorktree = Parameters[1] +type PreservedBranchWorktree = Pick | undefined type RemoveWorktreeSliceResult = Awaited> /** Route at the confirmed host when there is one, else the ordinary active-host route. */ @@ -251,16 +252,25 @@ export async function completeSameIdHostScopedRemoval(args: { cleanup, target }) - if (!suppressPreservedBranchToast) { - showPreservedBranchToast(removalResult, worktreeBeforeRemoval, (branch, expectedHead) => { - void get().forceDeletePreservedBranch(worktreeId, branch, expectedHead, { - hostId: requiredExecutionHostId, - ...runtimeEnvironment - }) - }) - } + const branchCleanupOptions = { hostId: requiredExecutionHostId, ...runtimeEnvironment } + const autoDeletedBranch = await completePreservedBranchCleanup({ + get, + worktreeId, + removalResult, + worktreeBeforeRemoval, + branchCleanupOptions, + suppressToast: suppressPreservedBranchToast + }) return { ok: true as const, - preservedBranch: { ...preservedBranch, hostId: requiredExecutionHostId, ...runtimeEnvironment } + ...(!autoDeletedBranch + ? { + preservedBranch: { + ...preservedBranch, + hostId: requiredExecutionHostId, + ...runtimeEnvironment + } + } + : {}) } } diff --git a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts index 5c7f0dc5f21..1f788a54352 100644 --- a/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts +++ b/src/renderer/src/store/slices/worktrees/teardown/remove-worktree.ts @@ -9,7 +9,6 @@ import { forgetHugeRepoWarningDismissalsForWorktrees } from '@/lib/source-contro import { forgetWorktreeSleepIntent } from '@/lib/worktree-sleep-intent' import { readIpcErrorDetail } from '@/lib/ipc-error' import { isArchiveHookRemovalError } from '../../../../../../shared/worktree/archive-hook-removal-gate' -import { showPreservedBranchToast } from '@/components/sidebar/preserved-branch-toast' import { resolveWorktreeOperationRouteResult, resolveWorktreeOperationRouteResultForHost @@ -42,6 +41,7 @@ import { dispatchWorktreeRemoval } from './dispatch-worktree-removal' import { tearDownRemovedWorktreeRendererState } from './removed-worktree-renderer-teardown' import { captureWorktreeStateBeforeRemoval } from './worktree-state-before-removal' import { resolveWorktreeRemovalPreference } from './worktree-removal-preference' +import { completePreservedBranchCleanup } from './complete-preserved-branch-cleanup' export function createRemoveWorktree( set: WorktreeSliceSet, @@ -268,18 +268,22 @@ export function createRemoveWorktree( target }) } - if (preservedBranch && options?.suppressPreservedBranchToast !== true) { - showPreservedBranchToast(removalResult, worktreeBeforeRemoval, (branch, expectedHead) => { - void get().forceDeletePreservedBranch(worktreeId, branch, expectedHead, { - ...(hostId ? { hostId } : {}), - ...(removalRoute?.runtimeEnvironmentId - ? { runtimeEnvironmentId: removalRoute.runtimeEnvironmentId } - : {}) - }) - }) + const branchCleanupOptions = { + ...(hostId ? { hostId } : {}), + ...(removalRoute?.runtimeEnvironmentId + ? { runtimeEnvironmentId: removalRoute.runtimeEnvironmentId } + : {}) } + const autoDeletedBranch = await completePreservedBranchCleanup({ + get, + worktreeId, + removalResult, + worktreeBeforeRemoval, + branchCleanupOptions, + suppressToast: options?.suppressPreservedBranchToast === true + }) pruneHostedReviewLinkMutationGenerations([worktreeId]) - return preservedBranch && cleanup + return preservedBranch && cleanup && !autoDeletedBranch ? { ok: true as const, preservedBranch: { diff --git a/src/shared/default-global-settings.ts b/src/shared/default-global-settings.ts index 4bd1d1310f6..9a71e4342ac 100644 --- a/src/shared/default-global-settings.ts +++ b/src/shared/default-global-settings.ts @@ -203,6 +203,7 @@ export function buildDefaultSettings(args: { claudeAgentTeamsDefaultDisabledMigrated: true, skipDeleteWorktreeConfirm: false, alwaysForceDeleteWorktrees: false, + alwaysForceDeletePreservedBranches: false, skipCloseTerminalWithRunningProcessConfirm: false, skipDeleteAutomationConfirm: false, skipDeleteArtifactConfirm: false, diff --git a/src/shared/global-settings-types.ts b/src/shared/global-settings-types.ts index c8dca7006ab..5794026f9fc 100644 --- a/src/shared/global-settings-types.ts +++ b/src/shared/global-settings-types.ts @@ -1,4 +1,5 @@ import type { NativeChatGlobalSettings } from './native-chat-appearance-settings' +import type { WorktreeDeletionPreferences as DeletePrefs } from './worktree-deletion-preferences' import type { ExecutionHostId } from './execution-host' import type { OrcaWorkspaceLayout } from './orca-workspace-layout' import type { GitHubProjectSettings } from './github/project-types' @@ -49,7 +50,9 @@ export type { WorktreeVisibilityDefaults } from './repo-types' /** MiniMax account region used to select the quota endpoint. */ export type MiniMaxEndpoint = 'overseas' | 'cn' -export type GlobalSettings = NativeChatGlobalSettings & { +type GlobalSettingsBase = NativeChatGlobalSettings & DeletePrefs + +export type GlobalSettings = GlobalSettingsBase & { workspaceDir: string /** Host-owned defaults used when a repository has no explicit visibility override. */ worktreeVisibilityDefaults?: WorktreeVisibilityDefaults @@ -366,10 +369,6 @@ export type GlobalSettings = NativeChatGlobalSettings & { devPluginPaths: string[] /** One-shot guard: start Claude Agent Teams hidden for existing profiles without overriding later opt-ins. */ claudeAgentTeamsDefaultDisabledMigrated?: boolean - /** Why: worktree deletion is destructive (rm -rf of the working dir), so confirm by default. */ - skipDeleteWorktreeConfirm: boolean - /** Opt-in: workspace deletion discards changes and may waive terminal-stop verification. */ - alwaysForceDeleteWorktrees?: boolean /** Why: closing a terminal with child processes kills foreground work; keep this skip separate from other confirmations. */ skipCloseTerminalWithRunningProcessConfirm: boolean /** Why: deleting an automation also deletes its run history; keep this skip separate from worktree deletion. */ diff --git a/src/shared/worktree-deletion-preferences.ts b/src/shared/worktree-deletion-preferences.ts new file mode 100644 index 00000000000..ee5cfcb121d --- /dev/null +++ b/src/shared/worktree-deletion-preferences.ts @@ -0,0 +1,8 @@ +export type WorktreeDeletionPreferences = { + /** Worktree deletion removes the working directory, so confirmation is on by default. */ + skipDeleteWorktreeConfirm: boolean + /** Opt-in: workspace deletion discards changes and may waive terminal-stop verification. */ + alwaysForceDeleteWorktrees?: boolean + /** Opt-in: delete preserved local branches after workspace removal. */ + alwaysForceDeletePreservedBranches?: boolean +}