promptFileParser.ts ×38

Frontier kind: Code frontier

unlabeled · c_97d88700a0e8

301 tests · 6715 LOC · 35 files · introduces 0 tests · 215 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
38 ranges215 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
993 ranges6715 lines · 35 files · Browse complete extent
All tests (intent)
301 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 215 introduced LOC across 38 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts 215 introduced LOC · 38 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- promptFileParser.ts
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 { Iterable } from '../../../../../base/common/iterator.js';
7 > import { dirname, joinPath } from '../../../../../base/common/resources.js';
8 > import { splitLinesIncludeSeparators } from '../../../../../base/common/strings.js';
9 > import { URI } from '../../../../../base/common/uri.js';
10 > import { parse, YamlNode, YamlParseError } from '../../../../../base/common/yaml.js';
11 > import { Range } from '../../../../../editor/common/core/range.js';
12 > import { PositionOffsetTransformer } from '../../../../../editor/common/core/text/positionToOffsetImpl.js';
13 >
14 > export class PromptFileParser {
15 > constructor() {
16 }
18 > public parse(uri: URI, content: string): ParsedPromptFile {
19 const linesWithEOL = splitLinesIncludeSeparators(content);
20 if (linesWithEOL.length === 0) {
43 return new ParsedPromptFile(uri, header, body);
44 }
46 >
47 >
48 > export class ParsedPromptFile {
49 > constructor(public readonly uri: URI, public readonly header?: PromptHeader, public readonly body?: PromptBody) {
50 }
52 >
53 > export interface ParseError {
54 > readonly message: string;
55 > readonly range: Range;
56 > readonly code: string;
57 > }
58 >
59 > interface ParsedHeader {
60 > readonly node: YamlNode | undefined;
61 > readonly errors: ParseError[];
62 > readonly attributes: IHeaderAttribute[];
63 > }
64 >
65 > export namespace PromptHeaderAttributes {
66 > export const name = 'name';
67 > export const description = 'description';
68 > export const agent = 'agent';
69 > export const mode = 'mode';
70 > export const model = 'model';
71 > export const applyTo = 'applyTo';
72 > export const paths = 'paths';
73 > export const tools = 'tools';
74 > export const handOffs = 'handoffs';
75 > export const advancedOptions = 'advancedOptions';
76 > export const argumentHint = 'argument-hint';
77 > export const excludeAgent = 'excludeAgent';
78 > export const target = 'target';
79 > export const infer = 'infer';
80 > export const license = 'license';
81 > export const compatibility = 'compatibility';
82 > export const metadata = 'metadata';
83 > export const agents = 'agents';
84 > export const userInvocable = 'user-invocable';
85 > export const disableModelInvocation = 'disable-model-invocation';
86 > export const hooks = 'hooks';
87 > export const context = 'context';
88 > }
89 >
90 > export class PromptHeader {
91 > private _parsed: ParsedHeader | undefined;
92 >
93 > constructor(public readonly range: Range, public readonly uri: URI, private readonly linesWithEOL: string[]) {
94 }
96 > private get _parsedHeader(): ParsedHeader {
97 if (this._parsed === undefined) {
98 const yamlErrors: YamlParseError[] = [];
137 return this._parsed;
138 }
140 > public get attributes(): IHeaderAttribute[] {
141 return this._parsedHeader.attributes;
142 }
144 > public getAttribute(key: string): IHeaderAttribute | undefined {
145 return this._parsedHeader.attributes.find(attr => attr.key === key);
146 }
148 > public get errors(): ParseError[] {
149 return this._parsedHeader.errors;
150 }
152 > private getStringAttribute(key: string): string | undefined {
153 const attribute = this._parsedHeader.attributes.find(attr => attr.key === key);
154 if (attribute?.value.type === 'scalar') {
157 return undefined;
158 }
160 > public get name(): string | undefined {
161 return this.getStringAttribute(PromptHeaderAttributes.name);
162 }
164 > public get description(): string | undefined {
165 return this.getStringAttribute(PromptHeaderAttributes.description);
166 }
168 > public get agent(): string | undefined {
169 return this.getStringAttribute(PromptHeaderAttributes.agent) ?? this.getStringAttribute(PromptHeaderAttributes.mode);
170 }
172 > public get model(): readonly string[] | undefined {
173 return this.getStringOrStringArrayAttribute(PromptHeaderAttributes.model);
174 }
176 > public get applyTo(): string | undefined {
177 return this.getStringAttribute(PromptHeaderAttributes.applyTo);
178 }
180 > /**
181 > * Gets the 'paths' attribute from the header.
182 > * The `paths` field supports a list of glob patterns that scope the instruction
183 > * to specific files (used by Claude rules). Returns a string array or undefined.
184 > */
185 > public get paths(): readonly string[] | undefined {
186 return this.getStringOrStringArrayAttribute(PromptHeaderAttributes.paths);
187 }
189 > public get argumentHint(): string | undefined {
190 return this.getStringAttribute(PromptHeaderAttributes.argumentHint);
191 }
193 > public get target(): string | undefined {
194 return this.getStringAttribute(PromptHeaderAttributes.target);
195 }
197 > public get infer(): boolean | undefined {
198 return this.getBooleanAttribute(PromptHeaderAttributes.infer);
199 }
201 > public get tools(): string[] | undefined {
202 const toolsAttribute = this._parsedHeader.attributes.find(attr => attr.key === PromptHeaderAttributes.tools);
203 if (!toolsAttribute) {
219 return undefined;
220 }
222 > public get handOffs(): IHandOff[] | undefined {
223 const handoffsAttribute = this._parsedHeader.attributes.find(attr => attr.key === PromptHeaderAttributes.handOffs);
224 if (!handoffsAttribute) {
268 return undefined;
269 }
271 > private getStringArrayAttribute(key: string): string[] | undefined {
272 const attribute = this._parsedHeader.attributes.find(attr => attr.key === key);
273 if (!attribute) {
285 return undefined;
286 }
288 > private getStringOrStringArrayAttribute(key: string): readonly string[] | undefined {
289 const attribute = this._parsedHeader.attributes.find(attr => attr.key === key);
290 if (!attribute) {
305 return undefined;
306 }
308 > public get agents(): string[] | undefined {
309 return this.getStringArrayAttribute(PromptHeaderAttributes.agents);
310 }
312 > public get userInvocable(): boolean | undefined {
313 return this.getBooleanAttribute(PromptHeaderAttributes.userInvocable);
314 }
316 > public get disableModelInvocation(): boolean | undefined {
317 return this.getBooleanAttribute(PromptHeaderAttributes.disableModelInvocation);
318 }
320 > public get context(): string | undefined {
321 return this.getStringAttribute(PromptHeaderAttributes.context);
322 }
324 > /**
325 > * Gets the raw 'hooks' attribute value from the header.
326 > * Returns the YAML map value if present, or undefined. The caller is
327 > * responsible for converting this to `ChatRequestHooks` via
328 > * {@link parseSubagentHooksFromYaml}.
329 > */
330 > public get hooksRaw(): IMapValue | undefined {
331 const attr = this._parsedHeader.attributes.find(a => a.key === PromptHeaderAttributes.hooks);
332 if (attr?.value.type === 'map') {
335 return undefined;
336 }
338 > private getBooleanAttribute(key: string): boolean | undefined {
339 const attribute = this._parsedHeader.attributes.find(attr => attr.key === key);
340 if (attribute?.value.type === 'scalar') {
343 return undefined;
344 }
346 >
347 function parseBoolean(stringValue: IScalarValue): boolean | undefined {
348 if (stringValue.value === 'true') {
353 return undefined;
354 }
356 > export interface IHandOff {
357 > readonly agent: string;
358 > readonly label: string;
359 > readonly prompt: string;
360 > readonly send?: boolean;
361 > readonly showContinueOn?: boolean; // treated exactly like send (optional boolean)
362 > readonly model?: string; // qualified model name to switch to (e.g., "GPT-5 (copilot)")
363 > }
364 >
365 > export interface IHeaderAttribute {
366 > readonly range: Range;
367 > readonly key: string;
368 > readonly value: IValue;
369 > }
370 >
371 > export interface IScalarValue {
372 > readonly type: 'scalar';
373 > readonly value: string;
374 > readonly range: Range;
375 > readonly format: 'single' | 'double' | 'none' | 'literal' | 'folded';
376 > }
377 >
378 > export interface ISequenceValue {
379 > readonly type: 'sequence';
380 > readonly items: readonly IValue[];
381 > readonly range: Range;
382 > }
383 >
384 > export interface IMapValue {
385 > readonly type: 'map';
386 > readonly properties: { key: IScalarValue; value: IValue }[];
387 > readonly range: Range;
388 > }
389 >
390 > export type IValue = IScalarValue | ISequenceValue | IMapValue;
391 >
392 >
393 > interface ParsedBody {
394 > readonly fileReferences: readonly IBodyFileReference[];
395 > readonly variableReferences: readonly IBodyVariableReference[];
396 > readonly bodyOffset: number;
397 > }
398 >
399 > export class PromptBody {
400 > private _parsed: ParsedBody | undefined;
401 >
402 > constructor(public readonly range: Range, private readonly linesWithEOL: string[], public readonly uri: URI) {
403 }
405 > public get fileReferences(): readonly IBodyFileReference[] {
406 return this.getParsedBody().fileReferences;
407 }
409 > public get variableReferences(): readonly IBodyVariableReference[] {
410 return this.getParsedBody().variableReferences;
411 }
413 > public get offset(): number {
414 return this.getParsedBody().bodyOffset;
415 }
417 > private getParsedBody(): ParsedBody {
418 if (this._parsed === undefined) {
419 const markdownLinkRanges: Range[] = [];
519 return this._parsed;
520 }
522 > public getContent(): string {
523 return this.linesWithEOL.slice(this.range.startLineNumber - 1, this.range.endLineNumber - 1).join('');
524 }
526 > public resolveFilePath(path: string): URI | undefined {
527 try {
528 if (path.startsWith('/')) {
538 }
539 }
541 >
542 > export interface IBodyFileReference {
543 > readonly content: string;
544 > readonly range: Range;
545 > readonly isMarkdownLink: boolean;
546 > }
547 >
548 > export interface IBodyVariableReference {
549 > readonly name: string;
550 > readonly range: Range;
551 > readonly offset: number;
552 > readonly fullLength: number;
553 > }
554 >
555 > /**
556 > * Parses a comma-separated list of values into an array of strings.
557 > * Values can be unquoted or quoted (single or double quotes).
558 > *
559 > * @param input A string containing comma-separated values
560 > * @returns An ISequenceValue containing the parsed values and their ranges
561 > */
562 > export function parseCommaSeparatedList(stringValue: IScalarValue): ISequenceValue {
563 const result: IScalarValue[] = [];
564 const input = stringValue.value;
625 return { type: 'sequence', items: result, range: stringValue.range };
626 }
628 > /**
629 > * Returns the effective `applyTo` pattern for an instruction file.
630 > * Claude rules use `paths` (defaulting to `**`), while regular instructions use `applyTo`.
631 > */
632 > export function evaluateApplyToPattern(header: PromptHeader | undefined, isClaudeRules: boolean): string | undefined {
633 if (isClaudeRules) {
634 return header?.paths?.join(', ') ?? '**';