Path: blob/main/src/vs/workbench/contrib/chat/common/chatModes.ts
5251 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 { constObservable, 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 { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';13import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';14import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';15import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';16import { ILogService } from '../../../../platform/log/common/log.js';17import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';18import { IChatAgentService } from './participants/chatAgents.js';19import { ChatContextKeys } from './actions/chatContextKeys.js';20import { ChatConfiguration, ChatModeKind } from './constants.js';21import { IHandOff, isTarget } from './promptSyntax/promptFileParser.js';22import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, ICustomAgentVisibility, IPromptsService, isCustomAgentVisibility, PromptsStorage, Target } from './promptSyntax/service/promptsService.js';23import { ThemeIcon } from '../../../../base/common/themables.js';24import { Codicon } from '../../../../base/common/codicons.js';25import { isString } from '../../../../base/common/types.js';2627export const IChatModeService = createDecorator<IChatModeService>('chatModeService');28export interface IChatModeService {29readonly _serviceBrand: undefined;3031// TODO expose an observable list of modes32readonly onDidChangeChatModes: Event<void>;33getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] };34findModeById(id: string): IChatMode | undefined;35findModeByName(name: string): IChatMode | undefined;36}3738export class ChatModeService extends Disposable implements IChatModeService {39declare readonly _serviceBrand: undefined;4041private static readonly CUSTOM_MODES_STORAGE_KEY = 'chat.customModes';4243private readonly hasCustomModes: IContextKey<boolean>;44private readonly agentModeDisabledByPolicy: IContextKey<boolean>;45private readonly _customModeInstances = new Map<string, CustomChatMode>();4647private readonly _onDidChangeChatModes = this._register(new Emitter<void>());48public readonly onDidChangeChatModes = this._onDidChangeChatModes.event;4950constructor(51@IPromptsService private readonly promptsService: IPromptsService,52@IChatAgentService private readonly chatAgentService: IChatAgentService,53@IContextKeyService contextKeyService: IContextKeyService,54@ILogService private readonly logService: ILogService,55@IStorageService private readonly storageService: IStorageService,56@IConfigurationService private readonly configurationService: IConfigurationService57) {58super();5960this.hasCustomModes = ChatContextKeys.Modes.hasCustomChatModes.bindTo(contextKeyService);61this.agentModeDisabledByPolicy = ChatContextKeys.Modes.agentModeDisabledByPolicy.bindTo(contextKeyService);6263// Initialize the policy context key64this.updateAgentModePolicyContextKey();6566// Load cached modes from storage first67this.loadCachedModes();6869void this.refreshCustomPromptModes(true);70this._register(this.promptsService.onDidChangeCustomAgents(() => {71void this.refreshCustomPromptModes(true);72}));73this._register(this.storageService.onWillSaveState(() => this.saveCachedModes()));7475// Listen for configuration changes that affect agent mode policy76this._register(this.configurationService.onDidChangeConfiguration(e => {77if (e.affectsConfiguration(ChatConfiguration.AgentEnabled)) {78this.updateAgentModePolicyContextKey();79this._onDidChangeChatModes.fire();80}81}));8283// Ideally we can get rid of the setting to disable agent mode?84let didHaveToolsAgent = this.chatAgentService.hasToolsAgent;85this._register(this.chatAgentService.onDidChangeAgents(() => {86if (didHaveToolsAgent !== this.chatAgentService.hasToolsAgent) {87didHaveToolsAgent = this.chatAgentService.hasToolsAgent;88this._onDidChangeChatModes.fire();89}90}));91}9293private loadCachedModes(): void {94try {95const cachedCustomModes = this.storageService.getObject(ChatModeService.CUSTOM_MODES_STORAGE_KEY, StorageScope.WORKSPACE);96if (cachedCustomModes) {97this.deserializeCachedModes(cachedCustomModes);98}99} catch (error) {100this.logService.error(error, 'Failed to load cached custom agents');101}102}103104private deserializeCachedModes(cachedCustomModes: unknown): void {105if (!Array.isArray(cachedCustomModes)) {106this.logService.error('Invalid cached custom modes data: expected array');107return;108}109110for (const cachedMode of cachedCustomModes) {111if (isCachedChatModeData(cachedMode) && cachedMode.uri) {112try {113const uri = URI.revive(cachedMode.uri);114const customChatMode: ICustomAgent = {115uri,116name: cachedMode.name,117description: cachedMode.description,118tools: cachedMode.customTools,119model: isString(cachedMode.model) ? [cachedMode.model] : cachedMode.model,120argumentHint: cachedMode.argumentHint,121agentInstructions: cachedMode.modeInstructions ?? { content: cachedMode.body ?? '', toolReferences: [] },122handOffs: cachedMode.handOffs,123target: cachedMode.target ?? Target.Undefined,124visibility: cachedMode.visibility ?? { userInvokable: true, agentInvokable: cachedMode.infer !== false },125agents: cachedMode.agents,126source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local }127};128const instance = new CustomChatMode(customChatMode);129this._customModeInstances.set(uri.toString(), instance);130} catch (error) {131this.logService.error(error, 'Failed to revive cached custom agent');132}133}134}135136this.hasCustomModes.set(this._customModeInstances.size > 0);137}138139private saveCachedModes(): void {140try {141const modesToCache = Array.from(this._customModeInstances.values());142this.storageService.store(ChatModeService.CUSTOM_MODES_STORAGE_KEY, modesToCache, StorageScope.WORKSPACE, StorageTarget.MACHINE);143} catch (error) {144this.logService.warn('Failed to save cached custom agents', error);145}146}147148private async refreshCustomPromptModes(fireChangeEvent?: boolean): Promise<void> {149try {150const customModes = await this.promptsService.getCustomAgents(CancellationToken.None);151152// Create a new set of mode instances, reusing existing ones where possible153const seenUris = new Set<string>();154155for (const customMode of customModes) {156if (!customMode.visibility.userInvokable) {157continue;158}159160const uriString = customMode.uri.toString();161seenUris.add(uriString);162163let modeInstance = this._customModeInstances.get(uriString);164if (modeInstance) {165// Update existing instance with new data166modeInstance.updateData(customMode);167} else {168// Create new instance169modeInstance = new CustomChatMode(customMode);170this._customModeInstances.set(uriString, modeInstance);171}172}173174// Clean up instances for modes that no longer exist175for (const [uriString] of this._customModeInstances.entries()) {176if (!seenUris.has(uriString)) {177this._customModeInstances.delete(uriString);178}179}180181this.hasCustomModes.set(this._customModeInstances.size > 0);182} catch (error) {183this.logService.error(error, 'Failed to load custom agents');184this._customModeInstances.clear();185this.hasCustomModes.set(false);186}187if (fireChangeEvent) {188this._onDidChangeChatModes.fire();189}190}191192getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } {193return {194builtin: this.getBuiltinModes(),195custom: this.getCustomModes(),196};197}198199findModeById(id: string | ChatModeKind): IChatMode | undefined {200return this.getBuiltinModes().find(mode => mode.id === id) ?? this._customModeInstances.get(id);201}202203findModeByName(name: string): IChatMode | undefined {204return this.getBuiltinModes().find(mode => mode.name.get() === name) ?? this.getCustomModes().find(mode => mode.name.get() === name);205}206207private getBuiltinModes(): IChatMode[] {208const builtinModes: IChatMode[] = [209ChatMode.Ask,210];211212// Include Agent mode if:213// - It's enabled (hasToolsAgent is true), OR214// - It's disabled by policy (so we can show it with a lock icon)215// But hide it if the user manually disabled it via settings216if (this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy()) {217builtinModes.unshift(ChatMode.Agent);218}219builtinModes.push(ChatMode.Edit);220return builtinModes;221}222223private getCustomModes(): IChatMode[] {224// Show custom modes when agent mode is enabled OR when disabled by policy (to show them in the policy-managed group)225return this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy() ? Array.from(this._customModeInstances.values()) : [];226}227228private updateAgentModePolicyContextKey(): void {229this.agentModeDisabledByPolicy.set(this.isAgentModeDisabledByPolicy());230}231232private isAgentModeDisabledByPolicy(): boolean {233return this.configurationService.inspect<boolean>(ChatConfiguration.AgentEnabled).policyValue === false;234}235}236237export interface IChatModeData {238readonly id: string;239readonly name: string;240readonly description?: string;241readonly kind: ChatModeKind;242readonly customTools?: readonly string[];243readonly model?: readonly string[] | string;244readonly argumentHint?: string;245readonly modeInstructions?: IChatModeInstructions;246readonly body?: string; /* deprecated */247readonly handOffs?: readonly IHandOff[];248readonly uri?: URI;249readonly source?: IChatModeSourceData;250readonly target?: Target;251readonly visibility?: ICustomAgentVisibility;252readonly agents?: readonly string[];253readonly infer?: boolean; // deprecated, only available in old cached data254}255256export interface IChatMode {257readonly id: string;258readonly name: IObservable<string>;259readonly label: IObservable<string>;260readonly icon: IObservable<ThemeIcon | undefined>;261readonly description: IObservable<string | undefined>;262readonly isBuiltin: boolean;263readonly kind: ChatModeKind;264readonly customTools?: IObservable<readonly string[] | undefined>;265readonly handOffs?: IObservable<readonly IHandOff[] | undefined>;266readonly model?: IObservable<readonly string[] | undefined>;267readonly argumentHint?: IObservable<string | undefined>;268readonly modeInstructions?: IObservable<IChatModeInstructions>;269readonly uri?: IObservable<URI>;270readonly source?: IAgentSource;271readonly target: IObservable<Target>;272readonly visibility?: IObservable<ICustomAgentVisibility | undefined>;273readonly agents?: IObservable<readonly string[] | undefined>;274}275276export interface IVariableReference {277readonly name: string;278readonly range: IOffsetRange;279}280281export interface IChatModeInstructions {282readonly content: string;283readonly toolReferences: readonly IVariableReference[];284readonly metadata?: Record<string, boolean | string | number>;285}286287function isCachedChatModeData(data: unknown): data is IChatModeData {288if (typeof data !== 'object' || data === null) {289return false;290}291292const mode = data as IChatModeData;293return typeof mode.id === 'string' &&294typeof mode.name === 'string' &&295typeof mode.kind === 'string' &&296(mode.description === undefined || typeof mode.description === 'string') &&297(mode.customTools === undefined || Array.isArray(mode.customTools)) &&298(mode.modeInstructions === undefined || (typeof mode.modeInstructions === 'object' && mode.modeInstructions !== null)) &&299(mode.model === undefined || typeof mode.model === 'string' || Array.isArray(mode.model)) &&300(mode.argumentHint === undefined || typeof mode.argumentHint === 'string') &&301(mode.handOffs === undefined || Array.isArray(mode.handOffs)) &&302(mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)) &&303(mode.source === undefined || isChatModeSourceData(mode.source)) &&304(mode.target === undefined || isTarget(mode.target)) &&305(mode.visibility === undefined || isCustomAgentVisibility(mode.visibility)) &&306(mode.agents === undefined || Array.isArray(mode.agents));307}308309export class CustomChatMode implements IChatMode {310private readonly _nameObservable: ISettableObservable<string>;311private readonly _descriptionObservable: ISettableObservable<string | undefined>;312private readonly _customToolsObservable: ISettableObservable<readonly string[] | undefined>;313private readonly _modeInstructions: ISettableObservable<IChatModeInstructions>;314private readonly _uriObservable: ISettableObservable<URI>;315private readonly _modelObservable: ISettableObservable<readonly string[] | undefined>;316private readonly _argumentHintObservable: ISettableObservable<string | undefined>;317private readonly _handoffsObservable: ISettableObservable<readonly IHandOff[] | undefined>;318private readonly _targetObservable: ISettableObservable<Target>;319private readonly _visibilityObservable: ISettableObservable<ICustomAgentVisibility | undefined>;320private readonly _agentsObservable: ISettableObservable<readonly string[] | undefined>;321private _source: IAgentSource;322323public readonly id: string;324325get name(): IObservable<string> {326return this._nameObservable;327}328329get description(): IObservable<string | undefined> {330return this._descriptionObservable;331}332333get icon(): IObservable<ThemeIcon | undefined> {334return constObservable(undefined);335}336337public get isBuiltin(): boolean {338return isBuiltinChatMode(this);339}340341get customTools(): IObservable<readonly string[] | undefined> {342return this._customToolsObservable;343}344345get model(): IObservable<readonly string[] | undefined> {346return this._modelObservable;347}348349get argumentHint(): IObservable<string | undefined> {350return this._argumentHintObservable;351}352353get modeInstructions(): IObservable<IChatModeInstructions> {354return this._modeInstructions;355}356357get uri(): IObservable<URI> {358return this._uriObservable;359}360361get label(): IObservable<string> {362return this.name;363}364365get handOffs(): IObservable<readonly IHandOff[] | undefined> {366return this._handoffsObservable;367}368369get source(): IAgentSource {370return this._source;371}372373get target(): IObservable<Target> {374return this._targetObservable;375}376377get visibility(): IObservable<ICustomAgentVisibility | undefined> {378return this._visibilityObservable;379}380381get agents(): IObservable<readonly string[] | undefined> {382return this._agentsObservable;383}384385public readonly kind = ChatModeKind.Agent;386387constructor(388customChatMode: ICustomAgent389) {390this.id = customChatMode.uri.toString();391this._nameObservable = observableValue('name', customChatMode.name);392this._descriptionObservable = observableValue('description', customChatMode.description);393this._customToolsObservable = observableValue('customTools', customChatMode.tools);394this._modelObservable = observableValue('model', customChatMode.model);395this._argumentHintObservable = observableValue('argumentHint', customChatMode.argumentHint);396this._handoffsObservable = observableValue('handOffs', customChatMode.handOffs);397this._targetObservable = observableValue('target', customChatMode.target);398this._visibilityObservable = observableValue('visibility', customChatMode.visibility);399this._agentsObservable = observableValue('agents', customChatMode.agents);400this._modeInstructions = observableValue('_modeInstructions', customChatMode.agentInstructions);401this._uriObservable = observableValue('uri', customChatMode.uri);402this._source = customChatMode.source;403}404405/**406* Updates the underlying data and triggers observable changes407*/408updateData(newData: ICustomAgent): void {409transaction(tx => {410this._nameObservable.set(newData.name, tx);411this._descriptionObservable.set(newData.description, tx);412this._customToolsObservable.set(newData.tools, tx);413this._modelObservable.set(newData.model, tx);414this._argumentHintObservable.set(newData.argumentHint, tx);415this._handoffsObservable.set(newData.handOffs, tx);416this._targetObservable.set(newData.target, tx);417this._visibilityObservable.set(newData.visibility, tx);418this._agentsObservable.set(newData.agents, tx);419this._modeInstructions.set(newData.agentInstructions, tx);420this._uriObservable.set(newData.uri, tx);421this._source = newData.source;422});423}424425toJSON(): IChatModeData {426return {427id: this.id,428name: this.name.get(),429description: this.description.get(),430kind: this.kind,431customTools: this.customTools.get(),432model: this.model.get(),433argumentHint: this.argumentHint.get(),434modeInstructions: this.modeInstructions.get(),435uri: this.uri.get(),436handOffs: this.handOffs.get(),437source: serializeChatModeSource(this._source),438target: this.target.get(),439visibility: this.visibility.get(),440agents: this.agents.get()441};442}443}444445type IChatModeSourceData =446| { readonly storage: PromptsStorage.extension; readonly extensionId: string; type?: ExtensionAgentSourceType }447| { readonly storage: PromptsStorage.local | PromptsStorage.user };448449function isChatModeSourceData(value: unknown): value is IChatModeSourceData {450if (typeof value !== 'object' || value === null) {451return false;452}453const data = value as { storage?: unknown; extensionId?: unknown };454if (data.storage === PromptsStorage.extension) {455return typeof data.extensionId === 'string';456}457return data.storage === PromptsStorage.local || data.storage === PromptsStorage.user;458}459460function serializeChatModeSource(source: IAgentSource | undefined): IChatModeSourceData | undefined {461if (!source) {462return undefined;463}464if (source.storage === PromptsStorage.extension) {465return { storage: PromptsStorage.extension, extensionId: source.extensionId.value, type: source.type };466}467return { storage: source.storage };468}469470function reviveChatModeSource(data: IChatModeSourceData | undefined): IAgentSource | undefined {471if (!data) {472return undefined;473}474if (data.storage === PromptsStorage.extension) {475return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId), type: data.type ?? ExtensionAgentSourceType.contribution };476}477return { storage: data.storage };478}479480export class BuiltinChatMode implements IChatMode {481public readonly name: IObservable<string>;482public readonly label: IObservable<string>;483public readonly description: IObservable<string>;484public readonly icon: IObservable<ThemeIcon>;485public readonly target: IObservable<Target>;486487constructor(488public readonly kind: ChatModeKind,489label: string,490description: string,491icon: ThemeIcon,492) {493this.name = constObservable(kind);494this.label = constObservable(label);495this.description = observableValue('description', description);496this.icon = constObservable(icon);497this.target = constObservable(Target.Undefined);498}499500public get isBuiltin(): boolean {501return isBuiltinChatMode(this);502}503504get id(): string {505// Need a differentiator?506return this.kind;507}508509/**510* Getters are not json-stringified511*/512toJSON(): IChatModeData {513return {514id: this.id,515name: this.name.get(),516description: this.description.get(),517kind: this.kind518};519}520}521522export namespace ChatMode {523export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question);524export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit);525export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"), Codicon.agent);526}527528export function isBuiltinChatMode(mode: IChatMode): boolean {529return mode.id === ChatMode.Ask.id ||530mode.id === ChatMode.Edit.id ||531mode.id === ChatMode.Agent.id;532}533534535