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 { derived, 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 { ThemeIcon } from '../../../../base/common/themables.js';
11
>
import { URI } from '../../../../base/common/uri.js';
12
>
import { localize } from '../../../../nls.js';
13
>
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
14
>
import { AICustomizationManagementSection, AICustomizationSource, BUILTIN_STORAGE } from './aiCustomizationWorkspaceService.js';
15
>
import { PromptsType } from './promptSyntax/promptTypes.js';
16
>
import { AGENT_MD_FILENAME } from './promptSyntax/config/promptFileLocations.js';
17
>
import { IAgentSource, IChatPromptSlashCommand, ICustomAgent, IPromptsService, IResolvedChatPromptSlashCommand, matchesSessionType, PromptsStorage } from './promptSyntax/service/promptsService.js';
18
>
import { CancellationToken } from '../../../../base/common/cancellation.js';
19
>
import { SessionType } from './chatSessionsService.js';
20
>
import { CustomAgent } from './promptSyntax/service/promptsServiceImpl.js';
21
>
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
22
>
import { getCanonicalPluginCommandId } from './plugins/agentPluginService.js';
23
>
import { getChatSessionType, LocalChatSessionUri } from './model/chatUri.js';
24
>
25
>
26
>
export const ICustomizationHarnessService = createDecorator<ICustomizationHarnessService>('customizationHarnessService');
27
>
28
>
/**
29
>
* Override for a management section's create-button behavior.
30
>
*/
31
>
export interface ISectionOverride {
32
>
/**
33
>
* Label for the primary button. Required when `commandId` or `rootFile`
34
>
* is set. Ignored otherwise (the widget uses its default label).
35
>
*/
36
>
readonly label?: string;
37
>
/** When set, the primary button invokes this command (e.g. hooks quick pick). */
38
>
readonly commandId?: string;
39
>
/** When set, the primary button creates this file at the workspace root. */
40
>
readonly rootFile?: string;
41
>
/**
42
>
* Custom type label for the dropdown workspace/user create actions
43
>
* (e.g. "Rule" instead of "Instruction"). When undefined, the
44
>
* section's default type label is used.
45
>
*/
46
>
readonly typeLabel?: string;
47
>
/**
48
>
* Root-level file shortcuts added to the dropdown (e.g. `['AGENTS.md']`).
49
>
* Each entry creates a "New {filename}" action that creates the file at
50
>
* the workspace root. Harnesses that don't support a file simply omit it.
51
>
*/
52
>
readonly rootFileShortcuts?: readonly string[];
53
>
/**
54
>
* File extension override for new files created under this section.
55
>
* When set, files are created with this extension (e.g. `.md` for
56
>
* Claude rules) instead of the default for the prompt type
57
>
* (e.g. `.instructions.md`).
58
>
*/
59
>
readonly fileExtension?: string;
60
>
}
61
>
62
>
export interface ICustomizationItemAction {
63
>
readonly id: string;
64
>
readonly label: string;
65
>
readonly tooltip?: string;
66
>
readonly icon?: ThemeIcon;
67
>
readonly enabled?: boolean;
68
>
run(): void | Promise<void>;
69
>
}
70
>
71
>
/**
72
>
* Describes a single harness option for the UI toggle.
73
>
*/
74
>
export interface IHarnessDescriptor {
75
>
/**
76
>
* The harness/session-type identifier.
77
>
*/
78
>
readonly id: string;
79
>
readonly label: string;
80
>
readonly icon: ThemeIcon;
81
>
/**
82
>
* Management sections that should be hidden when this harness is active.
83
>
* For example, Claude does not support prompt files so the Prompts
84
>
* section is hidden.
85
>
*/
86
>
readonly hiddenSections?: readonly string[];
87
>
/**
88
>
* When `true`, the "Generate with AI" sparkle button is hidden and replaced
89
>
* with a plain "New X" manual-creation button (like sessions).
90
>
*/
91
>
readonly hideGenerateButton?: boolean;
92
>
/**
93
>
* Per-section overrides for the create button behavior.
94
>
*
95
>
* A `commandId` entry replaces the button entirely with a command
96
>
* invocation (e.g. Claude hooks → `copilot.claude.hooks`).
97
>
*
98
>
* A `rootFile` entry makes the primary button create a specific file
99
>
* at the workspace root (e.g. Claude instructions → `CLAUDE.md`).
100
>
* When combined with `typeLabel`, the dropdown create actions use
101
>
* that label instead of the section's default (e.g. "Rule" instead
102
>
* of "Instruction").
103
>
*/
104
>
readonly sectionOverrides?: ReadonlyMap<string, ISectionOverride>;
105
>
/**
106
>
* The chat agent ID that must be registered for this harness to appear.
107
>
* When `undefined`, the harness is always available (e.g. Local).
108
>
*/
109
>
readonly requiredAgentId?: string;
110
>
/**
111
>
* When set, this harness is backed by an extension-contributed provider
112
>
* that can supply customization items directly (bypassing promptsService
113
>
* discovery and filtering).
114
>
*/
115
>
readonly itemProvider?: ICustomizationItemProvider;
116
>
/**
117
>
* When `true`, the "Troubleshoot" action is available in item context
118
>
* menus. This opens chat with the `/troubleshoot` command pre-filled
119
>
* for the selected customization.
120
>
*/
121
>
readonly supportsTroubleshoot?: boolean;
122
>
/**
123
>
* When set, this harness uses an opt-out sync model where all eligible
124
>
* local customizations are synced by default. The UI shows disable
125
>
* affordances when this harness is active.
126
>
*/
127
>
readonly syncProvider?: ICustomizationSyncProvider;
128
>
/**
129
>
* Optional plugin-management actions shown in the Plugins section add menu.
130
>
* Harnesses can use these to add environment-specific commands alongside
131
>
* the default install-from-source action (for example, configuring plugins on
132
>
* a remote agent host). The create action remains a separate toolbar button.
133
>
*/
134
>
readonly pluginActions?: readonly ICustomizationItemAction[];
135
>
}
136
>
137
>
/**
138
>
* Represents a customization item provided by any source.
139
>
*/
140
>
export interface ICustomizationItem {
141
>
/** Optional stable identity used by list widgets when URI alone is not unique. */
142
>
readonly itemKey?: string;
143
>
readonly uri: URI;
144
>
readonly type: string;
145
>
readonly name: string;
146
>
readonly description?: string;
147
>
/** Customization source (local, user, extension, plugin, builtin). Set by providers that know the source. */
148
>
readonly source: AICustomizationSource;
149
>
/** The extension identifier that contributed this customization, if any. */
150
>
readonly extensionId: string | undefined;
151
>
/** The URI of the plugin that contributed this customization, if any. */
152
>
readonly pluginUri: URI | undefined;
153
>
/** Human-readable name of the plugin that contributed this customization, if any. */
154
>
readonly pluginLabel?: string;
155
>
/** Server-reported loading status for this customization. */
156
>
readonly status?: 'loading' | 'loaded' | 'degraded' | 'error';
157
>
/** Human-readable status detail (e.g. error message or warning). */
158
>
readonly statusMessage?: string;
159
>
/** Whether this customization is currently enabled. */
160
>
readonly enabled?: boolean;
161
>
/** When set, items with the same groupKey are displayed under a shared collapsible header. */
162
>
readonly groupKey?: string;
163
>
/** When set, shows a small inline badge next to the item name (e.g. an applyTo glob pattern). */
164
>
readonly badge?: string;
165
>
/** Tooltip shown when hovering the badge. */
166
>
readonly badgeTooltip?: string;
167
>
/**
168
>
* Whether this customization item can be invoked by the user.
169
>
* Relevant for prompt / skill and custom agents
170
>
*/
171
>
readonly userInvocable?: boolean;
172
>
/** Optional inline/context-menu actions specific to this item. */
173
>
readonly actions?: readonly ICustomizationItemAction[];
174
>
}
175
>
176
>
export interface ICustomizationAgentRef {
177
>
readonly id: string;
178
>
179
>
readonly uri: URI;
180
>
/** Agent name (from frontmatter `name`, or file-derived) */
181
>
readonly name: string;
182
>
/** Optional short description for UI preview (from frontmatter `description`) */
183
>
readonly description?: string;
184
>
}
185
>
186
>
export function isPluginCustomizationItem(item: { readonly type: string }): boolean {
187
return item.type === 'plugin' || item.type === AICustomizationManagementSection.Plugins;
188
}
190
>
/**
191
>
* Provider interface for extension-contributed harnesses that supply
192
>
* customization items directly from their SDK.
193
>
*/
194
>
export interface ICustomizationItemProvider {
195
>
/**
196
>
* Event that fires when the provider's customizations change.
197
>
*/
198
>
readonly onDidChange: Event<void>;
199
>
/**
200
>
* Provide the customization items this harness supports.
201
>
*
202
>
* @param sessionResource URI of the chat session whose
203
>
* customizations should be included. Providers that surface
204
>
* session-scoped state (e.g. an agent host) should read from
205
>
* this session.
206
>
*/
207
>
provideChatSessionCustomizations(sessionResource: URI, token: CancellationToken): Promise<ICustomizationItem[] | undefined>;
208
>
209
>
/**
210
>
* Provide the custom agents this harness supports.
211
>
*
212
>
* @param sessionResource URI of the chat session whose
213
>
* customizations should be included. Providers that surface
214
>
* session-scoped state (e.g. an agent host) should read from
215
>
* this session.
216
>
*/
217
>
provideCustomAgents?(sessionResource: URI, token: CancellationToken): Promise<readonly ICustomAgent[]>;
218
>
219
>
/**
220
>
* Provide the directories where new customization files of the given
221
>
* type can be created for this session. The result includes both
222
>
* workspace-scoped and user-scoped folders; the caller is responsible
223
>
* for partitioning them by storage target.
224
>
*
225
>
* @param sessionResource URI of the chat session whose
226
>
* creation locations should be returned.
227
>
*/
228
>
provideSourceFolders?(sessionResource: URI, type: PromptsType, token: CancellationToken): Promise<readonly ICustomizationSourceFolder[] | undefined>;
229
>
}
230
>
231
>
/**
232
>
* A directory where new customization files of a given type can be created.
233
>
*/
234
>
export interface ICustomizationSourceFolder {
235
>
readonly uri: URI;
236
>
/** Display label for the picker when multiple folders are offered. */
237
>
readonly label: string;
238
>
/** Customization source for this folder (typically 'local' or 'user' for writable creation locations). */
239
>
readonly source: AICustomizationSource;
240
>
}
241
>
242
>
/**
243
>
* Provider interface for harnesses that use an opt-out sync model.
244
>
*
245
>
* Every eligible local customization is synced by default; the user
246
>
* can disable individual items. The persisted set captures only the
247
>
* user's opt-outs.
248
>
*/
249
>
export interface ICustomizationSyncProvider {
250
>
readonly onDidChange: Event<void>;
251
>
isDisabled(uri: URI): boolean;
252
>
setDisabled(uri: URI, disabled: boolean): void;
253
>
}
254
>
255
>
/**
256
>
* Service that manages the active customization harness and provides
257
>
* per-type storage source filters based on the selected harness.
258
>
*
259
>
* The default (core) registration exposes a single "VS Code" harness
260
>
* that shows all storage sources. The sessions window overrides this
261
>
* to provide CLI-scoped harnesses.
262
>
*/
263
>
export interface ICustomizationHarnessService {
264
>
readonly _serviceBrand: undefined;
265
>
266
>
/**
267
>
* The currently active chat session resource.
268
>
*/
269
>
readonly activeSessionResource: IObservable<URI>;
270
>
271
>
/**
272
>
* The currently active harness.
273
>
*/
274
>
readonly activeHarness: IObservable<string>;
275
>
276
>
/**
277
>
* All harnesses available in this window.
278
>
* When only one harness is available the UI should hide the toggle.
279
>
*/
280
>
readonly availableHarnesses: IObservable<readonly IHarnessDescriptor[]>;
281
>
282
>
/**
283
>
* Finds the descriptor of the harness with the given id, or `undefined` if no such harness exists.
284
>
* @param sessionType The harness id (sessionType)
285
>
*/
286
>
findHarnessById(sessionType: string): IHarnessDescriptor | undefined;
287
>
288
>
/**
289
>
* Changes the active session. The new session's type must be present in
290
>
* `availableHarnesses`.
291
>
*/
292
>
setActiveSession(sessionResource: URI): void;
293
>
294
>
/**
295
>
* Returns the descriptor of the currently active harness.
296
>
*/
297
>
getActiveDescriptor(): IHarnessDescriptor;
298
>
299
>
/**
300
>
* Registers an external harness contributed by an extension.
301
>
* The harness appears in the UI toggle alongside static harnesses.
302
>
* Returns a disposable that removes the harness when disposed.
303
>
*/
304
>
registerExternalHarness(descriptor: IHarnessDescriptor): IDisposable;
305
>
306
>
307
>
/**
308
>
* Fires when one of the provided slash commands changes.
309
>
*/
310
>
readonly onDidChangeSlashCommands: Event<{ readonly sessionType: string }>;
311
>
312
>
/**
313
>
* Fires when one of the provided custom agents changes.
314
>
*/
315
>
readonly onDidChangeCustomAgents: Event<{ readonly sessionType: string }>;
316
>
317
>
/**
318
>
* Returns the prompt and skill slash commands for the given session type.
319
>
* Provider-backed harnesses contribute their own items directly; the default
320
>
* VS Code harness falls back to the core prompts service.
321
>
*
322
>
* @param sessionResource URI of the chat session whose customizations
323
>
* should be considered. Forwarded to the underlying
324
>
* {@link ICustomizationItemProvider.provideChatSessionCustomizations}.
325
>
*/
326
>
getSlashCommands(sessionResource: URI, token: CancellationToken): Promise<readonly IChatPromptSlashCommand[]>;
327
>
328
>
/**
329
>
* Returns the custom agents for the given session type.
330
>
* Provider-backed harnesses select items via their own provider and resolve
331
>
* details via the core prompts service.
332
>
*
333
>
* @param sessionResource URI of the chat session whose customizations
334
>
* should be considered. Forwarded to the underlying
335
>
* {@link ICustomizationItemProvider.provideChatSessionCustomizations}.
336
>
*/
337
>
getCustomAgents(sessionResource: URI, token: CancellationToken): Promise<readonly ICustomAgent[]>;
338
>
339
>
/**
340
>
* Resolves a slash command to its full metadata, including the parsed prompt file for prompt commands.
341
>
* Provider-backed harnesses resolve their own items directly; the default VS Code harness falls back to the core prompts service.
342
>
*
343
>
* @param sessionResource URI of the chat session whose customizations
344
>
* should be considered when looking up the slash command.
345
>
*/
346
>
resolvePromptSlashCommand(name: string, sessionResource: URI, token: CancellationToken): Promise<IResolvedChatPromptSlashCommand | undefined>;
347
>
348
>
/**
349
>
* Returns the best session resource to use for a harness lookup.
350
>
* Implementations should prefer the most recently used session for the
351
>
* given session type and fall back to an untitled session resource.
352
>
*/
353
>
getSessionResourceForHarness(sessionType: string): URI;
354
>
}
355
>
356
>
/**
357
>
* Minimal slash-command metadata resolved from the active harness.
358
>
*/
359
>
export interface ICustomizationSlashCommand {
360
>
readonly uri: URI;
361
>
readonly type: PromptsType.prompt | PromptsType.skill;
362
>
readonly name: string;
363
>
readonly description?: string;
364
>
readonly userInvocable: boolean;
365
>
readonly sessionTypes?: readonly string[];
366
>
}
367
>
368
>
// #region Shared descriptor constants
369
>
370
>
/**
371
>
* Empty descriptor returned when no harness is registered yet.
372
>
*/
373
>
const EMPTY_DESCRIPTOR: IHarnessDescriptor = {
374
>
id: '',
375
>
label: '',
376
>
icon: Codicon.sparkle,
377
>
};
378
>
379
>
380
>
// #endregion
381
>
382
>
// #region Harness descriptor factories
383
>
384
>
/**
385
>
* Builds the full source list from the base set (local, user, plugin)
386
>
* plus any additional sources specific to the window type.
387
>
*
388
>
* Core passes `[PromptsStorage.extension]`; sessions passes its
389
>
* BUILTIN_STORAGE constant.
390
>
*/
391
>
392
>
/**
393
>
* Creates a "VS Code" harness descriptor that shows all storage sources
394
>
* with no user-root restrictions.
395
>
*/
396
>
export function createVSCodeHarnessDescriptor(): IHarnessDescriptor {
397
return {
398
id: SessionType.Local,