From ff8fc48853e4298c3a143b26087a5b23d6965240 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Thu, 4 Dec 2025 23:07:48 -0800 Subject: [PATCH 1/5] Add drag-and-drop file support to terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements drag-and-drop functionality for terminal, matching macOS Terminal behavior. Users can now drag files or folders from Finder directly into the terminal to insert their paths. Features: - Automatically inserts file paths when files are dropped - Handles multiple files (space-separated) - Auto-quotes paths containing spaces - Uses full file paths from Electron File API - Works with files, folders, and multiple selections Usage: Simply drag a file from Finder and drop it into any terminal block. The file path will be inserted at the cursor position. 🤖 Generated with Claude Code Co-Authored-By: Claude --- frontend/app/view/term/term.tsx | 63 ++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 387155752..1ac15a6e7 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -349,8 +349,69 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => const termBg = computeBgStyleFromMeta(blockData?.meta); + // Handle drag and drop + const handleDragOver = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + // Indicate that we can accept the drop + e.dataTransfer.dropEffect = "copy"; + }, []); + + const handleDrop = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Get files from the drop + const files = Array.from(e.dataTransfer.files); + if (files.length === 0) { + return; + } + + // Get the file path(s) - for Electron, we can get the full path + const paths = files.map((file: any) => { + // In Electron, File objects have a 'path' property with the full path + if (file.path) { + return file.path; + } + // Fallback to just the name if path is not available + return file.name; + }); + + // Insert the path(s) into the terminal + // If multiple files, separate with spaces and quote if necessary + const pathString = paths.map(path => { + // Quote paths that contain spaces + if (path.includes(" ")) { + return `"${path}"`; + } + return path; + }).join(" "); + + // Send the path to the terminal + if (model.termRef.current && pathString) { + model.sendDataToController(pathString); + } + }, [model]); + + const handleDragEnter = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDragLeave = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + return ( -
+
{termBg &&
} From 5032ac6940ca5c55b0ef7d3121bca49f99660a5f Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Thu, 4 Dec 2025 23:11:41 -0800 Subject: [PATCH 2/5] Fix drag-and-drop to use full file paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Electron's webUtils.getPathForFile() API to get the actual full file path instead of just the file name. This matches macOS Terminal behavior where dragging a file inserts its complete path. Before: CV_Document.pdf After: /Users/steven/Downloads/CV_Document.pdf 🤖 Generated with Claude Code Co-Authored-By: Claude --- frontend/app/view/term/term.tsx | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 1ac15a6e7..cb7c16df3 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -357,7 +357,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => e.dataTransfer.dropEffect = "copy"; }, []); - const handleDrop = React.useCallback((e: React.DragEvent) => { + const handleDrop = React.useCallback(async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -367,15 +367,24 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => return; } - // Get the file path(s) - for Electron, we can get the full path - const paths = files.map((file: any) => { - // In Electron, File objects have a 'path' property with the full path - if (file.path) { - return file.path; - } - // Fallback to just the name if path is not available - return file.name; - }); + // Get the file path(s) - Use Electron's webUtils to get real paths + let paths: string[] = []; + try { + // In Electron, we need to use webUtils.getPathForFile to get the actual path + const { webUtils } = await import("electron"); + paths = files.map((file: File) => { + try { + return webUtils.getPathForFile(file); + } catch (err) { + console.warn("Could not get path for file:", file.name, err); + return file.name; + } + }); + } catch (err) { + // If webUtils is not available (non-Electron environment), fallback to file.name + console.warn("webUtils not available, using file names only"); + paths = files.map(f => f.name); + } // Insert the path(s) into the terminal // If multiple files, separate with spaces and quote if necessary From ff642c509e82ec5a02d7ac0c31ddaa0b3056b505 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Thu, 4 Dec 2025 23:15:10 -0800 Subject: [PATCH 3/5] Add Electron API for getting file paths in drag-drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose webUtils.getPathForFile through the Electron preload API so the renderer process can get full file system paths from File objects during drag-and-drop operations. Changes: - Added getPathForFile method to preload script using webUtils - Updated ElectronApi TypeScript interface - Simplified terminal drag-drop to use new API via getApi() This properly implements macOS Terminal-style drag-and-drop with full file paths instead of just filenames. 🤖 Generated with Claude Code Co-Authored-By: Claude --- emain/preload.ts | 3 ++- frontend/app/view/term/term.tsx | 30 +++++++++++------------------- frontend/types/custom.d.ts | 1 + 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/emain/preload.ts b/emain/preload.ts index c6bdf1498..ee60c1360 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -1,7 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { contextBridge, ipcRenderer, Rectangle, WebviewTag } from "electron"; +import { contextBridge, ipcRenderer, Rectangle, webUtils, WebviewTag } from "electron"; // update type in custom.d.ts (ElectronApi type) contextBridge.exposeInMainWorld("api", { @@ -68,6 +68,7 @@ contextBridge.exposeInMainWorld("api", { openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId), setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId), doRefresh: () => ipcRenderer.send("do-refresh"), + getPathForFile: (file: File) => webUtils.getPathForFile(file), }); // Custom event for "new-window" diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index cb7c16df3..f2360fdca 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -357,7 +357,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => e.dataTransfer.dropEffect = "copy"; }, []); - const handleDrop = React.useCallback(async (e: React.DragEvent) => { + const handleDrop = React.useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -367,24 +367,16 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => return; } - // Get the file path(s) - Use Electron's webUtils to get real paths - let paths: string[] = []; - try { - // In Electron, we need to use webUtils.getPathForFile to get the actual path - const { webUtils } = await import("electron"); - paths = files.map((file: File) => { - try { - return webUtils.getPathForFile(file); - } catch (err) { - console.warn("Could not get path for file:", file.name, err); - return file.name; - } - }); - } catch (err) { - // If webUtils is not available (non-Electron environment), fallback to file.name - console.warn("webUtils not available, using file names only"); - paths = files.map(f => f.name); - } + // Get the file path(s) using the Electron API + const paths = files.map((file: File) => { + try { + // Use the exposed Electron API to get the full path + return getApi().getPathForFile(file); + } catch (err) { + console.warn("Could not get path for file:", file.name, err); + return file.name; + } + }); // Insert the path(s) into the terminal // If multiple files, separate with spaces and quote if necessary diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 3476fe539..b43f31c8c 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -130,6 +130,7 @@ declare global { openBuilder: (appId?: string) => void; // open-builder setBuilderWindowAppId: (appId: string) => void; // set-builder-window-appid doRefresh: () => void; // do-refresh + getPathForFile: (file: File) => string; // get-path-for-file }; type ElectronContextMenuItem = { From 1e9c99fa204e7c7055927fc51e8c0b580b916c85 Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Thu, 4 Dec 2025 23:19:51 -0800 Subject: [PATCH 4/5] Fix: Import getApi in terminal component for drag-drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added missing import of getApi from @/store/global to fix 'getApi is not defined' error when dropping files. Also added debug logging to help troubleshoot file path retrieval. 🤖 Generated with Claude Code Co-Authored-By: Claude --- frontend/app/view/term/term.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index f2360fdca..8104cc7f7 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -7,7 +7,7 @@ import { waveEventSubscribe } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import type { TermViewModel } from "@/app/view/term/term-model"; -import { atoms, getOverrideConfigAtom, getSettingsPrefixAtom, globalStore, WOS } from "@/store/global"; +import { atoms, getApi, getOverrideConfigAtom, getSettingsPrefixAtom, globalStore, WOS } from "@/store/global"; import { fireAndForget, useAtomValueSafe } from "@/util/util"; import { computeBgStyleFromMeta } from "@/util/waveutil"; import { ISearchOptions } from "@xterm/addon-search"; @@ -367,17 +367,23 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => return; } + console.log("Drop files:", files); + // Get the file path(s) using the Electron API const paths = files.map((file: File) => { try { // Use the exposed Electron API to get the full path - return getApi().getPathForFile(file); + const fullPath = getApi().getPathForFile(file); + console.log("File:", file.name, "-> Full path:", fullPath); + return fullPath; } catch (err) { - console.warn("Could not get path for file:", file.name, err); + console.error("Could not get path for file:", file.name, err); return file.name; } }); + console.log("Paths to insert:", paths); + // Insert the path(s) into the terminal // If multiple files, separate with spaces and quote if necessary const pathString = paths.map(path => { @@ -388,6 +394,8 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => return path; }).join(" "); + console.log("Final path string:", pathString); + // Send the path to the terminal if (model.termRef.current && pathString) { model.sendDataToController(pathString); From 16d7f51070fe1c31c380d0c33b7c9edb7a7aeaae Mon Sep 17 00:00:00 2001 From: Steven Vo Date: Mon, 15 Dec 2025 11:16:37 +0700 Subject: [PATCH 5/5] Address CodeRabbit feedback: only intercept file drops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add isFileDrop helper to check if drag contains files - Only preventDefault for file drops, allow text/URL drops through - Remove debug console.log statements - Improve path quoting to handle special shell characters (spaces, quotes) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- frontend/app/view/term/term.tsx | 42 +++++++++++++-------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 8104cc7f7..e34ca6fdf 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -350,31 +350,34 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => const termBg = computeBgStyleFromMeta(blockData?.meta); // Handle drag and drop + // Helper to check if drag event contains files + const isFileDrop = (e: React.DragEvent): boolean => { + return e.dataTransfer?.types?.includes("Files") ?? false; + }; + const handleDragOver = React.useCallback((e: React.DragEvent) => { + if (!isFileDrop(e)) return; e.preventDefault(); e.stopPropagation(); - // Indicate that we can accept the drop e.dataTransfer.dropEffect = "copy"; }, []); const handleDrop = React.useCallback((e: React.DragEvent) => { + if (!isFileDrop(e)) return; e.preventDefault(); e.stopPropagation(); - // Get files from the drop const files = Array.from(e.dataTransfer.files); - if (files.length === 0) { - return; - } - - console.log("Drop files:", files); + if (files.length === 0) return; - // Get the file path(s) using the Electron API + // Get file paths using Electron API const paths = files.map((file: File) => { try { - // Use the exposed Electron API to get the full path const fullPath = getApi().getPathForFile(file); - console.log("File:", file.name, "-> Full path:", fullPath); + // Quote paths with spaces or special shell characters + if (/[\s'"]/.test(fullPath)) { + return `"${fullPath}"`; + } return fullPath; } catch (err) { console.error("Could not get path for file:", file.name, err); @@ -382,32 +385,21 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => } }); - console.log("Paths to insert:", paths); - - // Insert the path(s) into the terminal - // If multiple files, separate with spaces and quote if necessary - const pathString = paths.map(path => { - // Quote paths that contain spaces - if (path.includes(" ")) { - return `"${path}"`; - } - return path; - }).join(" "); - - console.log("Final path string:", pathString); - - // Send the path to the terminal + // Send space-separated paths to terminal + const pathString = paths.join(" "); if (model.termRef.current && pathString) { model.sendDataToController(pathString); } }, [model]); const handleDragEnter = React.useCallback((e: React.DragEvent) => { + if (!isFileDrop(e)) return; e.preventDefault(); e.stopPropagation(); }, []); const handleDragLeave = React.useCallback((e: React.DragEvent) => { + if (!isFileDrop(e)) return; e.preventDefault(); e.stopPropagation(); }, []);