promptsServiceImpl.ts ×65

Frontier kind: Code frontier

unlabeled · c_16cd3f36444e

257 tests · 37193 LOC · 180 files · introduces 0 tests · 1045 LOC · 4 files

Introduces — evidence that enters the hierarchy at this concept

Code
105 ranges1045 lines · 4 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
3226 ranges37193 lines · 180 files · Browse complete extent
All tests (intent)
257 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.

4 files ranked by introduced lines: 1045 introduced LOC across 105 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts 362 introduced LOC · 7 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- promptFileAttributes.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 { dirname } from '../../../../../../base/common/resources.js';
7 > import { URI } from '../../../../../../base/common/uri.js';
8 > import { localize } from '../../../../../../nls.js';
9 > import { SpecedToolAliases } from '../../tools/languageModelToolsService.js';
10 > import { CLAUDE_AGENTS_SOURCE_FOLDER, isInClaudeRulesFolder } from '../config/promptFileLocations.js';
11 > import { PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js';
12 > import { PromptsType, Target } from '../promptTypes.js';
13 >
14 > export namespace GithubPromptHeaderAttributes {
15 > export const mcpServers = 'mcp-servers';
16 > export const github = 'github';
17 > }
18 >
19 > export namespace ClaudeHeaderAttributes {
20 > export const disallowedTools = 'disallowedTools';
21 > }
22 >
23 > export function isTarget(value: unknown): value is Target {
24 return value === Target.VSCode || value === Target.GitHubCopilot || value === Target.Claude || value === Target.Undefined;
25 }
27 >
28 > interface IAttributeDefinition {
29 > readonly type: string;
30 > readonly description: string;
31 > readonly defaults?: readonly string[];
32 > readonly items?: readonly { name: string; description?: string }[];
33 > readonly enums?: readonly { name: string; description?: string }[];
34 > }
35 >
36 > const booleanAttributeEnumValues: readonly IValueEntry[] = [
37 > { name: 'true' },
38 > { name: 'false' }
39 > ];
40 >
41 > const targetAttributeEnumValues: readonly IValueEntry[] = [
42 > { name: 'vscode' },
43 > { name: 'github-copilot' },
44 > ];
45 >
46 > // Attribute metadata for prompt files (`*.prompt.md`).
47 > export const promptFileAttributes: Record<string, IAttributeDefinition> = {
48 > [PromptHeaderAttributes.name]: {
49 > type: 'scalar',
50 > description: localize('promptHeader.prompt.name', 'The name of the prompt. This is also the name of the slash command that will run this prompt.'),
51 > },
52 > [PromptHeaderAttributes.description]: {
53 > type: 'scalar',
54 > description: localize('promptHeader.prompt.description', 'The description of the reusable prompt, what it does and when to use it.'),
55 > },
56 > [PromptHeaderAttributes.argumentHint]: {
57 > type: 'scalar',
58 > description: localize('promptHeader.prompt.argumentHint', 'The argument-hint describes what inputs the prompt expects or supports.'),
59 > },
60 > [PromptHeaderAttributes.model]: {
61 > type: 'scalar | sequence',
62 > description: localize('promptHeader.prompt.model', 'The model to use in this prompt. Can also be a list of models. The first available model will be used.'),
63 > },
64 > [PromptHeaderAttributes.tools]: {
65 > type: 'scalar | sequence',
66 > description: localize('promptHeader.prompt.tools', 'The tools to use in this prompt.'),
67 > defaults: ['[]', '[\'search\', \'edit\', \'web\']'],
68 > },
69 > [PromptHeaderAttributes.agent]: {
70 > type: 'scalar',
71 > description: localize('promptHeader.prompt.agent.description', 'The agent to use when running this prompt.'),
72 > },
73 > [PromptHeaderAttributes.mode]: {
74 > type: 'scalar',
75 > description: localize('promptHeader.prompt.agent.description', 'The agent to use when running this prompt.'),
76 > },
77 > };
78 >
79 > // Attribute metadata for instructions files (`*.instructions.md`).
80 > export const instructionAttributes: Record<string, IAttributeDefinition> = {
81 > [PromptHeaderAttributes.name]: {
82 > type: 'scalar',
83 > description: localize('promptHeader.instructions.name', 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'),
84 > },
85 > [PromptHeaderAttributes.description]: {
86 > type: 'scalar',
87 > description: localize('promptHeader.instructions.description', 'The description of the instruction file. It can be used to provide additional context or information about the instructions and is passed to the language model as part of the prompt.'),
88 > },
89 > [PromptHeaderAttributes.applyTo]: {
90 > type: 'scalar',
91 > description: localize('promptHeader.instructions.applyToRange', 'One or more glob pattern (separated by comma) that describe for which files the instructions apply to. Based on these patterns, the file is automatically included in the prompt, when the context contains a file that matches one or more of these patterns. Use `**` when you want this file to always be added.\nExample: `**/*.ts`, `**/*.js`, `client/**`'),
92 > defaults: [
93 > '\'**\'',
94 > '\'**/*.ts, **/*.js\'',
95 > '\'**/*.php\'',
96 > '\'**/*.py\''
97 > ],
98 > },
99 > [PromptHeaderAttributes.excludeAgent]: {
100 > type: 'scalar | sequence',
101 > description: localize('promptHeader.instructions.excludeAgent', 'One or more agents to exclude from using this instruction file.'),
102 > },
103 > };
104 >
105 > // Attribute metadata for custom agent files (`*.agent.md`).
106 > export const customAgentAttributes: Record<string, IAttributeDefinition> = {
107 > [PromptHeaderAttributes.name]: {
108 > type: 'scalar',
109 > description: localize('promptHeader.agent.name', 'The name of the agent as shown in the UI.'),
110 > },
111 > [PromptHeaderAttributes.description]: {
112 > type: 'scalar',
113 > description: localize('promptHeader.agent.description', 'The description of the custom agent, what it does and when to use it.'),
114 > },
115 > [PromptHeaderAttributes.argumentHint]: {
116 > type: 'scalar',
117 > description: localize('promptHeader.agent.argumentHint', 'The argument-hint describes what inputs the custom agent expects or supports.'),
118 > },
119 > [PromptHeaderAttributes.model]: {
120 > type: 'scalar | sequence',
121 > description: localize('promptHeader.agent.model', 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.'),
122 > },
123 > [PromptHeaderAttributes.tools]: {
124 > type: 'scalar | sequence',
125 > description: localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.'),
126 > defaults: ['[]', '[search, edit, web]'],
127 > },
128 > [PromptHeaderAttributes.handOffs]: {
129 > type: 'sequence',
130 > description: localize('promptHeader.agent.handoffs', 'Possible handoff actions when the agent has completed its task.'),
131 > },
132 > [PromptHeaderAttributes.target]: {
133 > type: 'scalar',
134 > description: localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'),
135 > enums: targetAttributeEnumValues,
136 > },
137 > [PromptHeaderAttributes.infer]: {
138 > type: 'scalar',
139 > description: localize('promptHeader.agent.infer', 'Controls visibility of the agent.'),
140 > enums: booleanAttributeEnumValues,
141 > },
142 > [PromptHeaderAttributes.agents]: {
143 > type: 'sequence',
144 > description: localize('promptHeader.agent.agents', 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'),
145 > defaults: ['["*"]'],
146 > },
147 > [PromptHeaderAttributes.userInvocable]: {
148 > type: 'scalar',
149 > description: localize('promptHeader.agent.userInvocable', 'Whether the agent can be selected and invoked by users in the UI.'),
150 > enums: booleanAttributeEnumValues,
151 > },
152 > [PromptHeaderAttributes.disableModelInvocation]: {
153 > type: 'scalar',
154 > description: localize('promptHeader.agent.disableModelInvocation', 'If true, prevents the agent from being invoked as a subagent.'),
155 > enums: booleanAttributeEnumValues,
156 > },
157 > [PromptHeaderAttributes.advancedOptions]: {
158 > type: 'map',
159 > description: localize('promptHeader.agent.advancedOptions', 'Advanced options for custom agent behavior.'),
160 > },
161 > [GithubPromptHeaderAttributes.github]: {
162 > type: 'map',
163 > description: localize('promptHeader.agent.github', 'GitHub-specific configuration for the agent, such as token permissions.'),
164 > },
165 > [PromptHeaderAttributes.hooks]: {
166 > type: 'map',
167 > description: localize('promptHeader.agent.hooks', 'Lifecycle hooks scoped to this agent. Define hooks that run only while this agent is active.'),
168 > },
169 > };
170 >
171 > // Attribute metadata for skill files (`SKILL.md`).
172 > export const skillAttributes: Record<string, IAttributeDefinition> = {
173 > [PromptHeaderAttributes.name]: {
174 > type: 'scalar',
175 > description: localize('promptHeader.skill.name', 'The name of the skill.'),
176 > },
177 > [PromptHeaderAttributes.description]: {
178 > type: 'scalar',
179 > description: localize('promptHeader.skill.description', 'The description of the skill. The description is added to every request and will be used by the agent to decide when to load the skill.'),
180 > },
181 > [PromptHeaderAttributes.argumentHint]: {
182 > type: 'scalar',
183 > description: localize('promptHeader.skill.argumentHint', 'Hint shown during autocomplete to indicate expected arguments. Example: [issue-number] or [filename] [format]'),
184 > },
185 > [PromptHeaderAttributes.userInvocable]: {
186 > type: 'scalar',
187 > description: localize('promptHeader.skill.userInvocable', 'Set to false to hide from the / menu. Use for background knowledge users should not invoke directly. Default: true.'),
188 > enums: booleanAttributeEnumValues,
189 > },
190 > [PromptHeaderAttributes.disableModelInvocation]: {
191 > type: 'scalar',
192 > description: localize('promptHeader.skill.disableModelInvocation', 'Set to true to prevent the agent from automatically loading this skill. Use for workflows you want to trigger manually with /name. Default: false.'),
193 > enums: booleanAttributeEnumValues,
194 > },
195 > [PromptHeaderAttributes.license]: {
196 > type: 'scalar | map',
197 > description: localize('promptHeader.skill.license', 'License information for the skill.'),
198 > },
199 > [PromptHeaderAttributes.compatibility]: {
200 > type: 'scalar | map',
201 > description: localize('promptHeader.skill.compatibility', 'Compatibility metadata for environments or runtimes.'),
202 > },
203 > [PromptHeaderAttributes.metadata]: {
204 > type: 'map',
205 > description: localize('promptHeader.skill.metadata', 'Additional metadata for the skill.'),
206 > },
207 > [PromptHeaderAttributes.context]: {
208 > type: 'scalar',
209 > description: localize('promptHeader.skill.context', 'Controls how the skill is loaded. Set to \'fork\' to spawn a subagent with the skill instructions instead of returning them inline.'),
210 > enums: [{ name: 'fork', description: localize('promptHeader.skill.context.fork', 'Spawn a subagent with the skill instructions injected as system context.') }],
211 > },
212 > };
213 >
214 > const allAttributeNames: Record<PromptsType, string[]> = {
215 > [PromptsType.prompt]: Object.keys(promptFileAttributes),
216 > [PromptsType.instructions]: Object.keys(instructionAttributes),
217 > [PromptsType.agent]: Object.keys(customAgentAttributes),
218 > [PromptsType.skill]: Object.keys(skillAttributes),
219 > [PromptsType.hook]: [], // hooks are JSON files, not markdown with YAML frontmatter
220 > };
221 > const githubCopilotAgentAttributeNames = [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.tools, PromptHeaderAttributes.target, GithubPromptHeaderAttributes.mcpServers, GithubPromptHeaderAttributes.github, PromptHeaderAttributes.infer];
222 > const recommendedAttributeNames: Record<PromptsType, string[]> = {
223 > [PromptsType.prompt]: allAttributeNames[PromptsType.prompt].filter(name => !isNonRecommendedAttribute(name)),
224 > [PromptsType.instructions]: allAttributeNames[PromptsType.instructions].filter(name => !isNonRecommendedAttribute(name)),
225 > [PromptsType.agent]: allAttributeNames[PromptsType.agent].filter(name => !isNonRecommendedAttribute(name)),
226 > [PromptsType.skill]: allAttributeNames[PromptsType.skill].filter(name => !isNonRecommendedAttribute(name)),
227 > [PromptsType.hook]: [], // hooks are JSON files, not markdown with YAML frontmatter
228 > };
229 >
230 > export function getValidAttributeNames(promptType: PromptsType, includeNonRecommended: boolean, target: Target): string[] {
231 if (target === Target.Claude) {
232 if (promptType === PromptsType.instructions) {
241 return includeNonRecommended ? allAttributeNames[promptType] : recommendedAttributeNames[promptType];
242 }
244 > export function isNonRecommendedAttribute(attributeName: string): boolean {
245 > return attributeName === PromptHeaderAttributes.advancedOptions || attributeName === PromptHeaderAttributes.excludeAgent || attributeName === PromptHeaderAttributes.mode || attributeName === PromptHeaderAttributes.infer;
246 > }
247 >
248 > export function getAttributeDefinition(attributeName: string, promptType: PromptsType, target: Target): IAttributeDefinition | undefined {
249 switch (promptType) {
250 case PromptsType.instructions:
266 }
267 }
269 > // The list of tools known to be used by GitHub Copilot custom agents
270 > export const knownGithubCopilotTools = [
271 > { name: SpecedToolAliases.execute, description: localize('githubCopilot.execute', 'Execute commands') },
272 > { name: SpecedToolAliases.read, description: localize('githubCopilot.read', 'Read files') },
273 > { name: SpecedToolAliases.edit, description: localize('githubCopilot.edit', 'Edit files') },
274 > { name: SpecedToolAliases.search, description: localize('githubCopilot.search', 'Search files') },
275 > { name: SpecedToolAliases.agent, description: localize('githubCopilot.agent', 'Use subagents') },
276 > ];
277 >
278 > export interface IValueEntry {
279 > readonly name: string;
280 > readonly description?: string;
281 > }
282 >
283 > export const knownClaudeTools = [
284 > { name: 'Bash', description: localize('claude.bash', 'Execute shell commands'), toolEquivalent: [SpecedToolAliases.execute] },
285 > { name: 'Edit', description: localize('claude.edit', 'Make targeted file edits'), toolEquivalent: ['edit/editNotebook', 'edit/editFiles'] },
286 > { name: 'Glob', description: localize('claude.glob', 'Find files by pattern'), toolEquivalent: ['search/fileSearch'] },
287 > { name: 'Grep', description: localize('claude.grep', 'Search file contents with regex'), toolEquivalent: ['search/textSearch'] },
288 > { name: 'Read', description: localize('claude.read', 'Read file contents'), toolEquivalent: ['read/readFile', 'read/getNotebookSummary'] },
289 > { name: 'Write', description: localize('claude.write', 'Create/overwrite files'), toolEquivalent: ['edit/createDirectory', 'edit/createFile', 'edit/createJupyterNotebook'] },
290 > { name: 'WebFetch', description: localize('claude.webFetch', 'Fetch URL content'), toolEquivalent: [SpecedToolAliases.web] },
291 > { name: 'WebSearch', description: localize('claude.webSearch', 'Perform web searches'), toolEquivalent: [SpecedToolAliases.web] },
292 > { name: 'Task', description: localize('claude.task', 'Run subagents for complex tasks'), toolEquivalent: [SpecedToolAliases.agent] },
293 > { name: 'Skill', description: localize('claude.skill', 'Execute skills'), toolEquivalent: [] },
294 > { name: 'LSP', description: localize('claude.lsp', 'Code intelligence (requires plugin)'), toolEquivalent: [] },
295 > { name: 'NotebookEdit', description: localize('claude.notebookEdit', 'Modify Jupyter notebooks'), toolEquivalent: ['edit/editNotebook'] },
296 > { name: 'AskUserQuestion', description: localize('claude.askUserQuestion', 'Ask multiple-choice questions'), toolEquivalent: ['vscode/askQuestions'] },
297 > { name: 'MCPSearch', description: localize('claude.mcpSearch', 'Searches for MCP tools when tool search is enabled'), toolEquivalent: [] }
298 > ];
299 >
300 > export const knownClaudeModels = [
301 > { name: 'sonnet', description: localize('claude.sonnet', 'Latest Claude Sonnet'), modelEquivalent: 'Claude Sonnet 4.5 (copilot)' },
302 > { name: 'opus', description: localize('claude.opus', 'Latest Claude Opus'), modelEquivalent: 'Claude Opus 4.6 (copilot)' },
303 > { name: 'haiku', description: localize('claude.haiku', 'Latest Claude Haiku, fast for simple tasks'), modelEquivalent: 'Claude Haiku 4.5 (copilot)' },
304 > { name: 'inherit', description: localize('claude.inherit', 'Inherit model from parent agent or prompt'), modelEquivalent: undefined },
305 > ];
306 >
307 > export function mapClaudeModels(claudeModelNames: readonly string[]): readonly string[] {
308 const result = [];
309 for (const name of claudeModelNames) {
315 return result;
316 }
318 > /**
319 > * Maps Claude tool names to their VS Code tool equivalents.
320 > */
321 > export function mapClaudeTools(claudeToolNames: readonly string[]): string[] {
322 const result: string[] = [];
323 for (const name of claudeToolNames) {
329 return result;
330 }
332 > export const claudeAgentAttributes: Record<string, IAttributeDefinition> = {
333 > 'name': {
334 > type: 'scalar',
335 > description: localize('attribute.name', "Unique identifier using lowercase letters and hyphens (required)"),
336 > },
337 > 'description': {
338 > type: 'scalar',
339 > description: localize('attribute.description', "When to delegate to this subagent (required)"),
340 > },
341 > 'tools': {
342 > type: 'sequence',
343 > description: localize('attribute.tools', "Array of tools the subagent can use. Inherits all tools if omitted"),
344 > defaults: ['Read, Edit, Bash'],
345 > items: knownClaudeTools
346 > },
347 > 'disallowedTools': {
348 > type: 'sequence',
349 > description: localize('attribute.disallowedTools', "Tools to deny, removed from inherited or specified list"),
350 > defaults: ['Write, Edit, Bash'],
351 > items: knownClaudeTools
352 > },
353 > 'model': {
354 > type: 'scalar',
355 > description: localize('attribute.model', "Model to use: sonnet, opus, haiku, or inherit. Defaults to inherit."),
356 > defaults: ['sonnet', 'opus', 'haiku', 'inherit'],
357 > enums: knownClaudeModels
358 > },
359 > 'permissionMode': {
360 > type: 'scalar',
361 > description: localize('attribute.permissionMode', "Permission mode: default, acceptEdits, dontAsk, bypassPermissions, or plan."),
362 > defaults: ['default', 'acceptEdits', 'dontAsk', 'bypassPermissions', 'plan'],
363 > enums: [
364 > { name: 'default', description: localize('claude.permissionMode.default', 'Standard behavior: prompts for permission on first use of each tool.') },
365 > { name: 'acceptEdits', description: localize('claude.permissionMode.acceptEdits', 'Automatically accepts file edit permissions for the session.') },
366 > { name: 'plan', description: localize('claude.permissionMode.plan', 'Plan Mode: Claude can analyze but not modify files or execute commands.') },
367 > { name: 'delegate', description: localize('claude.permissionMode.delegate', 'Coordination-only mode for agent team leads. Only available when an agent team is active.') },
368 > { name: 'dontAsk', description: localize('claude.permissionMode.dontAsk', 'Auto-denies tools unless pre-approved via /permissions or permissions.allow rules.') },
369 > { name: 'bypassPermissions', description: localize('claude.permissionMode.bypassPermissions', 'Skips all permission prompts (requires safe environment like containers).') }
370 > ]
371 > },
372 > 'skills': {
373 > type: 'sequence',
374 > description: localize('attribute.skills', "Skills to load into the subagent's context at startup."),
375 > },
376 > 'mcpServers': {
377 > type: 'sequence',
378 > description: localize('attribute.mcpServers', "MCP servers available to this subagent."),
379 > },
380 > 'hooks': {
381 > type: 'object',
382 > description: localize('attribute.hooks', "Lifecycle hooks scoped to this subagent."),
383 > },
384 > 'memory': {
385 > type: 'scalar',
386 > description: localize('attribute.memory', "Persistent memory scope: user, project, or local. Enables cross-session learning."),
387 > defaults: ['user', 'project', 'local'],
388 > enums: [
389 > { name: 'user', description: localize('claude.memory.user', "Remember learnings across all projects.") },
390 > { name: 'project', description: localize('claude.memory.project', "The subagent's knowledge is project-specific and shareable via version control.") },
391 > { name: 'local', description: localize('claude.memory.local', "The subagent's knowledge is project-specific but should not be checked into version control.") }
392 > ]
393 > }
394 > };
395 >
396 > /**
397 > * Attributes supported in Claude rules files (`.claude/rules/*.md`).
398 > * Claude rules use `paths` instead of `applyTo` for glob patterns.
399 > */
400 > export const claudeRulesAttributes: Record<string, IAttributeDefinition> = {
401 > 'description': {
402 > type: 'scalar',
403 > description: localize('attribute.rules.description', "A description of what this rule covers, used to provide context about when it applies."),
404 > },
405 > 'paths': {
406 > type: 'sequence',
407 > description: localize('attribute.rules.paths', "Array of glob patterns that describe for which files the rule applies. Based on these patterns, the file is automatically included in the prompt when the context contains a file that matches.\nExample: `['src/**/*.ts', 'test/**']`"),
408 > },
409 > };
410 >
411 > export function isVSCodeOrDefaultTarget(target: Target): boolean {
412 return target === Target.VSCode || target === Target.Undefined;
413 }
415 > export function getTarget(promptType: PromptsType, header: PromptHeader | URI): Target {
416 const uri = header instanceof URI ? header : header.uri;
417 if (promptType === PromptsType.agent) {
src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts 338 introduced LOC · 65 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- promptsServiceImpl.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 { CancellationToken, CancellationTokenPool } from '../../../../../../base/common/cancellation.js';
7 > import { CancellationError, isCancellationError } from '../../../../../../base/common/errors.js';
8 > import { Emitter, Event } from '../../../../../../base/common/event.js';
9 > import { ParseError, parse as parseJSONC } from '../../../../../../base/common/json.js';
10 > import { getParseErrorMessage } from '../../../../../../base/common/jsonErrorMessages.js';
11 > import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
12 > import { StopWatch } from '../../../../../../base/common/stopwatch.js';
13 > import { autorun, IReader } from '../../../../../../base/common/observable.js';
14 > import { ResourceMap, ResourceSet } from '../../../../../../base/common/map.js';
15 > import { basename, dirname, isEqual } from '../../../../../../base/common/resources.js';
16 > import { URI } from '../../../../../../base/common/uri.js';
17 > import { OffsetRange } from '../../../../../../editor/common/core/ranges/offsetRange.js';
18 > import { type ITextModel } from '../../../../../../editor/common/model.js';
19 > import { IModelService } from '../../../../../../editor/common/services/model.js';
20 > import { localize } from '../../../../../../nls.js';
21 > import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
22 > import { IExtensionDescription } from '../../../../../../platform/extensions/common/extensions.js';
23 > import { FileOperationError, FileOperationResult, IFileService } from '../../../../../../platform/files/common/files.js';
24 > import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
25 > import { ILabelService } from '../../../../../../platform/label/common/label.js';
26 > import { ILogService } from '../../../../../../platform/log/common/log.js';
27 > import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
28 > import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js';
29 > import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js';
30 > import { IVariableReference } from '../../chatModes.js';
31 > import { PromptsConfig } from '../config/config.js';
32 > import { AGENT_MD_FILENAME, CLAUDE_CONFIG_FOLDER, CLAUDE_LOCAL_MD_FILENAME, CLAUDE_MD_FILENAME, COPILOT_CONFIG_FOLDER, COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, getCleanPromptName, getSkillFolderName, GITHUB_CONFIG_FOLDER, IResolvedPromptSourceFolder, isInClaudeRulesFolder } from '../config/promptFileLocations.js';
33 > import { PROMPT_LANGUAGE_ID, PromptFileSource, PromptsType, Target, getPromptsTypeForLanguageId } from '../promptTypes.js';
34 > import { IWorkspaceInstructionFile, PromptFilesLocator } from '../utils/promptFilesLocator.js';
35 > import { evaluateApplyToPattern, PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../promptFileParser.js';
36 > import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, IConfiguredHooksInfo, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPluginPromptPath, IBuiltinPromptPath, IPromptPath, IPromptsService, IAgentSkill, IInstructionDiscoveryInfo, IInstructionDiscoveryResult, IInstructionFile, IUserPromptPath, PromptsStorage, IPromptFileContext, IPromptFileResource, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, IPromptSourceFolderResult, ICustomAgentVisibility, IAgentInstructionFile, AgentInstructionFileType, Logger, ISlashCommandDiscoveryInfo, ISlashCommandDiscoveryResult, IAgentDiscoveryInfo, IAgentDiscoveryResult, IHookDiscoveryInfo, IResolvedChatPromptSlashCommand, matchesSessionType } from './promptsService.js';
37 > import { Delayer, raceCancellationError } from '../../../../../../base/common/async.js';
38 > import { Schemas } from '../../../../../../base/common/network.js';
39 > import { ChatRequestHooks, parseSubagentHooksFromYaml } from '../hookSchema.js';
40 > import { type IParsedHookCommand } from '../../../../../../platform/agentPlugins/common/pluginParsers.js';
41 > import { HookType } from '../hookTypes.js';
42 > import { HookSourceFormat, parseHooksFromFile } from '../hookCompatibility.js';
43 > import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js';
44 > import { IWorkspaceTrustManagementService } from '../../../../../../platform/workspace/common/workspaceTrust.js';
45 > import { IPathService } from '../../../../../services/path/common/pathService.js';
46 > import { getTarget, mapClaudeModels, mapClaudeTools } from '../languageProviders/promptFileAttributes.js';
47 > import { getCanonicalPluginCommandId, IAgentPlugin, IAgentPluginService } from '../../plugins/agentPluginService.js';
48 > import { isContributionEnabled } from '../../enablement.js';
49 > import { assertNever } from '../../../../../../base/common/assert.js';
50 > import { ExtensionPromptFileService } from './extensionPromptFileService.js';
51 >
52 > /**
53 > * Provides prompt services.
54 > */
55 > export class PromptsService extends Disposable implements IPromptsService {
56 > public declare readonly _serviceBrand: undefined;
57 >
58 > /**
59 > * Prompt files locator utility.
60 > */
61 > private readonly fileLocator: PromptFilesLocator;
62 >
63 > /**
64 > * Cached agent discovery info.
65 > */
66 > private readonly cachedCustomAgents: CachedPromise<IAgentDiscoveryInfo>;
67 >
68 > /**
69 > * Cached slash command discovery info.
70 > */
71 > private readonly cachedSlashCommands: CachedPromise<ISlashCommandDiscoveryInfo>;
72 >
73 > /**
74 > * Cached hooks. Invalidated when hook files change.
75 > */
76 > private readonly cachedHooks: CachedPromise<IHookDiscoveryInfo>;
77 >
78 > /**
79 > * Cached skill discovery info.
80 > */
81 > private readonly cachedSkills: CachedPromise<IPromptDiscoveryInfo>;
82 >
83 > /**
84 > * Cached instructions.
85 > */
86 > private readonly cachedInstructions: CachedPromise<IInstructionDiscoveryInfo>;
87 > private readonly agentInstructionsWatcher = this._register(new MutableDisposable<IDisposable>());
88 > private readonly _onDidChangeAgentInstructions = this._register(new Emitter<void>({
89 > onWillAddFirstListener: () => {
90 const store = new DisposableStore();
91 const agentInstructionsUpdatedEvent = this.fileLocator.createAgentInstructionsUpdatedEvent();
94 this.agentInstructionsWatcher.value = store;
95 },
96 > onDidRemoveLastListener: () => { promptsServiceImpl.ts
97 > this.agentInstructionsWatcher.clear();
98 > }
99 > }));
100 >
101 > /**
102 > * Synchronous mirror of the names exposed by {@link getPromptSlashCommands},
103 > * maintained for {@link hasPromptSlashCommand} so callers (e.g. the chat request
104 > * parser) can disambiguate `<cmd>:<sub>` vs bare `<cmd>` without an async hop.
105 > */
106 > private readonly knownPromptSlashCommandNames = new Set<string>();
107 >
108 > /**
109 > * Cache for parsed prompt files keyed by URI.
110 > * The number in the returned tuple is textModel.getVersionId(), which is an internal VS Code counter that increments every time the text model's content changes.
111 > */
112 > private readonly cachedParsedPromptFromModels = new ResourceMap<[number, ParsedPromptFile]>();
113 >
114 > /**
115 > * Cached file locations commands. Caching only happens if the corresponding `fileLocatorEvents` event is used.
116 > */
117 > private readonly cachedFileLocations: { [key in PromptsType]?: Promise<readonly IPromptPath[]> } = {};
118 >
119 > /**
120 > * Lazily created events that notify listeners when the file locations for a given prompt type change.
121 > * An event is created on demand for each prompt type and can be used by consumers to react to updates
122 > * in the set of prompt files (e.g., when prompt files are added, removed, or modified).
123 > */
124 > private readonly fileLocatorEvents: { [key in PromptsType]?: Event<void> } = {};
125 >
126 >
127 > /**
128 > * Owns the registry of extension-contributed prompt files (both via
129 > * contribution points and via provider API).
130 > */
131 > private readonly extensionPromptFiles: ExtensionPromptFileService;
132 >
133 > private readonly _onDidPluginPromptFilesChange = this._register(new Emitter<PromptsType>());
134 > private readonly _onDidPluginHooksChange = this._register(new Emitter<void>());
135 > private _pluginPromptFilesByType = new Map<PromptsType, readonly IPluginPromptPath[]>();
136 >
137 > constructor(
138 @ILogService public readonly logger: ILogService,
139 @ILabelService private readonly labelService: ILabelService,
260 }));
261 }
263 > private watchPluginPromptFilesForType(
264 type: PromptsType,
265 getItems: (plugin: IAgentPlugin, reader: IReader) => readonly { uri: URI; name: string }[],
291 });
292 }
294 > protected createPromptFilesLocator(): PromptFilesLocator {
295 return this.instantiationService.createInstance(PromptFilesLocator);
296 }
298 > private getFileLocatorEvent(type: PromptsType): Event<void> {
299 let event = this.fileLocatorEvents[type];
300 if (!event) {
306 return event;
307 }
309 > public getParsedPromptFile(textModel: ITextModel): ParsedPromptFile {
310 const cached = this.cachedParsedPromptFromModels.get(textModel.uri);
311 if (cached && cached[0] === textModel.getVersionId()) {
318 return ast;
319 }
321 > public async listPromptFiles(type: PromptsType, token: CancellationToken): Promise<readonly IPromptPath[]> {
322 let listPromise = this.cachedFileLocations[type];
323 if (!listPromise) {
331 return listPromise;
332 }
334 > private async computeListPromptFiles(type: PromptsType, token: CancellationToken): Promise<readonly IPromptPath[]> {
335 const prompts = await Promise.all([
336 this.fileLocator.listFiles(type, PromptsStorage.user, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.user, type } satisfies IUserPromptPath))),
343 return prompts.flat();
344 }
346 > /**
347 > * Collects diagnostic information about which source folders were searched for display in the debug panel.
348 > */
349 > private async _collectSourceFolderDiagnostics(type: PromptsType): Promise<IPromptSourceFolderResult[]> {
350 const resolvedFolders = await this.fileLocator.getSourceFoldersInDiscoveryOrder(type);
351 return resolvedFolders.map(folder => ({
354 }));
355 }
357 > /**
358 > * Registers a prompt file provider (CustomAgentProvider, InstructionsProvider, or PromptFileProvider).
359 > * This will be called by the extension host bridge when
360 > * an extension registers a provider via vscode.chat.registerCustomAgentProvider(),
361 > * registerInstructionsProvider(), or registerPromptFileProvider().
362 > */
363 > public registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: {
364 onDidChangePromptFiles?: Event<void>;
365 providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise<IPromptFileResource[] | undefined>;
367 return this.extensionPromptFiles.registerPromptFileProvider(extension, type, provider);
368 }
370 >
371 > public async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise<readonly IPromptPath[]> {
372 let promptPaths: readonly IPromptPath[];
373 switch (storage) {
393 return promptPaths;
394 }
396 > private getExtensionPromptFiles(type: PromptsType, token: CancellationToken): Promise<readonly IExtensionPromptPath[]> {
397 return this.extensionPromptFiles.getExtensionPromptFiles(type, token);
398 }
400 > /**
401 > * Returns the built-in prompt files of the given type. The base service ships
402 > * no built-in prompts; subclasses (e.g. the Agents app) override this to
403 > * contribute bundled prompts such as built-in skills.
404 > */
405 > protected async getBuiltinPromptFiles(type: PromptsType, token: CancellationToken): Promise<readonly IBuiltinPromptPath[]> {
406 return [];
407 }
409 > public async getSourceFolders(type: PromptsType): Promise<readonly IPromptPath[]> {
410 const result: IPromptPath[] = [];
411
431 return result;
432 }
434 > public async getResolvedSourceFolders(type: PromptsType): Promise<readonly IResolvedPromptSourceFolder[]> {
435 return this.fileLocator.getResolvedSourceFolders(type);
436 }
438 > // slash prompt commands
439 >
440 > /**
441 > * Emitter for slash commands change events.
442 > */
443 > public get onDidChangeSlashCommands(): Event<void> {
444 return this.cachedSlashCommands.onDidChangePromise;
445 }
447 > public async getPromptSlashCommands(token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]> {
448 const discoveryInfo = await this.cachedSlashCommands.get(token);
449 const result = this.slashCommandsFromDiscoveryInfo(discoveryInfo);
450 return result;
451 }
453 > /**
454 > * Computes discovery info for slash commands, combining prompts and skills.
455 > */
456 > private async computeSlashCommandDiscoveryInfo(token: CancellationToken): Promise<ISlashCommandDiscoveryInfo> {
457 const stopWatch = StopWatch.create(true);
458 const promptFiles = await this.listPromptFiles(PromptsType.prompt, token);
527 return { type: PromptsType.prompt, files, sourceFolders, durationInMillis: stopWatch.elapsed() };
528 }
530 > /**
531 > * Derives IChatPromptSlashCommand[] from cached discovery info.
532 > */
533 > private slashCommandsFromDiscoveryInfo(discoveryInfo: ISlashCommandDiscoveryInfo): readonly IChatPromptSlashCommand[] {
534 const result: IChatPromptSlashCommand[] = [];
535 const seen = new ResourceSet();
554 return result;
555 }
557 > public isValidSlashCommandName(command: string): boolean {
558 return command.match(/^[\p{L}\d_\-\.:]+$/u) !== null;
559 }
561 > public hasPromptSlashCommand(name: string): boolean {
562 if (!this.knownPromptSlashCommandsHydrationStarted) {
563 this.knownPromptSlashCommandsHydrationStarted = true;
567 return this.knownPromptSlashCommandNames.has(name);
568 }
570 > private knownPromptSlashCommandsHydrationStarted = false;
571 >
572 > private refreshKnownPromptSlashCommandNames(): void {
573 this.getPromptSlashCommands(CancellationToken.None).then(commands => {
574 this.knownPromptSlashCommandNames.clear();
578 }, () => { /* discovery failures already logged; sync cache stays as-is */ });
579 }
581 > public async resolvePromptSlashCommand(name: string, sessionType: string | undefined, token: CancellationToken): Promise<IResolvedChatPromptSlashCommand | undefined> {
582 const commands = await this.getPromptSlashCommands(token);
583 const command = commands.find(cmd => cmd.name === name && matchesSessionType(cmd.sessionTypes, sessionType));
590 return undefined;
591 }
593 > private asChatPromptSlashCommand(argumentHint: string | undefined, userInvocable: boolean | undefined, promptPath: IPromptPath): IChatPromptSlashCommand {
594 let name = promptPath.name ?? getCleanPromptName(promptPath.uri);
595 name = name.replace(/[^\p{L}\d_\-\.:]+/gu, '-'); // replace spaces with dashes
609 };
610 }
612 > public async getPromptSlashCommandName(uri: URI, token: CancellationToken): Promise<string> {
613 const slashCommands = await this.getPromptSlashCommands(token);
614 const slashCommand = slashCommands.find(c => isEqual(c.uri, uri));
618 return slashCommand.name;
619 }
621 > // custom agents
622 >
623 > /**
624 > * Emitter for custom agents change events.
625 > */
626 > public get onDidChangeCustomAgents(): Event<void> {
627 return this.cachedCustomAgents.onDidChangePromise;
628 }
630 > public get onDidChangeInstructions(): Event<void> {
631 return this.cachedInstructions.onDidChangePromise;
632 }
634 > public get onDidChangeAgentInstructions(): Event<void> {
635 return this._onDidChangeAgentInstructions.event;
636 }
638 > public async getCustomAgents(token: CancellationToken): Promise<readonly ICustomAgent[]> {
639 const discoveryInfo = await this.cachedCustomAgents.get(token);
640 const result = this.agentsFromDiscoveryInfo(discoveryInfo);
641 return result;
642 }
644 > /**
645 > * Derives ICustomAgent[] from cached discovery info.
646 > */
647 > private agentsFromDiscoveryInfo(discoveryInfo: IAgentDiscoveryInfo): readonly ICustomAgent[] {
648 const result: ICustomAgent[] = [];
649 for (const file of discoveryInfo.files) {
654 return result;
655 }
657 > private async computeAgentDiscoveryInfo(token: CancellationToken): Promise<IAgentDiscoveryInfo> {
658 const stopWatch = StopWatch.create(true);
659 const allAgentFiles = await this.listPromptFiles(PromptsType.agent, token);
714 return { type: PromptsType.agent, files, sourceFolders, durationInMillis: stopWatch.elapsed() };
715 }
717 >
718 > public async parseNew(uri: URI, token: CancellationToken): Promise<ParsedPromptFile> {
719 const model = this.modelService.getModel(uri);
720 if (model) {
728 return new PromptFileParser().parse(uri, fileContent.value.toString());
729 }
731 > public registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name?: string, description?: string, when?: string, sessionTypes?: readonly string[]) {
732 return this.extensionPromptFiles.registerContributedFile(type, uri, extension, name, description, when, sessionTypes);
733 }
735 > getPromptLocationLabel(promptPath: IPromptPath): string {
736 switch (promptPath.storage) {
737 case PromptsStorage.local: return this.labelService.getUriLabel(dirname(promptPath.uri), { relative: true });
745 }
746 }
748 > public async listNestedAgentMDs(token: CancellationToken): Promise<IAgentInstructionFile[]> {
749 const useAgentMD = this.configurationService.getValue(PromptsConfig.USE_AGENT_MD);
750 if (!useAgentMD) {
757 return [];
758 }
760 > public async listAgentInstructions(token: CancellationToken, logger: Logger | undefined): Promise<IAgentInstructionFile[]> {
761 const resolvedAgentFiles: IAgentInstructionFile[] = [];
762 const promises: Promise<IAgentInstructionFile[]>[] = [];
822 return result.sort((a, b) => a.uri.toString().localeCompare(b.uri.toString()));
823 }
825 > public getAgentFileURIFromModeFile(oldURI: URI): URI | undefined {
826 return this.fileLocator.getAgentFileURIFromModeFile(oldURI);
827 }
829 > // --- Enabled Prompt Files -----------------------------------------------------------
830 >
831 > private readonly disabledPromptsStorageKeyPrefix = 'chat.disabledPromptFiles.';
832 >
833 > public getDisabledPromptFiles(type: PromptsType): ResourceSet {
834 // Migration: if disabled key absent but legacy enabled key present, convert once.
835 const disabledKey = this.disabledPromptsStorageKeyPrefix + type;
852 return result;
853 }
855 > public setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void {
856 const disabled = Array.from(uris).map(uri => uri.toJSON());
857 this.storageService.store(this.disabledPromptsStorageKeyPrefix + type, JSON.stringify(disabled), StorageScope.PROFILE, StorageTarget.USER);
863 }
864 }
866 > // Agent skills
867 >
868 > private sanitizeAgentSkillText(text: string): string {
869 // Remove XML tags
870 return text.replace(/<[^>]+>/g, '');
871 }
873 > private truncateAgentSkillName(name: string, uri: URI): string {
874 const MAX_NAME_LENGTH = 64;
875 const sanitized = this.sanitizeAgentSkillText(name);
883 return sanitized;
884 }
886 > private truncateAgentSkillDescription(description: string | undefined, uri: URI): string | undefined {
887 if (!description) {
888 return undefined;
899 return sanitized;
900 }
902 > public get onDidChangeSkills(): Event<void> {
903 return this.cachedSkills.onDidChangePromise;
904 }
906 > public get onDidChangeHooks(): Event<void> {
907 return this.cachedHooks.onDidChangePromise;
908 }
910 > public async findAgentSkills(token: CancellationToken): Promise<IAgentSkill[] | undefined> {
911 const useAgentSkills = this.configurationService.getValue(PromptsConfig.USE_AGENT_SKILLS);
912 if (!useAgentSkills) {
918 return result;
919 }
921 > /**
922 > * Derives IAgentSkill[] from cached discovery info.
923 > */
924 > private skillsFromDiscoveryInfo(discoveryInfo: IPromptDiscoveryInfo): IAgentSkill[] {
925 const result: IAgentSkill[] = [];
926 for (const file of discoveryInfo.files) {
943 return result;
944 }
946 > /**
947 > * Computes the full skill discovery info, including source folders and telemetry.
948 > */
949 > private async computeSkillDiscovery(token: CancellationToken): Promise<IPromptDiscoveryInfo> {
950 const stopWatch = StopWatch.create(true);
951 const files = await this.computeSkillDiscoveryInfo(token);
1047 return { type: PromptsType.skill, files, sourceFolders, durationInMillis: stopWatch.elapsed() };
1048 }
1050 > public async getHooks(token: CancellationToken): Promise<IConfiguredHooksInfo | undefined> {
1051 const discoveryInfo = await this.cachedHooks.get(token);
1052 const result = discoveryInfo.hooksInfo;
1053 return result;
1054 }
1056 > public async getDiscoveryInfo(type: PromptsType, token: CancellationToken): Promise<IPromptDiscoveryInfo> {
1057 switch (type) {
1058 case PromptsType.instructions:
1068 }
1069 }
1071 > public async getInstructionFiles(token: CancellationToken): Promise<readonly IInstructionFile[]> {
1072 const discoveryInfo = await this.cachedInstructions.get(token);
1073 const result = this.instructionsFromDiscoveryInfo(discoveryInfo);
1074 return result;
1075 }
1077 > private instructionsFromDiscoveryInfo(discoveryInfo: IInstructionDiscoveryInfo): IInstructionFile[] {
1078 const result: IInstructionFile[] = [];
1079 for (const file of discoveryInfo.files) {
1094 return result;
1095 }
1097 > private withPromptPathMetadata(promptPath: IPromptPath, name: string | undefined, description: string | undefined): IPromptPath {
1098 return { ...promptPath, name, description };
1099 }
1101 > private async computeInstructionFiles(token: CancellationToken): Promise<IInstructionDiscoveryInfo> {
1102 return await this.getInstructionsDiscoveryInfo(token);
1103 }
1105 > private async computeHooks(token: CancellationToken): Promise<IHookDiscoveryInfo> {
1106 const stopWatch = StopWatch.create(true);
1107 const useChatHooks = this.configurationService.getValue(PromptsConfig.USE_CHAT_HOOKS);
1303 return { type: PromptsType.hook, files, sourceFolders, hooksInfo: { hooks: result, hasDisabledClaudeHooks }, durationInMillis: stopWatch.elapsed() };
1304 }
1306 > /**
1307 > * Precedence used when deduplicating skills that share the same canonical
1308 > * name: workspace > personal > plugin > extension API > extension contribution.
1309 > * Lower numbers win.
1310 > */
1311 > private getSkillPriority(skill: IPromptPath): number {
1312 if (skill.storage === PromptsStorage.local) {
1313 return 0; // workspace
1327 return 5;
1328 }
1330 > /**
1331 > * Returns the discovery results for skill files.
1332 > */
1333 > private async computeSkillDiscoveryInfo(token: CancellationToken): Promise<IPromptFileDiscoveryResult[]> {
1334 const files: IPromptFileDiscoveryResult[] = [];
1335 const seenNames = new Set<string>();
1397 return files;
1398 }
1400 > private async getInstructionsDiscoveryInfo(token: CancellationToken): Promise<IInstructionDiscoveryInfo> {
1401 const stopWatch = StopWatch.create(true);
1402 const files: IInstructionDiscoveryResult[] = [];
1429 return { type: PromptsType.instructions, files, sourceFolders, durationInMillis: stopWatch.elapsed() };
1430 }
1432 >
1433 > // helpers
1434 >
1435 > class CachedPromise<T> extends Disposable {
1436 > private cachedPromise: Promise<T> | undefined = undefined;
1437 > private cachedPool: CancellationTokenPool | undefined = undefined;
1438 > private readonly onDidUpdatePromiseEmitter: Emitter<void>;
1439 >
1440 > constructor(private readonly computeFn: (token: CancellationToken) => Promise<T>, private readonly getEvent: () => Event<void>, private readonly delay: number = 0) {
1441 super();
1442 this.onDidUpdatePromiseEmitter = this._register(new Emitter<void>());
1447 }));
1448 }
1450 > public get onDidChangePromise(): Event<void> {
1451 return this.onDidUpdatePromiseEmitter.event;
1452 }
1454 > public get(token: CancellationToken): Promise<T> {
1455 // If a previous in-flight computation had all of its callers cancel, the pool's
1456 // token will have fired and the computation may have rejected/aborted. A new
1486 return raceCancellationError(this.cachedPromise, token);
1487 }
1489 > public refresh(): void {
1490 this.cachedPromise = undefined;
1491 this.onDidUpdatePromiseEmitter?.fire();
1492 }
1494 >
1495 > interface ModelChangeEvent {
1496 > readonly promptType: PromptsType;
1497 > readonly uri: URI;
1498 > }
1499 >
1500 > class ModelChangeTracker extends Disposable {
1501 >
1502 > private readonly listeners = new ResourceMap<IDisposable>();
1503 > private readonly onDidPromptModelChange: Emitter<ModelChangeEvent>;
1504 >
1505 > public get onDidPromptChange(): Event<ModelChangeEvent> {
1506 > return this.onDidPromptModelChange.event;
1507 > }
1508 >
1509 > constructor(modelService: IModelService) {
1510 super();
1511 this.onDidPromptModelChange = this._register(new Emitter<ModelChangeEvent>());
1540 this._register(modelService.onModelRemoved(model => onRemove(model.getLanguageId(), model.uri)));
1541 }
1543 > public override dispose(): void {
1544 super.dispose();
1545 this.listeners.forEach(listener => listener.dispose());
1546 this.listeners.clear();
1547 }
1549 >
1550 > export namespace CustomAgent {
1551 > export function fromParsedPromptFile(ast: ParsedPromptFile, extra: { name?: string; description?: string; source: IAgentSource; hooks?: ChatRequestHooks; sessionTypes: readonly string[] | undefined; enabled: boolean }): ICustomAgent {
1552 const uri = ast.uri;
1553 const { hooks, sessionTypes, enabled } = extra;
src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts 201 introduced LOC · 19 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- filesConfigurationService.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 { localize } from '../../../../nls.js';
7 > import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
8 > import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
9 > import { Event, Emitter } from '../../../../base/common/event.js';
10 > import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
11 > import { RawContextKey, IContextKeyService, IContextKey } from '../../../../platform/contextkey/common/contextkey.js';
12 > import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13 > import { IFilesConfiguration, AutoSaveConfiguration, HotExitConfiguration, FILES_READONLY_INCLUDE_CONFIG, FILES_READONLY_EXCLUDE_CONFIG, IFileStatWithMetadata, IFileService, IBaseFileStat, hasReadonlyCapability, IFilesConfigurationNode } from '../../../../platform/files/common/files.js';
14 > import { equals } from '../../../../base/common/objects.js';
15 > import { URI } from '../../../../base/common/uri.js';
16 > import { isWeb } from '../../../../base/common/platform.js';
17 > import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
18 > import { ResourceGlobMatcher } from '../../../common/resources.js';
19 > import { GlobalIdleValue } from '../../../../base/common/async.js';
20 > import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
21 > import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
22 > import { LRUCache, ResourceMap } from '../../../../base/common/map.js';
23 > import { IMarkdownString } from '../../../../base/common/htmlContent.js';
24 > import { EditorInput } from '../../../common/editor/editorInput.js';
25 > import { EditorResourceAccessor, SaveReason, SideBySideEditor } from '../../../common/editor.js';
26 > import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';
27 > import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js';
28 > import { IStringDictionary } from '../../../../base/common/collections.js';
29 >
30 > export const AutoSaveAfterShortDelayContext = new RawContextKey<boolean>('autoSaveAfterShortDelayContext', false, true);
31 >
32 > export interface IAutoSaveConfiguration {
33 > autoSave?: 'afterDelay' | 'onFocusChange' | 'onWindowChange';
34 > autoSaveDelay?: number;
35 > autoSaveWorkspaceFilesOnly?: boolean;
36 > autoSaveWhenNoErrors?: boolean;
37 > }
38 >
39 > interface ICachedAutoSaveConfiguration extends IAutoSaveConfiguration {
40 >
41 > // Some extra state that we cache to reduce the amount
42 > // of lookup we have to do since auto save methods
43 > // are being called very often, e.g. when content changes
44 >
45 > isOutOfWorkspace?: boolean;
46 > isShortAutoSaveDelay?: boolean;
47 > }
48 >
49 > export const enum AutoSaveMode {
50 > OFF,
51 > AFTER_SHORT_DELAY,
52 > AFTER_LONG_DELAY,
53 > ON_FOCUS_CHANGE,
54 > ON_WINDOW_CHANGE
55 > }
56 >
57 > export const enum AutoSaveDisabledReason {
58 > SETTINGS = 1,
59 > OUT_OF_WORKSPACE,
60 > ERRORS,
61 > DISABLED
62 > }
63 >
64 > export type IAutoSaveMode = IEnabledAutoSaveMode | IDisabledAutoSaveMode;
65 >
66 > export interface IEnabledAutoSaveMode {
67 > readonly mode: AutoSaveMode.AFTER_SHORT_DELAY | AutoSaveMode.AFTER_LONG_DELAY | AutoSaveMode.ON_FOCUS_CHANGE | AutoSaveMode.ON_WINDOW_CHANGE;
68 > }
69 >
70 > export interface IDisabledAutoSaveMode {
71 > readonly mode: AutoSaveMode.OFF;
72 > readonly reason: AutoSaveDisabledReason;
73 > }
74 >
75 > export const IFilesConfigurationService = createDecorator<IFilesConfigurationService>('filesConfigurationService');
76 >
77 > export interface IFilesConfigurationService {
78 >
79 > readonly _serviceBrand: undefined;
80 >
81 > //#region Auto Save
82 >
83 > readonly onDidChangeAutoSaveConfiguration: Event<void>;
84 >
85 > readonly onDidChangeAutoSaveDisabled: Event<URI>;
86 >
87 > getAutoSaveConfiguration(resourceOrEditor: EditorInput | URI | undefined): IAutoSaveConfiguration;
88 >
89 > hasShortAutoSaveDelay(resourceOrEditor: EditorInput | URI | undefined): boolean;
90 >
91 > getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined, saveReason?: SaveReason): IAutoSaveMode;
92 >
93 > toggleAutoSave(): Promise<void>;
94 >
95 > enableAutoSaveAfterShortDelay(resourceOrEditor: EditorInput | URI): IDisposable;
96 > disableAutoSave(resourceOrEditor: EditorInput | URI): IDisposable;
97 >
98 > //#endregion
99 >
100 > //#region Configured Readonly
101 >
102 > readonly onDidChangeReadonly: Event<void>;
103 >
104 > isReadonly(resource: URI, stat?: IBaseFileStat): boolean | IMarkdownString;
105 >
106 > updateReadonly(resource: URI, readonly: true | false | 'toggle' | 'reset'): Promise<void>;
107 > updateReadonly(resource: URI[], readonly: true | false | 'reset'): Promise<void>;
108 >
109 > //#endregion
110 >
111 > readonly onDidChangeFilesAssociation: Event<void>;
112 >
113 > readonly isHotExitEnabled: boolean;
114 >
115 > readonly hotExitConfiguration: string | undefined;
116 >
117 > preventSaveConflicts(resource: URI, language?: string): boolean;
118 > }
119 >
120 > export class FilesConfigurationService extends Disposable implements IFilesConfigurationService {
121 >
122 > declare readonly _serviceBrand: undefined;
123 >
124 > private static readonly DEFAULT_AUTO_SAVE_MODE = isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF;
125 > private static readonly DEFAULT_AUTO_SAVE_DELAY = 1000;
126 >
127 > private static readonly READONLY_MESSAGES = {
128 > providerReadonly: { value: localize('providerReadonly', "Editor is read-only because the file system of the file is read-only."), isTrusted: true },
129 > sessionReadonly: { value: localize({ key: 'sessionReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only in this session. [Click here](command:{0}) to set writeable.", 'workbench.action.files.setActiveEditorWriteableInSession'), isTrusted: true },
130 > configuredReadonly: { value: localize({ key: 'configuredReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only via settings. [Click here](command:{0}) to configure or [toggle for this session](command:{1}).", `workbench.action.openSettings?${encodeURIComponent('["files.readonly"]')}`, 'workbench.action.files.toggleActiveEditorReadonlyInSession'), isTrusted: true },
131 > fileLocked: { value: localize({ key: 'fileLocked', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because of file permissions. [Click here](command:{0}) to set writeable anyway.", 'workbench.action.files.setActiveEditorWriteableInSession'), isTrusted: true },
132 > fileReadonly: { value: localize('fileReadonly', "Editor is read-only because the file is read-only."), isTrusted: true }
133 > };
134 >
135 > private readonly _onDidChangeAutoSaveConfiguration = this._register(new Emitter<void>());
136 > readonly onDidChangeAutoSaveConfiguration = this._onDidChangeAutoSaveConfiguration.event;
137 >
138 > private readonly _onDidChangeAutoSaveDisabled = this._register(new Emitter<URI>());
139 > readonly onDidChangeAutoSaveDisabled = this._onDidChangeAutoSaveDisabled.event;
140 >
141 > private readonly _onDidChangeFilesAssociation = this._register(new Emitter<void>());
142 > readonly onDidChangeFilesAssociation = this._onDidChangeFilesAssociation.event;
143 >
144 > private readonly _onDidChangeReadonly = this._register(new Emitter<void>());
145 > readonly onDidChangeReadonly = this._onDidChangeReadonly.event;
146 >
147 > private currentGlobalAutoSaveConfiguration: IAutoSaveConfiguration;
148 > private currentFilesAssociationConfiguration: IStringDictionary<string> | undefined;
149 > private currentHotExitConfiguration: string;
150 >
151 > private readonly autoSaveConfigurationCache = new LRUCache<URI, ICachedAutoSaveConfiguration>(1000);
152 >
153 > private readonly autoSaveAfterShortDelayOverrides = new ResourceMap<number /* counter */>();
154 > private readonly autoSaveDisabledOverrides = new ResourceMap<number /* counter */>();
155 >
156 > private readonly autoSaveAfterShortDelayContext: IContextKey<boolean>;
157 >
158 > private readonly readonlyIncludeMatcher = this._register(new GlobalIdleValue(() => this.createReadonlyMatcher(FILES_READONLY_INCLUDE_CONFIG)));
159 > private readonly readonlyExcludeMatcher = this._register(new GlobalIdleValue(() => this.createReadonlyMatcher(FILES_READONLY_EXCLUDE_CONFIG)));
160 > private configuredReadonlyFromPermissions: boolean | undefined;
161 >
162 > private readonly sessionReadonlyOverrides = new ResourceMap<boolean>(resource => this.uriIdentityService.extUri.getComparisonKey(resource));
163 >
164 > constructor(
165 @IContextKeyService contextKeyService: IContextKeyService,
166 @IConfigurationService private readonly configurationService: IConfigurationService,
186 this.registerListeners();
187 }
189 > private createReadonlyMatcher(config: string) {
190 const matcher = this._register(new ResourceGlobMatcher(
191 resource => this.configurationService.getValue(config, { resource }),
199 return matcher;
200 }
202 > isReadonly(resource: URI, stat?: IBaseFileStat): boolean | IMarkdownString {
203
204 // if the entire file system provider is readonly, we respect that
240 return false;
241 }
243 > async updateReadonly(resource: URI | URI[], readonly: true | false | 'toggle' | 'reset'): Promise<void> {
244 if (Array.isArray(resource)) {
245 for (const r of resource) {
266 this._onDidChangeReadonly.fire();
267 }
269 > private applyReadonly(resource: URI, readonly: true | false | 'reset'): void {
270 if (readonly === 'reset') {
271 this.sessionReadonlyOverrides.delete(resource);
284 }));
285 }
287 > protected onFilesConfigurationChange(configuration: IFilesConfiguration, fromEvent: boolean): void {
288
289 // Auto Save
321 }
322 }
324 > getAutoSaveConfiguration(resourceOrEditor: EditorInput | URI | undefined): ICachedAutoSaveConfiguration {
325 const resource = this.toResource(resourceOrEditor);
326 if (resource) {
336 return this.currentGlobalAutoSaveConfiguration;
337 }
339 > private computeAutoSaveConfiguration(resource: URI | undefined, filesConfiguration: IFilesConfigurationNode | undefined): ICachedAutoSaveConfiguration {
340 let autoSave: 'afterDelay' | 'onFocusChange' | 'onWindowChange' | undefined;
341 let autoSaveDelay: number | undefined;
386 };
387 }
389 > private toResource(resourceOrEditor: EditorInput | URI | undefined): URI | undefined {
390 if (resourceOrEditor instanceof EditorInput) {
391 return EditorResourceAccessor.getOriginalUri(resourceOrEditor, { supportSideBySide: SideBySideEditor.PRIMARY });
394 return resourceOrEditor;
395 }
397 > hasShortAutoSaveDelay(resourceOrEditor: EditorInput | URI | undefined): boolean {
398 const resource = this.toResource(resourceOrEditor);
399
408 return false;
409 }
411 > getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined, saveReason?: SaveReason): IAutoSaveMode {
412 const resource = this.toResource(resourceOrEditor);
413 if (resource && this.autoSaveAfterShortDelayOverrides.has(resource)) {
461 }
462 }
464 > async toggleAutoSave(): Promise<void> {
465 const currentSetting = this.configurationService.getValue('files.autoSave');
466
474 return this.configurationService.updateValue('files.autoSave', newAutoSaveValue);
475 }
477 > enableAutoSaveAfterShortDelay(resourceOrEditor: EditorInput | URI): IDisposable {
478 const resource = this.toResource(resourceOrEditor);
479 if (!resource) {
493 });
494 }
496 > disableAutoSave(resourceOrEditor: EditorInput | URI): IDisposable {
497 const resource = this.toResource(resourceOrEditor);
498 if (!resource) {
517 });
518 }
520 > get isHotExitEnabled(): boolean {
521 if (this.contextService.getWorkspace().transient) {
522 // Transient workspace: hot exit is disabled because
527 return this.currentHotExitConfiguration !== HotExitConfiguration.OFF;
528 }
530 > get hotExitConfiguration(): string {
531 return this.currentHotExitConfiguration;
532 }
534 > preventSaveConflicts(resource: URI, language?: string): boolean {
535 return this.configurationService.getValue('files.saveConflictResolution', { resource, overrideIdentifier: language }) !== 'overwriteFileOnDisk';
536 }
538 >
539 > registerSingleton(IFilesConfigurationService, FilesConfigurationService, InstantiationType.Eager);
src/vs/workbench/contrib/chat/common/promptSyntax/service/extensionPromptFileService.ts 144 introduced LOC · 14 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- extensionPromptFileService.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 { CancellationToken } from '../../../../../../base/common/cancellation.js';
7 > import { CancellationError } from '../../../../../../base/common/errors.js';
8 > import { Emitter, Event } from '../../../../../../base/common/event.js';
9 > import { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js';
10 > import { ResourceMap } from '../../../../../../base/common/map.js';
11 > import { URI } from '../../../../../../base/common/uri.js';
12 > import { IModelService } from '../../../../../../editor/common/services/model.js';
13 > import { ContextKeyExpr, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
14 > import { IExtensionDescription } from '../../../../../../platform/extensions/common/extensions.js';
15 > import { IFileService } from '../../../../../../platform/files/common/files.js';
16 > import { ILogService } from '../../../../../../platform/log/common/log.js';
17 > import { IExtensionService } from '../../../../../services/extensions/common/extensions.js';
18 > import { IFilesConfigurationService } from '../../../../../services/filesConfiguration/common/filesConfigurationService.js';
19 > import { getSkillFolderName } from '../config/promptFileLocations.js';
20 > import { ParsedPromptFile, PromptFileParser } from '../promptFileParser.js';
21 > import { PromptFileSource, PromptsType } from '../promptTypes.js';
22 > import {
23 > CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT,
24 > IExtensionPromptPath,
25 > INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT,
26 > IPromptFileContext,
27 > IPromptFileResource,
28 > PROMPT_FILE_PROVIDER_ACTIVATION_EVENT,
29 > PromptsStorage,
30 > SKILL_PROVIDER_ACTIVATION_EVENT,
31 > } from './promptsService.js';
32 >
33 > /**
34 > * Event payload emitted by {@link ExtensionPromptFileService.onDidChange}.
35 > */
36 > export interface IExtensionPromptFilesChangeEvent {
37 > readonly type: PromptsType;
38 > }
39 >
40 > type PromptFileProviderEntry = {
41 > readonly extension: IExtensionDescription;
42 > readonly type: PromptsType;
43 > readonly onDidChangePromptFiles?: Event<void>;
44 > readonly providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise<IPromptFileResource[] | undefined>;
45 > };
46 >
47 > const ALL_PROMPT_TYPES: readonly PromptsType[] = [
48 > PromptsType.prompt,
49 > PromptsType.instructions,
50 > PromptsType.agent,
51 > PromptsType.skill,
52 > PromptsType.hook,
53 > ];
54 >
55 > /**
56 > * Owns the registry of prompt files contributed by extensions, both via
57 > * static contribution points (see {@link registerContributedFile}) and via
58 > * dynamic providers registered through the proposed extension API (see
59 > * {@link registerPromptFileProvider}).
60 > *
61 > * Exposes a per-type getter ({@link getExtensionPromptFiles}) that merges
62 > * both sources and applies any `when` clauses, plus a single change event
63 > * ({@link onDidChange}) carrying the affected {@link PromptsType}.
64 > */
65 > export class ExtensionPromptFileService extends Disposable {
66 >
67 > /**
68 > * Files contributed via extension contribution points, keyed by type then URI.
69 > */
70 > private readonly contributedFiles = {
71 > [PromptsType.prompt]: new ResourceMap<Promise<IExtensionPromptPath>>(),
72 > [PromptsType.instructions]: new ResourceMap<Promise<IExtensionPromptPath>>(),
73 > [PromptsType.agent]: new ResourceMap<Promise<IExtensionPromptPath>>(),
74 > [PromptsType.skill]: new ResourceMap<Promise<IExtensionPromptPath>>(),
75 > [PromptsType.hook]: new ResourceMap<Promise<IExtensionPromptPath>>(),
76 > };
77 >
78 > /**
79 > * Providers registered via the proposed extension API.
80 > */
81 > private readonly _promptFileProviders: PromptFileProviderEntry[] = [];
82 >
83 > /**
84 > * Context keys referenced by tracked `when` clauses (from contributed
85 > * files and provider results). Used to know when to re-evaluate.
86 > */
87 > private readonly _contributedWhenKeys = new Set<string>();
88 > private readonly _contributedWhenClauses = new Map<string, string>();
89 > private readonly _providerWhenClauses = new Map<PromptFileProviderEntry, readonly string[]>();
90 >
91 > private readonly _onDidChange = this._register(new Emitter<IExtensionPromptFilesChangeEvent>());
92 > public readonly onDidChange: Event<IExtensionPromptFilesChangeEvent> = this._onDidChange.event;
93 >
94 > /**
95 > * Pending URIs to mark as readonly, flushed on the next microtask.
96 > * Batches multiple `registerContributedFile` calls (which happen
97 > * synchronously in the extension point handler) into a single
98 > * `updateReadonly` call to avoid firing `onDidChangeReadonly` per file.
99 > */
100 > private _pendingReadonlyUris: URI[] = [];
101 > private _pendingReadonlyFlush = false;
102 >
103 > constructor(
104 @ILogService private readonly logger: ILogService,
105 @IFileService private readonly fileService: IFileService,
122 }));
123 }
125 > /**
126 > * Returns the merged list of extension-contributed prompt files for the
127 > * given type, filtered by their `when` clause.
128 > */
129 > public async getExtensionPromptFiles(type: PromptsType, token: CancellationToken): Promise<readonly IExtensionPromptPath[]> {
130 await this.extensionService.whenInstalledExtensionsRegistered();
131 const settledResults = await Promise.allSettled(this.contributedFiles[type].values());
149 });
150 }
152 > /**
153 > * Registers a file contributed via a static contribution point. Returns
154 > * a disposable that removes the contribution.
155 > */
156 > public registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name?: string, description?: string, when?: string, sessionTypes?: readonly string[]): IDisposable {
157 const bucket = this.contributedFiles[type];
158 if (bucket.has(uri)) {
198 };
199 }
201 > /**
202 > * Registers a prompt file provider (CustomAgentProvider, InstructionsProvider, or PromptFileProvider).
203 > * This is called by the extension host bridge when an extension registers a provider via
204 > * vscode.chat.registerCustomAgentProvider(), registerInstructionsProvider(), or
205 > * registerPromptFileProvider().
206 > */
207 > public registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: {
208 onDidChangePromptFiles?: Event<void>;
209 providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise<IPromptFileResource[] | undefined>;
236 return disposables;
237 }
239 > private async _listFromProviders(type: PromptsType, activationEvent: string, token: CancellationToken): Promise<IExtensionPromptPath[]> {
240 const result: IExtensionPromptPath[] = [];
241 const readonlyUris: URI[] = [];
284 return result;
285 }
287 > private _getProviderActivationEvent(type: PromptsType): string | undefined {
288 switch (type) {
289 case PromptsType.agent:
299 }
300 }
302 > private _enqueueReadonlyUpdate(uri: URI): void {
303 this._pendingReadonlyUris.push(uri);
304 if (!this._pendingReadonlyFlush) {
312 }
313 }
315 > private _updateContributedWhenKeys(): void {
316 this._contributedWhenKeys.clear();
317 for (const whenClause of this._contributedWhenClauses.values()) {
330 }
331 }
333 > // Skill validation
334 >
335 > private async _validateAndSanitizeSkillFile(uri: URI, token: CancellationToken): Promise<{ name: string; description: string | undefined }> {
336 const parsedFile = await this._parsePromptFile(uri, token);
337 const folderName = getSkillFolderName(uri);
357 return { name: sanitizedName, description: sanitizedDescription };
358 }
360 > private async _parsePromptFile(uri: URI, token: CancellationToken): Promise<ParsedPromptFile> {
361 const model = this.modelService.getModel(uri);
362 if (model) {
369 return new PromptFileParser().parse(uri, fileContent.value.toString());
370 }
372 > private _sanitizeAgentSkillText(text: string): string {
373 // Remove XML tags
374 return text.replace(/<[^>]+>/g, '');
375 }
377 > private _truncateAgentSkillName(name: string, uri: URI): string {
378 const MAX_NAME_LENGTH = 64;
379 const sanitized = this._sanitizeAgentSkillText(name);
387 return sanitized;
388 }
390 > private _truncateAgentSkillDescription(description: string, uri: URI): string {
391 const MAX_DESCRIPTION_LENGTH = 1024;
392 const sanitized = this._sanitizeAgentSkillText(description);