Path: blob/main/src/vs/workbench/contrib/chat/common/chatModes.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../../base/common/cancellation.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { Disposable } from '../../../../base/common/lifecycle.js';8import { IObservable, ISettableObservable, observableValue, transaction } from '../../../../base/common/observable.js';9import { URI } from '../../../../base/common/uri.js';10import { IOffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';11import { localize } from '../../../../nls.js';12import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';13import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';14import { ILogService } from '../../../../platform/log/common/log.js';15import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';16import { IChatAgentService } from './chatAgents.js';17import { ChatContextKeys } from './chatContextKeys.js';18import { ChatModeKind } from './constants.js';19import { ICustomChatMode, IPromptsService } from './promptSyntax/service/promptsService.js';2021export const IChatModeService = createDecorator<IChatModeService>('chatModeService');22export interface IChatModeService {23readonly _serviceBrand: undefined;2425// TODO expose an observable list of modes26onDidChangeChatModes: Event<void>;27getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] };28findModeById(id: string): IChatMode | undefined;29findModeByName(name: string): IChatMode | undefined;30}3132export class ChatModeService extends Disposable implements IChatModeService {33declare readonly _serviceBrand: undefined;3435private static readonly CUSTOM_MODES_STORAGE_KEY = 'chat.customModes';3637private readonly hasCustomModes: IContextKey<boolean>;38private readonly _customModeInstances = new Map<string, CustomChatMode>();3940private readonly _onDidChangeChatModes = new Emitter<void>();41public readonly onDidChangeChatModes = this._onDidChangeChatModes.event;4243constructor(44@IPromptsService private readonly promptsService: IPromptsService,45@IChatAgentService private readonly chatAgentService: IChatAgentService,46@IContextKeyService contextKeyService: IContextKeyService,47@ILogService private readonly logService: ILogService,48@IStorageService private readonly storageService: IStorageService49) {50super();5152this.hasCustomModes = ChatContextKeys.Modes.hasCustomChatModes.bindTo(contextKeyService);5354// Load cached modes from storage first55this.loadCachedModes();5657void this.refreshCustomPromptModes(true);58this._register(this.promptsService.onDidChangeCustomChatModes(() => {59void this.refreshCustomPromptModes(true);60}));61this._register(this.storageService.onWillSaveState(() => this.saveCachedModes()));6263// Ideally we can get rid of the setting to disable agent mode?64let didHaveToolsAgent = this.chatAgentService.hasToolsAgent;65this._register(this.chatAgentService.onDidChangeAgents(() => {66if (didHaveToolsAgent !== this.chatAgentService.hasToolsAgent) {67didHaveToolsAgent = this.chatAgentService.hasToolsAgent;68this._onDidChangeChatModes.fire();69}70}));71}7273private loadCachedModes(): void {74try {75const cachedCustomModes = this.storageService.getObject(ChatModeService.CUSTOM_MODES_STORAGE_KEY, StorageScope.WORKSPACE);76if (cachedCustomModes) {77this.deserializeCachedModes(cachedCustomModes);78}79} catch (error) {80this.logService.error(error, 'Failed to load cached custom chat modes');81}82}8384private deserializeCachedModes(cachedCustomModes: any): void {85if (!Array.isArray(cachedCustomModes)) {86this.logService.error('Invalid cached custom modes data: expected array');87return;88}8990for (const cachedMode of cachedCustomModes) {91if (isCachedChatModeData(cachedMode) && cachedMode.uri) {92try {93const uri = URI.revive(cachedMode.uri);94const customChatMode: ICustomChatMode = {95uri,96name: cachedMode.name,97description: cachedMode.description,98tools: cachedMode.customTools,99model: cachedMode.model,100body: cachedMode.body || '',101variableReferences: cachedMode.variableReferences || [],102};103const instance = new CustomChatMode(customChatMode);104this._customModeInstances.set(uri.toString(), instance);105} catch (error) {106this.logService.error(error, 'Failed to create custom chat mode instance from cached data');107}108}109}110111this.hasCustomModes.set(this._customModeInstances.size > 0);112}113114private saveCachedModes(): void {115try {116const modesToCache = Array.from(this._customModeInstances.values());117this.storageService.store(ChatModeService.CUSTOM_MODES_STORAGE_KEY, modesToCache, StorageScope.WORKSPACE, StorageTarget.MACHINE);118} catch (error) {119this.logService.warn('Failed to save cached custom chat modes', error);120}121}122123private async refreshCustomPromptModes(fireChangeEvent?: boolean): Promise<void> {124try {125const customModes = await this.promptsService.getCustomChatModes(CancellationToken.None);126127// Create a new set of mode instances, reusing existing ones where possible128const seenUris = new Set<string>();129130for (const customMode of customModes) {131const uriString = customMode.uri.toString();132seenUris.add(uriString);133134let modeInstance = this._customModeInstances.get(uriString);135if (modeInstance) {136// Update existing instance with new data137modeInstance.updateData(customMode);138} else {139// Create new instance140modeInstance = new CustomChatMode(customMode);141this._customModeInstances.set(uriString, modeInstance);142}143}144145// Clean up instances for modes that no longer exist146for (const [uriString] of this._customModeInstances.entries()) {147if (!seenUris.has(uriString)) {148this._customModeInstances.delete(uriString);149}150}151152this.hasCustomModes.set(this._customModeInstances.size > 0);153} catch (error) {154this.logService.error(error, 'Failed to load custom chat modes');155this._customModeInstances.clear();156this.hasCustomModes.set(false);157}158if (fireChangeEvent) {159this._onDidChangeChatModes.fire();160}161}162163getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } {164return {165builtin: this.getBuiltinModes(),166custom: this.getCustomModes(),167};168}169170findModeById(id: string | ChatModeKind): IChatMode | undefined {171return this.getBuiltinModes().find(mode => mode.id === id) ?? this.getCustomModes().find(mode => mode.id === id);172}173174findModeByName(name: string): IChatMode | undefined {175return this.getBuiltinModes().find(mode => mode.name === name) ?? this.getCustomModes().find(mode => mode.name === name);176}177178private getBuiltinModes(): IChatMode[] {179const builtinModes: IChatMode[] = [180ChatMode.Ask,181];182183if (this.chatAgentService.hasToolsAgent) {184builtinModes.unshift(ChatMode.Agent);185}186builtinModes.push(ChatMode.Edit);187return builtinModes;188}189190private getCustomModes(): IChatMode[] {191return this.chatAgentService.hasToolsAgent ? Array.from(this._customModeInstances.values()) : [];192}193}194195export interface IChatModeData {196readonly id: string;197readonly name: string;198readonly description?: string;199readonly kind: ChatModeKind;200readonly customTools?: readonly string[];201readonly model?: string;202readonly body?: string;203readonly variableReferences?: readonly IVariableReference[];204readonly uri?: URI;205}206207export interface IChatMode {208readonly id: string;209readonly name: string;210readonly label: string;211readonly description: IObservable<string | undefined>;212readonly isBuiltin: boolean;213readonly kind: ChatModeKind;214readonly customTools?: IObservable<readonly string[] | undefined>;215readonly model?: IObservable<string | undefined>;216readonly body?: IObservable<string>;217readonly variableReferences?: IObservable<readonly IVariableReference[]>;218readonly uri?: IObservable<URI>;219}220221export interface IVariableReference {222readonly name: string;223readonly range: IOffsetRange;224}225226function isCachedChatModeData(data: unknown): data is IChatModeData {227if (typeof data !== 'object' || data === null) {228return false;229}230231const mode = data as any;232return typeof mode.id === 'string' &&233typeof mode.name === 'string' &&234typeof mode.kind === 'string' &&235(mode.description === undefined || typeof mode.description === 'string') &&236(mode.customTools === undefined || Array.isArray(mode.customTools)) &&237(mode.body === undefined || typeof mode.body === 'string') &&238(mode.variableReferences === undefined || Array.isArray(mode.variableReferences)) &&239(mode.model === undefined || typeof mode.model === 'string') &&240(mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null));241}242243export class CustomChatMode implements IChatMode {244private readonly _descriptionObservable: ISettableObservable<string | undefined>;245private readonly _customToolsObservable: ISettableObservable<readonly string[] | undefined>;246private readonly _bodyObservable: ISettableObservable<string>;247private readonly _variableReferencesObservable: ISettableObservable<readonly IVariableReference[]>;248private readonly _uriObservable: ISettableObservable<URI>;249private readonly _modelObservable: ISettableObservable<string | undefined>;250251public readonly id: string;252public readonly name: string;253254get description(): IObservable<string | undefined> {255return this._descriptionObservable;256}257258public get isBuiltin(): boolean {259return isBuiltinChatMode(this);260}261262get customTools(): IObservable<readonly string[] | undefined> {263return this._customToolsObservable;264}265266get model(): IObservable<string | undefined> {267return this._modelObservable;268}269270get body(): IObservable<string> {271return this._bodyObservable;272}273274get variableReferences(): IObservable<readonly IVariableReference[]> {275return this._variableReferencesObservable;276}277278get uri(): IObservable<URI> {279return this._uriObservable;280}281282get label(): string {283return this.name;284}285286public readonly kind = ChatModeKind.Agent;287288constructor(289customChatMode: ICustomChatMode290) {291this.id = customChatMode.uri.toString();292this.name = customChatMode.name;293this._descriptionObservable = observableValue('description', customChatMode.description);294this._customToolsObservable = observableValue('customTools', customChatMode.tools);295this._modelObservable = observableValue('model', customChatMode.model);296this._bodyObservable = observableValue('body', customChatMode.body);297this._variableReferencesObservable = observableValue('variableReferences', customChatMode.variableReferences);298this._uriObservable = observableValue('uri', customChatMode.uri);299}300301/**302* Updates the underlying data and triggers observable changes303*/304updateData(newData: ICustomChatMode): void {305transaction(tx => {306// Note- name is derived from ID, it can't change307this._descriptionObservable.set(newData.description, tx);308this._customToolsObservable.set(newData.tools, tx);309this._modelObservable.set(newData.model, tx);310this._bodyObservable.set(newData.body, tx);311this._variableReferencesObservable.set(newData.variableReferences, tx);312this._uriObservable.set(newData.uri, tx);313});314}315316toJSON(): IChatModeData {317return {318id: this.id,319name: this.name,320description: this.description.get(),321kind: this.kind,322customTools: this.customTools.get(),323model: this.model.get(),324body: this.body.get(),325variableReferences: this.variableReferences.get(),326uri: this.uri.get()327};328}329}330331export class BuiltinChatMode implements IChatMode {332public readonly description: IObservable<string>;333334constructor(335public readonly kind: ChatModeKind,336public readonly label: string,337description: string338) {339this.description = observableValue('description', description);340}341342public get isBuiltin(): boolean {343return isBuiltinChatMode(this);344}345346get id(): string {347// Need a differentiator?348return this.kind;349}350351get name(): string {352return this.kind;353}354355/**356* Getters are not json-stringified357*/358toJSON(): IChatModeData {359return {360id: this.id,361name: this.name,362description: this.description.get(),363kind: this.kind364};365}366}367368export namespace ChatMode {369export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Ask a question."));370export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit files."));371export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Provide instructions."));372}373374export function isBuiltinChatMode(mode: IChatMode): boolean {375return mode.id === ChatMode.Ask.id ||376mode.id === ChatMode.Edit.id ||377mode.id === ChatMode.Agent.id;378}379380381