-
Notifications
You must be signed in to change notification settings - Fork 963
[codex] Add v2 workspace delete hotkey support #4673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
969d46e
2f3f2f9
07181cd
892fe81
5786e9b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| 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); | ||
| expect(shouldConfirmDeleteDialogKey({ ...plainEnter, ctrlKey: true })).toBe( | ||
| false, | ||
| ); | ||
| expect(shouldConfirmDeleteDialogKey({ ...plainEnter, altKey: 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, | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<DeleteTarget | null>(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() { | |
| </div> | ||
| <div id="workspace-right-sidebar-slot" className="flex h-full shrink-0" /> | ||
| <AddRepositoryModals /> | ||
| {deleteTarget && ( | ||
| {deleteTarget?.version === "v1" && ( | ||
| <DeleteWorkspaceDialog | ||
| workspaceId={deleteTarget.workspaceId} | ||
| workspaceName={deleteTarget.workspaceName} | ||
|
|
@@ -163,6 +215,22 @@ function DashboardLayout() { | |
| }} | ||
| /> | ||
| )} | ||
| {deleteTarget?.version === "v2" && ( | ||
| <DashboardSidebarDeleteDialog | ||
| workspaceId={deleteTarget.workspaceId} | ||
| workspaceName={deleteTarget.workspaceName} | ||
| open={deleteTarget.open} | ||
| onOpenChange={(open) => { | ||
| setDeleteTarget((target) => | ||
| target?.version === "v2" ? { ...target, open } : target, | ||
| ); | ||
| }} | ||
|
Comment on lines
+222
to
+227
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the user dismisses the v2 delete dialog (Escape or clicking outside), Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx
Line: 222-227
Comment:
**V2 dismiss leaks a mounted-but-hidden dialog**
When the user dismisses the v2 delete dialog (Escape or clicking outside), `onOpenChange(false)` sets `deleteTarget.open = false` but leaves `DashboardSidebarDeleteDialog` mounted indefinitely. The v1 path sets `deleteTarget` to `null` on close, which unmounts the component. Because `useDestroyDialogState` already resets both `error` and `inspectState` to their initial values when `open` goes `false`, there is no state worth preserving across a dismiss — setting `deleteTarget` to `null` here, as v1 does, would match that behavior and avoid keeping the component alive unnecessarily.
How can I resolve this? If you propose a fix, please make it concise. |
||
| onDeleted={() => { | ||
| removeWorkspaceFromSidebar(deleteTarget.workspaceId); | ||
| setDeleteTarget(null); | ||
| }} | ||
| /> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
windowkeydown listener fires during the bubble phase for every keydown in the document, including when focus is on the Cancel button. Pressing Enter with the Cancel button focused callsevent.preventDefault()(suppressing the button's native Enter→click activation) and then callsonConfirm(), which deletes the workspace. The user intended to cancel but instead confirms a destructive action.The same pattern applies to
TeardownFailedPane— pressing Enter on its Cancel button callsonForceDelete().The fix is to exit early when the event target is any button element, letting normal button click-from-Enter behavior proceed unimpeded: add
if (event.target instanceof HTMLButtonElement) return;before theshouldConfirmDeleteDialogKeycall in both components.Prompt To Fix With AI