22 * Copyright (c) Microsoft Corporation. All rights reserved.
33 * Licensed under the MIT License. See License.txt in the project root for license information.
44 *--------------------------------------------------------------------------------------------*/
5-
5+ import { VSBuffer } from '../../../../base/common/buffer.js' ;
66import { CancellationToken } from '../../../../base/common/cancellation.js' ;
7+ import { Codicon } from '../../../../base/common/codicons.js' ;
78import { createStringDataTransferItem , IDataTransferItem , IReadonlyVSDataTransfer , VSDataTransfer } from '../../../../base/common/dataTransfer.js' ;
89import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js' ;
10+ import { Disposable } from '../../../../base/common/lifecycle.js' ;
11+ import { Mimes } from '../../../../base/common/mime.js' ;
12+ import { basename , joinPath } from '../../../../base/common/resources.js' ;
13+ import { URI , UriComponents } from '../../../../base/common/uri.js' ;
914import { IRange } from '../../../../editor/common/core/range.js' ;
1015import { DocumentPasteContext , DocumentPasteEdit , DocumentPasteEditProvider , DocumentPasteEditsSession } from '../../../../editor/common/languages.js' ;
1116import { ITextModel } from '../../../../editor/common/model.js' ;
1217import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js' ;
13- import { Disposable } from '../../../../base/common/lifecycle.js' ;
14- import { ChatInputPart } from './chatInputPart.js' ;
15- import { IChatWidgetService } from './chat.js' ;
16- import { Codicon } from '../../../../base/common/codicons.js' ;
18+ import { IModelService } from '../../../../editor/common/services/model.js' ;
1719import { localize } from '../../../../nls.js' ;
18- import { IChatRequestPasteVariableEntry , IChatRequestVariableEntry } from '../common/chatModel.js' ;
20+ import { IEnvironmentService } from '../../../../platform/environment/common/environment.js' ;
21+ import { IFileService } from '../../../../platform/files/common/files.js' ;
22+ import { ILogService } from '../../../../platform/log/common/log.js' ;
1923import { IExtensionService , isProposedApiEnabled } from '../../../services/extensions/common/extensions.js' ;
20- import { Mimes } from '../../../../base/common/mime.js' ;
21- import { IModelService } from '../../../../editor/common/services/model.js' ;
22- import { URI , UriComponents } from '../../../../base/common/uri.js' ;
23- import { basename } from '../../../../base/common/resources.js' ;
24+ import { IChatRequestPasteVariableEntry , IChatRequestVariableEntry } from '../common/chatModel.js' ;
25+ import { IChatWidgetService } from './chat.js' ;
26+ import { ChatInputPart } from './chatInputPart.js' ;
2427import { resizeImage } from './imageUtils.js' ;
2528
2629const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data' ;
@@ -31,6 +34,7 @@ interface SerializedCopyData {
3134}
3235
3336export class PasteImageProvider implements DocumentPasteEditProvider {
37+ private readonly imagesFolder : URI ;
3438
3539 public readonly kind = new HierarchicalKind ( 'chat.attach.image' ) ;
3640 public readonly providedPasteEditKinds = [ this . kind ] ;
@@ -41,7 +45,13 @@ export class PasteImageProvider implements DocumentPasteEditProvider {
4145 constructor (
4246 private readonly chatWidgetService : IChatWidgetService ,
4347 private readonly extensionService : IExtensionService ,
44- ) { }
48+ @IFileService private readonly fileService : IFileService ,
49+ @IEnvironmentService private readonly environmentService : IEnvironmentService ,
50+ @ILogService private readonly logService : ILogService ,
51+ ) {
52+ this . imagesFolder = joinPath ( this . environmentService . workspaceStorageHome , 'vscode-chat-images' ) ;
53+ this . cleanupOldImages ( ) ;
54+ }
4555
4656 async provideDocumentPasteEdits ( model : ITextModel , ranges : readonly IRange [ ] , dataTransfer : IReadonlyVSDataTransfer , context : DocumentPasteContext , token : CancellationToken ) : Promise < DocumentPasteEditsSession | undefined > {
4757 if ( ! this . extensionService . extensions . some ( ext => isProposedApiEnabled ( ext , 'chatReferenceBinaryData' ) ) ) {
@@ -90,12 +100,17 @@ export class PasteImageProvider implements DocumentPasteEditProvider {
90100 tempDisplayName = `${ displayName } ${ appendValue } ` ;
91101 }
92102
103+ const fileReference = await this . createFileForMedia ( currClipboard , mimeType ) ;
104+ if ( token . isCancellationRequested || ! fileReference ) {
105+ return ;
106+ }
107+
93108 const scaledImageData = await resizeImage ( currClipboard ) ;
94109 if ( token . isCancellationRequested || ! scaledImageData ) {
95110 return ;
96111 }
97112
98- const scaledImageContext = await getImageAttachContext ( scaledImageData , mimeType , token , tempDisplayName ) ;
113+ const scaledImageContext = await getImageAttachContext ( scaledImageData , mimeType , token , tempDisplayName , fileReference ) ;
99114 if ( token . isCancellationRequested || ! scaledImageContext ) {
100115 return ;
101116 }
@@ -111,21 +126,75 @@ export class PasteImageProvider implements DocumentPasteEditProvider {
111126 const edit = createCustomPasteEdit ( model , scaledImageContext , mimeType , this . kind , localize ( 'pastedImageAttachment' , 'Pasted Image Attachment' ) , this . chatWidgetService ) ;
112127 return createEditSession ( edit ) ;
113128 }
129+
130+ private async createFileForMedia (
131+ dataTransfer : Uint8Array ,
132+ mimeType : string ,
133+ ) : Promise < URI | undefined > {
134+ const exists = await this . fileService . exists ( this . imagesFolder ) ;
135+ if ( ! exists ) {
136+ await this . fileService . createFolder ( this . imagesFolder ) ;
137+ }
138+
139+ const ext = mimeType . split ( '/' ) [ 1 ] || 'png' ;
140+ const filename = `image-${ Date . now ( ) } .${ ext } ` ;
141+ const fileUri = joinPath ( this . imagesFolder , filename ) ;
142+
143+ const buffer = VSBuffer . wrap ( dataTransfer ) ;
144+ await this . fileService . writeFile ( fileUri , buffer ) ;
145+
146+ return fileUri ;
147+ }
148+
149+ private async cleanupOldImages ( ) : Promise < void > {
150+ const exists = await this . fileService . exists ( this . imagesFolder ) ;
151+ if ( ! exists ) {
152+ return ;
153+ }
154+
155+ const duration = 7 * 24 * 60 * 60 * 1000 ; // 7 days
156+ const files = await this . fileService . resolve ( this . imagesFolder ) ;
157+ if ( ! files . children ) {
158+ return ;
159+ }
160+
161+ await Promise . all ( files . children . map ( async ( file ) => {
162+ try {
163+ const timestamp = this . getTimestampFromFilename ( file . name ) ;
164+ if ( timestamp && ( Date . now ( ) - timestamp > duration ) ) {
165+ await this . fileService . del ( file . resource ) ;
166+ }
167+ } catch ( err ) {
168+ this . logService . error ( 'Failed to clean up old images' , err ) ;
169+ }
170+ } ) ) ;
171+ }
172+
173+ private getTimestampFromFilename ( filename : string ) : number | undefined {
174+ const match = filename . match ( / i m a g e - ( \d + ) \. / ) ;
175+ if ( match ) {
176+ return parseInt ( match [ 1 ] , 10 ) ;
177+ }
178+ return undefined ;
179+ }
114180}
115181
116- async function getImageAttachContext ( data : Uint8Array , mimeType : string , token : CancellationToken , displayName : string ) : Promise < IChatRequestVariableEntry | undefined > {
182+ async function getImageAttachContext ( data : Uint8Array , mimeType : string , token : CancellationToken , displayName : string , resource : URI ) : Promise < IChatRequestVariableEntry | undefined > {
117183 const imageHash = await imageToHash ( data ) ;
118184 if ( token . isCancellationRequested ) {
119185 return undefined ;
120186 }
121187
122188 return {
189+ kind : 'image' ,
123190 value : data ,
124191 id : imageHash ,
125192 name : displayName ,
126193 isImage : true ,
127194 icon : Codicon . fileMedia ,
128- mimeType
195+ mimeType,
196+ isPasted : true ,
197+ references : [ { reference : resource , kind : 'reference' } ]
129198 } ;
130199}
131200
@@ -308,10 +377,13 @@ export class ChatPasteProvidersFeature extends Disposable {
308377 @ILanguageFeaturesService languageFeaturesService : ILanguageFeaturesService ,
309378 @IChatWidgetService chatWidgetService : IChatWidgetService ,
310379 @IExtensionService extensionService : IExtensionService ,
311- @IModelService modelService : IModelService
380+ @IFileService fileService : IFileService ,
381+ @IModelService modelService : IModelService ,
382+ @IEnvironmentService environmentService : IEnvironmentService ,
383+ @ILogService logService : ILogService ,
312384 ) {
313385 super ( ) ;
314- this . _register ( languageFeaturesService . documentPasteEditProvider . register ( { scheme : ChatInputPart . INPUT_SCHEME , pattern : '*' , hasAccessToAllModels : true } , new PasteImageProvider ( chatWidgetService , extensionService ) ) ) ;
386+ this . _register ( languageFeaturesService . documentPasteEditProvider . register ( { scheme : ChatInputPart . INPUT_SCHEME , pattern : '*' , hasAccessToAllModels : true } , new PasteImageProvider ( chatWidgetService , extensionService , fileService , environmentService , logService ) ) ) ;
315387 this . _register ( languageFeaturesService . documentPasteEditProvider . register ( { scheme : ChatInputPart . INPUT_SCHEME , pattern : '*' , hasAccessToAllModels : true } , new PasteTextProvider ( chatWidgetService , modelService ) ) ) ;
316388 this . _register ( languageFeaturesService . documentPasteEditProvider . register ( '*' , new CopyTextProvider ( ) ) ) ;
317389 }
0 commit comments