Path: blob/main/extensions/copilot/src/extension/prompt/common/chatVariablesCollection.ts
13399 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 type * as vscode from 'vscode';6import { sessionResourceToId } from '../../../platform/chat/common/chatDebugFileLoggerService';7import { URI } from '../../../util/vs/base/common/uri';89export interface PromptVariable {10readonly reference: vscode.ChatPromptReference;11readonly originalName: string;12readonly uniqueName: string;13readonly value: string | vscode.Uri | vscode.Location | unknown;14readonly range?: [start: number, end: number];15readonly isMarkedReadonly: boolean | undefined;16}1718export class ChatVariablesCollection {19private _variables: PromptVariable[] | null = null;2021static merge(...collections: ChatVariablesCollection[]): ChatVariablesCollection {22const allReferences: vscode.ChatPromptReference[] = [];23const seen = new Set<string>();24for (const collection of collections) {25for (const variable of collection) {26const ref = variable.reference;2728// simple dedupe29let key: string;30try {31key = JSON.stringify(ref.value);32} catch {33key = ref.id + String(ref.value);34}3536if (!seen.has(key)) {37seen.add(key);38allReferences.push(ref);39}40}41}4243return new ChatVariablesCollection(allReferences);44}4546constructor(47private readonly _source: readonly vscode.ChatPromptReference[] = []48) { }4950private _getVariables(): PromptVariable[] {51if (!this._variables) {52this._variables = [];53for (let i = 0; i < this._source.length; i++) {54const variable = this._source[i];55// Rewrite the message to use the variable header name56if (variable.value) {57const originalName = variable.name;58const uniqueName = this.uniqueFileName(originalName, this._source.slice(0, i));59this._variables.push({ reference: variable, originalName, uniqueName, value: variable.value, range: variable.range, isMarkedReadonly: false });60}61}62}63return this._variables;64}6566public reverse() {67const sourceCopy = this._source.slice(0);68sourceCopy.reverse();69return new ChatVariablesCollection(sourceCopy);70}7172public find(predicate: (v: PromptVariable) => boolean): PromptVariable | undefined {73return this._getVariables().find(predicate);74}7576public filter(predicate: (v: PromptVariable) => boolean): ChatVariablesCollection {77const resultingReferences: vscode.ChatPromptReference[] = [];78for (const variable of this._getVariables()) {79if (predicate(variable)) {80resultingReferences.push(variable.reference);81}82}83return new ChatVariablesCollection(resultingReferences);84}8586public *[Symbol.iterator](): IterableIterator<PromptVariable> {87yield* this._getVariables();88}8990public substituteVariablesWithReferences(userQuery: string): string {91// no rewriting at the moment92return userQuery;93}9495public hasVariables(): boolean {96return this._getVariables().length > 0;97}9899private uniqueFileName(name: string, variables: vscode.ChatPromptReference[]): string {100const count = variables.filter(v => v.name === name).length;101return count === 0 ? name : `${name}-${count}`;102}103104}105106/**107* Check if provided variable is a "prompt file".108*/109export function isPromptFile(variable: PromptVariable): variable is PromptVariable & { value: vscode.Uri } {110return variable.reference.id.startsWith(PromptFileIdPrefix);111}112113export const PromptFileIdPrefix = 'vscode.prompt.file';114115/**116* Check if provided variable is an "instruction file".117*/118export function isInstructionFile(variable: PromptVariable): variable is PromptVariable & { value: vscode.Uri } {119return variable.reference.id.startsWith(InstructionFileIdPrefix);120}121122export const InstructionFileIdPrefix = 'vscode.instructions.file';123124/**125* Check if provided variable is the workspace "customizations index" file.126*/127export function isCustomizationsIndex(variable: PromptVariable): variable is PromptVariable & { value: string } {128return variable.reference.id === CustomizationsIndexId;129}130131export const CustomizationsIndexId = 'vscode.customizations.index';132133/**134* URI schemes used for chat session references.135*/136export const SessionReferenceSchemes: ReadonlySet<string> = new Set(['vscode-chat-session', 'copilotcli', 'claude-code']);137138/**139* Check if a URI scheme identifies a chat session reference.140*/141export function isSessionReferenceScheme(scheme: string): boolean {142return SessionReferenceSchemes.has(scheme);143}144145/**146* Check if provided variable is a session reference.147*/148export function isSessionReference(variable: PromptVariable): variable is PromptVariable & { value: vscode.Uri } {149return URI.isUri(variable.value) && isSessionReferenceScheme(variable.value.scheme);150}151152/**153* Build the attributes for rendering a session reference as an `<attachment>` tag.154* Callers can pass the result to `<Tag name='attachment' attrs={...} />`.155*/156export function sessionReferenceAttachmentAttrs(variable: PromptVariable & { value: vscode.Uri }): Record<string, string> {157const attrs: Record<string, string> = {};158if (variable.uniqueName) {159attrs.id = `${variable.uniqueName} (${sessionResourceToId(variable.value)})`;160}161attrs.filePath = variable.value.toString();162return attrs;163}164165/**166* Extract debug-target session IDs from chat prompt references.167* Returns `undefined` when no session references are present.168*/169export function extractDebugTargetSessionIds(references: readonly vscode.ChatPromptReference[]): readonly string[] | undefined {170const sessionRefs = references.filter(ref => URI.isUri(ref.value) && isSessionReferenceScheme(ref.value.scheme));171return sessionRefs.length > 0 ? sessionRefs.map(ref => sessionResourceToId(ref.value as URI)) : undefined;172}173174export interface PromptFileSlashCommandId {175readonly name: string;176readonly id: string;177}178179/**180* Extracts the effective slash command ID and display name for a prompt file variable.181* - For skills (SKILL.md), the ID is the parent folder name.182* - For prompt files (.prompt.md), the ID is the filename without the .prompt.md extension.183* - Otherwise, the ID is the reference name.184*/185export function getPromptFileSlashCommandId(variable: PromptVariable): PromptFileSlashCommandId {186const name = variable.reference.name;187const uri = variable.value;188const pathSegments = URI.isUri(uri) ? uri.path.split('/').filter(Boolean) : [];189const lastSegment = pathSegments[pathSegments.length - 1];190const isSkillFile = lastSegment?.toLowerCase() === 'skill.md';191let id: string;192if (isSkillFile && pathSegments.length >= 2) {193id = pathSegments[pathSegments.length - 2];194} else if (lastSegment?.endsWith('.prompt.md')) {195id = lastSegment.slice(0, -'.prompt.md'.length);196} else {197id = name;198}199return { name, id };200}201202export interface ParsedSlashCommand {203/** The matched prompt file slash command ID. */204readonly promptFile: PromptFileSlashCommandId;205/** The matched PromptVariable (the prompt file reference). */206readonly variable: PromptVariable;207/** The raw slash command string parsed from the query (without the leading `/`). */208readonly command: string;209/** Any trailing arguments after the slash command. */210readonly args: string;211}212213/**214* Parses a query for a `/command` pattern and matches it against prompt file references.215* Returns the matched prompt file and parsed arguments, or `undefined` if no match.216*/217export function parseSlashCommand(query: string, chatVariables: ChatVariablesCollection): ParsedSlashCommand | undefined {218const slashCommandMatch = query.match(/^\s*\/(?<command>\S+)(?:\s+(?<args>.*))?$/s);219const slashCommand = slashCommandMatch?.groups?.command;220if (!slashCommand) {221return undefined;222}223const args = slashCommandMatch?.groups?.args?.trim() ?? '';224for (const variable of chatVariables) {225if (!isPromptFile(variable)) {226continue;227}228const promptFile = getPromptFileSlashCommandId(variable);229if (promptFile.id === slashCommand) {230return { promptFile, variable, command: slashCommand, args };231}232}233return undefined;234}235236237