Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatContext.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*--------------------------------------------------------------------------------------------*/4import { CancellationToken } from '../../../../../base/common/cancellation.js';5import { Codicon } from '../../../../../base/common/codicons.js';6import { Disposable } from '../../../../../base/common/lifecycle.js';7import { isElectron } from '../../../../../base/common/platform.js';8import { dirname } from '../../../../../base/common/resources.js';9import { ThemeIcon } from '../../../../../base/common/themables.js';10import { localize } from '../../../../../nls.js';11import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';12import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';13import { ILabelService } from '../../../../../platform/label/common/label.js';14import { IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';15import { IWorkbenchContribution } from '../../../../common/contributions.js';16import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js';17import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js';18import { IEditorService } from '../../../../services/editor/common/editorService.js';19import { IHostService } from '../../../../services/host/browser/host.js';20import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js';21import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.js';22import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js';23import { IChatContextPickService, IChatContextValueItem, IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPicker } from '../chatContextPickService.js';24import { IChatEditingService } from '../../common/chatEditingService.js';25import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IImageVariableEntry, OmittedState, toToolSetVariableEntry, toToolVariableEntry } from '../../common/chatVariableEntries.js';26import { ToolDataSource, ToolSet } from '../../common/languageModelToolsService.js';27import { IChatWidget } from '../chat.js';28import { imageToHash, isImage } from '../chatPasteProviders.js';29import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js';30import { ChatInstructionsPickerPick } from '../promptSyntax/attachInstructionsAction.js';313233export class ChatContextContributions extends Disposable implements IWorkbenchContribution {3435static readonly ID = 'chat.contextContributions';3637constructor(38@IInstantiationService instantiationService: IInstantiationService,39@IChatContextPickService contextPickService: IChatContextPickService,40) {41super();4243// ###############################################################################################44//45// Default context picks/values which are "native" to chat. This is NOT the complete list46// and feature area specific context, like for notebooks, problems, etc, should be contributed47// by the feature area.48//49// ###############################################################################################5051this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ToolsContextPickerPick)));52this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ChatInstructionsPickerPick)));53this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(OpenEditorContextValuePick)));54this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(RelatedFilesContextPickerPick)));55this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ClipboardImageContextValuePick)));56this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ScreenshotContextValuePick)));57}58}5960class ToolsContextPickerPick implements IChatContextPickerItem {6162readonly type = 'pickerPick';63readonly label: string = localize('chatContext.tools', 'Tools...');64readonly icon: ThemeIcon = Codicon.tools;65readonly ordinal = -500;6667asPicker(widget: IChatWidget): IChatContextPicker {6869type Pick = IChatContextPickerPickItem & { toolInfo: { ordinal: number; label: string } };70const items: Pick[] = [];7172for (const [entry, enabled] of widget.input.selectedToolsModel.entriesMap.get()) {73if (enabled) {74if (entry instanceof ToolSet) {75items.push({76toolInfo: ToolDataSource.classify(entry.source),77label: entry.referenceName,78description: entry.description,79asAttachment: (): IChatRequestToolSetEntry => toToolSetVariableEntry(entry)80});81} else {82items.push({83toolInfo: ToolDataSource.classify(entry.source),84label: entry.toolReferenceName ?? entry.displayName,85description: entry.userDescription ?? entry.modelDescription,86asAttachment: (): IChatRequestToolEntry => toToolVariableEntry(entry)87});88}89}90}9192items.sort((a, b) => {93let res = a.toolInfo.ordinal - b.toolInfo.ordinal;94if (res === 0) {95res = a.toolInfo.label.localeCompare(b.toolInfo.label);96}97if (res === 0) {98res = a.label.localeCompare(b.label);99}100return res;101});102103let lastGroupLabel: string | undefined;104const picks: (IQuickPickSeparator | Pick)[] = [];105106for (const item of items) {107if (lastGroupLabel !== item.toolInfo.label) {108picks.push({ type: 'separator', label: item.toolInfo.label });109lastGroupLabel = item.toolInfo.label;110}111picks.push(item);112}113114return {115placeholder: localize('chatContext.tools.placeholder', 'Select a tool'),116picks: Promise.resolve(picks)117};118}119120121}122123124125class OpenEditorContextValuePick implements IChatContextValueItem {126127readonly type = 'valuePick';128readonly label: string = localize('chatContext.editors', 'Open Editors');129readonly icon: ThemeIcon = Codicon.file;130readonly ordinal = 800;131132constructor(133@IEditorService private _editorService: IEditorService,134@ILabelService private _labelService: ILabelService,135) { }136137isEnabled(): Promise<boolean> | boolean {138return this._editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0;139}140141async asAttachment(): Promise<IChatRequestVariableEntry[]> {142const result: IChatRequestVariableEntry[] = [];143for (const editor of this._editorService.editors) {144if (!(editor instanceof FileEditorInput || editor instanceof DiffEditorInput || editor instanceof UntitledTextEditorInput || editor instanceof NotebookEditorInput)) {145continue;146}147const uri = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY });148if (!uri) {149continue;150}151result.push({152kind: 'file',153id: uri.toString(),154value: uri,155name: this._labelService.getUriBasenameLabel(uri),156});157}158return result;159}160161}162163class RelatedFilesContextPickerPick implements IChatContextPickerItem {164165readonly type = 'pickerPick';166167readonly label: string = localize('chatContext.relatedFiles', 'Related Files');168readonly icon: ThemeIcon = Codicon.sparkle;169readonly ordinal = 300;170171constructor(172@IChatEditingService private readonly _chatEditingService: IChatEditingService,173@ILabelService private readonly _labelService: ILabelService,174) { }175176isEnabled(widget: IChatWidget): boolean {177return this._chatEditingService.hasRelatedFilesProviders() && (Boolean(widget.getInput()) || widget.attachmentModel.fileAttachments.length > 0);178}179180asPicker(widget: IChatWidget): IChatContextPicker {181182const picks = (async () => {183const chatSessionId = widget.viewModel?.sessionId;184if (!chatSessionId) {185return [];186}187const relatedFiles = await this._chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None);188if (!relatedFiles) {189return [];190}191const attachments = widget.attachmentModel.getAttachmentIDs();192return this._chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None)193.then((files) => (files ?? []).reduce<(IChatContextPickerPickItem | IQuickPickSeparator)[]>((acc, cur) => {194acc.push({ type: 'separator', label: cur.group });195for (const file of cur.files) {196const label = this._labelService.getUriBasenameLabel(file.uri);197acc.push({198label: label,199description: this._labelService.getUriLabel(dirname(file.uri), { relative: true }),200disabled: attachments.has(file.uri.toString()),201asAttachment: () => {202return {203kind: 'file',204id: file.uri.toString(),205value: file.uri,206name: label,207omittedState: OmittedState.NotOmitted208};209}210});211}212return acc;213}, []));214})();215216return {217placeholder: localize('relatedFiles', 'Add related files to your working set'),218picks,219};220}221}222223224class ClipboardImageContextValuePick implements IChatContextValueItem {225readonly type = 'valuePick';226readonly label = localize('imageFromClipboard', 'Image from Clipboard');227readonly icon = Codicon.fileMedia;228229constructor(230@IClipboardService private readonly _clipboardService: IClipboardService,231) { }232233async isEnabled(widget: IChatWidget) {234if (!widget.input.selectedLanguageModel?.metadata.capabilities?.vision) {235return false;236}237const imageData = await this._clipboardService.readImage();238return isImage(imageData);239}240241async asAttachment(): Promise<IImageVariableEntry> {242const fileBuffer = await this._clipboardService.readImage();243return {244id: await imageToHash(fileBuffer),245name: localize('pastedImage', 'Pasted Image'),246fullName: localize('pastedImage', 'Pasted Image'),247value: fileBuffer,248kind: 'image',249};250}251}252253class ScreenshotContextValuePick implements IChatContextValueItem {254255readonly type = 'valuePick';256readonly icon = Codicon.deviceCamera;257readonly label = (isElectron258? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window')259: localize('chatContext.attachScreenshot.labelWeb', 'Screenshot'));260261constructor(262@IHostService private readonly _hostService: IHostService,263) { }264265async isEnabled(widget: IChatWidget) {266return !!widget.input.selectedLanguageModel?.metadata.capabilities?.vision;267}268269async asAttachment(): Promise<IChatRequestVariableEntry | undefined> {270const blob = await this._hostService.getScreenshot();271return blob && convertBufferToScreenshotVariable(blob);272}273}274275276