src/vs/workbench/contrib/chat/common/chatModes.ts

811 LOC · 620 covered · 191 uncovered · 115 ranges · 220 concepts · 20 introducers · 93 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

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 related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- chatModes.ts ×50
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, CancellationTokenSource } from '../../../../base/common/cancellation.js';
7 > import { Emitter, Event } from '../../../../base/common/event.js';
8 > import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
9 > import { constObservable, IObservable, ISettableObservable, observableValue, transaction } from '../../../../base/common/observable.js';
10 > import { isUriComponents, URI } from '../../../../base/common/uri.js';
11 > import { IOffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';
12 > import { localize } from '../../../../nls.js';
13 > import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
14 > import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
15 > import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
16 > import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
17 > import { ILogService } from '../../../../platform/log/common/log.js';
18 > import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
19 > import { IChatAgentService } from './participants/chatAgents.js';
20 > import { ChatContextKeys } from './actions/chatContextKeys.js';
21 > import { getChatSessionType, LocalChatSessionUri } from './model/chatUri.js';
22 > import { ChatConfiguration, ChatModeKind } from './constants.js';
23 > import { IHandOff } from './promptSyntax/promptFileParser.js';
24 > import { IAgentSource, ICustomAgent, ICustomAgentVisibility, isCustomAgentVisibility, PromptsStorage } from './promptSyntax/service/promptsService.js';
25 > import { ICustomizationHarnessService } from './customizationHarnessService.js';
26 > import { PromptFileSource, Target } from './promptSyntax/promptTypes.js';
27 > import { ThemeIcon } from '../../../../base/common/themables.js';
28 > import { Codicon } from '../../../../base/common/codicons.js';
29 > import { hash } from '../../../../base/common/hash.js';
30 > import { isString } from '../../../../base/common/types.js';
31 > import { isTarget } from './promptSyntax/languageProviders/promptFileAttributes.js';
32 > import { equals as arraysEqual } from '../../../../base/common/arrays.js';
33 > import { isEqual as isURLEquals } from '../../../../base/common/resources.js';
34 > import { equals as objectEquals } from '../../../../base/common/objects.js';
35 > import { Delayer } from '../../../../base/common/async.js';
36 > import { isCancellationError } from '../../../../base/common/errors.js';
37 >
38 >
39 > export const IChatModeService = createDecorator<IChatModeService>('chatModeService');
40 > export interface IChatModeService {
41 > readonly _serviceBrand: undefined;
42 >
43 > /**
44 > * Returns the chat modes available for the given session resource.
45 > *
46 > * Instances need to be disposed by the caller when no longer needed
47 > */
48 > createModes(sessionResource: URI): IChatModes & IDisposable;
49 >
50 > /**
51 > * Returns the local chat modes after awaiting any in-flight refresh.
52 > */
53 > getLocalModes(): Promise<IChatModes>;
54 > }
55 >
56 > /**
57 > * The set of chat modes available for a particular session type, partitioned
58 > * into builtin and custom modes, with helpers for lookup by id or name.
59 > */
60 > export interface IChatModes {
61 > readonly onDidChange: Event<void>;
62 > readonly builtin: readonly IChatMode[];
63 > readonly custom: readonly IChatMode[];
64 > findModeById(id: string): IChatMode | undefined;
65 > findModeByName(name: string): IChatMode | undefined;
66 >
67 > /**
68 > * Awaits the most recently scheduled update of custom prompt modes.
69 > * After this resolves, {@link custom} reflects the latest data from the
70 > * prompts service.
71 > */
72 > waitForPendingUpdates(): Promise<void>;
73 > }
74 >
75 > class ChatModes extends Disposable implements IChatModes {
76 >
77 > private static readonly CUSTOM_MODES_STORAGE_KEY_PREFIX = 'chat.customModes.';
78 >
79 > private readonly hasCustomModes: IContextKey<boolean>;
80 > private readonly _customModeInstances = new Map<string, CustomChatMode>();
81 > private readonly _storageKey: string;
82 >
83 > private readonly _onDidChange = this._register(new Emitter<void>());
84 > readonly onDidChange = this._onDidChange.event;
85 >
86 > /** Tracks the most recent refresh of custom prompt modes. */
87 > private _pendingRefresh: Promise<void> = Promise.resolve();
88 >
89 > private _refreshCancellationSource: CancellationTokenSource | undefined;
90 > private readonly _refreshThrottler = this._register(new Delayer<void>(100));
91 >
92 > constructor(
93 > private readonly sessionResource: URI, chatModes.ts ×23
94 > @IChatAgentService private readonly chatAgentService: IChatAgentService,
95 > @IContextKeyService contextKeyService: IContextKeyService,
96 > @ILogService private readonly logService: ILogService,
97 > @IStorageService private readonly storageService: IStorageService,
98 > @IConfigurationService private readonly configurationService: IConfigurationService,
99 > @ICustomizationHarnessService private readonly customizationHarnessService: ICustomizationHarnessService,
100 > ) {
101 > super();
102 >
103 > const sessionType = getChatSessionType(sessionResource);
104 >
105 > this._storageKey = ChatModes.CUSTOM_MODES_STORAGE_KEY_PREFIX + sessionType;
106 > this.hasCustomModes = ChatContextKeys.Modes.hasCustomChatModes.bindTo(contextKeyService);
107 >
108 > // Load cached modes from storage first
109 > this.loadCachedModes();
110 >
111 > this._pendingRefresh = this.triggerRefresh();
112 > // When the harness service is the source, also react to its change events for our session type.
113 > this._register(this.customizationHarnessService.onDidChangeCustomAgents(e => {
114 > if (e.sessionType === sessionType) { chatModes.ts ×8
115 > this._pendingRefresh = this.triggerRefresh();
116 > }
117 > })); chatModes.ts ×23
118 > this._register(this.storageService.onWillSaveState(() => this.saveCachedModes()));
119 >
120 > // Builtin mode availability depends on configuration policy and tools-agent availability.
121 > this._register(this.configurationService.onDidChangeConfiguration(e => {
122 if (e.affectsConfiguration(ChatConfiguration.AgentEnabled)) {
123 this._onDidChange.fire();
124 }
125 > })); chatModes.ts ×23
126 > let didHaveToolsAgent = this.chatAgentService.hasToolsAgent;
127 > this._register(this.chatAgentService.onDidChangeAgents(() => {
128 > if (didHaveToolsAgent !== this.chatAgentService.hasToolsAgent) { chatModes.ts ×2
129 > didHaveToolsAgent = this.chatAgentService.hasToolsAgent;
130 > this._onDidChange.fire();
131 > }
132 > })); chatModes.ts ×23
133 > }
135 > get builtin(): readonly IChatMode[] {
136 > return this.getBuiltinModes(); chatModes.ts ×1
137 > }
139 > get custom(): readonly IChatMode[] {
140 > return this.getCustomModes(); chatModes.ts ×2
141 > }
143 > findModeById(id: string | ChatModeKind): IChatMode | undefined {
144 > return this.getBuiltinModes().find(mode => mode.id === id) ?? this._customModeInstances.get(id); chatModes.ts ×1
145 > }
147 > findModeByName(name: string): IChatMode | undefined {
148 return this.getBuiltinModes().find(mode => mode.name.get() === name) ?? this.getCustomModes().find(mode => mode.name.get() === name || mode.id === name);
149 }
151 > waitForPendingUpdates(): Promise<void> {
152 > return this._pendingRefresh; chatModes.ts ×23
153 > }
155 > private loadCachedModes(): void {
156 > try { chatModes.ts ×23
157 > const cachedCustomModes = this.storageService.getObject(this._storageKey, StorageScope.WORKSPACE);
158 > if (cachedCustomModes) {
159 this.deserializeCachedModes(cachedCustomModes);
160 }
161 > } catch (error) { chatModes.ts ×23
162 this.logService.error(error, 'Failed to load cached custom agents');
163 }
166 > private deserializeCachedModes(cachedCustomModes: unknown): void {
167 if (!Array.isArray(cachedCustomModes)) {
168 this.logService.error('Invalid cached custom modes data: expected array');
169 return;
170 }
171
172 for (const cachedMode of cachedCustomModes) {
173 if (isCachedChatModeData(cachedMode) && cachedMode.uri) {
174 try {
175 const visibility = cachedMode.visibility ?? { userInvocable: true, agentInvocable: cachedMode.infer !== false };
176 if (!visibility.userInvocable) {
177 continue;
178 }
179 const uri = URI.revive(cachedMode.uri);
180 const customChatMode: ICustomAgent = {
181 id: cachedMode.id,
182 uri,
183 name: cachedMode.name,
184 description: cachedMode.description,
185 tools: cachedMode.customTools,
186 model: isString(cachedMode.model) ? [cachedMode.model] : cachedMode.model,
187 argumentHint: cachedMode.argumentHint,
188 agentInstructions: cachedMode.modeInstructions ?? { content: cachedMode.body ?? '', toolReferences: [] },
189 handOffs: cachedMode.handOffs,
190 target: cachedMode.target ?? Target.Undefined,
191 visibility,
192 agents: cachedMode.agents,
193 sessionTypes: cachedMode.sessionTypes,
194 source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local },
195 enabled: true
196 };
197 const instance = new CustomChatMode(customChatMode);
198 this._customModeInstances.set(uri.toString(), instance);
199 } catch (error) {
200 this.logService.error(error, 'Failed to revive cached custom agent');
201 }
202 }
203 }
204
205 this.hasCustomModes.set(this._customModeInstances.size > 0);
206 }
208 > private saveCachedModes(): void {
209 try {
210 const modesToCache = Array.from(this._customModeInstances.values());
211 this.storageService.store(this._storageKey, modesToCache, StorageScope.WORKSPACE, StorageTarget.MACHINE);
212 } catch (error) {
213 this.logService.warn('Failed to save cached custom agents', error);
214 }
215 }
217 > private triggerRefresh(): Promise<void> {
218 > this._refreshCancellationSource?.cancel(); chatModes.ts ×23
219 > this._refreshCancellationSource?.dispose();
220 > const refreshCancellationSource = this._refreshCancellationSource = new CancellationTokenSource();
221 > return this._refreshThrottler.trigger(async () => {
222 > try {
223 > await this.refreshCustomPromptModes(refreshCancellationSource.token);
224 > } finally {
225 > if (this._refreshCancellationSource === refreshCancellationSource) {
226 > this._refreshCancellationSource = undefined;
227 > }
228 > refreshCancellationSource.dispose();
229 > }
230 > });
231 > }
233 > override dispose(): void {
234 > this._refreshCancellationSource?.cancel(); chatModes.ts ×23
235 > this._refreshCancellationSource?.dispose();
236 > this._refreshCancellationSource = undefined;
237 > super.dispose();
238 > }
240 > private async refreshCustomPromptModes(token: CancellationToken): Promise<void> {
241 > let hasChanges = false; chatModes.ts ×23
242 > try {
243 > if (token.isCancellationRequested) {
244 return;
245 }
246 > const customModes = await this.customizationHarnessService.getCustomAgents(this.sessionResource, token); chatModes.ts ×23
247 > if (token.isCancellationRequested) {
248 return;
249 }
251 > // Create a new set of mode instances, reusing existing ones where possible
252 > const seenUris = new Set<string>();
253 > for (const customMode of customModes) {
254 > if (!customMode.visibility.userInvocable || !customMode.enabled) { chatModes.ts ×8
255 continue;
256 }
258 > const uriString = customMode.uri.toString();
259 > seenUris.add(uriString);
260 >
261 > let modeInstance = this._customModeInstances.get(uriString);
262 > if (modeInstance) {
263 > // Update existing instance with new data chatModes.ts ×8
264 > if (modeInstance.updateData(customMode)) {
265 > hasChanges = true; chatModes.ts ×3
266 > }
267 > } else { chatModes.ts ×8
268 > // Create new instance
269 > modeInstance = new CustomChatMode(customMode);
270 > this._customModeInstances.set(uriString, modeInstance);
271 > hasChanges = true;
272 > }
273 > }
275 > // Clean up instances for modes that no longer exist
276 > for (const [uriString] of this._customModeInstances.entries()) {
277 > if (!seenUris.has(uriString)) { chatModes.ts ×8
278 > this._customModeInstances.delete(uriString); chatModes.ts ×2
279 > hasChanges = true;
280 > }
283 > this.hasCustomModes.set(this._customModeInstances.size > 0);
284 > } catch (error) {
285 if (isCancellationError(error)) {
286 return;
287 }
288 this.logService.error(error, 'Failed to load custom agents');
289 this._customModeInstances.clear();
290 this.hasCustomModes.set(false);
291 hasChanges = true;
292 }
293 > if (hasChanges) { chatModes.ts ×23
294 > this._onDidChange.fire(); chatModes.ts ×8
295 > }
298 > private getBuiltinModes(): IChatMode[] {
299 > const builtinModes: IChatMode[] = [ chatModes.ts ×1
300 > ChatMode.Ask,
301 > ];
302 >
303 > // Include Agent mode if:
304 > // - It's enabled (hasToolsAgent is true), OR
305 > // - It's disabled by policy (so we can show it with a lock icon)
306 > // But hide it if the user manually disabled it via settings
307 > if (this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy()) {
308 > builtinModes.unshift(ChatMode.Agent);
309 > }
310 > builtinModes.push(ChatMode.Edit);
311 > return builtinModes;
312 > }
314 > private getCustomModes(): IChatMode[] {
315 > // Show custom modes when agent mode is enabled OR when disabled by policy (to show them in the policy-managed group) chatModes.ts ×2
316 > return this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy() ? Array.from(this._customModeInstances.values()) : [];
317 > }
319 > private isAgentModeDisabledByPolicy(): boolean {
320 > return this.configurationService.inspect<boolean>(ChatConfiguration.AgentEnabled).policyValue === false; chatModes.ts ×2
321 > }
323 >
324 > export class ChatModeService extends Disposable implements IChatModeService {
325 > declare readonly _serviceBrand: undefined;
326 >
327 > private readonly agentModeDisabledByPolicy: IContextKey<boolean>;
328 > private localMode: Promise<IChatModes> | undefined;
329 >
330 > constructor(
331 > @IInstantiationService private readonly instantiationService: IInstantiationService, chatModes.ts ×23
332 > @IContextKeyService contextKeyService: IContextKeyService,
333 > @IConfigurationService private readonly configurationService: IConfigurationService,
334 > ) {
335 > super();
336 >
337 > this.agentModeDisabledByPolicy = ChatContextKeys.Modes.agentModeDisabledByPolicy.bindTo(contextKeyService);
338 >
339 > // Initialize the policy context key
340 > this.updateAgentModePolicyContextKey();
341 >
342 > // Listen for configuration changes that affect agent mode policy
343 > this._register(this.configurationService.onDidChangeConfiguration(e => {
344 if (e.affectsConfiguration(ChatConfiguration.AgentEnabled)) {
345 this.updateAgentModePolicyContextKey();
346 }
347 > })); chatModes.ts ×23
348 > }
350 > createModes(sessionResource: URI): IChatModes & IDisposable {
351 > return this.instantiationService.createInstance(ChatModes, sessionResource); chatModes.ts ×23
352 > }
354 > async getLocalModes(): Promise<IChatModes> {
355 > if (!this.localMode) { chatModes.ts ×23
356 > this.localMode = (async () => {
357 > const modes = this._register(this.createModes(LocalChatSessionUri.getNewSessionUri())); // we make up a new session. Local mdes fall back to the promptService and are not actually tied to the session, so it doesn't matter which one we use here.
358 > await modes.waitForPendingUpdates();
359 > return modes;
360 > })();
361 > }
362 > return this.localMode;
363 > }
365 > private updateAgentModePolicyContextKey(): void {
366 > this.agentModeDisabledByPolicy.set(this.isAgentModeDisabledByPolicy()); chatModes.ts ×23
367 > }
369 > private isAgentModeDisabledByPolicy(): boolean {
370 > return this.configurationService.inspect<boolean>(ChatConfiguration.AgentEnabled).policyValue === false; chatModes.ts ×23
371 > }
373 >
374 > export interface IChatModeData {
375 > readonly id: string;
376 > readonly name: string;
377 > readonly description?: string;
378 > readonly kind: ChatModeKind;
379 > readonly customTools?: readonly string[];
380 > readonly model?: readonly string[] | string;
381 > readonly argumentHint?: string;
382 > readonly modeInstructions?: IChatModeInstructions;
383 > readonly body?: string; /* deprecated */
384 > readonly handOffs?: readonly IHandOff[];
385 > readonly uri?: URI;
386 > readonly source?: IChatModeSourceData;
387 > readonly target?: Target;
388 > readonly visibility?: ICustomAgentVisibility;
389 > readonly agents?: readonly string[];
390 > readonly sessionTypes?: readonly string[];
391 > readonly infer?: boolean; // deprecated, only available in old cached data
392 > }
393 >
394 > export interface IChatMode {
395 > readonly id: string;
396 > readonly name: IObservable<string>;
397 > readonly label: IObservable<string>;
398 > readonly icon: IObservable<ThemeIcon | undefined>;
399 > readonly description: IObservable<string | undefined>;
400 > readonly isBuiltin: boolean;
401 > readonly kind: ChatModeKind;
402 > readonly customTools?: IObservable<readonly string[] | undefined>;
403 > readonly handOffs?: IObservable<readonly IHandOff[] | undefined>;
404 > readonly model?: IObservable<readonly string[] | undefined>;
405 > readonly argumentHint?: IObservable<string | undefined>;
406 > readonly modeInstructions?: IObservable<IChatModeInstructions>;
407 > readonly uri?: IObservable<URI>;
408 > readonly source?: IAgentSource;
409 > readonly target: IObservable<Target>;
410 > readonly visibility?: IObservable<ICustomAgentVisibility | undefined>;
411 > readonly agents?: IObservable<readonly string[] | undefined>;
412 > readonly sessionTypes?: readonly string[];
413 > }
414 >
415 > export interface IVariableReference {
416 > readonly name: string;
417 > readonly range: IOffsetRange;
418 > }
419 >
420 > export interface IChatModeInstructions {
421 > readonly content: string;
422 > readonly toolReferences: readonly IVariableReference[];
423 > readonly metadata?: Record<string, boolean | string | number>;
424 > }
425 >
426 > export namespace IChatModeInstructions {
427 > export function isEquals(a: IChatModeInstructions | undefined, b: IChatModeInstructions | undefined): boolean {
428 > if (a === b) { chatModes.ts ×8
429 > return true; chatModes.ts ×2
430 > }
431 > if (!a || !b) { chatModes.ts ×8
432 return false;
433 }
434 > return a.content === b.content && chatModes.ts ×1
435 > objectEquals(a.toolReferences, b.toolReferences) && chatModes.ts ×1
436 > objectEquals(a.metadata, b.metadata);
439 > }
440 >
441 function isCachedChatModeData(data: unknown): data is IChatModeData {
442 if (typeof data !== 'object' || data === null) {
443 return false;
444 }
445
446 const mode = data as IChatModeData;
447 return typeof mode.id === 'string' &&
448 typeof mode.name === 'string' &&
449 typeof mode.kind === 'string' &&
450 (mode.description === undefined || typeof mode.description === 'string') &&
451 (mode.customTools === undefined || Array.isArray(mode.customTools)) &&
452 (mode.modeInstructions === undefined || (typeof mode.modeInstructions === 'object' && mode.modeInstructions !== null)) &&
453 (mode.model === undefined || typeof mode.model === 'string' || Array.isArray(mode.model)) &&
454 (mode.argumentHint === undefined || typeof mode.argumentHint === 'string') &&
455 (mode.handOffs === undefined || Array.isArray(mode.handOffs)) &&
456 (mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)) &&
457 (mode.source === undefined || isChatModeSourceData(mode.source)) &&
458 (mode.target === undefined || isTarget(mode.target)) &&
459 (mode.visibility === undefined || isCustomAgentVisibility(mode.visibility)) &&
460 (mode.agents === undefined || Array.isArray(mode.agents)) &&
461 (mode.sessionTypes === undefined || Array.isArray(mode.sessionTypes));
462 }
464 > export class CustomChatMode implements IChatMode {
465 > private readonly _nameObservable: ISettableObservable<string>;
466 > private readonly _descriptionObservable: ISettableObservable<string | undefined>;
467 > private readonly _customToolsObservable: ISettableObservable<readonly string[] | undefined>;
468 > private readonly _modeInstructions: ISettableObservable<IChatModeInstructions>;
469 > private readonly _uriObservable: ISettableObservable<URI>;
470 > private readonly _modelObservable: ISettableObservable<readonly string[] | undefined>;
471 > private readonly _argumentHintObservable: ISettableObservable<string | undefined>;
472 > private readonly _handoffsObservable: ISettableObservable<readonly IHandOff[] | undefined>;
473 > private readonly _targetObservable: ISettableObservable<Target>;
474 > private readonly _visibilityObservable: ISettableObservable<ICustomAgentVisibility | undefined>;
475 > private readonly _agentsObservable: ISettableObservable<readonly string[] | undefined>;
476 > private _source: IAgentSource;
477 > private _sessionTypes: readonly string[] | undefined;
478 >
479 > public readonly id: string;
480 >
481 > get name(): IObservable<string> {
482 > return this._nameObservable;
483 > }
484 >
485 > get description(): IObservable<string | undefined> {
486 > return this._descriptionObservable; chatModes.ts ×4
487 > }
489 > get icon(): IObservable<ThemeIcon | undefined> {
490 return constObservable(undefined);
491 }
493 > public get isBuiltin(): boolean {
494 return isBuiltinChatMode(this);
495 }
497 > get customTools(): IObservable<readonly string[] | undefined> {
498 > return this._customToolsObservable; chatModes.ts ×4
499 > }
501 > get model(): IObservable<readonly string[] | undefined> {
502 > return this._modelObservable; chatModes.ts ×3
503 > }
505 > get argumentHint(): IObservable<string | undefined> {
506 return this._argumentHintObservable;
507 }
509 > get modeInstructions(): IObservable<IChatModeInstructions> {
510 > return this._modeInstructions; chatModes.ts ×4
511 > }
513 > get uri(): IObservable<URI> {
514 > return this._uriObservable; chatModes.ts ×2
515 > }
517 > get label(): IObservable<string> {
518 > return this.name; chatModes.ts ×1
519 > }
521 > get handOffs(): IObservable<readonly IHandOff[] | undefined> {
522 > return this._handoffsObservable; chatModes.ts ×2
523 > }
525 > get source(): IAgentSource {
526 > return this._source; chatModes.ts ×4
527 > }
529 > get target(): IObservable<Target> {
530 return this._targetObservable;
531 }
533 > get visibility(): IObservable<ICustomAgentVisibility | undefined> {
534 return this._visibilityObservable;
535 }
537 > get agents(): IObservable<readonly string[] | undefined> {
538 return this._agentsObservable;
539 }
541 > get sessionTypes(): readonly string[] | undefined {
542 return this._sessionTypes;
543 }
545 > public readonly kind = ChatModeKind.Agent;
546 >
547 > constructor(
548 > customChatMode: ICustomAgent chatModes.ts ×8
549 > ) {
550 > this.id = customChatMode.uri.toString();
551 > this._nameObservable = observableValue('name', customChatMode.name);
552 > this._descriptionObservable = observableValue('description', customChatMode.description);
553 > this._customToolsObservable = observableValue('customTools', customChatMode.tools);
554 > this._modelObservable = observableValue('model', customChatMode.model);
555 > this._argumentHintObservable = observableValue('argumentHint', customChatMode.argumentHint);
556 > this._handoffsObservable = observableValue('handOffs', customChatMode.handOffs);
557 > this._targetObservable = observableValue('target', customChatMode.target);
558 > this._visibilityObservable = observableValue('visibility', customChatMode.visibility);
559 > this._agentsObservable = observableValue('agents', customChatMode.agents);
560 > this._modeInstructions = observableValue('_modeInstructions', customChatMode.agentInstructions);
561 > this._uriObservable = observableValue('uri', customChatMode.uri);
562 > this._source = customChatMode.source;
563 > this._sessionTypes = customChatMode.sessionTypes;
564 > }
566 > /**
567 > * Updates the underlying data and triggers observable changes
568 > */
569 > updateData(newData: ICustomAgent): boolean {
570 > let hasChanges = false; chatModes.ts ×8
571 >
572 > transaction(tx => {
573 > const update = <T>(observable: ISettableObservable<T | undefined>, newValue: T | undefined, equals: (a: T | undefined, b: T | undefined) => boolean = (a, b) => a === b) => {
574 > if (!equals(observable.get(), newValue)) {
575 > observable.set(newValue, tx); chatModes.ts ×3
576 > hasChanges = true;
577 > }
578 > }; chatModes.ts ×8
579 > update(this._nameObservable, newData.name);
580 > update(this._descriptionObservable, newData.description);
581 > update(this._customToolsObservable, newData.tools, arraysEqual);
582 > update(this._modelObservable, newData.model, arraysEqual);
583 > update(this._argumentHintObservable, newData.argumentHint);
584 > update(this._modeInstructions, newData.agentInstructions, IChatModeInstructions.isEquals);
585 > update(this._uriObservable, newData.uri, isURLEquals);
586 > update(this._handoffsObservable, newData.handOffs, objectEquals);
587 > update(this._targetObservable, newData.target);
588 > update(this._visibilityObservable, newData.visibility, objectEquals);
589 > update(this._agentsObservable, newData.agents, arraysEqual);
590 > if (!IAgentSource.isEquals(this._source, newData.source)) {
591 this._source = newData.source;
592 hasChanges = true;
593 }
594 > if (!arraysEqual(this._sessionTypes, newData.sessionTypes)) { chatModes.ts ×8
595 this._sessionTypes = newData.sessionTypes;
596 hasChanges = true;
597 }
598 > }); chatModes.ts ×8
599 > return hasChanges;
600 > }
602 > toJSON(): IChatModeData {
603 return {
604 id: this.id,
605 name: this.name.get(),
606 description: this.description.get(),
607 kind: this.kind,
608 customTools: this.customTools.get(),
609 model: this.model.get(),
610 argumentHint: this.argumentHint.get(),
611 modeInstructions: this.modeInstructions.get(),
612 uri: this.uri.get(),
613 handOffs: this.handOffs.get(),
614 source: serializeChatModeSource(this._source),
615 target: this.target.get(),
616 visibility: this.visibility.get(),
617 agents: this.agents.get(),
618 sessionTypes: this.sessionTypes,
619 };
620 }
622 >
623 > type IChatModeSourceData =
624 > | { readonly storage: PromptsStorage.extension; readonly extensionId: string; type?: PromptFileSource.ExtensionContribution | PromptFileSource.ExtensionAPI }
625 > | { readonly storage: PromptsStorage.local | PromptsStorage.user | PromptsStorage.builtIn }
626 > | { readonly storage: PromptsStorage.plugin; readonly pluginUri: URI };
627 >
628 function isChatModeSourceData(value: unknown): value is IChatModeSourceData {
629 if (typeof value !== 'object' || value === null) {
630 return false;
631 }
632 const data = value as { storage?: unknown; extensionId?: unknown; pluginUri?: unknown };
633 if (data.storage === PromptsStorage.extension) {
634 return typeof data.extensionId === 'string';
635 }
636 if (data.storage === PromptsStorage.plugin) {
637 return isUriComponents(data.pluginUri);
638 }
639 return data.storage === PromptsStorage.local || data.storage === PromptsStorage.user || data.storage === PromptsStorage.builtIn;
640 }
642 function serializeChatModeSource(source: IAgentSource | undefined): IChatModeSourceData | undefined {
643 if (!source) {
644 return undefined;
645 }
646 if (source.storage === PromptsStorage.extension) {
647 return { storage: PromptsStorage.extension, extensionId: source.extensionId.value };
648 }
649 if (source.storage === PromptsStorage.plugin) {
650 return { storage: PromptsStorage.plugin, pluginUri: source.pluginUri };
651 }
652 return { storage: source.storage };
653 }
655 function reviveChatModeSource(data: IChatModeSourceData | undefined): IAgentSource | undefined {
656 if (!data) {
657 return undefined;
658 }
659 if (data.storage === PromptsStorage.extension) {
660 return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId) };
661 }
662 if (data.storage === PromptsStorage.plugin) {
663 return { storage: PromptsStorage.plugin, pluginUri: URI.revive(data.pluginUri) };
664 }
665 return { storage: data.storage };
666 }
668 > export class BuiltinChatMode implements IChatMode {
669 > public readonly name: IObservable<string>;
670 > public readonly label: IObservable<string>;
671 > public readonly description: IObservable<string>;
672 > public readonly icon: IObservable<ThemeIcon>;
673 > public readonly target: IObservable<Target>;
674 >
675 > constructor(
676 > public readonly kind: ChatModeKind,
677 > label: string,
678 > description: string,
679 > icon: ThemeIcon,
680 > ) {
681 > this.name = constObservable(kind);
682 > this.label = constObservable(label);
683 > this.description = observableValue('description', description);
684 > this.icon = constObservable(icon);
685 > this.target = constObservable(Target.Undefined);
686 > }
687 >
688 > public get isBuiltin(): boolean {
689 return isBuiltinChatMode(this);
690 }
692 > get id(): string {
693 > // Need a differentiator? chatModes.ts ×1
694 > return this.kind;
695 > }
697 > /**
698 > * Getters are not json-stringified
699 > */
700 > toJSON(): IChatModeData {
701 return {
702 id: this.id,
703 name: this.name.get(),
704 description: this.description.get(),
705 kind: this.kind
706 };
707 }
709 >
710 > export namespace ChatMode {
711 > export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question);
712 > export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit);
713 > export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build"), Codicon.agent);
714 > }
715 >
716 > export function isBuiltinChatMode(mode: IChatMode): boolean {
717 return mode.id === ChatMode.Ask.id ||
718 mode.id === ChatMode.Edit.id ||
719 mode.id === ChatMode.Agent.id;
720 }
722 > /**
723 > * Returns a telemetry-safe mode name. User/local mode names are hashed
724 > * to avoid leaking PII; builtin and extension mode names are returned as-is.
725 > */
726 > export function getModeNameForTelemetry(mode: IChatMode): string {
727 const modeStorage = mode.source?.storage;
728 if (modeStorage === PromptsStorage.local || modeStorage === PromptsStorage.user) {
729 return String(hash(mode.name.get()));
730 }
731 return mode.name.get();
732 }
734 > /**
735 > * Generates a stable identifier for a handoff by combining the target agent
736 > * name with a slugified version of the display label.
737 > *
738 > * Within a single source agent, the combination of `agent` + `label` must be
739 > * unique for IDs to be unambiguous.
740 > *
741 > * @example
742 > * ```
743 > * getHandoffId({ agent: 'agent', label: 'Continue', prompt: '...' })
744 > * // => 'agent:continue'
745 > * ```
746 > */
747 > export function getHandoffId(handoff: IHandOff): string {
748 > const slug = handoff.label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); chatModes.ts ×1
749 > return `${handoff.agent}:${slug}`;
750 > }
752 > /**
753 > * Describes a single handoff defined in a custom agent's `.agent.md` file.
754 > */
755 > export interface IHandoffInfo {
756 > /** Stable identifier for programmatic matching (format: `<agent>:<slugified-label>`). */
757 > readonly id: string;
758 > readonly label: string;
759 > readonly agent: string;
760 > readonly prompt: string;
761 > readonly send?: boolean;
762 > readonly showContinueOn?: boolean;
763 > readonly model?: string;
764 > }
765 >
766 > /**
767 > * Describes a custom agent (or built-in mode) and the handoffs it defines.
768 > */
769 > export interface ICustomAgentInfo {
770 > readonly id: string;
771 > readonly name: string;
772 > readonly isBuiltin: boolean;
773 > readonly visibility: {
774 > readonly userInvocable: boolean;
775 > readonly agentInvocable: boolean;
776 > };
777 > readonly handoffs: IHandoffInfo[];
778 > }
779 >
780 > /**
781 > * Builds an array of {@link ICustomAgentInfo} with handoff metadata for the given agents/modes.
782 > *
783 > * @param modes - The set of agents/modes to include. Pass all modes to get a
784 > * complete picture, or a filtered subset to scope the result.
785 > * @returns One entry per agent/mode, each containing the agent's metadata and
786 > * its declared handoffs.
787 > */
788 > export function buildCustomAgentHandoffsInfo(modes: readonly IChatMode[]): ICustomAgentInfo[] {
789 > return modes.map(mode => { chatModes.ts ×2
790 > const handoffs = mode.handOffs?.get() ?? [];
791 > const visibility = mode.visibility?.get();
792 > return {
793 > id: mode.id,
794 > name: mode.name.get(),
795 > isBuiltin: mode.isBuiltin,
796 > visibility: {
797 > userInvocable: visibility?.userInvocable ?? true,
798 > agentInvocable: visibility?.agentInvocable ?? true,
799 > },
800 > handoffs: handoffs.map(h => ({
801 > id: getHandoffId(h), chatModes.ts ×1
802 > label: h.label,
803 > agent: h.agent,
804 > prompt: h.prompt,
805 > ...(h.send !== undefined ? { send: h.send } : {}),
806 > ...(h.showContinueOn !== undefined ? { showContinueOn: h.showContinueOn } : {}),
807 > ...(h.model !== undefined ? { model: h.model } : {}),
808 > })), chatModes.ts ×2
809 > };
810 > });
811 > }