Skip to content

fix(dropzone): keep dropped files alive across the handle await (GH-91) - #93

Merged
rodrigogs merged 2 commits into
devfrom
fix/gh-91-dropzone-snapshot
Sep 28, 2026
Merged

rodrigogs merged 2 commits into
devfrom
fix/gh-91-dropzone-snapshot

Conversation

@rodrigogs

Copy link
Copy Markdown
Owner

Root cause

FileDropZone.svelte's handleDrop awaited Promise.all(getAsFileSystemHandle()) before reading e.dataTransfer.files. Chromium invalidates DataTransfer access as soon as the drop handler's first await resolves (drag_controller.cc sets the access policy to kNumb on return), so .files returns an empty list past that point — drops silently did nothing in Chrome/Edge/Brave/Electron while the file picker (no intervening await) kept working. Introduced in #66 when the handler became async; the existing comment only protected .items, not .files.

Fix

Snapshot dt.files and start the handle promises synchronously, before any await, in a new pure helper snapshotDroppedFiles (src/lib/helpers/drop-files.ts). handleDrop is now a thin caller. onFilesSelected's prop type moved from FileList to readonly File[]; updated the three +page.svelte callers and dropped the new DataTransfer() round-trips that only existed to keep producing a FileList.

Second commit: getElectronFilePath relied on File#path, removed in Electron 32 (this project ships electron ^39.8.6), so dropped/picked files in the desktop app never got a path for persistence. Now prefers window.electronAPI.getPathForFile (backed by webUtils.getPathForFile, exposed via contextBridge in the preload script), falling back to the old 'path' in file check. Not runnable on the CI/dev host that produced this PR (no display server) — verified via lint/check/build/unit tests only.

Mutation evidence (RED → GREEN) for tests/drop-zone/snapshot-dropped-files.test.ts

RED — temporarily changed snapshotDroppedFiles to await Promise.resolve() before reading dt.files, reproducing the bug the helper exists to prevent:

 FAIL  tests/drop-zone/snapshot-dropped-files.test.ts > snapshotDroppedFiles > captures files BEFORE they become invalid across async boundaries
AssertionError: expected +0 to equal 1
 ❯ tests/drop-zone/snapshot-dropped-files.test.ts:86:24
     85|   assert.equal(snapshot.files.length, 1);

 FAIL  tests/drop-zone/snapshot-dropped-files.test.ts > snapshotDroppedFiles > returns an array of files (not a FileList) for immutability
AssertionError: The expression evaluated to a falsy value: (Array.isArray(snapshot.files))
 ❯ tests/drop-zone/snapshot-dropped-files.test.ts:106:3

 FAIL  tests/drop-zone/snapshot-dropped-files.test.ts > snapshotDroppedFiles > handles multiple dropped files
TypeError: Cannot read properties of undefined (reading 'length')
 ❯ tests/drop-zone/snapshot-dropped-files.test.ts:117:31

 FAIL  tests/drop-zone/snapshot-dropped-files.test.ts > snapshotDroppedFiles > handles empty drop
TypeError: Cannot read properties of undefined (reading 'length')
 ❯ tests/drop-zone/snapshot-dropped-files.test.ts:127:31

 Test Files  1 failed (1)
      Tests  4 failed (4)

(The mutation makes snapshotDroppedFiles return a Promise instead of the snapshot object, since the fake DataTransfer in the test isn't awaited by the test's synchronous calls — which is itself proof the mutation broke the "no await before reading files" contract the test enforces.)

GREEN — reverted the mutation:

 Test Files  1 passed (1)
      Tests  4 passed (4)
   Duration  157ms

Gates run locally

  • npm ci — clean install
  • npm run lint — Checked 134 files, no fixes applied
  • npm run check — svelte-check found 0 errors and 0 warnings
  • npm test — Test Files 5 passed (5), Tests 61 passed (61)
  • npm run build — succeeds

npm run test:global-search needs Playwright browsers, which cannot launch on this host (no display) — left to CI to prove that leg.

Closes #91

Chromium invalidates DataTransfer access as soon as the drop handler's
first await resolves (drag_controller.cc sets the access policy to
kNumb on return), so DataTransfer.files() returns an empty list after
that point. handleDrop awaited Promise.all(getAsFileSystemHandle())
before reading e.dataTransfer.files, so the file list was always empty
by the time onFilesSelected ran in Chrome/Edge/Brave/Electron -- drops
silently did nothing while the file picker (no await in between)
worked fine.

Extract snapshotDroppedFiles() (src/lib/helpers/drop-files.ts), a pure
helper that reads dt.files and starts the getAsFileSystemHandle()
promises synchronously, before any await. handleDrop is now a thin
caller. onFilesSelected's prop type changes from FileList to
readonly File[]; update the three +page.svelte callers accordingly
(handleFilesSelected, handleSidebarImport, and the file-input handler)
and drop the now-pointless new DataTransfer() round-trips that only
existed to keep producing a FileList.

Regression test: tests/drop-zone/snapshot-dropped-files.test.ts models
the Chromium behaviour with a fake DataTransfer whose .files getter
returns [] once an async boundary has been crossed, and asserts the
snapshot still has the files afterwards.

Closes #91
Electron 32 removed the non-standard File#path property that
getElectronFilePath relied on (project is on electron ^39.8.6), so a
file dropped or picked in the desktop app never got a path, and fell
back to the reselect-on-next-launch flow instead of persisting.

Expose webUtils.getPathForFile via the preload script's contextBridge
(electron/preload.cjs), declare it on ElectronAPI (src/app.d.ts), and
make getElectronFilePath prefer window.electronAPI.getPathForFile(file)
over the removed 'path' in file check, which is kept only as a fallback
for older Electron builds. getPathForFile returns '' (not undefined)
for a File not backed by a real path, so that is normalized to
undefined at the same call site.

Not runnable on this host: no display server, so the Electron app
itself cannot be launched to exercise this manually. Verified via
lint/check/build/unit-test only; webUtils.getPathForFile's presence
and signature were confirmed against node_modules/electron/electron.d.ts.
@rodrigogs
rodrigogs merged commit bde3b54 into dev Sep 28, 2026
6 checks passed
@rodrigogs
rodrigogs deleted the fix/gh-91-dropzone-snapshot branch September 28, 2026 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Drag and drop for chats doesn't work

1 participant