web: route native macOS Finder drops to the live chat composer (#179)
* web: route native macOS Finder drops to the live chat composer The native Mac shell (LChatMac) can't hand a WKWebView a Finder file-drop, so it fills the page's static #file-input and dispatches `change` (see ChatFeedWebView's __lchatAddFiles). Since the mini-apps refactor the live composer is built in apps/chat.js with its OWN (id-less) file input and its own stageFiles path, so that native drop was landing on the retired classic #file-input — whose onchange runs the dead addAttachment path into a hidden composer that can never be sent. Drops silently did nothing. Wire #file-input's change to the live thread's staging path: buildSkeleton exposes root._dropFiles, and render() points #file-input.onchange at the thread on screen (re-pointed like the hotkey / native-composer dispatchers, so a chat switch routes drops to the chat you're looking at). Web-only — no native rebuild; it reaches the already-installed Mac app. Also stopPropagation on the app's own web drop handler so the retired #thread-view drop listener can't double-stage the same files in a browser. Verified end-to-end (Playwright): simulating the exact native __lchatAddFiles contract now stages the file in the live composer (1 chip); before, the live composer stayed empty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * web: drop the now-obsolete double-upload guard after #178 #178 deleted the classic #thread-view drop handler and #file-input wiring, so the stopPropagation guard (against a double-stage into the dead composer) has nothing to guard and the comments referenced deleted code. Remove it and reword. The native-drop bridge (#file-input change -> live stageFiles) is unchanged and still the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> ---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 files changed,
+25
−0
changelog/2026-07-04-fix-mac-drop-upload.md+1 −0web/static/apps/chat.js+24 −0
diff --git a/changelog/2026-07-04-fix-mac-drop-upload.md b/changelog/2026-07-04-fix-mac-drop-upload.md| new file mode 100644 |
| index 0000000..99f948d |
| --- /dev/null |
| +++ b/changelog/2026-07-04-fix-mac-drop-upload.md |
| @@ -0,0 +1 @@ |
| +Fixed drag-and-drop file uploads in the Mac app again: a photo or file dropped onto a chat now lands in the message box ready to send, instead of being silently swallowed. |
diff --git a/web/static/apps/chat.js b/web/static/apps/chat.js| index 8e8a633..1a31e8c 100644 |
| --- a/web/static/apps/chat.js |
| +++ b/web/static/apps/chat.js |
| @@ -318,6 +318,15 @@ export const app = defineApp({ |
| if (root.isConnected && root._native && host && host.registerNative) { |
| host.registerNative(root._native); |
| } |
| + // Same again for the native macOS shell's Finder file-drop: it fills the |
| + // page's single static #file-input and fires `change`, so point that input at |
| + // the thread on screen (not the last-built one). Idempotent .onchange = so |
| + // re-rendering doesn't stack listeners. (The classic composer that used to own |
| + // #file-input is gone, so nothing else is listening.) |
| + if (root.isConnected && root._dropFiles) { |
| + const fileDropInput = document.getElementById("file-input"); |
| + if (fileDropInput) fileDropInput.onchange = root._dropFiles; |
| + } |
| // Note: the notices strip is NOT cleared on re-render — a command reply owns |
| // its own lifecycle now (auto-dismiss countdown + ✕), and clearing here wiped |
| // the reply the instant its own event folded a bubble into this thread's feed |
| @@ -2200,6 +2209,21 @@ function buildSkeleton(el, ctx) { |
| } |
| }); |
| } |
| + // The native macOS shell (LChatMac) can't hand Finder file-drops to the page — |
| + // a WKWebView doesn't deliver them and the web drop listeners above are skipped |
| + // in native mode — so it fills the page's static #file-input and dispatches |
| + // `change` (see ChatFeedWebView's __lchatAddFiles). The live composer uses its |
| + // own input, so expose a handler that pulls the files off that static input |
| + // into this thread's staging path; render() wires it to #file-input for the |
| + // thread on screen. (A plain browser never reaches this — its drops land on the |
| + // web listeners above.) |
| + root._dropFiles = () => { |
| + const el = document.getElementById("file-input"); |
| + if (!el) return; |
| + const files = [...(el.files || [])]; |
| + el.value = ""; |
| + if (files.length) stageFiles(files); |
| + }; |
| // ---- :emoji / @mention typeahead — the original menu, verbatim ---- |
| // composerToken (pure) detects the in-progress token at the caret; the host |