Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotCLIImageSupport.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as fs from 'fs/promises';
7
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
8
import { createDirectoryIfNotExists, IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
9
import { ILogService } from '../../../../platform/log/common/logService';
10
import { createServiceIdentifier } from '../../../../util/common/services';
11
import { Lazy } from '../../../../util/vs/base/common/lazy';
12
import { ResourceSet } from '../../../../util/vs/base/common/map';
13
import { URI } from '../../../../util/vs/base/common/uri';
14
15
export interface ICopilotCLIImageSupport {
16
readonly _serviceBrand: undefined;
17
storeImage(imageData: Uint8Array, mimeType: string): Promise<URI>;
18
isTrustedImage(imageUri: URI): boolean;
19
}
20
21
export function isImageMimeType(mimeType: string): boolean {
22
const map: Record<string, string> = {
23
'image/png': '.png',
24
'image/jpeg': '.jpg',
25
'image/jpg': '.jpg',
26
'image/gif': '.gif',
27
'image/webp': '.webp',
28
'image/bmp': '.bmp',
29
};
30
return mimeType.toLowerCase() in map;
31
}
32
33
export const ICopilotCLIImageSupport = createServiceIdentifier<ICopilotCLIImageSupport>('ICopilotCLIImageSupport');
34
35
export class CopilotCLIImageSupport implements ICopilotCLIImageSupport {
36
readonly _serviceBrand: undefined;
37
private readonly storageDir: URI;
38
private readonly initialized: Lazy<Promise<void>>;
39
private readonly trustedImages = new ResourceSet();
40
constructor(
41
@IVSCodeExtensionContext private readonly context: IVSCodeExtensionContext,
42
@ILogService private readonly logService: ILogService,
43
@IFileSystemService private readonly fileSystemService: IFileSystemService,
44
) {
45
this.storageDir = URI.joinPath(this.context.globalStorageUri, 'copilot-cli-images');
46
this.initialized = new Lazy<Promise<void>>(() => this.initialize());
47
void this.initialized.value;
48
}
49
50
private async initialize(): Promise<void> {
51
try {
52
await createDirectoryIfNotExists(this.fileSystemService, this.storageDir);
53
void this.cleanupOldImages();
54
} catch (error) {
55
this.logService.error(`[CopilotCLISession] ImageStorage: Failed to initialize`, error);
56
}
57
}
58
59
isTrustedImage(imageUri: URI): boolean {
60
return this.trustedImages.has(imageUri);
61
}
62
63
async storeImage(imageData: Uint8Array, mimeType: string): Promise<URI> {
64
await this.initialized.value;
65
const timestamp = Date.now();
66
const randomId = Math.random().toString(36).substring(2, 10);
67
const extension = this.getExtension(mimeType);
68
const filename = `${timestamp}-${randomId}${extension}`;
69
const imageUri = URI.file(URI.joinPath(this.storageDir, filename).fsPath);
70
71
await fs.writeFile(imageUri.fsPath, imageData);
72
this.trustedImages.add(imageUri);
73
return imageUri;
74
}
75
76
async cleanupOldImages(maxAgeMs: number = 7 * 24 * 60 * 60 * 1000): Promise<void> {
77
try {
78
const entries = await fs.readdir(this.storageDir.fsPath, { withFileTypes: true });
79
const now = Date.now();
80
const cutoff = now - maxAgeMs;
81
82
for (const entry of entries) {
83
if (entry.isFile()) {
84
const fileUri = URI.joinPath(this.storageDir, entry.name);
85
try {
86
const stat = await fs.stat(fileUri.fsPath);
87
if (stat.mtime.getTime() < cutoff) {
88
await fs.unlink(fileUri.fsPath);
89
}
90
} catch {
91
// Skip files we can't access
92
}
93
}
94
}
95
} catch (error) {
96
console.error('ImageStorage: Failed to cleanup old images', error);
97
}
98
}
99
100
private getExtension(mimeType: string): string {
101
const map: Record<string, string> = {
102
'image/png': '.png',
103
'image/jpeg': '.jpg',
104
'image/jpg': '.jpg',
105
'image/gif': '.gif',
106
'image/webp': '.webp',
107
'image/bmp': '.bmp',
108
};
109
return map[mimeType.toLowerCase()] || '.bin';
110
}
111
}
112
113