diff --git a/CHANGELOG.md b/CHANGELOG.md index 04c61d65f..a4dcec709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- Plugins in the desktop app can drag files out to Explorer or Finder - Termix actions: plugins can declare what they do once, and it works from the API with permission checks, dry runs, run history and approvals - API keys can be limited to reading or running actions, when an admin makes them - A click on a host can open its host page, when a plugin offers one diff --git a/electron/main.cjs b/electron/main.cjs index a179f0aab..1cf643bff 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -657,6 +657,12 @@ function createManagedTempFile(fileName, content, encoding = "utf8") { return { tempId, path: filePath }; } +function dragIcon() { + // Windows cancels a drag with an empty icon + const buf = Buffer.alloc(16 * 16 * 4, 0x99); + return nativeImage.createFromBitmap(buf, { width: 16, height: 16 }); +} + function cleanupManagedTempFile(tempId) { const temp = tempFiles.get(tempId); if (!temp) return; @@ -1961,6 +1967,23 @@ ipcMain.handle("start-drag-to-desktop", (event, dragData) => { } }); +// Must run synchronously inside the renderer's dragstart, files must exist. +ipcMain.on("start-drag-files", (event, tempIds) => { + try { + const files = (Array.isArray(tempIds) ? tempIds : []) + .map((id) => tempFiles.get(id)?.path) + .filter((p) => p && fs.existsSync(p)); + if (files.length === 0) return; + event.sender.startDrag({ + file: files[0], + files, + icon: dragIcon(), + }); + } catch (error) { + logToFile("Failed to start drag:", error.message); + } +}); + ipcMain.handle("cleanup-temp-file", (_event, tempId) => { try { cleanupManagedTempFile(tempId); diff --git a/electron/preload.js b/electron/preload.js index 4ceb8fb2b..46afb1eac 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -91,6 +91,7 @@ contextBridge.exposeInMainWorld("electronAPI", { ipcRenderer.invoke("create-temp-folder", folderData), startDragToDesktop: (dragData) => ipcRenderer.invoke("start-drag-to-desktop", dragData), + startDragFiles: (tempIds) => ipcRenderer.send("start-drag-files", tempIds), cleanupTempFile: (tempId) => ipcRenderer.invoke("cleanup-temp-file", tempId), // Native web views placed over an element of the page (WebContentsView). diff --git a/packages/plugin-sdk/src/desktop.ts b/packages/plugin-sdk/src/desktop.ts index 811bee573..b515edd7a 100644 --- a/packages/plugin-sdk/src/desktop.ts +++ b/packages/plugin-sdk/src/desktop.ts @@ -112,6 +112,9 @@ export interface ElectronAPI { error?: string; }>; + /** Start a native drag of temp files. Call it synchronously in dragstart. */ + startDragFiles?: (tempIds: string[]) => void; + cleanupTempFile: (tempId: string) => Promise<{ success: boolean; error?: string;