Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompt/common/chatVariablesCollection.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type * as vscode from 'vscode';
7
import { sessionResourceToId } from '../../../platform/chat/common/chatDebugFileLoggerService';
8
import { URI } from '../../../util/vs/base/common/uri';
9
10
export interface PromptVariable {
11
readonly reference: vscode.ChatPromptReference;
12
readonly originalName: string;
13
readonly uniqueName: string;
14
readonly value: string | vscode.Uri | vscode.Location | unknown;
15
readonly range?: [start: number, end: number];
16
readonly isMarkedReadonly: boolean | undefined;
17
}
18
19
export class ChatVariablesCollection {
20
private _variables: PromptVariable[] | null = null;
21
22
static merge(...collections: ChatVariablesCollection[]): ChatVariablesCollection {
23
const allReferences: vscode.ChatPromptReference[] = [];
24
const seen = new Set<string>();
25
for (const collection of collections) {
26
for (const variable of collection) {
27
const ref = variable.reference;
28
29
// simple dedupe
30
let key: string;
31
try {
32
key = JSON.stringify(ref.value);
33
} catch {
34
key = ref.id + String(ref.value);
35
}
36
37
if (!seen.has(key)) {
38
seen.add(key);
39
allReferences.push(ref);
40
}
41
}
42
}
43
44
return new ChatVariablesCollection(allReferences);
45
}
46
47
constructor(
48
private readonly _source: readonly vscode.ChatPromptReference[] = []
49
) { }
50
51
private _getVariables(): PromptVariable[] {
52
if (!this._variables) {
53
this._variables = [];
54
for (let i = 0; i < this._source.length; i++) {
55
const variable = this._source[i];
56
// Rewrite the message to use the variable header name
57
if (variable.value) {
58
const originalName = variable.name;
59
const uniqueName = this.uniqueFileName(originalName, this._source.slice(0, i));
60
this._variables.push({ reference: variable, originalName, uniqueName, value: variable.value, range: variable.range, isMarkedReadonly: false });
61
}
62
}
63
}
64
return this._variables;
65
}
66
67
public reverse() {
68
const sourceCopy = this._source.slice(0);
69
sourceCopy.reverse();
70
return new ChatVariablesCollection(sourceCopy);
71
}
72
73
public find(predicate: (v: PromptVariable) => boolean): PromptVariable | undefined {
74
return this._getVariables().find(predicate);
75
}
76
77
public filter(predicate: (v: PromptVariable) => boolean): ChatVariablesCollection {
78
const resultingReferences: vscode.ChatPromptReference[] = [];
79
for (const variable of this._getVariables()) {
80
if (predicate(variable)) {
81
resultingReferences.push(variable.reference);
82
}
83
}
84
return new ChatVariablesCollection(resultingReferences);
85
}
86
87
public *[Symbol.iterator](): IterableIterator<PromptVariable> {
88
yield* this._getVariables();
89
}
90
91
public substituteVariablesWithReferences(userQuery: string): string {
92
// no rewriting at the moment
93
return userQuery;
94
}
95
96
public hasVariables(): boolean {
97
return this._getVariables().length > 0;
98
}
99
100
private uniqueFileName(name: string, variables: vscode.ChatPromptReference[]): string {
101
const count = variables.filter(v => v.name === name).length;
102
return count === 0 ? name : `${name}-${count}`;
103
}
104
105
}
106
107
/**
108
* Check if provided variable is a "prompt file".
109
*/
110
export function isPromptFile(variable: PromptVariable): variable is PromptVariable & { value: vscode.Uri } {
111
return variable.reference.id.startsWith(PromptFileIdPrefix);
112
}
113
114
export const PromptFileIdPrefix = 'vscode.prompt.file';
115
116
/**
117
* Check if provided variable is an "instruction file".
118
*/
119
export function isInstructionFile(variable: PromptVariable): variable is PromptVariable & { value: vscode.Uri } {
120
return variable.reference.id.startsWith(InstructionFileIdPrefix);
121
}
122
123
export const InstructionFileIdPrefix = 'vscode.instructions.file';
124
125
/**
126
* Check if provided variable is the workspace "customizations index" file.
127
*/
128
export function isCustomizationsIndex(variable: PromptVariable): variable is PromptVariable & { value: string } {
129
return variable.reference.id === CustomizationsIndexId;
130
}
131
132
export const CustomizationsIndexId = 'vscode.customizations.index';
133
134
/**
135
* URI schemes used for chat session references.
136
*/
137
export const SessionReferenceSchemes: ReadonlySet<string> = new Set(['vscode-chat-session', 'copilotcli', 'claude-code']);
138
139
/**
140
* Check if a URI scheme identifies a chat session reference.
141
*/
142
export function isSessionReferenceScheme(scheme: string): boolean {
143
return SessionReferenceSchemes.has(scheme);
144
}
145
146
/**
147
* Check if provided variable is a session reference.
148
*/
149
export function isSessionReference(variable: PromptVariable): variable is PromptVariable & { value: vscode.Uri } {
150
return URI.isUri(variable.value) && isSessionReferenceScheme(variable.value.scheme);
151
}
152
153
/**
154
* Build the attributes for rendering a session reference as an `<attachment>` tag.
155
* Callers can pass the result to `<Tag name='attachment' attrs={...} />`.
156
*/
157
export function sessionReferenceAttachmentAttrs(variable: PromptVariable & { value: vscode.Uri }): Record<string, string> {
158
const attrs: Record<string, string> = {};
159
if (variable.uniqueName) {
160
attrs.id = `${variable.uniqueName} (${sessionResourceToId(variable.value)})`;
161
}
162
attrs.filePath = variable.value.toString();
163
return attrs;
164
}
165
166
/**
167
* Extract debug-target session IDs from chat prompt references.
168
* Returns `undefined` when no session references are present.
169
*/
170
export function extractDebugTargetSessionIds(references: readonly vscode.ChatPromptReference[]): readonly string[] | undefined {
171
const sessionRefs = references.filter(ref => URI.isUri(ref.value) && isSessionReferenceScheme(ref.value.scheme));
172
return sessionRefs.length > 0 ? sessionRefs.map(ref => sessionResourceToId(ref.value as URI)) : undefined;
173
}
174
175
export interface PromptFileSlashCommandId {
176
readonly name: string;
177
readonly id: string;
178
}
179
180
/**
181
* Extracts the effective slash command ID and display name for a prompt file variable.
182
* - For skills (SKILL.md), the ID is the parent folder name.
183
* - For prompt files (.prompt.md), the ID is the filename without the .prompt.md extension.
184
* - Otherwise, the ID is the reference name.
185
*/
186
export function getPromptFileSlashCommandId(variable: PromptVariable): PromptFileSlashCommandId {
187
const name = variable.reference.name;
188
const uri = variable.value;
189
const pathSegments = URI.isUri(uri) ? uri.path.split('/').filter(Boolean) : [];
190
const lastSegment = pathSegments[pathSegments.length - 1];
191
const isSkillFile = lastSegment?.toLowerCase() === 'skill.md';
192
let id: string;
193
if (isSkillFile && pathSegments.length >= 2) {
194
id = pathSegments[pathSegments.length - 2];
195
} else if (lastSegment?.endsWith('.prompt.md')) {
196
id = lastSegment.slice(0, -'.prompt.md'.length);
197
} else {
198
id = name;
199
}
200
return { name, id };
201
}
202
203
export interface ParsedSlashCommand {
204
/** The matched prompt file slash command ID. */
205
readonly promptFile: PromptFileSlashCommandId;
206
/** The matched PromptVariable (the prompt file reference). */
207
readonly variable: PromptVariable;
208
/** The raw slash command string parsed from the query (without the leading `/`). */
209
readonly command: string;
210
/** Any trailing arguments after the slash command. */
211
readonly args: string;
212
}
213
214
/**
215
* Parses a query for a `/command` pattern and matches it against prompt file references.
216
* Returns the matched prompt file and parsed arguments, or `undefined` if no match.
217
*/
218
export function parseSlashCommand(query: string, chatVariables: ChatVariablesCollection): ParsedSlashCommand | undefined {
219
const slashCommandMatch = query.match(/^\s*\/(?<command>\S+)(?:\s+(?<args>.*))?$/s);
220
const slashCommand = slashCommandMatch?.groups?.command;
221
if (!slashCommand) {
222
return undefined;
223
}
224
const args = slashCommandMatch?.groups?.args?.trim() ?? '';
225
for (const variable of chatVariables) {
226
if (!isPromptFile(variable)) {
227
continue;
228
}
229
const promptFile = getPromptFileSlashCommandId(variable);
230
if (promptFile.id === slashCommand) {
231
return { promptFile, variable, command: slashCommand, args };
232
}
233
}
234
return undefined;
235
}
236
237