From 969d46ebe6ef23a5bce69e80b529957221f2e8ca Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 17 May 2026 10:58:59 -0700 Subject: [PATCH 1/5] Add v2 workspace delete hotkey support --- .../DestroyConfirmPane/DestroyConfirmPane.tsx | 17 +++- .../TeardownFailedPane/TeardownFailedPane.tsx | 15 ++++ .../shouldConfirmDeleteDialogKey.test.ts | 37 +++++++++ .../utils/shouldConfirmDeleteDialogKey.ts | 22 +++++ .../_authenticated/_dashboard/layout.tsx | 82 +++++++++++++++++-- 5 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/utils/shouldConfirmDeleteDialogKey.test.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/utils/shouldConfirmDeleteDialogKey.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/DestroyConfirmPane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/DestroyConfirmPane.tsx index d59fdb5eb7d..ba21c967244 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/DestroyConfirmPane.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/DestroyConfirmPane.tsx @@ -9,7 +9,8 @@ import { import { Button } from "@superset/ui/button"; import { Checkbox } from "@superset/ui/checkbox"; import { Label } from "@superset/ui/label"; -import { useId } from "react"; +import { useEffect, useId } from "react"; +import { shouldConfirmDeleteDialogKey } from "../../utils/shouldConfirmDeleteDialogKey"; interface DestroyConfirmPaneProps { open: boolean; @@ -40,6 +41,20 @@ export function DestroyConfirmPane({ }: DestroyConfirmPaneProps) { const checkboxId = useId(); const hasWarnings = hasChanges || hasUnpushedCommits; + + useEffect(() => { + if (!open || !canConfirm) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (!shouldConfirmDeleteDialogKey(event)) return; + event.preventDefault(); + onConfirm(); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [canConfirm, onConfirm, open]); + return ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/TeardownFailedPane/TeardownFailedPane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/TeardownFailedPane/TeardownFailedPane.tsx index 90b3555ba7d..2892e63404e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/TeardownFailedPane/TeardownFailedPane.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/TeardownFailedPane/TeardownFailedPane.tsx @@ -8,7 +8,9 @@ import { AlertDialogTitle, } from "@superset/ui/alert-dialog"; import { Button } from "@superset/ui/button"; +import { useEffect } from "react"; import stripAnsi from "strip-ansi"; +import { shouldConfirmDeleteDialogKey } from "../../utils/shouldConfirmDeleteDialogKey"; import { formatTeardownReason } from "./formatTeardownReason"; interface TeardownFailedPaneProps { @@ -30,6 +32,19 @@ export function TeardownFailedPane({ // Strip ANSI so raw PTY bytes render readably in the
.
 	const cleanTail = stripAnsi(cause.outputTail ?? "");
 
+	useEffect(() => {
+		if (!open) return;
+
+		const handleKeyDown = (event: KeyboardEvent) => {
+			if (!shouldConfirmDeleteDialogKey(event)) return;
+			event.preventDefault();
+			onForceDelete();
+		};
+
+		window.addEventListener("keydown", handleKeyDown);
+		return () => window.removeEventListener("keydown", handleKeyDown);
+	}, [onForceDelete, open]);
+
 	return (
 		
 			
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/utils/shouldConfirmDeleteDialogKey.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/utils/shouldConfirmDeleteDialogKey.test.ts
new file mode 100644
index 00000000000..ce61b6d37f6
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/utils/shouldConfirmDeleteDialogKey.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, test } from "bun:test";
+import { shouldConfirmDeleteDialogKey } from "./shouldConfirmDeleteDialogKey";
+
+const plainEnter = {
+	key: "Enter",
+	shiftKey: false,
+	metaKey: false,
+	ctrlKey: false,
+	altKey: false,
+};
+
+describe("shouldConfirmDeleteDialogKey", () => {
+	test("accepts unmodified Enter", () => {
+		expect(shouldConfirmDeleteDialogKey(plainEnter)).toBe(true);
+	});
+
+	test("rejects modified Enter", () => {
+		expect(shouldConfirmDeleteDialogKey({ ...plainEnter, metaKey: true })).toBe(
+			false,
+		);
+		expect(
+			shouldConfirmDeleteDialogKey({ ...plainEnter, shiftKey: true }),
+		).toBe(false);
+	});
+
+	test("rejects composition and non-Enter keys", () => {
+		expect(
+			shouldConfirmDeleteDialogKey({ ...plainEnter, isComposing: true }),
+		).toBe(false);
+		expect(shouldConfirmDeleteDialogKey({ ...plainEnter, keyCode: 229 })).toBe(
+			false,
+		);
+		expect(shouldConfirmDeleteDialogKey({ ...plainEnter, key: " " })).toBe(
+			false,
+		);
+	});
+});
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/utils/shouldConfirmDeleteDialogKey.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/utils/shouldConfirmDeleteDialogKey.ts
new file mode 100644
index 00000000000..1d7b103c549
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/utils/shouldConfirmDeleteDialogKey.ts
@@ -0,0 +1,22 @@
+interface ConfirmDeleteDialogKeyEvent {
+	key: string;
+	shiftKey: boolean;
+	metaKey: boolean;
+	ctrlKey: boolean;
+	altKey: boolean;
+	isComposing?: boolean;
+	keyCode?: number;
+}
+
+export function shouldConfirmDeleteDialogKey(
+	event: ConfirmDeleteDialogKeyEvent,
+): boolean {
+	if (event.isComposing || event.keyCode === 229) return false;
+	return (
+		event.key === "Enter" &&
+		!event.shiftKey &&
+		!event.metaKey &&
+		!event.ctrlKey &&
+		!event.altKey
+	);
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
index 07dd12a22c1..c1c0822a9ee 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
@@ -1,3 +1,5 @@
+import { eq } from "@tanstack/db";
+import { useLiveQuery } from "@tanstack/react-db";
 import {
 	createFileRoute,
 	Outlet,
@@ -10,7 +12,10 @@ import { useIsV2CloudEnabled } from "renderer/hooks/useIsV2CloudEnabled";
 import { useHotkey } from "renderer/hotkeys";
 import { electronTrpc } from "renderer/lib/electron-trpc";
 import { DashboardSidebar } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar";
+import { DashboardSidebarDeleteDialog } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog";
+import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState";
 import { useDevSeedV2Sidebar } from "renderer/routes/_authenticated/hooks/useDevSeedV2Sidebar";
+import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
 import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel";
 import { WorkspaceSidebar } from "renderer/screens/main/components/WorkspaceSidebar";
 import { DeleteWorkspaceDialog } from "renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components";
@@ -30,10 +35,26 @@ export const Route = createFileRoute("/_authenticated/_dashboard")({
 	component: DashboardLayout,
 });
 
+type DeleteTarget =
+	| {
+			version: "v1";
+			workspaceId: string;
+			workspaceName: string;
+			workspaceType: "worktree" | "branch";
+	  }
+	| {
+			version: "v2";
+			workspaceId: string;
+			workspaceName: string;
+			open: boolean;
+	  };
+
 function DashboardLayout() {
 	const navigate = useNavigate();
 	const openNewWorkspaceModal = useOpenNewWorkspaceModal();
 	const isV2CloudEnabled = useIsV2CloudEnabled();
+	const collections = useCollections();
+	const { removeWorkspaceFromSidebar } = useDashboardSidebarState();
 	useDevSeedV2Sidebar();
 	// Get current workspace from route to pre-select project in new workspace modal
 	const matchRoute = useMatchRoute();
@@ -47,6 +68,8 @@ function DashboardLayout() {
 		to: "/v2-workspace/$workspaceId",
 		fuzzy: true,
 	});
+	const currentV2WorkspaceId =
+		v2WorkspaceMatch !== false ? v2WorkspaceMatch.workspaceId : null;
 	const onV1WorkspaceRoute = currentWorkspaceMatch !== false;
 	const onV2WorkspaceRoute = v2WorkspaceMatch !== false;
 	const versionMismatch =
@@ -58,6 +81,18 @@ function DashboardLayout() {
 		{ enabled: !!currentWorkspaceId },
 	);
 
+	const { data: currentV2Workspaces = [] } = useLiveQuery(
+		(q) =>
+			q
+				.from({ workspaces: collections.v2Workspaces })
+				.where(({ workspaces }) =>
+					eq(workspaces.id, currentV2WorkspaceId ?? ""),
+				),
+		[collections, currentV2WorkspaceId],
+	);
+	const currentV2Workspace =
+		currentV2WorkspaceId != null ? (currentV2Workspaces[0] ?? null) : null;
+
 	const {
 		isOpen: isWorkspaceSidebarOpen,
 		toggleCollapsed: toggleWorkspaceSidebarCollapsed,
@@ -83,11 +118,7 @@ function DashboardLayout() {
 		openNewWorkspaceModal(currentWorkspace?.projectId),
 	);
 
-	const [deleteTarget, setDeleteTarget] = useState<{
-		workspaceId: string;
-		workspaceName: string;
-		workspaceType: "worktree" | "branch";
-	} | null>(null);
+	const [deleteTarget, setDeleteTarget] = useState(null);
 
 	useHotkey(
 		"CLOSE_WORKSPACE",
@@ -97,10 +128,31 @@ function DashboardLayout() {
 					workspaceId: currentWorkspaceId,
 					workspaceName: currentWorkspace.name,
 					workspaceType: currentWorkspace.type,
+					version: "v1",
+				});
+				return;
+			}
+
+			if (
+				currentV2WorkspaceId &&
+				currentV2Workspace &&
+				currentV2Workspace.type !== "main"
+			) {
+				setDeleteTarget({
+					workspaceId: currentV2WorkspaceId,
+					workspaceName: currentV2Workspace.name || currentV2Workspace.branch,
+					version: "v2",
+					open: true,
 				});
 			}
 		},
-		{ enabled: !!currentWorkspaceId },
+		{
+			enabled:
+				(!!currentWorkspaceId && !!currentWorkspace) ||
+				(!!currentV2WorkspaceId &&
+					!!currentV2Workspace &&
+					currentV2Workspace.type !== "main"),
+		},
 	);
 
 	const sidebarPanel = isWorkspaceSidebarOpen && (
@@ -152,7 +204,7 @@ function DashboardLayout() {
 			
 			
- {deleteTarget && ( + {deleteTarget?.version === "v1" && ( )} + {deleteTarget?.version === "v2" && ( + { + setDeleteTarget((target) => + target?.version === "v2" ? { ...target, open } : target, + ); + }} + onDeleted={() => { + removeWorkspaceFromSidebar(deleteTarget.workspaceId); + setDeleteTarget(null); + }} + /> + )}
); } From 2f3f2f97f81435057e9a80b45cf1ec5d2bc62627 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 17 May 2026 11:06:44 -0700 Subject: [PATCH 2/5] Show workspace delete hotkey in context menus --- .../DashboardSidebarWorkspaceItem.tsx | 2 ++ .../DashboardSidebarWorkspaceContextMenu.tsx | 10 ++++++++++ .../WorkspaceListItem/CollapsedWorkspaceItem.tsx | 7 +++++++ .../WorkspaceListItem/WorkspaceContextMenu.tsx | 10 ++++++++++ .../WorkspaceListItem/WorkspaceListItem.tsx | 1 + 5 files changed, 30 insertions(+) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index 0f4a79be2bb..1bf8bc301cd 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -164,6 +164,7 @@ export function DashboardSidebarWorkspaceItem({ isLocalWorkspace={hostType === "local-device"} isPinned={isMainWorkspace && hostType === "local-device"} onCreateSection={handleCreateSection} + showDeleteHotkey={isActive} onMoveToSection={(targetSectionId) => moveWorkspaceToSection(id, projectId, targetSectionId) } @@ -254,6 +255,7 @@ export function DashboardSidebarWorkspaceItem({ isLocalWorkspace={hostType === "local-device"} isPinned={isMainWorkspace && hostType === "local-device"} onOpenInFinder={handleOpenInFinder} + showDeleteHotkey={isActive} onCopyPath={handleCopyPath} onCopyBranchName={handleCopyBranchName} onRemoveFromSidebar={handleRemoveFromSidebar} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx index 5915c47e000..4b79bc26a94 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx @@ -3,6 +3,7 @@ import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator, + ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, @@ -23,6 +24,7 @@ import { LuTrash2, LuX, } from "react-icons/lu"; +import { useHotkeyDisplay } from "renderer/hotkeys"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useDashboardSidebarHover } from "../../../../providers/DashboardSidebarHoverProvider"; @@ -32,6 +34,7 @@ interface DashboardSidebarWorkspaceContextMenuProps { isLocalWorkspace: boolean; isPinned?: boolean; isUnread: boolean; + showDeleteHotkey?: boolean; onCreateSection: () => void; onMoveToSection: (sectionId: string | null) => void; onOpenInFinder: () => void; @@ -50,6 +53,7 @@ export function DashboardSidebarWorkspaceContextMenu({ isLocalWorkspace, isPinned = false, isUnread, + showDeleteHotkey = false, onCreateSection, onMoveToSection, onOpenInFinder, @@ -63,6 +67,9 @@ export function DashboardSidebarWorkspaceContextMenu({ }: DashboardSidebarWorkspaceContextMenuProps) { const collections = useCollections(); const { setContextMenuOpen } = useDashboardSidebarHover(); + const deleteHotkeyText = useHotkeyDisplay("CLOSE_WORKSPACE").text; + const showDeleteShortcut = + showDeleteHotkey && deleteHotkeyText !== "Unassigned"; const { data: sections = [] } = useLiveQuery( (q) => q @@ -174,6 +181,9 @@ export function DashboardSidebarWorkspaceContextMenu({ > Delete + {showDeleteShortcut && ( + {deleteHotkeyText} + )} ) : null} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/CollapsedWorkspaceItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/CollapsedWorkspaceItem.tsx index 14a052c8356..55f34aee474 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/CollapsedWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/CollapsedWorkspaceItem.tsx @@ -3,6 +3,7 @@ import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator, + ContextMenuShortcut, ContextMenuTrigger, } from "@superset/ui/context-menu"; import { @@ -14,6 +15,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { type RefObject, useMemo, useState } from "react"; import { LuCopy, LuGitBranch, LuX } from "react-icons/lu"; +import { useHotkeyDisplay } from "renderer/hotkeys"; import { createContextMenuDeleteDialogCoordinator } from "renderer/react-query/workspaces/useWorkspaceDeleteHandler"; import type { ActivePaneStatus } from "shared/tabs-types"; import { STROKE_WIDTH } from "../constants"; @@ -69,6 +71,8 @@ export function CollapsedWorkspaceItem({ const [renameBranchTarget, setRenameBranchTarget] = useState( null, ); + const deleteHotkeyText = useHotkeyDisplay("CLOSE_WORKSPACE").text; + const showDeleteShortcut = isActive && deleteHotkeyText !== "Unassigned"; const collapsedButton = (