Dragging a selection out of cwdio into Explorer used to be nominally possible and effectively absent. It was Alt+drag, it appeared in no menu or help text, it meant something else on Windows, it crashed the main process from a remote panel, and a plain drag out showed the no-drop cursor — which reads as “this app can’t do that”. ADR 0030 replaced it with one gesture. This is why the obvious fixes did not work and what did.
The shape of the problem
In a file manager the destination of a drag is not known at grab time. You pick up files and then
decide — the other pane, a folder row, another window, Explorer. Any design that forces the choice at
dragstart fights the user’s model. A modifier held at grab time is exactly such a design.
But on Windows dragstart is precisely where the choice must be made: an in-flight HTML5 drag cannot be
upgraded into a native one, because Chromium is already inside its own modal drag loop. So the gesture has
to commit to native or HTML5 the instant it starts, without knowing where it will end.
What was ruled out
- A native addon calling
DoDragDrop— the classic answer, dead since Electron 28: an addon cannot construct the nested-loop permission Chromium requires, so its modal loop starves mouse events. drag-rs, the best current implementation, has no Node bindings and would hit the same wall in-process.- Chromium’s
DownloadURLpromise drag needs nopreventDefault(), so it would leave the HTML5 drag intact — but it is single-file, and on Windows setting it clobbers the other formats, breaking the very in-app drag it was meant to preserve. - A pointer-driven internal drag that hands off at the window edge — the higher-fidelity end state, and still the follow-up — is a large rewrite that was not needed to make the gesture work.
The decision
There is exactly one row-drag gesture, and for anything with a real path it is a native OS drag. The
controller calls webContents.startDrag at dragstart and the panel hands the gesture over. Released on
the other pane, a folder row, another cwdio window, Explorer or any app that takes files, the files go there
— nothing decided at grab time.
The part that makes it viable: cwdio recognises its own drag. The controller captures the dragged set at
dragstart and holds it for the drag’s duration. While that set is armed, a Files drop landing on one of
its own panels is routed through the internal drop path, so an in-app release keeps the full internal
semantics — the guards against dropping a folder into itself or its own subtree, Shift as move,
conflict prompts, undo. Only when the controller has no armed set is a drop treated as a genuine import from
outside.
That needed startDrag to become a call that reports: the access policy is still enforced in the main
process and a refusal has to surface, and — because startDrag does not return until the drop completes —
its return is what disarms the captured set. Without it, a drag released outside the window would leave the
set armed and the next real Explorer drop would be mistaken for our own.
Rows with no local path — remote, in-archive — keep Chromium’s ordinary HTML5 drag. Going native and then finding nothing to hand the OS would cancel the drag and strand the captured set; both were real defects caught in review of the first cut.
Measured, not assumed
Four platform assumptions were tested against Electron 42 with a throwaway probe: an app can receive its
own startDrag back (the 2016 blocker is gone); multi-file works; cross-window works — window #1 to window
#2 lands as a real file drop; and startDrag does not freeze the app: it holds the call for the drag’s
duration, roughly half a second to a second in the measurements, but dragenter/dragover/drop all fire
inside that window. A nested loop that keeps pumping, not a freeze.
The re-entry result is why changing your mind mid-drag recovers fully — and why cross-window drag, silently broken before because the dragged set lived per renderer, now works.
What was given up, knowingly
- The cursor cannot show move. A native drag’s allowed effect is copy-only, and asking for more makes Chromium refuse the drop. Shift still performs a move — read at drop time — it just cannot be advertised.
- Drag-out is copy-only, and stays that way. An accidental drag that moved files out of a file manager is the worst failure the product has; Explorer itself copies across volumes. The deliberate, keyboard-first way to move files out is Ctrl+X and paste — which is what ADR 0031 then built.
- The drop highlight expires rather than waiting to be told. Every “the drag ended” signal proved
unreliable — Chromium skips the final
dragleaveon a crossed panel,dragendnever fires for a native drag, a drop outside the window sends nothing. Two attempts to find a terminal signal both stranded the outline. It now lapses whendragoverstops repeating, which also covers foreign drags from Explorer. - The gesture cannot be end-to-end tested. A native drag cannot be driven by Playwright by any means:
dragToleaves a real drag in flight, and synthetic events still reachstartDrag, which with no real mouse gesture sometimes returns at once and sometimes blocks the main process forever — hanging the app and the whole test worker. Three separate attempts each hung the suite. The pure decisions and the transfer engine are covered; the renderer wiring between them is not, and the ADR says so plainly.
Follow-ups named in the record: staging remote and archive rows to a temp folder so they too can leave the app, and the pointer-driven internal drag that would restore the move cursor and make the gesture testable. The user-facing summary is on the Windows integration page.