Skip to content

Commit e0fa31f

Browse files
committed
dragable-into-terminal (squashed)
1 parent f1e4a9c commit e0fa31f

3 files changed

Lines changed: 37 additions & 3 deletions

File tree

src/preload/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { contextBridge, ipcRenderer } from 'electron'
1+
import { contextBridge, ipcRenderer, webUtils } from 'electron'
22

33
type StatusCallback = (id: string, status: string) => void
44
type DataCallback = (id: string, data: string) => void
@@ -147,6 +147,9 @@ contextBridge.exposeInMainWorld('api', {
147147
// Shell
148148
openExternal: (url: string) => ipcRenderer.send('shell:openExternal', url),
149149

150+
// Resolve a dropped File's absolute path. File.path was removed in Electron 32+.
151+
getFilePath: (file: File) => webUtils.getPathForFile(file),
152+
150153
// App-level events from menu
151154
onOpenSettings: (callback: () => void) => {
152155
const handler = (): void => callback()

src/renderer/components/XTerminal.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState } from 'react'
1+
import React, { useEffect, useRef, useState } from 'react'
22
import { Terminal } from '@xterm/xterm'
33
import { FitAddon } from '@xterm/addon-fit'
44
import { SerializeAddon } from '@xterm/addon-serialize'
@@ -356,8 +356,38 @@ export function XTerminal({ terminalId, cwd, type, visible, claudeCommand, sessi
356356
}
357357
}, [visible, terminalId])
358358

359+
const handleDragOver = (e: React.DragEvent<HTMLDivElement>): void => {
360+
if (!e.dataTransfer.types.includes('Files')) return
361+
e.preventDefault()
362+
e.dataTransfer.dropEffect = 'copy'
363+
}
364+
365+
const handleDrop = (e: React.DragEvent<HTMLDivElement>): void => {
366+
const files = Array.from(e.dataTransfer.files)
367+
if (files.length === 0) return
368+
e.preventDefault()
369+
const paths = files
370+
.map((f) => window.api.getFilePath(f))
371+
.filter((p) => p && p.length > 0)
372+
if (paths.length === 0) return
373+
// Match iTerm2: shell-quote each path, join with spaces, wrap in
374+
// bracketed-paste markers so Claude (and other readline-style prompts)
375+
// treat it as a paste rather than per-keystroke input.
376+
// Claude Code detects image paths in pasted text and renders them as
377+
// attachments, but only when escaped iTerm2-style (backslash-escaped
378+
// special chars, not POSIX single-quoting).
379+
const escapeForDrop = (p: string): string => p.replace(/([ \t"'`$\\!?*()[\]{}|;<>&#])/g, '\\$1')
380+
const text = paths.map(escapeForDrop).join(' ')
381+
window.api.writeTerminal(terminalId, '\x1b[200~' + text + '\x1b[201~')
382+
terminalRef.current?.focus()
383+
}
384+
359385
return (
360-
<div className="w-full h-full relative">
386+
<div
387+
className="w-full h-full relative"
388+
onDragOver={handleDragOver}
389+
onDrop={handleDrop}
390+
>
361391
<div ref={containerRef} className="w-full h-full" />
362392
{loading && (
363393
<div className="absolute inset-0 flex items-center justify-center bg-app/70 pointer-events-none">

src/renderer/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export interface ElectronAPI {
218218
onUpdaterStatus(callback: (status: UpdaterStatus) => void): () => void
219219

220220
openExternal(url: string): void
221+
getFilePath(file: File): string
221222
onOpenSettings(callback: () => void): () => void
222223

223224
checkHooks(worktreePath: string): Promise<boolean>

0 commit comments

Comments
 (0)