Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatContext.ts
5297 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 { Codicon } from '../../../../../base/common/codicons.js';5import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';6import { isElectron } from '../../../../../base/common/platform.js';7import { ThemeIcon } from '../../../../../base/common/themables.js';8import { localize } from '../../../../../nls.js';9import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js';10import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';11import { ILabelService } from '../../../../../platform/label/common/label.js';12import { IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';13import { IWorkbenchContribution } from '../../../../common/contributions.js';14import { EditorResourceAccessor, SideBySideEditor } from '../../../../common/editor.js';15import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js';16import { IEditorService } from '../../../../services/editor/common/editorService.js';17import { IHostService } from '../../../../services/host/browser/host.js';18import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js';19import { FileEditorInput } from '../../../files/browser/editors/fileEditorInput.js';20import { NotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js';21import { IChatContextPickService, IChatContextValueItem, IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPicker } from '../attachments/chatContextPickService.js';22import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IImageVariableEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js';23import { isToolSet, ToolDataSource } from '../../common/tools/languageModelToolsService.js';24import { IChatWidget } from '../chat.js';25import { imageToHash, isImage } from '../widget/input/editor/chatPasteProviders.js';26import { convertBufferToScreenshotVariable } from '../attachments/chatScreenshotContext.js';27import { ChatInstructionsPickerPick } from '../promptSyntax/attachInstructionsAction.js';28import { ITerminalService } from '../../../terminal/browser/terminal.js';29import { URI } from '../../../../../base/common/uri.js';30import { ITerminalCommand, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.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(ClipboardImageContextValuePick)));55this._store.add(contextPickService.registerChatContextItem(instantiationService.createInstance(ScreenshotContextValuePick)));56}57}5859class ToolsContextPickerPick implements IChatContextPickerItem {6061readonly type = 'pickerPick';62readonly label: string = localize('chatContext.tools', 'Tools...');63readonly icon: ThemeIcon = Codicon.tools;64readonly ordinal = -500;6566isEnabled(widget: IChatWidget): boolean {67return !!widget.attachmentCapabilities.supportsToolAttachments;68}6970asPicker(widget: IChatWidget): IChatContextPicker {7172type Pick = IChatContextPickerPickItem & { toolInfo: { ordinal: number; label: string } };73const items: Pick[] = [];7475for (const [entry, enabled] of widget.input.selectedToolsModel.entriesMap.get()) {76if (enabled) {77if (isToolSet(entry)) {78items.push({79toolInfo: ToolDataSource.classify(entry.source),80label: entry.referenceName,81description: entry.description,82asAttachment: (): IChatRequestToolSetEntry => toToolSetVariableEntry(entry)83});84} else {85items.push({86toolInfo: ToolDataSource.classify(entry.source),87label: entry.toolReferenceName ?? entry.displayName,88description: entry.userDescription ?? entry.modelDescription,89asAttachment: (): IChatRequestToolEntry => toToolVariableEntry(entry)90});91}92}93}9495items.sort((a, b) => {96let res = a.toolInfo.ordinal - b.toolInfo.ordinal;97if (res === 0) {98res = a.toolInfo.label.localeCompare(b.toolInfo.label);99}100if (res === 0) {101res = a.label.localeCompare(b.label);102}103return res;104});105106let lastGroupLabel: string | undefined;107const picks: (IQuickPickSeparator | Pick)[] = [];108109for (const item of items) {110if (lastGroupLabel !== item.toolInfo.label) {111picks.push({ type: 'separator', label: item.toolInfo.label });112lastGroupLabel = item.toolInfo.label;113}114picks.push(item);115}116117return {118placeholder: localize('chatContext.tools.placeholder', 'Select a tool'),119picks: Promise.resolve(picks)120};121}122123124}125126127128class OpenEditorContextValuePick implements IChatContextValueItem {129130readonly type = 'valuePick';131readonly label: string = localize('chatContext.editors', 'Open Editors');132readonly icon: ThemeIcon = Codicon.file;133readonly ordinal = 800;134135constructor(136@IEditorService private _editorService: IEditorService,137@ILabelService private _labelService: ILabelService,138) { }139140isEnabled(): Promise<boolean> | boolean {141return this._editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0;142}143144async asAttachment(): Promise<IChatRequestVariableEntry[]> {145const result: IChatRequestVariableEntry[] = [];146for (const editor of this._editorService.editors) {147if (!(editor instanceof FileEditorInput || editor instanceof DiffEditorInput || editor instanceof UntitledTextEditorInput || editor instanceof NotebookEditorInput)) {148continue;149}150const uri = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY });151if (!uri) {152continue;153}154result.push({155kind: 'file',156id: uri.toString(),157value: uri,158name: this._labelService.getUriBasenameLabel(uri),159});160}161return result;162}163164}165166167class ClipboardImageContextValuePick implements IChatContextValueItem {168readonly type = 'valuePick';169readonly label = localize('imageFromClipboard', 'Image from Clipboard');170readonly icon = Codicon.fileMedia;171172constructor(173@IClipboardService private readonly _clipboardService: IClipboardService,174) { }175176async isEnabled(widget: IChatWidget) {177if (!widget.attachmentCapabilities.supportsImageAttachments) {178return false;179}180if (!widget.input.selectedLanguageModel.get()?.metadata.capabilities?.vision) {181return false;182}183const imageData = await this._clipboardService.readImage();184return isImage(imageData);185}186187async asAttachment(): Promise<IImageVariableEntry> {188const fileBuffer = await this._clipboardService.readImage();189return {190id: await imageToHash(fileBuffer),191name: localize('pastedImage', 'Pasted Image'),192fullName: localize('pastedImage', 'Pasted Image'),193value: fileBuffer,194kind: 'image',195};196}197}198199export class TerminalContext implements IChatContextValueItem {200201readonly type = 'valuePick';202readonly icon = Codicon.terminal;203readonly label = localize('terminal', 'Terminal');204constructor(private readonly _resource: URI, @ITerminalService private readonly _terminalService: ITerminalService) {205206}207isEnabled(widget: IChatWidget) {208const terminal = this._terminalService.getInstanceFromResource(this._resource);209return !!widget.attachmentCapabilities.supportsTerminalAttachments && terminal?.isDisposed === false;210}211async asAttachment(widget: IChatWidget): Promise<IChatRequestVariableEntry | undefined> {212const terminal = this._terminalService.getInstanceFromResource(this._resource);213if (!terminal) {214return;215}216const params = new URLSearchParams(this._resource.query);217const command = terminal.capabilities.get(TerminalCapability.CommandDetection)?.commands.find(cmd => cmd.id === params.get('command'));218if (!command) {219return;220}221const attachment: IChatRequestVariableEntry = {222kind: 'terminalCommand',223id: `terminalCommand:${Date.now()}}`,224value: this.asValue(command),225name: command.command,226command: command.command,227output: command.getOutput(),228exitCode: command.exitCode,229resource: this._resource230};231const cleanup = new DisposableStore();232let disposed = false;233const disposeCleanup = () => {234if (disposed) {235return;236}237disposed = true;238cleanup.dispose();239};240cleanup.add(widget.attachmentModel.onDidChange(e => {241if (e.deleted.includes(attachment.id)) {242disposeCleanup();243}244}));245cleanup.add(terminal.onDisposed(() => {246widget.attachmentModel.delete(attachment.id);247widget.refreshParsedInput();248disposeCleanup();249}));250return attachment;251}252253private asValue(command: ITerminalCommand): string {254let value = `Command: ${command.command}`;255const output = command.getOutput();256if (output) {257value += `\nOutput:\n${output}`;258}259if (typeof command.exitCode === 'number') {260value += `\nExit Code: ${command.exitCode}`;261}262return value;263}264}265266class ScreenshotContextValuePick implements IChatContextValueItem {267268readonly type = 'valuePick';269readonly icon = Codicon.deviceCamera;270readonly label = (isElectron271? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window')272: localize('chatContext.attachScreenshot.labelWeb', 'Screenshot'));273274constructor(275@IHostService private readonly _hostService: IHostService,276) { }277278async isEnabled(widget: IChatWidget) {279return !!widget.attachmentCapabilities.supportsImageAttachments && !!widget.input.selectedLanguageModel.get()?.metadata.capabilities?.vision;280}281282async asAttachment(): Promise<IChatRequestVariableEntry | undefined> {283const blob = await this._hostService.getScreenshot();284return blob && convertBufferToScreenshotVariable(blob);285}286}287288289