diff --git a/src/script.ts b/src/script.ts index 4c949e2..7380925 100644 --- a/src/script.ts +++ b/src/script.ts @@ -72,6 +72,7 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt const fileSearchInput = document.getElementById('fileSearchInput'); const fileList = document.getElementById('fileList'); const imageBtn = document.getElementById('imageBtn'); + const inputContainer = document.getElementById('inputContainer'); let isProcessRunning = false; let filteredFiles = []; @@ -1334,6 +1335,148 @@ const getScript = (isTelemetryEnabled: boolean, opencreditsApiUrl: string = 'htt // but ensure paste will work when selected }); + // Drag & drop support for images (from OS) and files (from VS Code explorer/editor tabs) + let dragCounter = 0; + + function resetDragState() { + dragCounter = 0; + inputContainer.classList.remove('drag-over'); + } + + function isRelevantDrag(dataTransfer) { + if (!dataTransfer || !dataTransfer.types) { + return false; + } + return dataTransfer.types.indexOf('Files') !== -1 || dataTransfer.types.indexOf('text/uri-list') !== -1; + } + + function insertFileReference(filePath) { + const cursorPos = messageInput.selectionStart; + const textBefore = messageInput.value.substring(0, cursorPos); + const textAfter = messageInput.value.substring(cursorPos); + const newText = textBefore + '@' + filePath + ' ' + textAfter; + + messageInput.value = newText; + messageInput.focus(); + + const newCursorPos = textBefore.length + filePath.length + 2; + messageInput.setSelectionRange(newCursorPos, newCursorPos); + adjustTextareaHeight(); + } + + function fileUriToPath(uri) { + if (!uri || uri.indexOf('file://') !== 0) { + return null; + } + try { + let filePath = decodeURIComponent(uri.substring('file://'.length)); + // Strip leading slash from Windows drive paths like /c:/Users/... + if (/^\\/[a-zA-Z]:\\//.test(filePath)) { + filePath = filePath.substring(1); + } + return filePath; + } catch (error) { + console.error('Failed to decode dropped file URI:', uri, error); + return null; + } + } + + document.body.addEventListener('dragenter', (e) => { + if (!isRelevantDrag(e.dataTransfer)) { + return; + } + e.preventDefault(); + e.stopPropagation(); + dragCounter++; + inputContainer.classList.add('drag-over'); + }); + + document.body.addEventListener('dragover', (e) => { + if (!isRelevantDrag(e.dataTransfer)) { + return; + } + e.preventDefault(); + e.stopPropagation(); + }); + + document.body.addEventListener('dragleave', (e) => { + if (!isRelevantDrag(e.dataTransfer)) { + return; + } + e.preventDefault(); + e.stopPropagation(); + dragCounter = Math.max(0, dragCounter - 1); + if (dragCounter === 0) { + inputContainer.classList.remove('drag-over'); + } + }); + + // Safety net: iframe boundaries in VS Code webviews often don't fire dragleave + // (e.g. dropping outside the webview, or aborting the drag with Esc) + window.addEventListener('dragend', resetDragState); + window.addEventListener('blur', resetDragState); + + document.body.addEventListener('drop', (e) => { + if (!isRelevantDrag(e.dataTransfer)) { + return; + } + e.preventDefault(); + e.stopPropagation(); + resetDragState(); + + const dataTransfer = e.dataTransfer; + if (!dataTransfer) { + return; + } + + // Case 1: files dropped from the OS (e.g. Explorer/Finder) - attach images + if (dataTransfer.files && dataTransfer.files.length > 0) { + let hasSkippedFile = false; + for (let i = 0; i < dataTransfer.files.length; i++) { + const file = dataTransfer.files[i]; + if (file.type && file.type.startsWith('image/')) { + const reader = new FileReader(); + reader.onload = function(event) { + vscode.postMessage({ + type: 'createImageFile', + imageData: event.target.result, + imageType: file.type + }); + }; + reader.readAsDataURL(file); + } else { + hasSkippedFile = true; + } + } + if (hasSkippedFile) { + showToast('Only image files can be dropped directly - use @ to reference other files'); + } + return; + } + + // Case 2: items dropped from VS Code (explorer/editor tabs) - insert as @ file references + const uriList = dataTransfer.getData('text/uri-list'); + if (uriList) { + const uris = uriList.split('\\n') + .map(function(line) { return line.trim(); }) + .filter(function(line) { return line && line.indexOf('#') !== 0; }); + + let insertedCount = 0; + uris.forEach(function(uri) { + const filePath = fileUriToPath(uri); + if (filePath) { + insertedCount++; + insertFileReference(filePath); + } + }); + + // uris were present but none were local file:// paths (e.g. untitled:, vscode-remote://) + if (uris.length > 0 && insertedCount === 0) { + showToast('Only local files can be dropped here'); + } + } + }); + // Initialize textarea height adjustTextareaHeight(); diff --git a/src/ui-styles.ts b/src/ui-styles.ts index 76bd766..67785a8 100644 --- a/src/ui-styles.ts +++ b/src/ui-styles.ts @@ -1652,6 +1652,12 @@ const styles = ` position: relative; } + .input-container.drag-over { + outline: 2px dashed var(--vscode-focusBorder); + outline-offset: -2px; + background-color: rgba(139, 92, 246, 0.08); + } + .model-selector-row { display: flex; align-items: center;