Path: blob/main/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts
5272 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 { Codicon } from '../../../../../base/common/codicons.js';6import { IMarkdownString } from '../../../../../base/common/htmlContent.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 { IOffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';12import { isLocation, Location, SymbolKind } from '../../../../../editor/common/languages.js';13import { localize } from '../../../../../nls.js';14import { MarkerSeverity, IMarker } from '../../../../../platform/markers/common/markers.js';15import { ISCMHistoryItem } from '../../../scm/common/history.js';16import { IChatContentReference } from '../chatService/chatService.js';17import { IChatRequestVariableValue } from './chatVariables.js';18import { IToolData, IToolSet } from '../tools/languageModelToolsService.js';19import { decodeBase64, encodeBase64, VSBuffer } from '../../../../../base/common/buffer.js';20import { Mutable } from '../../../../../base/common/types.js';212223interface IBaseChatRequestVariableEntry {24readonly id: string;25readonly fullName?: string;26readonly icon?: ThemeIcon;27readonly name: string;28readonly modelDescription?: string;2930/**31* The offset-range in the prompt. This means this entry has been explicitly typed out32* by the user.33*/34readonly range?: IOffsetRange;35readonly value: IChatRequestVariableValue;36readonly references?: IChatContentReference[];3738omittedState?: OmittedState;39}4041export interface IGenericChatRequestVariableEntry extends IBaseChatRequestVariableEntry {42kind: 'generic';43tooltip?: IMarkdownString;44}4546export interface IChatRequestDirectoryEntry extends IBaseChatRequestVariableEntry {47kind: 'directory';48}4950export interface IChatRequestFileEntry extends IBaseChatRequestVariableEntry {51kind: 'file';52}5354export const enum OmittedState {55NotOmitted,56Partial,57Full,58}5960export interface IChatRequestToolEntry extends IBaseChatRequestVariableEntry {61readonly kind: 'tool';62}6364export interface IChatRequestToolSetEntry extends IBaseChatRequestVariableEntry {65readonly kind: 'toolset';66readonly value: IChatRequestToolEntry[];67}6869export type ChatRequestToolReferenceEntry = IChatRequestToolEntry | IChatRequestToolSetEntry;7071export interface StringChatContextValue {72value?: string;73name?: string;74modelDescription?: string;75icon?: ThemeIcon;76uri: URI;77resourceUri?: URI;78tooltip?: IMarkdownString;79/**80* Command ID to execute when this context item is clicked.81*/82readonly commandId?: string;83readonly handle: number;84}8586export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVariableEntry {87readonly kind: 'implicit';88readonly isFile: true;89readonly value: URI | Location | StringChatContextValue | undefined;90readonly uri: URI | undefined;91readonly isSelection: boolean;92enabled: boolean;93}9495export interface IChatRequestStringVariableEntry extends IBaseChatRequestVariableEntry {96readonly kind: 'string';97readonly value: string | undefined;98readonly modelDescription?: string;99readonly icon?: ThemeIcon;100readonly uri: URI;101readonly resourceUri?: URI;102readonly tooltip?: IMarkdownString;103/**104* Command ID to execute when this context item is clicked.105*/106readonly commandId?: string;107readonly handle: number;108}109110export interface IChatRequestWorkspaceVariableEntry extends IBaseChatRequestVariableEntry {111readonly kind: 'workspace';112readonly value: string;113readonly modelDescription?: string;114}115116117export interface IChatRequestPasteVariableEntry extends IBaseChatRequestVariableEntry {118readonly kind: 'paste';119readonly code: string;120readonly language: string;121readonly pastedLines: string;122123// This is only used for old serialized data and should be removed once we no longer support it124readonly fileName: string;125126// This is only undefined on old serialized data127readonly copiedFrom: {128readonly uri: URI;129readonly range: IRange;130} | undefined;131}132133export interface ISymbolVariableEntry extends IBaseChatRequestVariableEntry {134readonly kind: 'symbol';135readonly value: Location;136readonly symbolKind: SymbolKind;137}138139export interface ICommandResultVariableEntry extends IBaseChatRequestVariableEntry {140readonly kind: 'command';141}142143export interface IImageVariableEntry extends IBaseChatRequestVariableEntry {144readonly kind: 'image';145readonly isPasted?: boolean;146readonly isURL?: boolean;147readonly mimeType?: string;148}149150export interface INotebookOutputVariableEntry extends IBaseChatRequestVariableEntry {151readonly kind: 'notebookOutput';152readonly outputIndex?: number;153readonly mimeType?: string;154}155156export interface IDiagnosticVariableEntryFilterData {157readonly owner?: string;158readonly problemMessage?: string;159readonly filterUri?: URI;160readonly filterSeverity?: MarkerSeverity;161readonly filterRange?: IRange;162}163164165166export namespace IDiagnosticVariableEntryFilterData {167export const icon = Codicon.error;168169export function fromMarker(marker: IMarker): IDiagnosticVariableEntryFilterData {170return {171filterUri: marker.resource,172owner: marker.owner,173problemMessage: marker.message,174filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn }175};176}177178export function toEntry(data: IDiagnosticVariableEntryFilterData): IDiagnosticVariableEntry {179return {180id: id(data),181name: label(data),182icon,183value: data,184kind: 'diagnostic',185...data,186};187}188189export function id(data: IDiagnosticVariableEntryFilterData) {190return [data.filterUri, data.owner, data.filterSeverity, data.filterRange?.startLineNumber, data.filterRange?.startColumn].join(':');191}192193export function label(data: IDiagnosticVariableEntryFilterData) {194const enum TrimThreshold {195MaxChars = 30,196MaxSpaceLookback = 10,197}198if (data.problemMessage) {199if (data.problemMessage.length < TrimThreshold.MaxChars) {200return data.problemMessage;201}202203// Trim the message, on a space if it would not lose too much204// data (MaxSpaceLookback) or just blindly otherwise.205const lastSpace = data.problemMessage.lastIndexOf(' ', TrimThreshold.MaxChars);206if (lastSpace === -1 || lastSpace + TrimThreshold.MaxSpaceLookback < TrimThreshold.MaxChars) {207return data.problemMessage.substring(0, TrimThreshold.MaxChars) + '…';208}209return data.problemMessage.substring(0, lastSpace) + '…';210}211let labelStr = localize('chat.attachment.problems.all', "All Problems");212if (data.filterUri) {213labelStr = localize('chat.attachment.problems.inFile', "Problems in {0}", basename(data.filterUri));214}215216return labelStr;217}218}219220export interface IDiagnosticVariableEntry extends IBaseChatRequestVariableEntry, IDiagnosticVariableEntryFilterData {221readonly kind: 'diagnostic';222}223224export interface IElementVariableEntry extends IBaseChatRequestVariableEntry {225readonly kind: 'element';226}227228export interface IPromptFileVariableEntry extends IBaseChatRequestVariableEntry {229readonly kind: 'promptFile';230readonly value: URI;231readonly isRoot: boolean;232readonly originLabel?: string;233readonly modelDescription: string;234readonly automaticallyAdded: boolean;235readonly toolReferences?: readonly ChatRequestToolReferenceEntry[];236}237238export interface IPromptTextVariableEntry extends IBaseChatRequestVariableEntry {239readonly kind: 'promptText';240readonly value: string;241readonly settingId?: string;242readonly modelDescription: string;243readonly automaticallyAdded: boolean;244readonly toolReferences?: readonly ChatRequestToolReferenceEntry[];245}246247export interface ISCMHistoryItemVariableEntry extends IBaseChatRequestVariableEntry {248readonly kind: 'scmHistoryItem';249readonly value: URI;250readonly historyItem: ISCMHistoryItem;251}252253export interface ISCMHistoryItemChangeVariableEntry extends IBaseChatRequestVariableEntry {254readonly kind: 'scmHistoryItemChange';255readonly value: URI;256readonly historyItem: ISCMHistoryItem;257}258259export interface ISCMHistoryItemChangeRangeVariableEntry extends IBaseChatRequestVariableEntry {260readonly kind: 'scmHistoryItemChangeRange';261readonly value: URI;262readonly historyItemChangeStart: {263readonly uri: URI;264readonly historyItem: ISCMHistoryItem;265};266readonly historyItemChangeEnd: {267readonly uri: URI;268readonly historyItem: ISCMHistoryItem;269};270}271272export interface ITerminalVariableEntry extends IBaseChatRequestVariableEntry {273readonly kind: 'terminalCommand';274readonly value: string;275readonly resource: URI;276readonly command: string;277readonly output?: string;278readonly exitCode?: number;279}280281export interface IDebugVariableEntry extends IBaseChatRequestVariableEntry {282readonly kind: 'debugVariable';283readonly value: string;284readonly expression: string;285readonly type?: string;286}287288export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry289| ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry290| IChatRequestToolEntry | IChatRequestToolSetEntry291| IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry292| IPromptFileVariableEntry | IPromptTextVariableEntry293| ISCMHistoryItemVariableEntry | ISCMHistoryItemChangeVariableEntry | ISCMHistoryItemChangeRangeVariableEntry | ITerminalVariableEntry294| IChatRequestStringVariableEntry | IChatRequestWorkspaceVariableEntry | IDebugVariableEntry;295296export namespace IChatRequestVariableEntry {297298/**299* Returns URI of the passed variant entry. Return undefined if not found.300*/301export function toUri(entry: IChatRequestVariableEntry): URI | undefined {302return URI.isUri(entry.value)303? entry.value304: isLocation(entry.value)305? entry.value.uri306: undefined;307}308309export function toExport(v: IChatRequestVariableEntry): IChatRequestVariableEntry {310if (v.value instanceof Uint8Array) {311// 'dup' here is needed otherwise TS complains about the narrowed `value` in a spread operation312const dup: Mutable<IChatRequestVariableEntry> = { ...v };313dup.value = { $base64: encodeBase64(VSBuffer.wrap(v.value)) };314return dup;315}316317return v;318}319320export function fromExport(v: IChatRequestVariableEntry): IChatRequestVariableEntry {321// Old variables format322// eslint-disable-next-line local/code-no-in-operator323if (v && 'values' in v && Array.isArray(v.values)) {324return {325kind: 'generic',326id: v.id ?? '',327name: v.name,328value: v.values[0]?.value,329range: v.range,330modelDescription: v.modelDescription,331references: v.references332};333} else {334// eslint-disable-next-line local/code-no-in-operator335if (v.value && typeof v.value === 'object' && '$base64' in v.value && typeof v.value.$base64 === 'string') {336// 'dup' here is needed otherwise TS complains about the narrowed `value` in a spread operation337const dup: Mutable<IChatRequestVariableEntry> = { ...v };338dup.value = decodeBase64(v.value.$base64).buffer;339return dup;340}341342return v;343}344}345}346347export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry {348return obj.kind === 'implicit';349}350351export function isStringVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestStringVariableEntry {352return obj.kind === 'string';353}354355export function isTerminalVariableEntry(obj: IChatRequestVariableEntry): obj is ITerminalVariableEntry {356return obj.kind === 'terminalCommand';357}358359export function isDebugVariableEntry(obj: IChatRequestVariableEntry): obj is IDebugVariableEntry {360return obj.kind === 'debugVariable';361}362363export function isPasteVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestPasteVariableEntry {364return obj.kind === 'paste';365}366367export function isWorkspaceVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestWorkspaceVariableEntry {368return obj.kind === 'workspace';369}370371export function isImageVariableEntry(obj: IChatRequestVariableEntry): obj is IImageVariableEntry {372return obj.kind === 'image';373}374375export function isNotebookOutputVariableEntry(obj: IChatRequestVariableEntry): obj is INotebookOutputVariableEntry {376return obj.kind === 'notebookOutput';377}378379export function isElementVariableEntry(obj: IChatRequestVariableEntry): obj is IElementVariableEntry {380return obj.kind === 'element';381}382383export function isDiagnosticsVariableEntry(obj: IChatRequestVariableEntry): obj is IDiagnosticVariableEntry {384return obj.kind === 'diagnostic';385}386387export function isChatRequestFileEntry(obj: IChatRequestVariableEntry): obj is IChatRequestFileEntry {388return obj.kind === 'file';389}390391export function isPromptFileVariableEntry(obj: IChatRequestVariableEntry): obj is IPromptFileVariableEntry {392return obj.kind === 'promptFile';393}394395export function isPromptTextVariableEntry(obj: IChatRequestVariableEntry): obj is IPromptTextVariableEntry {396return obj.kind === 'promptText';397}398399export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry {400const entry = obj as IChatRequestVariableEntry;401return typeof entry === 'object' &&402entry !== null &&403typeof entry.id === 'string' &&404typeof entry.name === 'string';405}406407export function isSCMHistoryItemVariableEntry(obj: IChatRequestVariableEntry): obj is ISCMHistoryItemVariableEntry {408return obj.kind === 'scmHistoryItem';409}410411export function isSCMHistoryItemChangeVariableEntry(obj: IChatRequestVariableEntry): obj is ISCMHistoryItemChangeVariableEntry {412return obj.kind === 'scmHistoryItemChange';413}414415export function isSCMHistoryItemChangeRangeVariableEntry(obj: IChatRequestVariableEntry): obj is ISCMHistoryItemChangeRangeVariableEntry {416return obj.kind === 'scmHistoryItemChangeRange';417}418419export function isStringImplicitContextValue(value: unknown): value is StringChatContextValue {420const asStringImplicitContextValue = value as Partial<StringChatContextValue>;421return (422typeof asStringImplicitContextValue === 'object' &&423asStringImplicitContextValue !== null &&424(typeof asStringImplicitContextValue.value === 'string' || typeof asStringImplicitContextValue.value === 'undefined') &&425(typeof asStringImplicitContextValue.name === 'string' || typeof asStringImplicitContextValue.name === 'undefined') &&426(asStringImplicitContextValue.resourceUri === undefined || URI.isUri(asStringImplicitContextValue.resourceUri)) &&427(typeof asStringImplicitContextValue.name === 'string' || URI.isUri(asStringImplicitContextValue.resourceUri)) &&428(asStringImplicitContextValue.icon === undefined || ThemeIcon.isThemeIcon(asStringImplicitContextValue.icon)) &&429URI.isUri(asStringImplicitContextValue.uri) &&430typeof asStringImplicitContextValue.handle === 'number'431);432}433434export enum PromptFileVariableKind {435Instruction = 'vscode.prompt.instructions.root',436InstructionReference = `vscode.prompt.instructions`,437PromptFile = 'vscode.prompt.file'438}439440/**441* Utility to convert a {@link uri} to a chat variable entry.442* The `id` of the chat variable can be one of the following:443*444* - `vscode.prompt.instructions__<URI>`: for all non-root prompt instructions references445* - `vscode.prompt.instructions.root__<URI>`: for *root* prompt instructions references446* - `vscode.prompt.file__<URI>`: for prompt file references447*448* @param uri A resource URI that points to a prompt instructions file.449* @param kind The kind of the prompt file variable entry.450*/451export function toPromptFileVariableEntry(uri: URI, kind: PromptFileVariableKind, originLabel?: string, automaticallyAdded = false, toolReferences?: ChatRequestToolReferenceEntry[]): IPromptFileVariableEntry {452// `id` for all `prompt files` starts with the well-defined part that the copilot extension(or other chatbot) can rely on453return {454id: `${kind}__${uri.toString()}`,455name: `prompt:${basename(uri)}`,456value: uri,457kind: 'promptFile',458modelDescription: 'Prompt instructions file',459isRoot: kind !== PromptFileVariableKind.InstructionReference,460originLabel,461toolReferences,462automaticallyAdded463};464}465466export function toPromptTextVariableEntry(content: string, automaticallyAdded = false, toolReferences?: ChatRequestToolReferenceEntry[]): IPromptTextVariableEntry {467return {468id: `vscode.prompt.instructions.text`,469name: `prompt:instructionsList`,470value: content,471kind: 'promptText',472modelDescription: 'Prompt instructions list',473automaticallyAdded,474toolReferences475};476}477478export function toFileVariableEntry(uri: URI, range?: IRange): IChatRequestFileEntry {479return {480kind: 'file',481value: range ? { uri, range } : uri,482id: uri.toString() + (range?.toString() ?? ''),483name: basename(uri),484};485}486487export function toToolVariableEntry(entry: IToolData, range?: IOffsetRange): IChatRequestToolEntry {488return {489kind: 'tool',490id: entry.id,491icon: ThemeIcon.isThemeIcon(entry.icon) ? entry.icon : undefined,492name: entry.displayName,493value: undefined,494range495};496}497498export function toToolSetVariableEntry(entry: IToolSet, range?: IOffsetRange): IChatRequestToolSetEntry {499return {500kind: 'toolset',501id: entry.id,502icon: entry.icon,503name: entry.referenceName,504value: Array.from(entry.getTools()).map(t => toToolVariableEntry(t)),505range506};507}508509export class ChatRequestVariableSet {510private _ids = new Set<string>();511private _entries: IChatRequestVariableEntry[] = [];512513constructor(entries?: IChatRequestVariableEntry[]) {514if (entries) {515this.add(...entries);516}517}518519public add(...entry: IChatRequestVariableEntry[]): void {520for (const e of entry) {521if (!this._ids.has(e.id)) {522this._ids.add(e.id);523this._entries.push(e);524}525}526}527528public insertFirst(entry: IChatRequestVariableEntry): void {529if (!this._ids.has(entry.id)) {530this._ids.add(entry.id);531this._entries.unshift(entry);532}533}534535public remove(entry: IChatRequestVariableEntry): void {536this._ids.delete(entry.id);537this._entries = this._entries.filter(e => e.id !== entry.id);538}539540public has(entry: IChatRequestVariableEntry): boolean {541return this._ids.has(entry.id);542}543544public asArray(): IChatRequestVariableEntry[] {545return this._entries.slice(0); // return a copy546}547548public get length(): number {549return this._entries.length;550}551}552553554