Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/customizationHarnessService.ts
13401 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 { Codicon } from '../../../../base/common/codicons.js';
7
import { IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js';
8
import { IDisposable } from '../../../../base/common/lifecycle.js';
9
import { Emitter, Event } from '../../../../base/common/event.js';
10
import { joinPath } from '../../../../base/common/resources.js';
11
import { ThemeIcon } from '../../../../base/common/themables.js';
12
import { URI } from '../../../../base/common/uri.js';
13
import { localize } from '../../../../nls.js';
14
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
15
import { AICustomizationManagementSection, IStorageSourceFilter } from './aiCustomizationWorkspaceService.js';
16
import { PromptsType } from './promptSyntax/promptTypes.js';
17
import { AGENT_MD_FILENAME } from './promptSyntax/config/promptFileLocations.js';
18
import { IAgentSource, IChatPromptSlashCommand, ICustomAgent, IPromptsService, IResolvedChatPromptSlashCommand, matchesSessionType, PromptsStorage } from './promptSyntax/service/promptsService.js';
19
import { CancellationToken } from '../../../../base/common/cancellation.js';
20
import { SessionType } from './chatSessionsService.js';
21
import { CustomAgent } from './promptSyntax/service/promptsServiceImpl.js';
22
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
23
import { getCanonicalPluginCommandId } from './plugins/agentPluginService.js';
24
25
export const ICustomizationHarnessService = createDecorator<ICustomizationHarnessService>('customizationHarnessService');
26
27
/**
28
* Override for a management section's create-button behavior.
29
*/
30
export interface ISectionOverride {
31
/**
32
* Label for the primary button. Required when `commandId` or `rootFile`
33
* is set. Ignored otherwise (the widget uses its default label).
34
*/
35
readonly label?: string;
36
/** When set, the primary button invokes this command (e.g. hooks quick pick). */
37
readonly commandId?: string;
38
/** When set, the primary button creates this file at the workspace root. */
39
readonly rootFile?: string;
40
/**
41
* Custom type label for the dropdown workspace/user create actions
42
* (e.g. "Rule" instead of "Instruction"). When undefined, the
43
* section's default type label is used.
44
*/
45
readonly typeLabel?: string;
46
/**
47
* Root-level file shortcuts added to the dropdown (e.g. `['AGENTS.md']`).
48
* Each entry creates a "New {filename}" action that creates the file at
49
* the workspace root. Harnesses that don't support a file simply omit it.
50
*/
51
readonly rootFileShortcuts?: readonly string[];
52
/**
53
* File extension override for new files created under this section.
54
* When set, files are created with this extension (e.g. `.md` for
55
* Claude rules) instead of the default for the prompt type
56
* (e.g. `.instructions.md`).
57
*/
58
readonly fileExtension?: string;
59
}
60
61
export interface ICustomizationItemAction {
62
readonly id: string;
63
readonly label: string;
64
readonly tooltip?: string;
65
readonly icon?: ThemeIcon;
66
readonly enabled?: boolean;
67
run(): void | Promise<void>;
68
}
69
70
/**
71
* Describes a single harness option for the UI toggle.
72
*/
73
export interface IHarnessDescriptor {
74
/**
75
* The harness/session-type identifier.
76
*/
77
readonly id: string;
78
readonly label: string;
79
readonly icon: ThemeIcon;
80
/**
81
* Management sections that should be hidden when this harness is active.
82
* For example, Claude does not support prompt files so the Prompts
83
* section is hidden.
84
*/
85
readonly hiddenSections?: readonly string[];
86
/**
87
* Workspace sub-paths that this harness recognizes for file creation.
88
* When set, the directory picker for new customization files only offers
89
* workspace directories under these sub-paths (e.g. `.claude` for Claude).
90
* When `undefined`, all workspace directories are shown (Local harness).
91
*/
92
readonly workspaceSubpaths?: readonly string[];
93
/**
94
* When `true`, the "Generate with AI" sparkle button is hidden and replaced
95
* with a plain "New X" manual-creation button (like sessions).
96
*/
97
readonly hideGenerateButton?: boolean;
98
/**
99
* Per-section overrides for the create button behavior.
100
*
101
* A `commandId` entry replaces the button entirely with a command
102
* invocation (e.g. Claude hooks → `copilot.claude.hooks`).
103
*
104
* A `rootFile` entry makes the primary button create a specific file
105
* at the workspace root (e.g. Claude instructions → `CLAUDE.md`).
106
* When combined with `typeLabel`, the dropdown create actions use
107
* that label instead of the section's default (e.g. "Rule" instead
108
* of "Instruction").
109
*/
110
readonly sectionOverrides?: ReadonlyMap<string, ISectionOverride>;
111
/**
112
* The chat agent ID that must be registered for this harness to appear.
113
* When `undefined`, the harness is always available (e.g. Local).
114
*/
115
readonly requiredAgentId?: string;
116
/**
117
* Instruction file patterns that this harness recognizes.
118
* Each entry is either an exact filename (e.g. `'CLAUDE.md'`) or a
119
* path prefix ending with `/` (e.g. `'.claude/rules/'`).
120
* When set, instruction items that don't match any pattern are filtered out.
121
* When `undefined`, all instruction files are shown.
122
*/
123
readonly instructionFileFilter?: readonly string[];
124
/**
125
* Returns the storage source filter that should be applied to customization
126
* items of the given type when this harness is active.
127
*/
128
getStorageSourceFilter(type: PromptsType): IStorageSourceFilter;
129
/**
130
* When set, this harness is backed by an extension-contributed provider
131
* that can supply customization items directly (bypassing promptsService
132
* discovery and filtering).
133
*/
134
readonly itemProvider?: ICustomizationItemProvider;
135
/**
136
* When `true`, the "Troubleshoot" action is available in item context
137
* menus. This opens chat with the `/troubleshoot` command pre-filled
138
* for the selected customization.
139
*/
140
readonly supportsTroubleshoot?: boolean;
141
/**
142
* When set, this harness uses an opt-out sync model where all eligible
143
* local customizations are synced by default. The UI shows disable
144
* affordances when this harness is active.
145
*/
146
readonly syncProvider?: ICustomizationSyncProvider;
147
/**
148
* Optional plugin-management actions shown in the Plugins section toolbar.
149
* Harnesses can use these to replace the default local install/create
150
* actions with environment-specific commands (for example, configuring
151
* plugins on a remote agent host).
152
*/
153
readonly pluginActions?: readonly ICustomizationItemAction[];
154
}
155
156
/**
157
* Represents a customization item provided by any source.
158
*/
159
export interface ICustomizationItem {
160
/** Optional stable identity used by list widgets when URI alone is not unique. */
161
readonly itemKey?: string;
162
readonly uri: URI;
163
readonly type: string;
164
readonly name: string;
165
readonly description?: string;
166
/** Storage origin (local, user, extension, plugin). Set by providers that know the source. */
167
readonly storage?: PromptsStorage;
168
/** The extension identifier that contributed this customization, if any. */
169
readonly extensionId: string | undefined;
170
/** The URI of the plugin that contributed this customization, if any. */
171
readonly pluginUri: URI | undefined;
172
/** Server-reported loading status for this customization. */
173
readonly status?: 'loading' | 'loaded' | 'degraded' | 'error';
174
/** Human-readable status detail (e.g. error message or warning). */
175
readonly statusMessage?: string;
176
/** Whether this customization is currently enabled. */
177
readonly enabled?: boolean;
178
/** When set, items with the same groupKey are displayed under a shared collapsible header. */
179
readonly groupKey?: string;
180
/** When set, shows a small inline badge next to the item name (e.g. an applyTo glob pattern). */
181
readonly badge?: string;
182
/** Tooltip shown when hovering the badge. */
183
readonly badgeTooltip?: string;
184
/**
185
* Whether this customization item can be invoked by the user.
186
* Relevant for prompt / skill and custom agents
187
*/
188
readonly userInvocable?: boolean;
189
/** Optional inline/context-menu actions specific to this item. */
190
readonly actions?: readonly ICustomizationItemAction[];
191
}
192
193
/**
194
* Provider interface for extension-contributed harnesses that supply
195
* customization items directly from their SDK.
196
*/
197
export interface ICustomizationItemProvider {
198
/**
199
* Event that fires when the provider's customizations change.
200
*/
201
readonly onDidChange: Event<void>;
202
/**
203
* Provide the customization items this harness supports.
204
*/
205
provideChatSessionCustomizations(token: CancellationToken): Promise<ICustomizationItem[] | undefined>;
206
}
207
208
/**
209
* Provider interface for harnesses that use an opt-out sync model.
210
*
211
* Every eligible local customization is synced by default; the user
212
* can disable individual items. The persisted set captures only the
213
* user's opt-outs.
214
*/
215
export interface ICustomizationSyncProvider {
216
readonly onDidChange: Event<void>;
217
isDisabled(uri: URI): boolean;
218
setDisabled(uri: URI, disabled: boolean): void;
219
}
220
221
/**
222
* Service that manages the active customization harness and provides
223
* per-type storage source filters based on the selected harness.
224
*
225
* The default (core) registration exposes a single "VS Code" harness
226
* that shows all storage sources. The sessions window overrides this
227
* to provide CLI-scoped harnesses.
228
*/
229
export interface ICustomizationHarnessService {
230
readonly _serviceBrand: undefined;
231
232
/**
233
* The currently active harness.
234
*/
235
readonly activeHarness: IObservable<string>;
236
237
/**
238
* All harnesses available in this window.
239
* When only one harness is available the UI should hide the toggle.
240
*/
241
readonly availableHarnesses: IObservable<readonly IHarnessDescriptor[]>;
242
243
/**
244
* Finds the descriptor of the harness with the given id, or `undefined` if no such harness exists.
245
* @param sessionType The harness id (sessionType)
246
*/
247
findHarnessById(sessionType: string): IHarnessDescriptor | undefined;
248
249
/**
250
* Changes the active harness. The new id must be present in
251
* `availableHarnesses`.
252
*/
253
setActiveHarness(sessionType: string): void;
254
255
/**
256
* Convenience: returns the storage source filter for the active harness
257
* and the given customization type.
258
*/
259
getStorageSourceFilter(type: PromptsType): IStorageSourceFilter;
260
261
/**
262
* Returns the descriptor of the currently active harness.
263
*/
264
getActiveDescriptor(): IHarnessDescriptor;
265
266
/**
267
* Registers an external harness contributed by an extension.
268
* The harness appears in the UI toggle alongside static harnesses.
269
* Returns a disposable that removes the harness when disposed.
270
*/
271
registerExternalHarness(descriptor: IHarnessDescriptor): IDisposable;
272
273
274
/**
275
* Fires when one of the provided slash commands changes.
276
*/
277
readonly onDidChangeSlashCommands: Event<{ readonly sessionType: string }>;
278
279
/**
280
* Fires when one of the provided custom agents changes.
281
*/
282
readonly onDidChangeCustomAgents: Event<{ readonly sessionType: string }>;
283
284
/**
285
* Returns the prompt and skill slash commands for the given session type.
286
* Provider-backed harnesses contribute their own items directly; the default
287
* VS Code harness falls back to the core prompts service.
288
*/
289
getSlashCommands(sessionType: string, token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]>;
290
291
/**
292
* Returns the custom agents for the given session type.
293
* Provider-backed harnesses select items via their own provider and resolve
294
* details via the core prompts service.
295
*/
296
getCustomAgents(sessionType: string, token: CancellationToken): Promise<readonly ICustomAgent[]>;
297
298
/**
299
* Resolves a slash command to its full metadata, including the parsed prompt file for prompt commands.
300
* Provider-backed harnesses resolve their own items directly; the default VS Code harness falls back to the core prompts service.
301
*/
302
resolvePromptSlashCommand(name: string, sessionType: string, token: CancellationToken): Promise<IResolvedChatPromptSlashCommand | undefined>;
303
}
304
305
/**
306
* Minimal slash-command metadata resolved from the active harness.
307
*/
308
export interface ICustomizationSlashCommand {
309
readonly uri: URI;
310
readonly type: PromptsType.prompt | PromptsType.skill;
311
readonly name: string;
312
readonly description?: string;
313
readonly userInvocable: boolean;
314
readonly sessionTypes?: readonly string[];
315
}
316
317
// #region Shared filter constants
318
319
/**
320
* Empty filter returned when no harness is registered yet.
321
*/
322
const EMPTY_FILTER: IStorageSourceFilter = {
323
sources: [],
324
};
325
326
/**
327
* Empty descriptor returned when no harness is registered yet.
328
*/
329
const EMPTY_DESCRIPTOR: IHarnessDescriptor = {
330
id: '',
331
label: '',
332
icon: Codicon.sparkle,
333
getStorageSourceFilter: () => ({ sources: [] }),
334
};
335
336
337
/**
338
* Hooks filter — local, user, and plugin sources.
339
*/
340
const HOOKS_FILTER: IStorageSourceFilter = {
341
sources: [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin],
342
};
343
344
// #endregion
345
346
// #region Well-known user directories
347
348
/**
349
* Returns the user-home directories accessible to the Copilot CLI harness.
350
*/
351
export function getCliUserRoots(userHome: URI): readonly URI[] {
352
return [
353
joinPath(userHome, '.copilot'),
354
joinPath(userHome, '.claude'),
355
joinPath(userHome, '.agents'),
356
];
357
}
358
359
// #endregion
360
361
// #region Harness descriptor factories
362
363
/**
364
* Builds the full source list from the base set (local, user, plugin)
365
* plus any additional sources specific to the window type.
366
*
367
* Core passes `[PromptsStorage.extension]`; sessions passes its
368
* BUILTIN_STORAGE constant.
369
*/
370
function buildAllSources(extras: readonly string[]): readonly string[] {
371
return [PromptsStorage.local, PromptsStorage.user, PromptsStorage.plugin, ...extras];
372
}
373
374
/**
375
* Creates a "VS Code" harness descriptor that shows all storage sources
376
* with no user-root restrictions.
377
*/
378
export function createVSCodeHarnessDescriptor(extras: readonly string[]): IHarnessDescriptor {
379
const filter: IStorageSourceFilter = { sources: buildAllSources(extras) };
380
return {
381
id: SessionType.Local,
382
label: localize('harness.local', "Local"),
383
icon: ThemeIcon.fromId(Codicon.vm.id),
384
supportsTroubleshoot: true,
385
sectionOverrides: new Map([
386
[AICustomizationManagementSection.Instructions, {
387
rootFileShortcuts: [AGENT_MD_FILENAME],
388
}],
389
]),
390
getStorageSourceFilter: () => filter,
391
};
392
}
393
394
/**
395
* Creates a harness descriptor that restricts user-file roots for most
396
* types (agents, skills, instructions) while leaving hooks and prompts
397
* unrestricted. Used for restricted harnesses like CLI.
398
*/
399
interface IRestrictedHarnessOptions {
400
readonly hiddenSections?: readonly string[];
401
readonly workspaceSubpaths?: readonly string[];
402
readonly hideGenerateButton?: boolean;
403
readonly sectionOverrides?: ReadonlyMap<string, ISectionOverride>;
404
readonly requiredAgentId?: string;
405
readonly instructionFileFilter?: readonly string[];
406
}
407
408
function createRestrictedHarnessDescriptor(
409
id: string,
410
label: string,
411
icon: ThemeIcon,
412
restrictedUserRoots: readonly URI[],
413
extras: readonly string[],
414
options?: IRestrictedHarnessOptions,
415
): IHarnessDescriptor {
416
const allSources = buildAllSources(extras);
417
const allRootsFilter: IStorageSourceFilter = { sources: allSources };
418
const restrictedFilter: IStorageSourceFilter = { sources: allSources, includedUserFileRoots: restrictedUserRoots };
419
return {
420
id,
421
label,
422
icon,
423
hiddenSections: options?.hiddenSections,
424
workspaceSubpaths: options?.workspaceSubpaths,
425
hideGenerateButton: options?.hideGenerateButton,
426
sectionOverrides: options?.sectionOverrides,
427
requiredAgentId: options?.requiredAgentId,
428
instructionFileFilter: options?.instructionFileFilter,
429
getStorageSourceFilter(type: PromptsType): IStorageSourceFilter {
430
if (type === PromptsType.hook) {
431
return HOOKS_FILTER;
432
}
433
if (type === PromptsType.prompt) {
434
return allRootsFilter;
435
}
436
return restrictedFilter;
437
},
438
};
439
}
440
441
/**
442
* Creates a "Copilot CLI" harness descriptor.
443
*/
444
export function createCliHarnessDescriptor(cliUserRoots: readonly URI[], extras: readonly string[]): IHarnessDescriptor {
445
return createRestrictedHarnessDescriptor(
446
SessionType.CopilotCLI,
447
localize('harness.cli', "Copilot CLI"),
448
ThemeIcon.fromId(Codicon.copilot.id),
449
cliUserRoots,
450
extras,
451
{
452
hideGenerateButton: true,
453
requiredAgentId: 'copilotcli',
454
workspaceSubpaths: ['.github', '.copilot', '.agents', '.claude'],
455
sectionOverrides: new Map([
456
[AICustomizationManagementSection.Instructions, {
457
rootFileShortcuts: [AGENT_MD_FILENAME],
458
}],
459
]),
460
},
461
);
462
}
463
464
// #endregion
465
466
// #region Helpers
467
468
/**
469
* Tests whether a file path belongs to one of the given workspace sub-paths.
470
* Matches on path segment boundaries to avoid false positives
471
* (e.g. `.claude` must appear as `/.claude/` in the path, not as part of
472
* a longer segment like `not.claude`).
473
*/
474
export function matchesWorkspaceSubpath(filePath: string, subpaths: readonly string[]): boolean {
475
return subpaths.some(sp => filePath.includes(`/${sp}/`) || filePath.endsWith(`/${sp}`));
476
}
477
478
/**
479
* Tests whether an instruction file matches one of the harness's recognized
480
* instruction file patterns. Patterns can be exact filenames (e.g. `CLAUDE.md`)
481
* or path prefixes ending with `/` (e.g. `.claude/rules/`).
482
*/
483
export function matchesInstructionFileFilter(filePath: string, filters: readonly string[]): boolean {
484
const name = filePath.substring(filePath.lastIndexOf('/') + 1);
485
return filters.some(f => {
486
if (f.endsWith('/')) {
487
// Path prefix: check if the file is under this directory
488
return filePath.includes(`/${f}`) || filePath.startsWith(f);
489
}
490
return name === f;
491
});
492
}
493
494
// #endregion
495
496
// #region Base implementation
497
498
/**
499
* Reusable base implementation of {@link ICustomizationHarnessService}.
500
* Concrete registrations only need to supply the list of harness
501
* descriptors and a default harness id.
502
*/
503
export class CustomizationHarnessServiceBase implements ICustomizationHarnessService {
504
declare readonly _serviceBrand: undefined;
505
private readonly _onDidChangeSlashCommands = new Emitter<{ readonly sessionType: string }>();
506
readonly onDidChangeSlashCommands = this._onDidChangeSlashCommands.event;
507
private readonly _onDidChangeCustomAgents = new Emitter<{ readonly sessionType: string }>();
508
readonly onDidChangeCustomAgents = this._onDidChangeCustomAgents.event;
509
private readonly _providerListeners: IDisposable[] = [];
510
private _isDisposed = false;
511
512
private readonly _activeHarness: ISettableObservable<string>;
513
readonly activeHarness: IObservable<string>;
514
515
private readonly _staticHarnesses: readonly IHarnessDescriptor[];
516
private readonly _externalHarnesses: IHarnessDescriptor[] = [];
517
private readonly _availableHarnesses: ISettableObservable<readonly IHarnessDescriptor[]>;
518
readonly availableHarnesses: IObservable<readonly IHarnessDescriptor[]>;
519
520
constructor(
521
staticHarnesses: readonly IHarnessDescriptor[],
522
defaultHarness: string,
523
private readonly promptsService: IPromptsService,
524
) {
525
this._staticHarnesses = staticHarnesses;
526
this.promptsService = promptsService;
527
this._activeHarness = observableValue<string>(this, defaultHarness);
528
this.activeHarness = this._activeHarness;
529
this._availableHarnesses = observableValue<readonly IHarnessDescriptor[]>(this, [...this._staticHarnesses]);
530
this.availableHarnesses = this._availableHarnesses;
531
this._rebindProviderListeners();
532
}
533
534
private _getAllHarnesses(): readonly IHarnessDescriptor[] {
535
// External harnesses shadow static ones with the same id so that
536
// extension-contributed harnesses can upgrade a built-in entry.
537
const externalIds = new Set(this._externalHarnesses.map(h => h.id));
538
return [
539
...this._staticHarnesses.filter(h => !externalIds.has(h.id)),
540
...this._externalHarnesses,
541
];
542
}
543
544
private _refreshAvailableHarnesses(): void {
545
if (this._isDisposed) {
546
return;
547
}
548
this._availableHarnesses.set(this._getAllHarnesses(), undefined);
549
this._rebindProviderListeners();
550
}
551
552
private _rebindProviderListeners(): void {
553
for (const listener of this._providerListeners) {
554
listener.dispose();
555
}
556
this._providerListeners.length = 0;
557
for (const harness of this._getAllHarnesses()) {
558
const provider = harness.itemProvider;
559
if (!provider) {
560
this._providerListeners.push(this.promptsService.onDidChangeSlashCommands(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id })));
561
this._providerListeners.push(this.promptsService.onDidChangeCustomAgents(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id })));
562
} else {
563
this._providerListeners.push(provider.onDidChange(() => this._onDidChangeSlashCommands.fire({ sessionType: harness.id })));
564
this._providerListeners.push(provider.onDidChange(() => this._onDidChangeCustomAgents.fire({ sessionType: harness.id })));
565
}
566
}
567
}
568
569
dispose(): void {
570
this._isDisposed = true;
571
for (const listener of this._providerListeners) {
572
listener.dispose();
573
}
574
this._providerListeners.length = 0;
575
this._onDidChangeSlashCommands.dispose();
576
this._onDidChangeCustomAgents.dispose();
577
}
578
579
registerExternalHarness(descriptor: IHarnessDescriptor): IDisposable {
580
this._externalHarnesses.push(descriptor);
581
this._refreshAvailableHarnesses();
582
return {
583
dispose: () => {
584
if (this._isDisposed) {
585
return;
586
}
587
const idx = this._externalHarnesses.indexOf(descriptor);
588
if (idx >= 0) {
589
this._externalHarnesses.splice(idx, 1);
590
this._refreshAvailableHarnesses();
591
// If the removed harness was active, only fall back when no
592
// remaining harness (e.g. the restored static one) shares the id.
593
if (this._activeHarness.get() === descriptor.id) {
594
const all = this._getAllHarnesses();
595
if (!all.some(h => h.id === descriptor.id) && all.length > 0) {
596
this._activeHarness.set(all[0].id, undefined);
597
}
598
}
599
}
600
}
601
};
602
}
603
604
findHarnessById(id: string): IHarnessDescriptor | undefined {
605
return this._getAllHarnesses().find(h => h.id === id);
606
}
607
608
setActiveHarness(id: string): void {
609
const harness = this.findHarnessById(id);
610
if (harness) {
611
this._activeHarness.set(id, undefined);
612
}
613
}
614
615
getStorageSourceFilter(type: PromptsType): IStorageSourceFilter {
616
const activeId = this._activeHarness.get();
617
const all = this._getAllHarnesses();
618
if (all.length === 0) {
619
return EMPTY_FILTER;
620
}
621
const descriptor = all.find(h => h.id === activeId) ?? all[0];
622
return descriptor?.getStorageSourceFilter(type) ?? EMPTY_FILTER;
623
}
624
625
getActiveDescriptor(): IHarnessDescriptor {
626
const activeId = this._activeHarness.get();
627
const all = this._getAllHarnesses();
628
if (all.length === 0) {
629
return EMPTY_DESCRIPTOR;
630
}
631
return all.find(h => h.id === activeId) ?? all[0];
632
}
633
634
async getSlashCommands(sessionType: string, token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]> {
635
const harness = this.findHarnessById(sessionType);
636
if (!harness || !harness.itemProvider) {
637
const commands = await this.promptsService.getPromptSlashCommands(token);
638
return commands.filter(command => matchesSessionType(command.sessionTypes, sessionType));
639
}
640
641
const items = await harness.itemProvider.provideChatSessionCustomizations(token);
642
if (!items) {
643
return [];
644
}
645
const result = [];
646
for (const item of items) {
647
if ((item.enabled !== false) && (item.type === PromptsType.prompt || item.type === PromptsType.skill)) {
648
result.push({
649
uri: item.uri,
650
type: item.type as PromptsType.prompt | PromptsType.skill,
651
name: item.pluginUri ? getCanonicalPluginCommandId({ uri: item.pluginUri }, item.name) : item.name,
652
description: item.description,
653
userInvocable: item.userInvocable ?? true,
654
storage: item.storage ?? PromptsStorage.local,
655
sessionTypes: [sessionType],
656
});
657
}
658
}
659
return result;
660
}
661
662
async getCustomAgents(sessionType: string, token: CancellationToken): Promise<readonly ICustomAgent[]> {
663
const harness = this.findHarnessById(sessionType);
664
if (!harness || !harness.itemProvider) {
665
const allAgents = await this.promptsService.getCustomAgents(token);
666
return allAgents.filter(agent => matchesSessionType(agent.sessionTypes, sessionType));
667
}
668
669
const items = await harness.itemProvider.provideChatSessionCustomizations(token);
670
if (!items) {
671
return [];
672
}
673
674
const getSource = (item: ICustomizationItem): IAgentSource => {
675
if (item.storage === PromptsStorage.extension && item.extensionId) {
676
return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(item.extensionId) };
677
} else if (item.storage === PromptsStorage.plugin && item.pluginUri) {
678
return { storage: PromptsStorage.plugin, pluginUri: item.pluginUri };
679
} else if (item.storage === PromptsStorage.user) {
680
return { storage: PromptsStorage.user };
681
}
682
return { storage: PromptsStorage.local };
683
};
684
685
const result: ICustomAgent[] = [];
686
for (const item of items) {
687
if (item.type === PromptsType.agent) {
688
const promptFile = await this.promptsService.parseNew(item.uri, token);
689
const extra = {
690
name: item.name,
691
description: item.description,
692
sessionTypes: [sessionType],
693
hooks: undefined,
694
source: getSource(item),
695
type: PromptsType.agent,
696
enabled: item.enabled !== false,
697
};
698
result.push(CustomAgent.fromParsedPromptFile(promptFile, extra));
699
}
700
}
701
return result;
702
}
703
704
public async resolvePromptSlashCommand(name: string, sessionType: string, token: CancellationToken): Promise<IResolvedChatPromptSlashCommand | undefined> {
705
const commands = await this.getSlashCommands(sessionType, token);
706
const command = commands.find(cmd => cmd.name === name);
707
if (command) {
708
const parsedPromptFile = await this.promptsService.parseNew(command.uri, token);
709
return {
710
...command,
711
userInvocable: parsedPromptFile.header?.userInvocable ?? command.userInvocable,
712
parsedPromptFile,
713
};
714
}
715
return undefined;
716
}
717
}
718
719
// #endregion
720
721