Skip to content

Commit 778296d

Browse files
committed
Delete dialog: Add Trash/Delete toggle
Mirrors the Copy/Move toggle on the transfer dialog. The user can now flip between trash and permanent delete in-dialog rather than having to cancel and re-press a different shortcut. F8 still pre-selects Trash; Shift+F8 still pre-selects Delete — they just set the initial state of the toggle. - `DeleteDialog.svelte`: `isPermanent` is now a `$state` initialized from `initialIsPermanent`; toggle hidden on no-trash volumes (the existing warning banner still forces permanent there). Active Delete side uses `--color-error-bg` + `--color-error-text` to satisfy AA contrast (the strong `--color-error` fill against `--color-accent-fg` was too low). - `onConfirm` callback now passes `isPermanent` back so the parent uses the toggle's final value, not the dialog's initial prop. - `dialog-state.svelte.ts`: `handleDeleteConfirm(previewId, isPermanent)` reads the flag from the callback. MCP auto-confirm path keeps the initial-prop fallback. - Updated `delete/CLAUDE.md` to reflect the new flow.
1 parent 64db140 commit 778296d

5 files changed

Lines changed: 82 additions & 15 deletions

File tree

apps/desktop/src/lib/file-explorer/pane/DialogManager.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
onNewFileCreated: (fileName: string) => void
7979
onNewFileCancel: () => void
8080
onAlertClose: () => void
81-
onDeleteConfirm: (previewId: string | null) => void
81+
onDeleteConfirm: (previewId: string | null, isPermanent: boolean) => void
8282
onDeleteCancel: () => void
8383
} = $props()
8484
</script>

apps/desktop/src/lib/file-explorer/pane/DualPaneExplorer.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2282,8 +2282,8 @@
22822282
}}
22832283
showDeleteDialog={dialogs.showDeleteDialog}
22842284
deleteDialogProps={dialogs.deleteDialogProps}
2285-
onDeleteConfirm={(previewId: string | null) => {
2286-
dialogs.handleDeleteConfirm(previewId)
2285+
onDeleteConfirm={(previewId: string | null, isPermanent: boolean) => {
2286+
dialogs.handleDeleteConfirm(previewId, isPermanent)
22872287
}}
22882288
onDeleteCancel={() => {
22892289
dialogs.handleDeleteCancel()

apps/desktop/src/lib/file-explorer/pane/dialog-state.svelte.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,10 +294,9 @@ export function createDialogState(deps: DialogStateDeps) {
294294
deps.onRefocus()
295295
},
296296

297-
handleDeleteConfirm(previewId: string | null) {
297+
handleDeleteConfirm(previewId: string | null, isPermanent: boolean) {
298298
if (!deleteDialogProps) return
299299

300-
const isPermanent = deleteDialogProps.isPermanent || !deleteDialogProps.supportsTrash
301300
const opType: TransferOperationType = isPermanent ? 'delete' : 'trash'
302301

303302
// Collect per-item sizes for trash progress if available
@@ -508,8 +507,11 @@ export function createDialogState(deps: DialogStateDeps) {
508507
transferDialogProps.operationType,
509508
false, // scanInProgress not tracked when confirming programmatically
510509
)
511-
} else if (dialogType === 'delete-confirmation' && showDeleteDialog) {
512-
this.handleDeleteConfirm(null) // previewId not available
510+
} else if (dialogType === 'delete-confirmation' && showDeleteDialog && deleteDialogProps) {
511+
// previewId not available when confirming programmatically.
512+
// For MCP auto-confirm, honor whatever the props initialized with.
513+
const isPermanent = deleteDialogProps.isPermanent || !deleteDialogProps.supportsTrash
514+
this.handleDeleteConfirm(null, isPermanent)
513515
}
514516
},
515517
}

apps/desktop/src/lib/file-operations/delete/CLAUDE.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ dialog before acting. Reuses `TransferProgressDialog` for progress display.
1010
## Files
1111

1212
- **DeleteDialog.svelte**: Confirmation dialog with file list (max 10 items + overflow), live scan stats, symlink
13-
notice, and no-trash volume warning. Uses `ModalDialog` with `role="dialog"` for trash and `role="alertdialog"` for
14-
permanent delete.
13+
notice, no-trash volume warning, and a Trash/Delete segmented control that lets the user flip the operation in-dialog
14+
(hidden on no-trash volumes, where permanent is forced). Uses `ModalDialog` with `role="dialog"` for trash and
15+
`role="alertdialog"` for permanent delete — the role flips reactively when the toggle changes.
1516
- **delete-dialog-utils.ts**: Pure utility functions: `generateDeleteTitle()` (handles "N selected files" vs "1 file
1617
under cursor"), `abbreviatePath()`, `getSymlinkNotice()`, `countSymlinks()`.
1718
- **delete-dialog-utils.test.ts**: Vitest tests for the pure utilities.
@@ -23,15 +24,18 @@ dialog before acting. Reuses `TransferProgressDialog` for progress display.
2324
3. **Selection**: `DualPaneExplorer.openDeleteDialog(permanent)` builds props from selection or cursor item (same
2425
pattern as copy/move). Looks up `supportsTrash` from the source volume's `VolumeInfo`.
2526
4. **Dialog**: `DeleteDialog` opens with file list, scan preview starts in background via `startScanPreview()`
26-
5. **Confirm**: `dialog-state.svelte.ts::handleDeleteConfirm()` transitions to `TransferProgressDialog` with
27+
5. **Confirm**: `DeleteDialog` passes back the active `isPermanent` (from the toggle), and
28+
`dialog-state.svelte.ts::handleDeleteConfirm(previewId, isPermanent)` transitions to `TransferProgressDialog` with
2729
`operationType: 'trash'` or `'delete'`
2830
6. **Backend**: `trash_files_start()` or `delete_files_start()` in `write_operations/mod.rs` runs the operation
2931
7. **Progress**: `TransferProgressDialog` shows items/bytes progress with cancel support
3032
8. **Completion**: Toast notification, both panes refreshed, 400ms minimum display time
3133

3234
## Key design decisions
3335

34-
- **Trash by default**: F8 moves to trash. Permanent delete requires explicit Shift+F8. No setting to change this.
36+
- **Trash by default**: F8 moves to trash. Shift+F8 opens the same dialog with permanent preselected. Either way, the
37+
user can flip the mode via the Trash/Delete segmented control before confirming, so the shortcut just sets the initial
38+
state.
3539
- **Always show dialog**: No `confirmBeforeDelete` setting. Delete is destructive, so the user always sees what they're
3640
about to delete. Both delete settings were removed from the settings registry.
3741
- **No undo**: Cmdr doesn't implement undo, but items trashed via `NSFileManager.trashItemAtURL` support Finder's "Put

apps/desktop/src/lib/file-operations/delete/DeleteDialog.svelte

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
sortOrder: SortOrder
4141
/** When true, dialog auto-confirms without user interaction (MCP). */
4242
autoConfirm?: boolean
43-
onConfirm: (previewId: string | null) => void
43+
onConfirm: (previewId: string | null, isPermanent: boolean) => void
4444
onCancel: () => void
4545
}
4646
@@ -58,8 +58,8 @@
5858
onCancel,
5959
}: Props = $props()
6060
61-
// Force permanent when trash not supported
62-
const isPermanent = $derived(initialIsPermanent || !supportsTrash)
61+
// User-facing toggle. Forced to permanent on volumes that don't support trash.
62+
let isPermanent = $state(initialIsPermanent || !supportsTrash)
6363
6464
const dialogTitle = $derived(generateDeleteTitle(sourceItems, isFromCursor))
6565
const abbreviatedPath = $derived(abbreviatePath(sourceFolderPath))
@@ -158,7 +158,7 @@
158158
isPermanent,
159159
count: sourceItems.length,
160160
})
161-
onConfirm(previewId)
161+
onConfirm(previewId, isPermanent)
162162
}
163163
164164
function handleCancel() {
@@ -233,6 +233,22 @@
233233
</div>
234234
{/if}
235235

236+
<!-- Trash/Delete toggle -->
237+
{#if supportsTrash}
238+
<div class="operation-toggle">
239+
<button
240+
class="toggle-option"
241+
class:active={!isPermanent}
242+
onclick={() => (isPermanent = false)}>Trash</button
243+
>
244+
<button
245+
class="toggle-option toggle-option-danger"
246+
class:active={isPermanent}
247+
onclick={() => (isPermanent = true)}>Delete</button
248+
>
249+
</div>
250+
{/if}
251+
236252
<!-- Scrollable file list -->
237253
<div class="file-list-container">
238254
<div class="file-list" role="list">
@@ -465,4 +481,49 @@
465481
justify-content: center;
466482
padding: 0 var(--spacing-xl) var(--spacing-xl);
467483
}
484+
485+
/* Trash/Delete segmented control */
486+
.operation-toggle {
487+
display: flex;
488+
justify-content: center;
489+
gap: 0;
490+
padding: 0 var(--spacing-xl) var(--spacing-md);
491+
}
492+
493+
.toggle-option {
494+
padding: var(--spacing-xs) var(--spacing-lg);
495+
font-size: var(--font-size-sm);
496+
font-weight: 500;
497+
border: 1px solid var(--color-border-strong);
498+
background: transparent;
499+
color: var(--color-text-secondary);
500+
transition: all var(--transition-base);
501+
min-width: 60px;
502+
}
503+
504+
.toggle-option:first-child {
505+
border-radius: var(--radius-md) 0 0 var(--radius-md);
506+
border-right: none;
507+
}
508+
509+
.toggle-option:last-child {
510+
border-radius: 0 var(--radius-md) var(--radius-md) 0;
511+
}
512+
513+
.toggle-option.active {
514+
background: var(--color-accent);
515+
border-color: var(--color-accent);
516+
color: var(--color-accent-fg);
517+
}
518+
519+
.toggle-option-danger.active {
520+
background: var(--color-error-bg);
521+
border-color: var(--color-error);
522+
color: var(--color-error-text);
523+
}
524+
525+
.toggle-option:not(.active):hover {
526+
background: var(--color-bg-tertiary);
527+
color: var(--color-text-primary);
528+
}
468529
</style>

0 commit comments

Comments
 (0)