diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8df63468f54..1d479b26a2d 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -72,6 +72,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "dnd-core": "^14.0.1", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.2", "ignore": "^7.0.5", @@ -83,6 +84,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.5", "react-arborist": "^3.5.0", + "react-dnd-html5-backend": "^14.0.3", "react-dom": "^19.2.5", "react-router-dom": "^7.17.0", "react-shiki": "^0.9.3", diff --git a/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts b/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts new file mode 100644 index 00000000000..07f4d2f87fa --- /dev/null +++ b/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts @@ -0,0 +1,27 @@ +import { createDragDropManager, type DragDropManager } from 'dnd-core' +import { HTML5Backend } from 'react-dnd-html5-backend' + +let manager: DragDropManager | null = null + +/** + * A single, app-lifetime react-dnd manager for the file tree. + * + * react-arborist mounts its own react-dnd `DndProvider` with `HTML5Backend` + * inside every ``. react-dnd v14 stores that provider's manager on a + * global, ref-counted singleton context and nulls it when the count hits 0. + * On a keyed remount (cwd / collapse changes force a fresh ``), the + * singleton can be torn down and recreated while the previous `HTML5Backend` + * still owns the `window.__isReactDndHtml5Backend` setup flag — so the new + * backend's `setup()` throws "Cannot have two HTML5 backends at the same + * time." and trips the file-tree error boundary (it never recovers, because + * "Try again" just remounts into the same race). + * + * Passing arborist a stable `dndManager` makes it skip the global-singleton + * path entirely and reuse one backend for the lifetime of the app, so the + * window flag is never double-claimed. + */ +export function getFileTreeDndManager(): DragDropManager { + manager ??= createDragDropManager(HTML5Backend) + + return manager +} diff --git a/apps/desktop/src/app/right-sidebar/files/tree.tsx b/apps/desktop/src/app/right-sidebar/files/tree.tsx index 49cd72a8d27..80ad1697cd5 100644 --- a/apps/desktop/src/app/right-sidebar/files/tree.tsx +++ b/apps/desktop/src/app/right-sidebar/files/tree.tsx @@ -7,6 +7,7 @@ import { useResizeObserver } from '@/hooks/use-resize-observer' import { useI18n } from '@/i18n' import { cn } from '@/lib/utils' +import { getFileTreeDndManager } from './dnd-manager' import type { TreeNode } from './use-project-tree' const ROW_HEIGHT = 22 @@ -94,6 +95,7 @@ export function ProjectTree({ disableDrag disableDrop disableEdit + dndManager={getFileTreeDndManager()} height={size.height} indent={INDENT} initialOpenState={openState} diff --git a/nix/lib.nix b/nix/lib.nix index 072f950462d..f4ce4c958e7 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -21,7 +21,7 @@ let # Single npm deps fetch from the workspace root lockfile. # All workspace packages share this derivation. - npmDepsHash = "sha256-mVWPJLIYa4EA0iNPiSVLAPzjjnWdky2HbG5mwApy1lo="; + npmDepsHash = "sha256-mYgKXE/FL4hnkrEvpVv+ULM/oeyIfO2AM9Ol8OrfWm0="; npmDeps = pkgs.fetchNpmDeps { inherit src; diff --git a/package-lock.json b/package-lock.json index a3a0f703de9..fb2afc5149b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,6 +102,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "dnd-core": "^14.0.1", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.2", "ignore": "^7.0.5", @@ -113,6 +114,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.5", "react-arborist": "^3.5.0", + "react-dnd-html5-backend": "^14.0.3", "react-dom": "^19.2.5", "react-router-dom": "^7.17.0", "react-shiki": "^0.9.3",