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({
))}
)}
-
+