From 6e26d1727a758dc4b3cb1d3551321f7339d54b5b Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Jun 2026 10:45:36 +1000 Subject: [PATCH 1/2] feat: delete project with command shortcut Signed-off-by: Matt Toohey --- apps/staged/src-tauri/src/lib.rs | 9 ++++++ apps/staged/src/App.svelte | 31 +++++++++++++++++++ .../lib/features/projects/ProjectHome.svelte | 19 ++++++++++++ 3 files changed, 59 insertions(+) diff --git a/apps/staged/src-tauri/src/lib.rs b/apps/staged/src-tauri/src/lib.rs index 3d5e1fcc7..b7b810a14 100644 --- a/apps/staged/src-tauri/src/lib.rs +++ b/apps/staged/src-tauri/src/lib.rs @@ -1930,6 +1930,13 @@ pub fn run() { true, Some("CmdOrCtrl+Shift+G"), )?; + let delete_project_item = MenuItem::with_id( + handle, + "delete_project", + "Delete Project", + true, + Some("CmdOrCtrl+Backspace"), + )?; let zoom_in_item = MenuItem::with_id(handle, "zoom_in", "Zoom In", true, Some("CmdOrCtrl+="))?; let zoom_out_item = @@ -1982,6 +1989,7 @@ pub fn run() { &PredefinedMenuItem::cut(handle, None)?, &PredefinedMenuItem::copy(handle, None)?, &PredefinedMenuItem::paste(handle, None)?, + &delete_project_item, &PredefinedMenuItem::select_all(handle, None)?, &PredefinedMenuItem::separator(handle)?, &find_item, @@ -2169,6 +2177,7 @@ pub fn run() { "find" => Some("menu:find"), "find_next" => Some("menu:find-next"), "find_previous" => Some("menu:find-previous"), + "delete_project" => Some("menu:delete-project"), "zoom_in" => Some("menu:zoom-in"), "zoom_out" => Some("menu:zoom-out"), "zoom_reset" => Some("menu:zoom-reset"), diff --git a/apps/staged/src/App.svelte b/apps/staged/src/App.svelte index 6a522d64a..13cc22ba4 100644 --- a/apps/staged/src/App.svelte +++ b/apps/staged/src/App.svelte @@ -60,6 +60,7 @@ let unlistenFind: UnlistenFn | undefined; let unlistenFindNext: UnlistenFn | undefined; let unlistenFindPrevious: UnlistenFn | undefined; + let unlistenDeleteProject: UnlistenFn | undefined; let unlistenZoomIn: UnlistenFn | undefined; let unlistenZoomOut: UnlistenFn | undefined; let unlistenZoomReset: UnlistenFn | undefined; @@ -138,6 +139,24 @@ return true; } + function isTextInputActive(): boolean { + const target = document.activeElement; + if (!(target instanceof HTMLElement)) return false; + if (target.isContentEditable) return true; + return ( + target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT' + ); + } + + function requestDeleteCurrentProject(): boolean { + if (isTextInputActive()) return false; + if (navigation.currentRoute.kind !== 'project') return false; + + const event = new CustomEvent('staged:delete-current-project', { cancelable: true }); + window.dispatchEvent(event); + return event.defaultPrevented; + } + async function logUpdater(message: string) { console.warn(message); try { @@ -261,6 +280,9 @@ unlistenFindPrevious = listenToEvent('menu:find-previous', () => { if (!triggerShortcut('search-find-previous')) runSearchShortcut('previous'); }); + unlistenDeleteProject = listenToEvent('menu:delete-project', () => { + triggerShortcut('app-delete-project'); + }); unlistenZoomIn = listenToEvent('menu:zoom-in', () => { if (!triggerShortcut('view-increase-size')) increaseSize(); }); @@ -340,6 +362,14 @@ modifiers: { meta: true }, handler: navigateBack, }, + { + id: 'app-delete-project', + description: 'Remove current project', + category: 'app', + keys: ['Backspace', 'Delete'], + modifiers: { meta: true }, + handler: requestDeleteCurrentProject, + }, { id: 'search-find', description: 'Find in open note/session', @@ -449,6 +479,7 @@ unlistenFind?.(); unlistenFindNext?.(); unlistenFindPrevious?.(); + unlistenDeleteProject?.(); unlistenZoomIn?.(); unlistenZoomOut?.(); unlistenZoomReset?.(); diff --git a/apps/staged/src/lib/features/projects/ProjectHome.svelte b/apps/staged/src/lib/features/projects/ProjectHome.svelte index c27400089..0fa884443 100644 --- a/apps/staged/src/lib/features/projects/ProjectHome.svelte +++ b/apps/staged/src/lib/features/projects/ProjectHome.svelte @@ -138,6 +138,8 @@ const onCacheStale = () => loadData(); window.addEventListener('staged:new-project', onNewProject); window.addEventListener('cache-stale', onCacheStale); + const onDeleteCurrentProject = (event: Event) => handleDeleteCurrentProjectShortcut(event); + window.addEventListener('staged:delete-current-project', onDeleteCurrentProject); const unlistenDetection = listenToRepoActionsDetection((event) => { const matchingProjectIds = projects @@ -273,6 +275,7 @@ loadGeneration++; window.removeEventListener('staged:new-project', onNewProject); window.removeEventListener('cache-stale', onCacheStale); + window.removeEventListener('staged:delete-current-project', onDeleteCurrentProject); unlistenDetection(); unlistenProjectRepoAdded(); unlistenPrStatus(); @@ -871,6 +874,22 @@ } } + function handleDeleteCurrentProjectShortcut(event: Event) { + if ( + !selectedProject || + selectedProjectDeleting || + projectToDelete || + branchToDelete || + showNewProjectModal || + showAddRepoModal + ) { + return; + } + + event.preventDefault(); + void handleDeleteProjectRequest(selectedProject); + } + async function confirmDeleteProject() { if (!projectToDelete) return; const id = projectToDelete.id; From 4ec850eb5b5e8047e758f07762234a74d2fa9114 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 29 Jun 2026 13:52:05 +1000 Subject: [PATCH 2/2] fix(staged): guard delete shortcut against repeated fire Holding Cmd+Backspace on a safe-to-delete project could start multiple deleteProject calls, since none of the shortcut's guard conditions (selectedProjectDeleting, projectToDelete, etc.) are set until the async canDeleteProjectWithoutConfirmation check resolves. Repeated keydown events would each dispatch handleDeleteProjectRequest before any guard flipped. Add a synchronous deleteShortcutPending flag set before dispatching and cleared once the request settles, so a held key only deletes once. Signed-off-by: Matt Toohey --- apps/staged/src/lib/features/projects/ProjectHome.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/staged/src/lib/features/projects/ProjectHome.svelte b/apps/staged/src/lib/features/projects/ProjectHome.svelte index 0fa884443..415689cb5 100644 --- a/apps/staged/src/lib/features/projects/ProjectHome.svelte +++ b/apps/staged/src/lib/features/projects/ProjectHome.svelte @@ -115,6 +115,9 @@ let branchToDelete = $state<{ branch: Branch; project: Project } | null>(null); let deletingBranches = $state>(new Set()); let deletingProjectNames = $state>(new Map()); + // Guards the delete shortcut while the async safe-to-delete check is in flight, + // before projectToDelete/deletingProjectNames are set, so a held key only deletes once. + let deleteShortcutPending = $state(false); // Setup errors come from the shared workspace lifecycle orchestrator. let worktreeErrors = $derived(workspaceLifecycle.getWorktreeErrors()); @@ -877,6 +880,7 @@ function handleDeleteCurrentProjectShortcut(event: Event) { if ( !selectedProject || + deleteShortcutPending || selectedProjectDeleting || projectToDelete || branchToDelete || @@ -887,7 +891,10 @@ } event.preventDefault(); - void handleDeleteProjectRequest(selectedProject); + deleteShortcutPending = true; + void handleDeleteProjectRequest(selectedProject).finally(() => { + deleteShortcutPending = false; + }); } async function confirmDeleteProject() {