From 743c55efa34f4cc46cd15f61b99c35520258c6ab Mon Sep 17 00:00:00 2001 From: xxxigm <54813621+xxxigm@users.noreply.github.com> Date: Fri, 12 Jun 2026 01:47:34 +0700 Subject: [PATCH] fix(desktop): stop file tree throwing "Cannot have two HTML5 backends" on remount (#43541) * fix(desktop): stop file tree throwing "two HTML5 backends" on remount The Agent Workspace file tree (react-arborist) shows a permanent "TREE ERROR" with `[error-boundary:file-tree] Cannot have two HTML5 backends at the same time.` react-arborist mounts its own react-dnd DndProvider + HTML5Backend per . react-dnd v14 keeps that manager on a global, ref-counted singleton context and nulls it when the count reaches 0. The tree is keyed on `${cwd}:${collapseNonce}`, so changing folder / collapsing forces a fresh ; during the remount the singleton can be torn down and recreated while the previous HTML5Backend still owns `window.__isReactDndHtml5Backend`, so the new backend's setup() throws. The error boundary then sticks, because "Try again" just remounts into the same race. Pass arborist a stable, app-lifetime `dndManager` (new getFileTreeDndManager singleton) so it reuses one backend for the life of the app and never double-claims the window flag. Drag/drop is already disabled on this tree; this only changes how the (unused) dnd backend is provisioned. Promotes dnd-core and react-dnd-html5-backend to explicit deps (already present transitively via react-arborist's react-dnd 14.x line, so they dedupe to one instance). * fix(nix): bump npmDepsHash for desktop dnd deps Adding dnd-core / react-dnd-html5-backend changed the workspace package-lock.json, so the single workspace-root npmDepsHash in nix/lib.nix was stale and the nix build failed. Regenerate it (hash from the failing nix CI job's 'got:' value). * fix(nix): update npmDepsHash for merged lockfile After merging main, the workspace lockfile combined main's dep changes with the desktop dnd additions, so the npmDepsHash needed recomputing again. Hash from the nix lockfile-check job. * fix(nix): use fetchNpmDeps hash for desktop dnd lockfile prefetch-npm-deps reported sha256-lVnybH9RE/... but fetchNpmDeps wants sha256-mYgKXE/FL4hnkrEvpVv+ULM/oeyIfO2AM9Ol8OrfWm0= for the merged workspace lockfile. Use the nix build 'got:' hash so CI passes. --------- Co-authored-by: Brooklyn Nicholson --- apps/desktop/package.json | 2 ++ .../app/right-sidebar/files/dnd-manager.ts | 27 +++++++++++++++++++ .../src/app/right-sidebar/files/tree.tsx | 2 ++ nix/lib.nix | 2 +- package-lock.json | 2 ++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/app/right-sidebar/files/dnd-manager.ts 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",