diff --git a/src/components/PromptInput/PromptInput.tsx b/src/components/PromptInput/PromptInput.tsx index 27e6ff4d..37cbec82 100644 --- a/src/components/PromptInput/PromptInput.tsx +++ b/src/components/PromptInput/PromptInput.tsx @@ -67,6 +67,7 @@ import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'; import { getFastModeUnavailableReason, isFastModeAvailable, isFastModeCooldown, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js'; import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'; import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js'; +import { extractDraggedFilePaths } from '../../utils/dragDropPaths.js'; import { getImageFromClipboard, PASTE_THRESHOLD } from '../../utils/imagePaste.js'; import type { ImageDimensions } from '../../utils/imageResizer.js'; import { cacheImagePath, storeImage } from '../../utils/imageStore.js'; @@ -1204,6 +1205,22 @@ function PromptInput({ // Clean up pasted text - strip ANSI escape codes and normalize line endings and tabs let text = stripAnsi(rawText).replace(/\r/g, '\n').replaceAll('\t', ' '); + // Detect file paths from drag-and-drop and convert to @mentions. + // When files are dragged into the terminal, the terminal sends their + // absolute paths via bracketed paste. Image files are handled by the + // image paste handler upstream; here we handle non-image files by + // converting them to @mentions so they get attached on submit. + const draggedPaths = extractDraggedFilePaths(text); + if (draggedPaths.length > 0) { + const mentions = draggedPaths + .map(p => (p.includes(' ') ? `@"${p}"` : `@${p}`)) + .join(' '); + // Ensure spacing around the mention(s) relative to existing input + const charBefore = input[cursorOffset - 1]; + const prefix = charBefore && !/\s/.test(charBefore) ? ' ' : ''; + text = prefix + mentions + ' '; + } + // Match typed/auto-suggest: `!cmd` pasted into empty input enters bash mode. if (input.length === 0) { const pastedMode = getModeFromInput(text); diff --git a/src/utils/dragDropPaths.ts b/src/utils/dragDropPaths.ts new file mode 100644 index 00000000..19d3b519 --- /dev/null +++ b/src/utils/dragDropPaths.ts @@ -0,0 +1,47 @@ +import { isAbsolute } from 'path' +import { getFsImplementation } from './fsOperations.js' +import { isImageFilePath } from './imagePaste.js' + +/** + * Detect absolute file paths in pasted text (typically from drag-and-drop). + * Returns the cleaned paths if ALL segments are existing non-image files, + * or an empty array otherwise. + * + * Splitting logic mirrors usePasteHandler: space preceding `/` or a Windows + * drive letter, plus newline separators. + */ +export function extractDraggedFilePaths(text: string): string[] { + const segments = text + .split(/ (?=\/|[A-Za-z]:\\)/) + .flatMap(part => part.split('\n')) + .map(s => s.trim()) + .filter(Boolean) + + if (segments.length === 0) return [] + + const fs = getFsImplementation() + const cleaned: string[] = [] + + for (const raw of segments) { + // Strip outer quotes and shell-escape backslashes + let p = raw + if ( + (p.startsWith('"') && p.endsWith('"')) || + (p.startsWith("'") && p.endsWith("'")) + ) { + p = p.slice(1, -1) + } + if (process.platform !== 'win32') { + p = p.replace(/\\(.)/g, '$1') + } + + if (!isAbsolute(p)) return [] + // Image files are handled by the upstream image paste handler + if (isImageFilePath(raw)) return [] + // Verify the path actually exists on disk + if (!fs.existsSync(p)) return [] + cleaned.push(p) + } + + return cleaned +}