Skip to content

Windows: fix dropping files on the island - #114

Open
hieudepzai14122007-ship-it wants to merge 4 commits into
Louis-CFM:mainfrom
hieudepzai14122007-ship-it:windows-fix-file-drop
Open

hieudepzai14122007-ship-it wants to merge 4 commits into
Louis-CFM:mainfrom
hieudepzai14122007-ship-it:windows-fix-file-drop

Conversation

@hieudepzai14122007-ship-it

@hieudepzai14122007-ship-it hieudepzai14122007-ship-it commented Oct 2, 2026 •

Copy link
Copy Markdown

Bug

On Windows, dragging a file onto the island does nothing: Mochi never turns into a box, and coucou.log shows no drag line at all. This happens both when the island is open and when it is hidden.

Repro: Windows 11 (build 26200), WebView2 Runtime 154.0.4258.53. Drag any file from Explorer onto the island and release.

Causes and fixes

Five separate things stood in the way.

1. The native drop hook never fires

The innermost windows a drag passes over, Chrome_WidgetWin_1 and Chrome_RenderWidgetHostHWND, are owned by the msedgewebview2 process, not by Coucou. OLE stops at WebView2's own target on those windows, which wry has told to refuse (AllowExternalDrop(false)). It never reaches the target wry registers one level up, on Chrome_WidgetWin_0. unblock_webview_drops tries to revoke WebView2's target from our process, which isn't reliable for a window another process owns.

Fix: dragDropEnabled is off, and the island page takes the drop as a plain HTML5 drag and drop, which WebView2 delivers without trouble.

  • WebView2 gives the page the file's contents, not its path. The contents go to a new ingest_bytes command as a raw IPC body (name URI-encoded in a header), and Rust writes them into the inbox exactly like ingest does: same naming, same week-long sweep.
  • Files over 200 MB are refused, and folders get the same "can't be dropped yet" note.
  • unblock_webview_drops is removed.

2. The wake strip ended up click-through

In set_collapsed, a cursor-poll tick that was asleep while the island collapsed would wake up, compute "not on the island" for the new 6 px strip, and turn click-through back on. The hidden strip then ignored hovers and drags alike.

Fix: the poll is stopped before the flag is reset, and it treats the collapsed strip as always taking the mouse.

3. A 6 px strip is too thin to drop on

Fix: while the island is hidden and a drag is in flight, the strip grows into an invisible 720×150 zone at the top centre, and shrinks back on release. A drag in flight means:

  • the button is held;
  • the press began outside the zone;
  • the cursor has moved 8 px;
  • and it isn't a window move or resize.

Plain clicks in that area are never affected.

4. A status bar along the top edge can cover the strip

Another always-on-top window spanning the top of the screen sits above the strip, and drops land on it instead. Fix: the island re-asserts HWND_TOPMOST whenever it is placed, and every 2 s.

5. A drag that ends somewhere else left the drop view up

HTML5 drag and drop says when a drag leaves the page, but not when it is then dropped somewhere else. Dragging a file over the island and releasing it elsewhere left "Drop your files here" on screen for good. Fix: the cursor poll emits pointer-released when the button goes up. If the file had already left the island and was never dropped, the island closes the drop view.

Cost while hidden

Fixes 3 and 4 aren't strictly 0 % CPU while hidden: one GetAsyncKeyState every 50 ms, and one SetWindowPos every 2 s. Both are negligible, but I'm open to an event-driven alternative if you'd prefer.

Linux

dragDropEnabled is turned off in tauri.linux.conf.json too, so both platforms use the same HTML5 path. That is untested on Linux. The new watchers are Windows-only (they're skipped where CURSOR_POLL is false).

Testing

  • Windows 11 (build 26200): dragging a .docx from Explorer onto the island now logs drag enter → drag drop, Mochi swallows it, and the copy lands in %LOCALAPPDATA%\Coucou\inbox.
  • cargo check --release: no warnings. tsc --noEmit: clean.

🤖 Generated with Claude Code

Dragging a file onto the island did nothing: no drag event ever reached
the app. Four things stood in the way.

- The native drop hook never fired. The innermost windows a drag passes
  over (Chrome_WidgetWin_1, Chrome_RenderWidgetHostHWND) belong to the
  msedgewebview2 process, and OLE stops at WebView2's own target there
  before reaching the one wry registers on Chrome_WidgetWin_0. Revoking it
  from our process is not reliable for a window another process owns.
  dragDropEnabled is now off and the page takes the drop as plain HTML5
  drag and drop; the file's contents come over as a raw IPC body to a new
  ingest_bytes command, which writes them into the inbox like ingest does.
  unblock_webview_drops is gone.
- The wake strip ended up click-through. A cursor-poll tick asleep while
  the island collapsed recomputed "not on the island" for the new strip and
  turned click-through back on. The poll is now stopped before the flag is
  reset, and it treats the collapsed strip as always taking the mouse.
- A 6 px strip is too thin to drop on. While the island is hidden and a
  drag is in flight (button held, press outside the zone, moved 8 px, not a
  window move or resize), the strip grows into a 720x150 invisible zone at
  the top centre and shrinks back on release.
- An always-on-top status bar along the top edge can sit above the strip.
  The island re-asserts HWND_TOPMOST when it is placed and every 2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HTML5 drag and drop says when a drag leaves the page, but not when it is
then dropped somewhere else, so dragging a file over the island and
releasing it elsewhere left the "Drop your files here" view up for good.
The cursor poll now emits pointer-released on the button's falling edge,
and the island closes the drop view if the file had already left and was
never dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Grizzy8

Grizzy8 commented Oct 2, 2026

Copy link
Copy Markdown

Tested on Windows 11 Pro 26H2 (OS Build 26300.9457), WebView2 154.0.4258.48, built from this branch. Dropping a file straight onto the island now works: the log shows drag enter / drag drop <file> and the file lands in the inbox. This fixes #126 for me.

Two things I noticed:

1. The progress bar can stay at 0% after a drop.
The file is copied to the inbox correctly (checked in Explorer), but the island stays on Uploading <file> 0% and never advances, even after moving the mouse over it or clicking it. It happened when I dropped onto the hidden island; dropping onto an already-awake island completed normally in my first runs, so it seems timing-dependent.

I logged UploadSeq.isActive right before UploadSeq.performDrop(...) in swallow: it was false at the moment of the drop in both of my runs (island awake and island hidden). stepSequence() only runs while UploadSeq.isActive, and frame() returns the rest frame (progress 0) when inactive. performDrop doesn't re-activate it, only enterZone does. I haven't found what deactivates it between the drag enter and the drop.

As a workaround, setting this.isActive = true in performDrop makes the sequence run in both cases: the bar goes to 100 %, and the animation (swallow, bar, ✓, choose card) played correctly on my machine.

// upload/sequence.ts
performDrop(uploadDuration: number) {
  this.uploadDuration = uploadDuration;
  this.dropWall = this.now();
  this.isActive = true; // the sequence may already have been deactivated when the drop lands
  // ...rest unchanged
}

2. Minor regression: clicking "+" no longer keeps the drop view (the "PDF / Images" panel) open.
The island stays open, but the panel disappears again right away (on main it stays). Each click writes drag ended outside the island to the log. That line is only logged when State.view === "upload", so the click does switch to the upload view, and onPointerReleased then switches it back on the button release, because it doesn't check that a file drag was actually in progress.

I tried a small fix locally (island.ts only) and it works on my machine:

private fileDragSeen = false;

private onDragDrop(e: DragDropPayload) {
  if (e.type === "enter") this.fileDragSeen = true;
  // ...rest unchanged
}

private onPointerReleased() {
  const wasFileDrag = this.fileDragSeen;
  this.fileDragSeen = false;
  if (!wasFileDrag || State.fileDragOver || State.view !== "upload" || UploadSeq.dropped) return;
  // ...rest unchanged
}

With it:

  • clicking "+" keeps the drop view open, and nothing is logged;
  • dropping a file straight onto the island still works (drag enter / drag drop);
  • dragging a file in, out again, and releasing outside the island still closes the drop view (drag ended outside the island).

Tested only on one machine with these scenarios.

Two issues found while testing this branch (thanks Grizzy8):

- Dropping onto a hidden island could leave "Uploading 0%" on screen.
  Waking the island passes through the home view, and leaving the drop
  views on the way switches the upload sequence off, so the drop then ran
  with no sequence. The sequence is switched back on after waking, and
  performDrop now always activates it.
- Clicking "+" opened the drop panel and the button release closed it
  again. Only a release that ends a file drag which entered the island now
  closes the drop view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hieudepzai14122007-ship-it

Copy link
Copy Markdown
Author

Thanks for testing and for the detailed report! Both confirmed and fixed in 604c093. The 0 % bar came from waking the hidden island: it goes through the home view first, which switches the upload sequence off. The sequence is now turned back on after waking, and performDrop always activates it as you suggested. The "+" panel now only closes after a file drag that actually entered the island.

@Grizzy8

Grizzy8 commented Oct 3, 2026

Copy link
Copy Markdown

Thanks for the quick fix in 604c093: both the 0 % bar and the "+" panel behave on my machine now.

One more thing I ran into while testing the drop flow on Windows: on the "choose" card ("file.pdf is ready. What do you want to do with it?"), the Cancel button does nothing, and Ask a question only reacts on its left part.

Cause. The card is painted on the canvas, and the clicks are caught by invisible .upload-hit buttons inside #upload-layer. But #content comes after that layer in the DOM and covers the whole island, so it swallows the click. Two details make it worse:

  • syncDom() sets contentEl.style.pointerEvents = "auto" inline, so a stylesheet rule can't override it.
  • The (invisible) active view, .view.on, keeps pointer-events: auto even under #views.hidden-by-upload, so the hidden DOM "Ask a question" button catches clicks where it overlaps the painted one, and nothing else does.

dev/upload-preview.ts mounts only the canvas, which is probably why it wasn't caught.

Suggested fix (16 lines, tested on Windows):

--- a/windows/src/island/island.ts
+++ b/windows/src/island/island.ts
@@ -738,4 +738,6 @@ export class Island {
     this.uploadCanvas.el.classList.toggle("on", uploadActive);
     this.viewsEl.classList.toggle("hidden-by-upload", uploadActive);
+    // Lets the header stay clickable while #content passes clicks through.
+    this.contentEl.classList.toggle("upload-on", uploadActive);
 
     tickMiniBots(dt);
@@ -860,5 +862,7 @@ export class Island {
 
     this.contentEl.style.opacity = expanded && !greetingActive ? "1" : "0";
-    this.contentEl.style.pointerEvents = expanded && !greetingActive ? "auto" : "none";
+    // While the drop sequence owns the body, the content layer lets clicks through
+    // to the invisible hit areas under it (the header opts back in, see style.css).
+    this.contentEl.style.pointerEvents = expanded && !greetingActive && !this.uploadActive ? "auto" : "none";
     this.greetingCanvas.style.display = greetingActive ? "block" : "none";
--- a/windows/src/style.css
+++ b/windows/src/style.css
@@ -134,4 +134,15 @@ body {
 }
 
+/* The invisible views must not swallow the clicks meant for the buttons the
+   canvas paints (.upload-hit sits below #content). The header stays clickable. */
+#views.hidden-by-upload * {
+  pointer-events: none;
+}
+
+/* #content itself is switched to pointer-events: none from island.ts (inline). */
+#content.upload-on #header {
+  pointer-events: auto;
+}
+
 #bot-glow {
   position: absolute;

While the drop sequence owns the island, clicks reach the painted buttons; the header tabs stay clickable, and everything goes back to normal once the sequence ends (Ask → chat, Cancel → home).

An alternative would be to move the hit areas above #content instead of letting clicks pass through it. I haven't tried that, so I can't say how it compares.

The choose card is painted on the canvas, and its clicks are caught by
invisible .upload-hit buttons inside #upload-layer. #content comes after
that layer in the DOM and covers the whole island, so it swallowed the
click: Cancel did nothing and Ask only reacted where the hidden DOM
button happened to overlap it. While the drop sequence owns the body,
#content and the hidden views now let clicks through; the header opts
back in.

Diagnosis and fix by Grizzy8 in the PR thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hieudepzai14122007-ship-it

Copy link
Copy Markdown
Author

Confirmed: the canvas hit areas sat under #content, and the hidden view's DOM button was catching part of the click. Your fix is in 4972152 with credit in the commit message. Thanks again for the thorough testing!

softjuarez added a commit to softjuarez/coucou that referenced this pull request Oct 4, 2026
…ndows)

On Windows 11 build 26200 a file dragged from Explorer never reached
the island: the native drop hook does not fire on the windows WebView2
owns. PR Louis-CFM#114 (open upstream, by hieudepzai14122007-ship-it) takes the
drop as plain HTML5 drag and drop instead, and fixes the 0 % progress
bar and the choose card's buttons along the way.

The one conflict was the command list in lib.rs, where this branch adds
send_to_destination and the PR adds ingest_bytes; both are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants