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
3296 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 { 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 { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
14
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
15
import { ILogService } from '../../../../platform/log/common/log.js';
16
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
17
import { IChatAgentService } from './chatAgents.js';
18
import { ChatContextKeys } from './chatContextKeys.js';
19
import { ChatModeKind } from './constants.js';
20
import { ICustomChatMode, IPromptsService } from './promptSyntax/service/promptsService.js';
21
22
export const IChatModeService = createDecorator<IChatModeService>('chatModeService');
23
export interface IChatModeService {
24
readonly _serviceBrand: undefined;
25
26
// TODO expose an observable list of modes
27
onDidChangeChatModes: Event<void>;
28
getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] };
29
findModeById(id: string): IChatMode | undefined;
30
findModeByName(name: string): IChatMode | undefined;
31
}
32
33
export class ChatModeService extends Disposable implements IChatModeService {
34
declare readonly _serviceBrand: undefined;
35
36
private static readonly CUSTOM_MODES_STORAGE_KEY = 'chat.customModes';
37
38
private readonly hasCustomModes: IContextKey<boolean>;
39
private readonly _customModeInstances = new Map<string, CustomChatMode>();
40
41
private readonly _onDidChangeChatModes = new Emitter<void>();
42
public readonly onDidChangeChatModes = this._onDidChangeChatModes.event;
43
44
constructor(
45
@IPromptsService private readonly promptsService: IPromptsService,
46
@IChatAgentService private readonly chatAgentService: IChatAgentService,
47
@IContextKeyService contextKeyService: IContextKeyService,
48
@ILogService private readonly logService: ILogService,
49
@IStorageService private readonly storageService: IStorageService
50
) {
51
super();
52
53
this.hasCustomModes = ChatContextKeys.Modes.hasCustomChatModes.bindTo(contextKeyService);
54
55
// Load cached modes from storage first
56
this.loadCachedModes();
57
58
void this.refreshCustomPromptModes(true);
59
this._register(this.promptsService.onDidChangeCustomChatModes(() => {
60
void this.refreshCustomPromptModes(true);
61
}));
62
this._register(this.storageService.onWillSaveState(() => this.saveCachedModes()));
63
64
// Ideally we can get rid of the setting to disable agent mode?
65
let didHaveToolsAgent = this.chatAgentService.hasToolsAgent;
66
this._register(this.chatAgentService.onDidChangeAgents(() => {
67
if (didHaveToolsAgent !== this.chatAgentService.hasToolsAgent) {
68
didHaveToolsAgent = this.chatAgentService.hasToolsAgent;
69
this._onDidChangeChatModes.fire();
70
}
71
}));
72
}
73
74
private loadCachedModes(): void {
75
try {
76
const cachedCustomModes = this.storageService.getObject(ChatModeService.CUSTOM_MODES_STORAGE_KEY, StorageScope.WORKSPACE);
77
if (cachedCustomModes) {
78
this.deserializeCachedModes(cachedCustomModes);
79
}
80
} catch (error) {
81
this.logService.error(error, 'Failed to load cached custom chat modes');
82
}
83
}
84
85
private deserializeCachedModes(cachedCustomModes: any): void {
86
if (!Array.isArray(cachedCustomModes)) {
87
this.logService.error('Invalid cached custom modes data: expected array');
88
return;
89
}
90
91
for (const cachedMode of cachedCustomModes) {
92
if (isCachedChatModeData(cachedMode) && cachedMode.uri) {
93
try {
94
const uri = URI.revive(cachedMode.uri);
95
const customChatMode: ICustomChatMode = {
96
uri,
97
name: cachedMode.name,
98
description: cachedMode.description,
99
tools: cachedMode.customTools,
100
model: cachedMode.model,
101
body: cachedMode.body || '',
102
variableReferences: cachedMode.variableReferences || [],
103
};
104
const instance = new CustomChatMode(customChatMode);
105
this._customModeInstances.set(uri.toString(), instance);
106
} catch (error) {
107
this.logService.error(error, 'Failed to create custom chat mode instance from cached data');
108
}
109
}
110
}
111
112
this.hasCustomModes.set(this._customModeInstances.size > 0);
113
}
114
115
private saveCachedModes(): void {
116
try {
117
const modesToCache = Array.from(this._customModeInstances.values());
118
this.storageService.store(ChatModeService.CUSTOM_MODES_STORAGE_KEY, modesToCache, StorageScope.WORKSPACE, StorageTarget.MACHINE);
119
} catch (error) {
120
this.logService.warn('Failed to save cached custom chat modes', error);
121
}
122
}
123
124
private async refreshCustomPromptModes(fireChangeEvent?: boolean): Promise<void> {
125
try {
126
const customModes = await this.promptsService.getCustomChatModes(CancellationToken.None);
127
128
// Create a new set of mode instances, reusing existing ones where possible
129
const seenUris = new Set<string>();
130
131
for (const customMode of customModes) {
132
const uriString = customMode.uri.toString();
133
seenUris.add(uriString);
134
135
let modeInstance = this._customModeInstances.get(uriString);
136
if (modeInstance) {
137
// Update existing instance with new data
138
modeInstance.updateData(customMode);
139
} else {
140
// Create new instance
141
modeInstance = new CustomChatMode(customMode);
142
this._customModeInstances.set(uriString, modeInstance);
143
}
144
}
145
146
// Clean up instances for modes that no longer exist
147
for (const [uriString] of this._customModeInstances.entries()) {
148
if (!seenUris.has(uriString)) {
149
this._customModeInstances.delete(uriString);
150
}
151
}
152
153
this.hasCustomModes.set(this._customModeInstances.size > 0);
154
} catch (error) {
155
this.logService.error(error, 'Failed to load custom chat modes');
156
this._customModeInstances.clear();
157
this.hasCustomModes.set(false);
158
}
159
if (fireChangeEvent) {
160
this._onDidChangeChatModes.fire();
161
}
162
}
163
164
getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } {
165
return {
166
builtin: this.getBuiltinModes(),
167
custom: this.getCustomModes(),
168
};
169
}
170
171
findModeById(id: string | ChatModeKind): IChatMode | undefined {
172
return this.getBuiltinModes().find(mode => mode.id === id) ?? this.getCustomModes().find(mode => mode.id === id);
173
}
174
175
findModeByName(name: string): IChatMode | undefined {
176
return this.getBuiltinModes().find(mode => mode.name === name) ?? this.getCustomModes().find(mode => mode.name === name);
177
}
178
179
private getBuiltinModes(): IChatMode[] {
180
const builtinModes: IChatMode[] = [
181
ChatMode.Ask,
182
];
183
184
if (this.chatAgentService.hasToolsAgent) {
185
builtinModes.unshift(ChatMode.Agent);
186
}
187
builtinModes.push(ChatMode.Edit);
188
return builtinModes;
189
}
190
191
private getCustomModes(): IChatMode[] {
192
return this.chatAgentService.hasToolsAgent ? Array.from(this._customModeInstances.values()) : [];
193
}
194
}
195
196
export interface IChatModeData {
197
readonly id: string;
198
readonly name: string;
199
readonly description?: string;
200
readonly kind: ChatModeKind;
201
readonly customTools?: readonly string[];
202
readonly model?: string;
203
readonly body?: string;
204
readonly variableReferences?: readonly IVariableReference[];
205
readonly uri?: URI;
206
}
207
208
export interface IChatMode {
209
readonly id: string;
210
readonly name: string;
211
readonly label: string;
212
readonly description: IObservable<string | undefined>;
213
readonly isBuiltin: boolean;
214
readonly kind: ChatModeKind;
215
readonly customTools?: IObservable<readonly string[] | undefined>;
216
readonly model?: IObservable<string | undefined>;
217
readonly body?: IObservable<string>;
218
readonly variableReferences?: IObservable<readonly IVariableReference[]>;
219
readonly uri?: IObservable<URI>;
220
}
221
222
export interface IVariableReference {
223
readonly name: string;
224
readonly range: IOffsetRange;
225
}
226
227
function isCachedChatModeData(data: unknown): data is IChatModeData {
228
if (typeof data !== 'object' || data === null) {
229
return false;
230
}
231
232
const mode = data as any;
233
return typeof mode.id === 'string' &&
234
typeof mode.name === 'string' &&
235
typeof mode.kind === 'string' &&
236
(mode.description === undefined || typeof mode.description === 'string') &&
237
(mode.customTools === undefined || Array.isArray(mode.customTools)) &&
238
(mode.body === undefined || typeof mode.body === 'string') &&
239
(mode.variableReferences === undefined || Array.isArray(mode.variableReferences)) &&
240
(mode.model === undefined || typeof mode.model === 'string') &&
241
(mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null));
242
}
243
244
export class CustomChatMode implements IChatMode {
245
private readonly _descriptionObservable: ISettableObservable<string | undefined>;
246
private readonly _customToolsObservable: ISettableObservable<readonly string[] | undefined>;
247
private readonly _bodyObservable: ISettableObservable<string>;
248
private readonly _variableReferencesObservable: ISettableObservable<readonly IVariableReference[]>;
249
private readonly _uriObservable: ISettableObservable<URI>;
250
private readonly _modelObservable: ISettableObservable<string | undefined>;
251
252
public readonly id: string;
253
public readonly name: string;
254
255
get description(): IObservable<string | undefined> {
256
return this._descriptionObservable;
257
}
258
259
public get isBuiltin(): boolean {
260
return isBuiltinChatMode(this);
261
}
262
263
get customTools(): IObservable<readonly string[] | undefined> {
264
return this._customToolsObservable;
265
}
266
267
get model(): IObservable<string | undefined> {
268
return this._modelObservable;
269
}
270
271
get body(): IObservable<string> {
272
return this._bodyObservable;
273
}
274
275
get variableReferences(): IObservable<readonly IVariableReference[]> {
276
return this._variableReferencesObservable;
277
}
278
279
get uri(): IObservable<URI> {
280
return this._uriObservable;
281
}
282
283
get label(): string {
284
return this.name;
285
}
286
287
public readonly kind = ChatModeKind.Agent;
288
289
constructor(
290
customChatMode: ICustomChatMode
291
) {
292
this.id = customChatMode.uri.toString();
293
this.name = customChatMode.name;
294
this._descriptionObservable = observableValue('description', customChatMode.description);
295
this._customToolsObservable = observableValue('customTools', customChatMode.tools);
296
this._modelObservable = observableValue('model', customChatMode.model);
297
this._bodyObservable = observableValue('body', customChatMode.body);
298
this._variableReferencesObservable = observableValue('variableReferences', customChatMode.variableReferences);
299
this._uriObservable = observableValue('uri', customChatMode.uri);
300
}
301
302
/**
303
* Updates the underlying data and triggers observable changes
304
*/
305
updateData(newData: ICustomChatMode): void {
306
transaction(tx => {
307
// Note- name is derived from ID, it can't change
308
this._descriptionObservable.set(newData.description, tx);
309
this._customToolsObservable.set(newData.tools, tx);
310
this._modelObservable.set(newData.model, tx);
311
this._bodyObservable.set(newData.body, tx);
312
this._variableReferencesObservable.set(newData.variableReferences, tx);
313
this._uriObservable.set(newData.uri, tx);
314
});
315
}
316
317
toJSON(): IChatModeData {
318
return {
319
id: this.id,
320
name: this.name,
321
description: this.description.get(),
322
kind: this.kind,
323
customTools: this.customTools.get(),
324
model: this.model.get(),
325
body: this.body.get(),
326
variableReferences: this.variableReferences.get(),
327
uri: this.uri.get()
328
};
329
}
330
}
331
332
export class BuiltinChatMode implements IChatMode {
333
public readonly description: IObservable<string>;
334
335
constructor(
336
public readonly kind: ChatModeKind,
337
public readonly label: string,
338
description: string
339
) {
340
this.description = observableValue('description', description);
341
}
342
343
public get isBuiltin(): boolean {
344
return isBuiltinChatMode(this);
345
}
346
347
get id(): string {
348
// Need a differentiator?
349
return this.kind;
350
}
351
352
get name(): string {
353
return this.kind;
354
}
355
356
/**
357
* Getters are not json-stringified
358
*/
359
toJSON(): IChatModeData {
360
return {
361
id: this.id,
362
name: this.name,
363
description: this.description.get(),
364
kind: this.kind
365
};
366
}
367
}
368
369
export namespace ChatMode {
370
export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Ask a question."));
371
export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit files."));
372
export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Provide instructions."));
373
}
374
375
export function isBuiltinChatMode(mode: IChatMode): boolean {
376
return mode.id === ChatMode.Ask.id ||
377
mode.id === ChatMode.Edit.id ||
378
mode.id === ChatMode.Agent.id;
379
}
380
381