Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/chatModes.ts
5251 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 { CancellationToken } from '../../../../base/common/cancellation.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { Disposable } from '../../../../base/common/lifecycle.js';
9
import { constObservable, IObservable, ISettableObservable, observableValue, transaction } from '../../../../base/common/observable.js';
10
import { 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 } 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 { ChatConfiguration, ChatModeKind } from './constants.js';
22
import { IHandOff, isTarget } from './promptSyntax/promptFileParser.js';
23
import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, ICustomAgentVisibility, IPromptsService, isCustomAgentVisibility, PromptsStorage, Target } from './promptSyntax/service/promptsService.js';
24
import { ThemeIcon } from '../../../../base/common/themables.js';
25
import { Codicon } from '../../../../base/common/codicons.js';
26
import { isString } from '../../../../base/common/types.js';
27
28
export const IChatModeService = createDecorator<IChatModeService>('chatModeService');
29
export interface IChatModeService {
30
readonly _serviceBrand: undefined;
31
32
// TODO expose an observable list of modes
33
readonly onDidChangeChatModes: Event<void>;
34
getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] };
35
findModeById(id: string): IChatMode | undefined;
36
findModeByName(name: string): IChatMode | undefined;
37
}
38
39
export class ChatModeService extends Disposable implements IChatModeService {
40
declare readonly _serviceBrand: undefined;
41
42
private static readonly CUSTOM_MODES_STORAGE_KEY = 'chat.customModes';
43
44
private readonly hasCustomModes: IContextKey<boolean>;
45
private readonly agentModeDisabledByPolicy: IContextKey<boolean>;
46
private readonly _customModeInstances = new Map<string, CustomChatMode>();
47
48
private readonly _onDidChangeChatModes = this._register(new Emitter<void>());
49
public readonly onDidChangeChatModes = this._onDidChangeChatModes.event;
50
51
constructor(
52
@IPromptsService private readonly promptsService: IPromptsService,
53
@IChatAgentService private readonly chatAgentService: IChatAgentService,
54
@IContextKeyService contextKeyService: IContextKeyService,
55
@ILogService private readonly logService: ILogService,
56
@IStorageService private readonly storageService: IStorageService,
57
@IConfigurationService private readonly configurationService: IConfigurationService
58
) {
59
super();
60
61
this.hasCustomModes = ChatContextKeys.Modes.hasCustomChatModes.bindTo(contextKeyService);
62
this.agentModeDisabledByPolicy = ChatContextKeys.Modes.agentModeDisabledByPolicy.bindTo(contextKeyService);
63
64
// Initialize the policy context key
65
this.updateAgentModePolicyContextKey();
66
67
// Load cached modes from storage first
68
this.loadCachedModes();
69
70
void this.refreshCustomPromptModes(true);
71
this._register(this.promptsService.onDidChangeCustomAgents(() => {
72
void this.refreshCustomPromptModes(true);
73
}));
74
this._register(this.storageService.onWillSaveState(() => this.saveCachedModes()));
75
76
// Listen for configuration changes that affect agent mode policy
77
this._register(this.configurationService.onDidChangeConfiguration(e => {
78
if (e.affectsConfiguration(ChatConfiguration.AgentEnabled)) {
79
this.updateAgentModePolicyContextKey();
80
this._onDidChangeChatModes.fire();
81
}
82
}));
83
84
// Ideally we can get rid of the setting to disable agent mode?
85
let didHaveToolsAgent = this.chatAgentService.hasToolsAgent;
86
this._register(this.chatAgentService.onDidChangeAgents(() => {
87
if (didHaveToolsAgent !== this.chatAgentService.hasToolsAgent) {
88
didHaveToolsAgent = this.chatAgentService.hasToolsAgent;
89
this._onDidChangeChatModes.fire();
90
}
91
}));
92
}
93
94
private loadCachedModes(): void {
95
try {
96
const cachedCustomModes = this.storageService.getObject(ChatModeService.CUSTOM_MODES_STORAGE_KEY, StorageScope.WORKSPACE);
97
if (cachedCustomModes) {
98
this.deserializeCachedModes(cachedCustomModes);
99
}
100
} catch (error) {
101
this.logService.error(error, 'Failed to load cached custom agents');
102
}
103
}
104
105
private deserializeCachedModes(cachedCustomModes: unknown): void {
106
if (!Array.isArray(cachedCustomModes)) {
107
this.logService.error('Invalid cached custom modes data: expected array');
108
return;
109
}
110
111
for (const cachedMode of cachedCustomModes) {
112
if (isCachedChatModeData(cachedMode) && cachedMode.uri) {
113
try {
114
const uri = URI.revive(cachedMode.uri);
115
const customChatMode: ICustomAgent = {
116
uri,
117
name: cachedMode.name,
118
description: cachedMode.description,
119
tools: cachedMode.customTools,
120
model: isString(cachedMode.model) ? [cachedMode.model] : cachedMode.model,
121
argumentHint: cachedMode.argumentHint,
122
agentInstructions: cachedMode.modeInstructions ?? { content: cachedMode.body ?? '', toolReferences: [] },
123
handOffs: cachedMode.handOffs,
124
target: cachedMode.target ?? Target.Undefined,
125
visibility: cachedMode.visibility ?? { userInvokable: true, agentInvokable: cachedMode.infer !== false },
126
agents: cachedMode.agents,
127
source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local }
128
};
129
const instance = new CustomChatMode(customChatMode);
130
this._customModeInstances.set(uri.toString(), instance);
131
} catch (error) {
132
this.logService.error(error, 'Failed to revive cached custom agent');
133
}
134
}
135
}
136
137
this.hasCustomModes.set(this._customModeInstances.size > 0);
138
}
139
140
private saveCachedModes(): void {
141
try {
142
const modesToCache = Array.from(this._customModeInstances.values());
143
this.storageService.store(ChatModeService.CUSTOM_MODES_STORAGE_KEY, modesToCache, StorageScope.WORKSPACE, StorageTarget.MACHINE);
144
} catch (error) {
145
this.logService.warn('Failed to save cached custom agents', error);
146
}
147
}
148
149
private async refreshCustomPromptModes(fireChangeEvent?: boolean): Promise<void> {
150
try {
151
const customModes = await this.promptsService.getCustomAgents(CancellationToken.None);
152
153
// Create a new set of mode instances, reusing existing ones where possible
154
const seenUris = new Set<string>();
155
156
for (const customMode of customModes) {
157
if (!customMode.visibility.userInvokable) {
158
continue;
159
}
160
161
const uriString = customMode.uri.toString();
162
seenUris.add(uriString);
163
164
let modeInstance = this._customModeInstances.get(uriString);
165
if (modeInstance) {
166
// Update existing instance with new data
167
modeInstance.updateData(customMode);
168
} else {
169
// Create new instance
170
modeInstance = new CustomChatMode(customMode);
171
this._customModeInstances.set(uriString, modeInstance);
172
}
173
}
174
175
// Clean up instances for modes that no longer exist
176
for (const [uriString] of this._customModeInstances.entries()) {
177
if (!seenUris.has(uriString)) {
178
this._customModeInstances.delete(uriString);
179
}
180
}
181
182
this.hasCustomModes.set(this._customModeInstances.size > 0);
183
} catch (error) {
184
this.logService.error(error, 'Failed to load custom agents');
185
this._customModeInstances.clear();
186
this.hasCustomModes.set(false);
187
}
188
if (fireChangeEvent) {
189
this._onDidChangeChatModes.fire();
190
}
191
}
192
193
getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } {
194
return {
195
builtin: this.getBuiltinModes(),
196
custom: this.getCustomModes(),
197
};
198
}
199
200
findModeById(id: string | ChatModeKind): IChatMode | undefined {
201
return this.getBuiltinModes().find(mode => mode.id === id) ?? this._customModeInstances.get(id);
202
}
203
204
findModeByName(name: string): IChatMode | undefined {
205
return this.getBuiltinModes().find(mode => mode.name.get() === name) ?? this.getCustomModes().find(mode => mode.name.get() === name);
206
}
207
208
private getBuiltinModes(): IChatMode[] {
209
const builtinModes: IChatMode[] = [
210
ChatMode.Ask,
211
];
212
213
// Include Agent mode if:
214
// - It's enabled (hasToolsAgent is true), OR
215
// - It's disabled by policy (so we can show it with a lock icon)
216
// But hide it if the user manually disabled it via settings
217
if (this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy()) {
218
builtinModes.unshift(ChatMode.Agent);
219
}
220
builtinModes.push(ChatMode.Edit);
221
return builtinModes;
222
}
223
224
private getCustomModes(): IChatMode[] {
225
// Show custom modes when agent mode is enabled OR when disabled by policy (to show them in the policy-managed group)
226
return this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy() ? Array.from(this._customModeInstances.values()) : [];
227
}
228
229
private updateAgentModePolicyContextKey(): void {
230
this.agentModeDisabledByPolicy.set(this.isAgentModeDisabledByPolicy());
231
}
232
233
private isAgentModeDisabledByPolicy(): boolean {
234
return this.configurationService.inspect<boolean>(ChatConfiguration.AgentEnabled).policyValue === false;
235
}
236
}
237
238
export interface IChatModeData {
239
readonly id: string;
240
readonly name: string;
241
readonly description?: string;
242
readonly kind: ChatModeKind;
243
readonly customTools?: readonly string[];
244
readonly model?: readonly string[] | string;
245
readonly argumentHint?: string;
246
readonly modeInstructions?: IChatModeInstructions;
247
readonly body?: string; /* deprecated */
248
readonly handOffs?: readonly IHandOff[];
249
readonly uri?: URI;
250
readonly source?: IChatModeSourceData;
251
readonly target?: Target;
252
readonly visibility?: ICustomAgentVisibility;
253
readonly agents?: readonly string[];
254
readonly infer?: boolean; // deprecated, only available in old cached data
255
}
256
257
export interface IChatMode {
258
readonly id: string;
259
readonly name: IObservable<string>;
260
readonly label: IObservable<string>;
261
readonly icon: IObservable<ThemeIcon | undefined>;
262
readonly description: IObservable<string | undefined>;
263
readonly isBuiltin: boolean;
264
readonly kind: ChatModeKind;
265
readonly customTools?: IObservable<readonly string[] | undefined>;
266
readonly handOffs?: IObservable<readonly IHandOff[] | undefined>;
267
readonly model?: IObservable<readonly string[] | undefined>;
268
readonly argumentHint?: IObservable<string | undefined>;
269
readonly modeInstructions?: IObservable<IChatModeInstructions>;
270
readonly uri?: IObservable<URI>;
271
readonly source?: IAgentSource;
272
readonly target: IObservable<Target>;
273
readonly visibility?: IObservable<ICustomAgentVisibility | undefined>;
274
readonly agents?: IObservable<readonly string[] | undefined>;
275
}
276
277
export interface IVariableReference {
278
readonly name: string;
279
readonly range: IOffsetRange;
280
}
281
282
export interface IChatModeInstructions {
283
readonly content: string;
284
readonly toolReferences: readonly IVariableReference[];
285
readonly metadata?: Record<string, boolean | string | number>;
286
}
287
288
function isCachedChatModeData(data: unknown): data is IChatModeData {
289
if (typeof data !== 'object' || data === null) {
290
return false;
291
}
292
293
const mode = data as IChatModeData;
294
return typeof mode.id === 'string' &&
295
typeof mode.name === 'string' &&
296
typeof mode.kind === 'string' &&
297
(mode.description === undefined || typeof mode.description === 'string') &&
298
(mode.customTools === undefined || Array.isArray(mode.customTools)) &&
299
(mode.modeInstructions === undefined || (typeof mode.modeInstructions === 'object' && mode.modeInstructions !== null)) &&
300
(mode.model === undefined || typeof mode.model === 'string' || Array.isArray(mode.model)) &&
301
(mode.argumentHint === undefined || typeof mode.argumentHint === 'string') &&
302
(mode.handOffs === undefined || Array.isArray(mode.handOffs)) &&
303
(mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)) &&
304
(mode.source === undefined || isChatModeSourceData(mode.source)) &&
305
(mode.target === undefined || isTarget(mode.target)) &&
306
(mode.visibility === undefined || isCustomAgentVisibility(mode.visibility)) &&
307
(mode.agents === undefined || Array.isArray(mode.agents));
308
}
309
310
export class CustomChatMode implements IChatMode {
311
private readonly _nameObservable: ISettableObservable<string>;
312
private readonly _descriptionObservable: ISettableObservable<string | undefined>;
313
private readonly _customToolsObservable: ISettableObservable<readonly string[] | undefined>;
314
private readonly _modeInstructions: ISettableObservable<IChatModeInstructions>;
315
private readonly _uriObservable: ISettableObservable<URI>;
316
private readonly _modelObservable: ISettableObservable<readonly string[] | undefined>;
317
private readonly _argumentHintObservable: ISettableObservable<string | undefined>;
318
private readonly _handoffsObservable: ISettableObservable<readonly IHandOff[] | undefined>;
319
private readonly _targetObservable: ISettableObservable<Target>;
320
private readonly _visibilityObservable: ISettableObservable<ICustomAgentVisibility | undefined>;
321
private readonly _agentsObservable: ISettableObservable<readonly string[] | undefined>;
322
private _source: IAgentSource;
323
324
public readonly id: string;
325
326
get name(): IObservable<string> {
327
return this._nameObservable;
328
}
329
330
get description(): IObservable<string | undefined> {
331
return this._descriptionObservable;
332
}
333
334
get icon(): IObservable<ThemeIcon | undefined> {
335
return constObservable(undefined);
336
}
337
338
public get isBuiltin(): boolean {
339
return isBuiltinChatMode(this);
340
}
341
342
get customTools(): IObservable<readonly string[] | undefined> {
343
return this._customToolsObservable;
344
}
345
346
get model(): IObservable<readonly string[] | undefined> {
347
return this._modelObservable;
348
}
349
350
get argumentHint(): IObservable<string | undefined> {
351
return this._argumentHintObservable;
352
}
353
354
get modeInstructions(): IObservable<IChatModeInstructions> {
355
return this._modeInstructions;
356
}
357
358
get uri(): IObservable<URI> {
359
return this._uriObservable;
360
}
361
362
get label(): IObservable<string> {
363
return this.name;
364
}
365
366
get handOffs(): IObservable<readonly IHandOff[] | undefined> {
367
return this._handoffsObservable;
368
}
369
370
get source(): IAgentSource {
371
return this._source;
372
}
373
374
get target(): IObservable<Target> {
375
return this._targetObservable;
376
}
377
378
get visibility(): IObservable<ICustomAgentVisibility | undefined> {
379
return this._visibilityObservable;
380
}
381
382
get agents(): IObservable<readonly string[] | undefined> {
383
return this._agentsObservable;
384
}
385
386
public readonly kind = ChatModeKind.Agent;
387
388
constructor(
389
customChatMode: ICustomAgent
390
) {
391
this.id = customChatMode.uri.toString();
392
this._nameObservable = observableValue('name', customChatMode.name);
393
this._descriptionObservable = observableValue('description', customChatMode.description);
394
this._customToolsObservable = observableValue('customTools', customChatMode.tools);
395
this._modelObservable = observableValue('model', customChatMode.model);
396
this._argumentHintObservable = observableValue('argumentHint', customChatMode.argumentHint);
397
this._handoffsObservable = observableValue('handOffs', customChatMode.handOffs);
398
this._targetObservable = observableValue('target', customChatMode.target);
399
this._visibilityObservable = observableValue('visibility', customChatMode.visibility);
400
this._agentsObservable = observableValue('agents', customChatMode.agents);
401
this._modeInstructions = observableValue('_modeInstructions', customChatMode.agentInstructions);
402
this._uriObservable = observableValue('uri', customChatMode.uri);
403
this._source = customChatMode.source;
404
}
405
406
/**
407
* Updates the underlying data and triggers observable changes
408
*/
409
updateData(newData: ICustomAgent): void {
410
transaction(tx => {
411
this._nameObservable.set(newData.name, tx);
412
this._descriptionObservable.set(newData.description, tx);
413
this._customToolsObservable.set(newData.tools, tx);
414
this._modelObservable.set(newData.model, tx);
415
this._argumentHintObservable.set(newData.argumentHint, tx);
416
this._handoffsObservable.set(newData.handOffs, tx);
417
this._targetObservable.set(newData.target, tx);
418
this._visibilityObservable.set(newData.visibility, tx);
419
this._agentsObservable.set(newData.agents, tx);
420
this._modeInstructions.set(newData.agentInstructions, tx);
421
this._uriObservable.set(newData.uri, tx);
422
this._source = newData.source;
423
});
424
}
425
426
toJSON(): IChatModeData {
427
return {
428
id: this.id,
429
name: this.name.get(),
430
description: this.description.get(),
431
kind: this.kind,
432
customTools: this.customTools.get(),
433
model: this.model.get(),
434
argumentHint: this.argumentHint.get(),
435
modeInstructions: this.modeInstructions.get(),
436
uri: this.uri.get(),
437
handOffs: this.handOffs.get(),
438
source: serializeChatModeSource(this._source),
439
target: this.target.get(),
440
visibility: this.visibility.get(),
441
agents: this.agents.get()
442
};
443
}
444
}
445
446
type IChatModeSourceData =
447
| { readonly storage: PromptsStorage.extension; readonly extensionId: string; type?: ExtensionAgentSourceType }
448
| { readonly storage: PromptsStorage.local | PromptsStorage.user };
449
450
function isChatModeSourceData(value: unknown): value is IChatModeSourceData {
451
if (typeof value !== 'object' || value === null) {
452
return false;
453
}
454
const data = value as { storage?: unknown; extensionId?: unknown };
455
if (data.storage === PromptsStorage.extension) {
456
return typeof data.extensionId === 'string';
457
}
458
return data.storage === PromptsStorage.local || data.storage === PromptsStorage.user;
459
}
460
461
function serializeChatModeSource(source: IAgentSource | undefined): IChatModeSourceData | undefined {
462
if (!source) {
463
return undefined;
464
}
465
if (source.storage === PromptsStorage.extension) {
466
return { storage: PromptsStorage.extension, extensionId: source.extensionId.value, type: source.type };
467
}
468
return { storage: source.storage };
469
}
470
471
function reviveChatModeSource(data: IChatModeSourceData | undefined): IAgentSource | undefined {
472
if (!data) {
473
return undefined;
474
}
475
if (data.storage === PromptsStorage.extension) {
476
return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId), type: data.type ?? ExtensionAgentSourceType.contribution };
477
}
478
return { storage: data.storage };
479
}
480
481
export class BuiltinChatMode implements IChatMode {
482
public readonly name: IObservable<string>;
483
public readonly label: IObservable<string>;
484
public readonly description: IObservable<string>;
485
public readonly icon: IObservable<ThemeIcon>;
486
public readonly target: IObservable<Target>;
487
488
constructor(
489
public readonly kind: ChatModeKind,
490
label: string,
491
description: string,
492
icon: ThemeIcon,
493
) {
494
this.name = constObservable(kind);
495
this.label = constObservable(label);
496
this.description = observableValue('description', description);
497
this.icon = constObservable(icon);
498
this.target = constObservable(Target.Undefined);
499
}
500
501
public get isBuiltin(): boolean {
502
return isBuiltinChatMode(this);
503
}
504
505
get id(): string {
506
// Need a differentiator?
507
return this.kind;
508
}
509
510
/**
511
* Getters are not json-stringified
512
*/
513
toJSON(): IChatModeData {
514
return {
515
id: this.id,
516
name: this.name.get(),
517
description: this.description.get(),
518
kind: this.kind
519
};
520
}
521
}
522
523
export namespace ChatMode {
524
export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question);
525
export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit);
526
export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"), Codicon.agent);
527
}
528
529
export function isBuiltinChatMode(mode: IChatMode): boolean {
530
return mode.id === ChatMode.Ask.id ||
531
mode.id === ChatMode.Edit.id ||
532
mode.id === ChatMode.Agent.id;
533
}
534
535