From 9ee76272da93ef73dcc049e5dc26b8ebfb8a35a4 Mon Sep 17 00:00:00 2001 From: Guilherme Lima <60904312+guilimasp@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:35:00 -0300 Subject: [PATCH] Drop files onto the chat to attach them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is no way to hand a bot a file today: you copy the path out of Finder by hand, or you do not send it at all. Dropping one on the window did something worse — Electron navigated the window to the file and the app went blank. Dropping a file anywhere on the window now attaches it. The overlay says so while you drag; the file becomes a chip beside any pasted text, and on send it folds into the message as . By path rather than by content, on purpose: every driver here is an agent that can open a file itself, so a 200 MB video, a PDF, and a CSV all work the same way, in one line of prompt, for providers that have no attachment protocol at all. Only the preload can name a dropped file — Electron 32 removed File.path — so the bridge exposes webUtils.getPathForFile. A drag out of a web page carries no file on disk: small text lands as a pasted chip instead, anything else says so rather than attaching a path that does not exist. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0197wYuWWpF21iBeNgHZ8n3X --- electron/preload.cjs | 11 +- src/components/Composer.tsx | 10 +- src/components/ComposerAttachments.tsx | 179 +++++++++++++++++++++---- src/lib/composer-attachments.ts | 34 +++-- src/types/ogb.d.ts | 3 + 5 files changed, 202 insertions(+), 35 deletions(-) diff --git a/electron/preload.cjs b/electron/preload.cjs index 92366f935..5d4a98870 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -1,6 +1,6 @@ // Renderer bridge. contextIsolation stays on; the renderer only ever sees // this narrow surface (window.ogb), never Node or ipcRenderer itself. -const { contextBridge, ipcRenderer } = require("electron"); +const { contextBridge, ipcRenderer, webUtils } = require("electron"); contextBridge.exposeInMainWorld("ogb", { /** Host platform ("darwin" | "win32" | "linux") — for platform-aware UI. */ @@ -19,6 +19,15 @@ contextBridge.exposeInMainWorld("ogb", { ipcRenderer.on("speech:end", handler); return () => ipcRenderer.removeListener("speech:end", handler); }, + /** Absolute path of a dropped File — Electron 32 removed File.path, and + * only the preload can ask. "" when the drag carried no file on disk. */ + getPathForFile: (file) => { + try { + return webUtils.getPathForFile(file); + } catch { + return ""; + } + }, /** {mic} TCC status strings: granted|denied|not-determined|unknown. * No screen field — macOS 15+ caches that status per-process, so any * value here would lie for the whole session after a grant. */ diff --git a/src/components/Composer.tsx b/src/components/Composer.tsx index 3bda94207..d85be350b 100644 --- a/src/components/Composer.tsx +++ b/src/components/Composer.tsx @@ -47,6 +47,10 @@ export function Composer({ // pastes too long for the input ride along as chips and fold back // into the message on send const [attachments, setAttachments] = useState([]); + const addAttachments = useCallback( + (next: Attachment[]) => setAttachments((prev) => [...prev, ...next]), + [], + ); const removeAttachment = useCallback( (id: string) => setAttachments((prev) => prev.filter((a) => a.id !== id)), [], @@ -222,7 +226,11 @@ export function Composer({ ))} )} - +