44 *--------------------------------------------------------------------------------------------*/
55
66import * as dom from '../../../../base/browser/dom.js' ;
7+ import { DragAndDropObserver } from '../../../../base/browser/dom.js' ;
78import { Codicon } from '../../../../base/common/codicons.js' ;
89import { Disposable , DisposableStore } from '../../../../base/common/lifecycle.js' ;
910import { URI } from '../../../../base/common/uri.js' ;
1011import { CancellationToken , CancellationTokenSource } from '../../../../base/common/cancellation.js' ;
1112import { Emitter } from '../../../../base/common/event.js' ;
12- import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js' ;
13+ import { renderIcon , renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js' ;
1314import { localize } from '../../../../nls.js' ;
1415import { ThemeIcon } from '../../../../base/common/themables.js' ;
1516import { registerOpenEditorListeners } from '../../../../platform/editor/browser/editor.js' ;
@@ -29,7 +30,8 @@ import { IChatRequestVariableEntry, OmittedState } from '../../../../workbench/c
2930import { isLocation } from '../../../../editor/common/languages.js' ;
3031import { resizeImage } from '../../../../workbench/contrib/chat/browser/chatImageUtils.js' ;
3132import { imageToHash , isImage } from '../../../../workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.js' ;
32- import { getPathForFile } from '../../../../platform/dnd/browser/dnd.js' ;
33+ import { CodeDataTransfers , containsDragType , extractEditorsDropData , getPathForFile } from '../../../../platform/dnd/browser/dnd.js' ;
34+ import { DataTransfers } from '../../../../base/browser/dnd.js' ;
3335import { getExcludes , ISearchConfiguration , ISearchService , QueryType } from '../../../../workbench/services/search/common/search.js' ;
3436
3537/**
@@ -101,7 +103,7 @@ export class NewChatContextAttachments extends Disposable {
101103 const pill = dom . append ( this . _container , dom . $ ( '.sessions-chat-attachment-pill' ) ) ;
102104 pill . tabIndex = 0 ;
103105 pill . role = 'button' ;
104- const icon = entry . kind === 'image' ? Codicon . fileMedia : Codicon . file ;
106+ const icon = entry . kind === 'image' ? Codicon . fileMedia : entry . kind === 'directory' ? Codicon . folder : Codicon . file ;
105107 dom . append ( pill , renderIcon ( icon ) ) ;
106108 dom . append ( pill , dom . $ ( 'span.sessions-chat-attachment-name' , undefined , entry . name ) ) ;
107109
@@ -127,68 +129,85 @@ export class NewChatContextAttachments extends Disposable {
127129
128130 // --- Drag and drop ---
129131
130- registerDropTarget ( element : HTMLElement ) : void {
131- // Use a transparent overlay during drag to capture events over the Monaco editor
132- const overlay = dom . append ( element , dom . $ ( '.sessions-chat-drop-overlay' ) ) ;
132+ registerDropTarget ( dndContainer : HTMLElement ) : void {
133+ const overlay = dom . append ( dndContainer , dom . $ ( '.sessions-chat-dnd-overlay' ) ) ;
134+ let overlayText : HTMLElement | undefined ;
133135
134- // Use capture phase to intercept drag events before Monaco editor handles them
135- this . _register ( dom . addDisposableListener ( element , dom . EventType . DRAG_ENTER , ( e : DragEvent ) => {
136- if ( e . dataTransfer && Array . from ( e . dataTransfer . types ) . includes ( 'Files' ) ) {
137- e . preventDefault ( ) ;
138- e . dataTransfer . dropEffect = 'copy' ;
139- overlay . style . display = 'block' ;
140- element . classList . add ( 'sessions-chat-drop-active' ) ;
141- }
142- } , true ) ) ;
136+ const isDropSupported = ( e : DragEvent ) : boolean => {
137+ return containsDragType ( e , DataTransfers . FILES , CodeDataTransfers . EDITORS , CodeDataTransfers . FILES , DataTransfers . RESOURCES , DataTransfers . INTERNAL_URI_LIST ) ;
138+ } ;
143139
144- this . _register ( dom . addDisposableListener ( element , dom . EventType . DRAG_OVER , ( e : DragEvent ) => {
145- if ( e . dataTransfer && Array . from ( e . dataTransfer . types ) . includes ( 'Files' ) ) {
146- e . preventDefault ( ) ;
147- e . dataTransfer . dropEffect = 'copy' ;
148- if ( overlay . style . display !== 'block' ) {
149- overlay . style . display = 'block' ;
150- element . classList . add ( 'sessions-chat-drop-active' ) ;
151- }
140+ const showOverlay = ( ) => {
141+ overlay . classList . add ( 'visible' ) ;
142+ if ( ! overlayText ) {
143+ const label = localize ( 'attachAsContext' , "Attach as Context" ) ;
144+ const iconAndTextElements = renderLabelWithIcons ( `$(${ Codicon . attach . id } ) ${ label } ` ) ;
145+ const htmlElements = iconAndTextElements . map ( element => {
146+ if ( typeof element === 'string' ) {
147+ return dom . $ ( 'span.overlay-text' , undefined , element ) ;
148+ }
149+ return element ;
150+ } ) ;
151+ overlayText = dom . $ ( 'span.attach-context-overlay-text' , undefined , ...htmlElements ) ;
152+ overlay . appendChild ( overlayText ) ;
152153 }
153- } , true ) ) ;
154-
155- this . _register ( dom . addDisposableListener ( overlay , dom . EventType . DRAG_OVER , ( e ) => {
156- e . preventDefault ( ) ;
157- e . dataTransfer ! . dropEffect = 'copy' ;
158- } ) ) ;
154+ } ;
159155
160- this . _register ( dom . addDisposableListener ( overlay , dom . EventType . DRAG_LEAVE , ( e ) => {
161- if ( e . relatedTarget && element . contains ( e . relatedTarget as Node ) ) {
162- return ;
163- }
164- overlay . style . display = 'none' ;
165- element . classList . remove ( 'sessions-chat-drop-active' ) ;
166- } ) ) ;
156+ const hideOverlay = ( ) => {
157+ overlay . classList . remove ( 'visible' ) ;
158+ overlayText ?. remove ( ) ;
159+ overlayText = undefined ;
160+ } ;
167161
168- this . _register ( dom . addDisposableListener ( overlay , dom . EventType . DROP , async ( e ) => {
169- e . preventDefault ( ) ;
170- e . stopPropagation ( ) ;
171- overlay . style . display = 'none' ;
172- element . classList . remove ( 'sessions-chat-drop-active' ) ;
173-
174- // Try items first (for URI-based drops from VS Code tree views)
175- const items = e . dataTransfer ?. items ;
176- if ( items ) {
177- for ( const item of Array . from ( items ) ) {
178- if ( item . kind === 'file' ) {
179- const file = item . getAsFile ( ) ;
180- if ( ! file ) {
181- continue ;
162+ this . _register ( new DragAndDropObserver ( dndContainer , {
163+ onDragOver : ( e ) => {
164+ if ( isDropSupported ( e ) ) {
165+ e . preventDefault ( ) ;
166+ e . stopPropagation ( ) ;
167+ if ( e . dataTransfer ) {
168+ e . dataTransfer . dropEffect = 'copy' ;
169+ }
170+ showOverlay ( ) ;
171+ }
172+ } ,
173+ onDragLeave : ( ) => {
174+ hideOverlay ( ) ;
175+ } ,
176+ onDrop : async ( e ) => {
177+ e . preventDefault ( ) ;
178+ e . stopPropagation ( ) ;
179+ hideOverlay ( ) ;
180+
181+ // Extract editor data from VS Code internal drags (e.g., explorer view)
182+ const editorDropData = extractEditorsDropData ( e ) ;
183+ if ( editorDropData . length > 0 ) {
184+ for ( const editor of editorDropData ) {
185+ if ( editor . resource ) {
186+ await this . _attachFileUri ( editor . resource , basename ( editor . resource ) ) ;
182187 }
183- const filePath = getPathForFile ( file ) ;
184- if ( ! filePath ) {
185- continue ;
188+ }
189+ return ;
190+ }
191+
192+ // Fallback: try native file items
193+ const items = e . dataTransfer ?. items ;
194+ if ( items ) {
195+ for ( const item of Array . from ( items ) ) {
196+ if ( item . kind === 'file' ) {
197+ const file = item . getAsFile ( ) ;
198+ if ( ! file ) {
199+ continue ;
200+ }
201+ const filePath = getPathForFile ( file ) ;
202+ if ( ! filePath ) {
203+ continue ;
204+ }
205+ const uri = URI . file ( filePath ) ;
206+ await this . _attachFileUri ( uri , file . name ) ;
186207 }
187- const uri = URI . file ( filePath ) ;
188- await this . _attachFileUri ( uri , file . name ) ;
189208 }
190209 }
191- }
210+ } ,
192211 } ) ) ;
193212 }
194213
@@ -446,6 +465,23 @@ export class NewChatContextAttachments extends Disposable {
446465 }
447466
448467 private async _attachFileUri ( uri : URI , name : string ) : Promise < void > {
468+ let stat ;
469+ try {
470+ stat = await this . fileService . stat ( uri ) ;
471+ } catch {
472+ return ;
473+ }
474+
475+ if ( stat . isDirectory ) {
476+ this . _addAttachments ( {
477+ kind : 'directory' ,
478+ id : uri . toString ( ) ,
479+ value : uri ,
480+ name,
481+ } ) ;
482+ return ;
483+ }
484+
449485 if ( / \. ( p n g | j p g | j p e g | b m p | g i f | t i f f ) $ / i. test ( uri . path ) ) {
450486 const readFile = await this . fileService . readFile ( uri ) ;
451487 const resizedImage = await resizeImage ( readFile . value . buffer ) ;
0 commit comments