Path: blob/main/src/vs/workbench/contrib/chat/browser/chatAttachmentResolveService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { VSBuffer } from '../../../../base/common/buffer.js';6import { Codicon } from '../../../../base/common/codicons.js';7import { basename } from '../../../../base/common/resources.js';8import { ThemeIcon } from '../../../../base/common/themables.js';9import { URI } from '../../../../base/common/uri.js';10import { IRange } from '../../../../editor/common/core/range.js';11import { SymbolKinds } from '../../../../editor/common/languages.js';12import { ITextModelService } from '../../../../editor/common/services/resolverService.js';13import { localize } from '../../../../nls.js';14import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';15import { IDraggedResourceEditorInput, MarkerTransferData, DocumentSymbolTransferData, NotebookCellOutputTransferData } from '../../../../platform/dnd/browser/dnd.js';16import { IFileService } from '../../../../platform/files/common/files.js';17import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';18import { MarkerSeverity } from '../../../../platform/markers/common/markers.js';19import { isUntitledResourceEditorInput } from '../../../common/editor.js';20import { EditorInput } from '../../../common/editor/editorInput.js';21import { IEditorService } from '../../../services/editor/common/editorService.js';22import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';23import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js';24import { createNotebookOutputVariableEntry, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST } from '../../notebook/browser/contrib/chat/notebookChatUtils.js';25import { getOutputViewModelFromId } from '../../notebook/browser/controller/cellOutputActions.js';26import { getNotebookEditorFromEditorPane } from '../../notebook/browser/notebookBrowser.js';27import { SCMHistoryItemTransferData } from '../../scm/browser/scmHistoryChatContext.js';28import { CHAT_ATTACHABLE_IMAGE_MIME_TYPES, getAttachableImageExtension } from '../common/chatModel.js';29import { IChatRequestVariableEntry, OmittedState, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry, toPromptFileVariableEntry, PromptFileVariableKind, ISCMHistoryItemVariableEntry } from '../common/chatVariableEntries.js';30import { getPromptsTypeForLanguageId, PromptsType } from '../common/promptSyntax/promptTypes.js';31import { imageToHash } from './chatPasteProviders.js';32import { resizeImage } from './imageUtils.js';3334export const IChatAttachmentResolveService = createDecorator<IChatAttachmentResolveService>('IChatAttachmentResolveService');3536export interface IChatAttachmentResolveService {37_serviceBrand: undefined;3839resolveEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined>;40resolveUntitledEditorAttachContext(editor: IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined>;41resolveResourceAttachContext(resource: URI, isDirectory: boolean): Promise<IChatRequestVariableEntry | undefined>;4243resolveImageEditorAttachContext(resource: URI, data?: VSBuffer, mimeType?: string): Promise<IChatRequestVariableEntry | undefined>;44resolveImageAttachContext(images: ImageTransferData[]): Promise<IChatRequestVariableEntry[]>;45resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[];46resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): ISymbolVariableEntry[];47resolveNotebookOutputAttachContext(data: NotebookCellOutputTransferData): IChatRequestVariableEntry[];48resolveSourceControlHistoryItemAttachContext(data: SCMHistoryItemTransferData[]): ISCMHistoryItemVariableEntry[];49}5051export class ChatAttachmentResolveService implements IChatAttachmentResolveService {52_serviceBrand: undefined;5354constructor(55@IFileService private fileService: IFileService,56@IEditorService private editorService: IEditorService,57@ITextModelService private textModelService: ITextModelService,58@IExtensionService private extensionService: IExtensionService,59@IDialogService private dialogService: IDialogService60) { }6162// --- EDITORS ---6364public async resolveEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined> {65// untitled editor66if (isUntitledResourceEditorInput(editor)) {67return await this.resolveUntitledEditorAttachContext(editor);68}6970if (!editor.resource) {71return undefined;72}7374let stat;75try {76stat = await this.fileService.stat(editor.resource);77} catch {78return undefined;79}8081if (!stat.isDirectory && !stat.isFile) {82return undefined;83}8485const imageContext = await this.resolveImageEditorAttachContext(editor.resource);86if (imageContext) {87return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? imageContext : undefined;88}8990return await this.resolveResourceAttachContext(editor.resource, stat.isDirectory);91}9293public async resolveUntitledEditorAttachContext(editor: IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined> {94// If the resource is known, we can use it directly95if (editor.resource) {96return await this.resolveResourceAttachContext(editor.resource, false);97}9899// Otherwise, we need to check if the contents are already open in another editor100const openUntitledEditors = this.editorService.editors.filter(editor => editor instanceof UntitledTextEditorInput) as UntitledTextEditorInput[];101for (const canidate of openUntitledEditors) {102const model = await canidate.resolve();103const contents = model.textEditorModel?.getValue();104if (contents === editor.contents) {105return await this.resolveResourceAttachContext(canidate.resource, false);106}107}108109return undefined;110}111112public async resolveResourceAttachContext(resource: URI, isDirectory: boolean): Promise<IChatRequestVariableEntry | undefined> {113let omittedState = OmittedState.NotOmitted;114115if (!isDirectory) {116117let languageId: string | undefined;118try {119const createdModel = await this.textModelService.createModelReference(resource);120languageId = createdModel.object.getLanguageId();121createdModel.dispose();122} catch {123omittedState = OmittedState.Full;124}125126if (/\.(svg)$/i.test(resource.path)) {127omittedState = OmittedState.Full;128}129if (languageId) {130const promptsType = getPromptsTypeForLanguageId(languageId);131if (promptsType === PromptsType.prompt) {132return toPromptFileVariableEntry(resource, PromptFileVariableKind.PromptFile);133} else if (promptsType === PromptsType.instructions) {134return toPromptFileVariableEntry(resource, PromptFileVariableKind.Instruction);135}136}137}138139return {140kind: isDirectory ? 'directory' : 'file',141value: resource,142id: resource.toString(),143name: basename(resource),144omittedState145};146}147148// --- IMAGES ---149150public async resolveImageEditorAttachContext(resource: URI, data?: VSBuffer, mimeType?: string): Promise<IChatRequestVariableEntry | undefined> {151if (!resource) {152return undefined;153}154155if (mimeType) {156if (!getAttachableImageExtension(mimeType)) {157return undefined;158}159} else {160const match = SUPPORTED_IMAGE_EXTENSIONS_REGEX.exec(resource.path);161if (!match) {162return undefined;163}164165mimeType = getMimeTypeFromPath(match);166}167const fileName = basename(resource);168169let dataBuffer: VSBuffer | undefined;170if (data) {171dataBuffer = data;172} else {173174let stat;175try {176stat = await this.fileService.stat(resource);177} catch {178return undefined;179}180181const readFile = await this.fileService.readFile(resource);182183if (stat.size > 30 * 1024 * 1024) { // 30 MB184this.dialogService.error(localize('imageTooLarge', 'Image is too large'), localize('imageTooLargeMessage', 'The image {0} is too large to be attached.', fileName));185throw new Error('Image is too large');186}187188dataBuffer = readFile.value;189}190191const isPartiallyOmitted = /\.gif$/i.test(resource.path);192const imageFileContext = await this.resolveImageAttachContext([{193id: resource.toString(),194name: fileName,195data: dataBuffer.buffer,196icon: Codicon.fileMedia,197resource: resource,198mimeType: mimeType,199omittedState: isPartiallyOmitted ? OmittedState.Partial : OmittedState.NotOmitted200}]);201202return imageFileContext[0];203}204205public resolveImageAttachContext(images: ImageTransferData[]): Promise<IChatRequestVariableEntry[]> {206return Promise.all(images.map(async image => ({207id: image.id || await imageToHash(image.data),208name: image.name,209fullName: image.resource ? image.resource.path : undefined,210value: await resizeImage(image.data, image.mimeType),211icon: image.icon,212kind: 'image',213isFile: false,214isDirectory: false,215omittedState: image.omittedState || OmittedState.NotOmitted,216references: image.resource ? [{ reference: image.resource, kind: 'reference' }] : []217})));218}219220// --- MARKERS ---221222public resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[] {223return markers.map((marker): IDiagnosticVariableEntry => {224let filter: IDiagnosticVariableEntryFilterData;225if (!('severity' in marker)) {226filter = { filterUri: URI.revive(marker.uri), filterSeverity: MarkerSeverity.Warning };227} else {228filter = IDiagnosticVariableEntryFilterData.fromMarker(marker);229}230231return IDiagnosticVariableEntryFilterData.toEntry(filter);232});233}234235// --- SYMBOLS ---236237public resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): ISymbolVariableEntry[] {238return symbols.map(symbol => {239const resource = URI.file(symbol.fsPath);240return {241kind: 'symbol',242id: symbolId(resource, symbol.range),243value: { uri: resource, range: symbol.range },244symbolKind: symbol.kind,245icon: SymbolKinds.toIcon(symbol.kind),246fullName: symbol.name,247name: symbol.name,248};249});250}251252// --- NOTEBOOKS ---253254public resolveNotebookOutputAttachContext(data: NotebookCellOutputTransferData): IChatRequestVariableEntry[] {255const notebookEditor = getNotebookEditorFromEditorPane(this.editorService.activeEditorPane);256if (!notebookEditor) {257return [];258}259260const outputViewModel = getOutputViewModelFromId(data.outputId, notebookEditor);261if (!outputViewModel) {262return [];263}264265const mimeType = outputViewModel.pickedMimeType?.mimeType;266if (mimeType && NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST.includes(mimeType)) {267268const entry = createNotebookOutputVariableEntry(outputViewModel, mimeType, notebookEditor);269if (!entry) {270return [];271}272273return [entry];274}275276return [];277}278279// --- SOURCE CONTROL ---280281public resolveSourceControlHistoryItemAttachContext(data: SCMHistoryItemTransferData[]): ISCMHistoryItemVariableEntry[] {282return data.map(d => ({283id: d.historyItem.id,284name: d.name,285value: URI.revive(d.resource),286historyItem: {287...d.historyItem,288references: []289},290kind: 'scmHistoryItem'291} satisfies ISCMHistoryItemVariableEntry));292}293}294295function symbolId(resource: URI, range?: IRange): string {296let rangePart = '';297if (range) {298rangePart = `:${range.startLineNumber}`;299if (range.startLineNumber !== range.endLineNumber) {300rangePart += `-${range.endLineNumber}`;301}302}303return resource.fsPath + rangePart;304}305306export type ImageTransferData = {307data: Uint8Array;308name: string;309icon?: ThemeIcon;310resource?: URI;311id?: string;312mimeType?: string;313omittedState?: OmittedState;314};315const SUPPORTED_IMAGE_EXTENSIONS_REGEX = new RegExp(`\\.(${Object.keys(CHAT_ATTACHABLE_IMAGE_MIME_TYPES).join('|')})$`, 'i');316317function getMimeTypeFromPath(match: RegExpExecArray): string | undefined {318const ext = match[1].toLowerCase();319return CHAT_ATTACHABLE_IMAGE_MIME_TYPES[ext];320}321322323324