2
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
4
*--------------------------------------------------------------------------------------------*/
5
-
5
+ import { VSBuffer } from '../../../../base/common/buffer.js' ;
6
6
import { CancellationToken } from '../../../../base/common/cancellation.js' ;
7
+ import { Codicon } from '../../../../base/common/codicons.js' ;
7
8
import { createStringDataTransferItem , IDataTransferItem , IReadonlyVSDataTransfer , VSDataTransfer } from '../../../../base/common/dataTransfer.js' ;
8
9
import { 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' ;
9
14
import { IRange } from '../../../../editor/common/core/range.js' ;
10
15
import { DocumentPasteContext , DocumentPasteEdit , DocumentPasteEditProvider , DocumentPasteEditsSession } from '../../../../editor/common/languages.js' ;
11
16
import { ITextModel } from '../../../../editor/common/model.js' ;
12
17
import { 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' ;
17
19
import { 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' ;
19
23
import { 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' ;
24
27
import { resizeImage } from './imageUtils.js' ;
25
28
26
29
const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data' ;
@@ -31,6 +34,7 @@ interface SerializedCopyData {
31
34
}
32
35
33
36
export class PasteImageProvider implements DocumentPasteEditProvider {
37
+ private readonly imagesFolder : URI ;
34
38
35
39
public readonly kind = new HierarchicalKind ( 'chat.attach.image' ) ;
36
40
public readonly providedPasteEditKinds = [ this . kind ] ;
@@ -41,7 +45,13 @@ export class PasteImageProvider implements DocumentPasteEditProvider {
41
45
constructor (
42
46
private readonly chatWidgetService : IChatWidgetService ,
43
47
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
+ }
45
55
46
56
async provideDocumentPasteEdits ( model : ITextModel , ranges : readonly IRange [ ] , dataTransfer : IReadonlyVSDataTransfer , context : DocumentPasteContext , token : CancellationToken ) : Promise < DocumentPasteEditsSession | undefined > {
47
57
if ( ! this . extensionService . extensions . some ( ext => isProposedApiEnabled ( ext , 'chatReferenceBinaryData' ) ) ) {
@@ -90,12 +100,17 @@ export class PasteImageProvider implements DocumentPasteEditProvider {
90
100
tempDisplayName = `${ displayName } ${ appendValue } ` ;
91
101
}
92
102
103
+ const fileReference = await this . createFileForMedia ( currClipboard , mimeType ) ;
104
+ if ( token . isCancellationRequested || ! fileReference ) {
105
+ return ;
106
+ }
107
+
93
108
const scaledImageData = await resizeImage ( currClipboard ) ;
94
109
if ( token . isCancellationRequested || ! scaledImageData ) {
95
110
return ;
96
111
}
97
112
98
- const scaledImageContext = await getImageAttachContext ( scaledImageData , mimeType , token , tempDisplayName ) ;
113
+ const scaledImageContext = await getImageAttachContext ( scaledImageData , mimeType , token , tempDisplayName , fileReference ) ;
99
114
if ( token . isCancellationRequested || ! scaledImageContext ) {
100
115
return ;
101
116
}
@@ -111,21 +126,75 @@ export class PasteImageProvider implements DocumentPasteEditProvider {
111
126
const edit = createCustomPasteEdit ( model , scaledImageContext , mimeType , this . kind , localize ( 'pastedImageAttachment' , 'Pasted Image Attachment' ) , this . chatWidgetService ) ;
112
127
return createEditSession ( edit ) ;
113
128
}
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
+ }
114
180
}
115
181
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 > {
117
183
const imageHash = await imageToHash ( data ) ;
118
184
if ( token . isCancellationRequested ) {
119
185
return undefined ;
120
186
}
121
187
122
188
return {
189
+ kind : 'image' ,
123
190
value : data ,
124
191
id : imageHash ,
125
192
name : displayName ,
126
193
isImage : true ,
127
194
icon : Codicon . fileMedia ,
128
- mimeType
195
+ mimeType,
196
+ isPasted : true ,
197
+ references : [ { reference : resource , kind : 'reference' } ]
129
198
} ;
130
199
}
131
200
@@ -308,10 +377,13 @@ export class ChatPasteProvidersFeature extends Disposable {
308
377
@ILanguageFeaturesService languageFeaturesService : ILanguageFeaturesService ,
309
378
@IChatWidgetService chatWidgetService : IChatWidgetService ,
310
379
@IExtensionService extensionService : IExtensionService ,
311
- @IModelService modelService : IModelService
380
+ @IFileService fileService : IFileService ,
381
+ @IModelService modelService : IModelService ,
382
+ @IEnvironmentService environmentService : IEnvironmentService ,
383
+ @ILogService logService : ILogService ,
312
384
) {
313
385
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 ) ) ) ;
315
387
this . _register ( languageFeaturesService . documentPasteEditProvider . register ( { scheme : ChatInputPart . INPUT_SCHEME , pattern : '*' , hasAccessToAllModels : true } , new PasteTextProvider ( chatWidgetService , modelService ) ) ) ;
316
388
this . _register ( languageFeaturesService . documentPasteEditProvider . register ( '*' , new CopyTextProvider ( ) ) ) ;
317
389
}
0 commit comments