From fd1eb4b70b2492b27040c4106f569f773728a5dd Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Mon, 2 Feb 2026 11:09:57 -0800 Subject: [PATCH] feat(desktop): Allow dragging files from filetree and changes tab into terminal Add drag-and-drop support for files from multiple sources (file tree, search results, and changes tab) into the terminal. Paths are automatically shell-escaped when dropped. Refactored drag logic into a reusable `useFileDrag` hook to eliminate duplication across three components (FileItem, FileTreeItem, FileSearchResultItem). --- .../TabsContent/Terminal/Terminal.tsx | 14 ++++++++--- .../components/FileItem/FileItem.tsx | 5 +++- .../RightSidebar/ChangesView/hooks/index.ts | 1 + .../ChangesView/hooks/useFileDrag.ts | 24 +++++++++++++++++++ .../FileSearchResultItem.tsx | 5 +++- .../components/FileTreeItem/FileTreeItem.tsx | 5 +++- bun.lock | 2 +- 7 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/hooks/useFileDrag.ts diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx index f27e06fbc20..e0bd9e04748 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx @@ -283,9 +283,17 @@ export const Terminal = ({ paneId, tabId, workspaceId }: TerminalProps) => { const handleDrop = (event: React.DragEvent) => { event.preventDefault(); const files = Array.from(event.dataTransfer.files); - if (files.length === 0) return; - const paths = files.map((file) => window.webUtils.getPathForFile(file)); - const text = shellEscapePaths(paths); + let text: string; + if (files.length > 0) { + // Native file drop (from Finder, etc.) + const paths = files.map((file) => window.webUtils.getPathForFile(file)); + text = shellEscapePaths(paths); + } else { + // Internal drag (from file tree) - path is in text/plain + const plainText = event.dataTransfer.getData("text/plain"); + if (!plainText) return; + text = shellEscapePaths([plainText]); + } if (!isExitedRef.current) { writeRef.current({ paneId, data: text }); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx index dcca91b487f..7eae4406ec3 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/FileItem/FileItem.tsx @@ -29,7 +29,7 @@ import { } from "react-icons/lu"; import type { ChangeCategory, ChangedFile } from "shared/changes-types"; import { createFileKey, useScrollContext } from "../../../../ChangesContent"; -import { usePathActions } from "../../hooks"; +import { useFileDrag, usePathActions } from "../../hooks"; import { getStatusColor, getStatusIndicator } from "../../utils"; interface FileItemProps { @@ -106,6 +106,8 @@ export function FileItem({ cwd: worktreePath, }); + const fileDragProps = useFileDrag({ absolutePath }); + const handleClick = useCallback(() => { if (clickTimeoutRef.current) { clearTimeout(clickTimeoutRef.current); @@ -161,6 +163,7 @@ export function FileItem({ const fileContent = (
{ + if (!absolutePath) { + e.preventDefault(); + return; + } + e.dataTransfer.setData("text/plain", absolutePath); + e.dataTransfer.effectAllowed = "copy"; + }, + [absolutePath], + ); + + return { + draggable: Boolean(absolutePath), + onDragStart: handleDragStart, + }; +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx index b01220973f3..e4867e63e52 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileSearchResultItem/FileSearchResultItem.tsx @@ -17,7 +17,7 @@ import { LuTrash2, } from "react-icons/lu"; import type { DirectoryEntry } from "shared/file-tree-types"; -import { usePathActions } from "../../../ChangesView/hooks"; +import { useFileDrag, usePathActions } from "../../../ChangesView/hooks"; import { SEARCH_RESULT_ROW_HEIGHT } from "../../constants"; import { getFileIcon } from "../../utils"; @@ -83,6 +83,8 @@ export function FileSearchResultItem({ cwd: worktreePath, }); + const fileDragProps = useFileDrag({ absolutePath: entry.path }); + const handleClick = () => { if (!entry.isDirectory) { onActivate(entry); @@ -104,6 +106,7 @@ export function FileSearchResultItem({ const itemContent = (
{ e.stopPropagation(); if (isFolder) { @@ -102,6 +104,7 @@ export function FileTreeItem({ const itemContent = (