diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 4e6f9cc9b86..6c41f9582fd 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -65,6 +65,7 @@ import { CommandsConverter } from './extHostCommands.js'; import { getPrivateApiFor } from './extHostTestingPrivateApi.js'; import * as types from './extHostTypes.js'; import { LanguageModelDataPart, LanguageModelPromptTsxPart, LanguageModelTextPart } from './extHostTypes.js'; +import { IChatRequestModeInstructions } from '../../contrib/chat/common/chatModel.js'; export namespace Command { @@ -3129,7 +3130,7 @@ export namespace ChatAgentRequest { model, editedFileEvents: request.editedFileEvents, modeInstructions: request.modeInstructions?.content, - modeInstructionsToolReferences: request.modeInstructions?.toolReferences?.map(ChatLanguageModelToolReference.to), + modeInstructions2: ChatRequestModeInstructions.to(request.modeInstructions), }; if (!isProposedApiEnabled(extension, 'chatParticipantPrivate')) { @@ -3259,6 +3260,16 @@ export namespace ChatLanguageModelToolReference { } } +export namespace ChatRequestModeInstructions { + export function to(mode: IChatRequestModeInstructions | undefined): vscode.ChatRequestModeInstructions | undefined { + return mode ? { + content: mode.content, + toolReferences: mode.toolReferences?.map(ref => ChatLanguageModelToolReference.to(ref)) ?? [], + metadata: mode.metadata + } : undefined; + } +} + export namespace ChatAgentCompletionItem { export function from(item: vscode.ChatCompletionItem, commandsConverter: CommandsConverter, disposables: DisposableStore): extHostProtocol.IChatAgentCompletionItem { return { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 1b9259e7430..8b082c56fb6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -315,13 +315,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const mode = this._currentModeObservable.get(); const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; + const modeInstructions = mode.modeInstructions?.get(); return { kind: this.currentModeKind, isBuiltin: mode.isBuiltin, - instructions: { - content: mode.body?.get(), - toolReferences: mode.variableReferences ? this.toolService.toToolReferences(mode.variableReferences.get()) : undefined - }, + modeInstructions: modeInstructions ? { + content: modeInstructions.content, + toolReferences: this.toolService.toToolReferences(modeInstructions.toolReferences), + metadata: modeInstructions.metadata, + } : undefined, modeId: modeId, applyCodeBlockSuggestionId: undefined, }; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index d86965601cb..94ec229bf63 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -215,14 +215,15 @@ const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { re export interface IChatRequestModeInfo { kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply' isBuiltin: boolean; - instructions: IChatRequestModeInstructions | undefined; + modeInstructions: IChatRequestModeInstructions | undefined; modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; applyCodeBlockSuggestionId: EditSuggestionId | undefined; } export interface IChatRequestModeInstructions { - readonly content: string | undefined; - readonly toolReferences: readonly ChatRequestToolReferenceEntry[] | undefined; + readonly content: string; + readonly toolReferences: readonly ChatRequestToolReferenceEntry[]; + readonly metadata?: Record; } export interface IChatRequestModelParameters { diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 40b84d443a1..fba7f476521 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -98,8 +98,7 @@ export class ChatModeService extends Disposable implements IChatModeService { description: cachedMode.description, tools: cachedMode.customTools, model: cachedMode.model, - body: cachedMode.body || '', - variableReferences: cachedMode.variableReferences || [], + modeInstructions: cachedMode.modeInstructions ?? { content: cachedMode.body ?? '', toolReferences: [] }, }; const instance = new CustomChatMode(customChatMode); this._customModeInstances.set(uri.toString(), instance); @@ -200,8 +199,8 @@ export interface IChatModeData { readonly kind: ChatModeKind; readonly customTools?: readonly string[]; readonly model?: string; - readonly body?: string; - readonly variableReferences?: readonly IVariableReference[]; + readonly modeInstructions?: IChatModeInstructions; + readonly body?: string; /* deprecated */ readonly uri?: URI; } @@ -214,8 +213,7 @@ export interface IChatMode { readonly kind: ChatModeKind; readonly customTools?: IObservable; readonly model?: IObservable; - readonly body?: IObservable; - readonly variableReferences?: IObservable; + readonly modeInstructions?: IObservable; readonly uri?: IObservable; } @@ -224,6 +222,12 @@ export interface IVariableReference { readonly range: IOffsetRange; } +export interface IChatModeInstructions { + readonly content: string; + readonly toolReferences: readonly IVariableReference[]; + readonly metadata?: Record; +} + function isCachedChatModeData(data: unknown): data is IChatModeData { if (typeof data !== 'object' || data === null) { return false; @@ -235,8 +239,7 @@ function isCachedChatModeData(data: unknown): data is IChatModeData { typeof mode.kind === 'string' && (mode.description === undefined || typeof mode.description === 'string') && (mode.customTools === undefined || Array.isArray(mode.customTools)) && - (mode.body === undefined || typeof mode.body === 'string') && - (mode.variableReferences === undefined || Array.isArray(mode.variableReferences)) && + (mode.modeInstructions === undefined || (typeof mode.modeInstructions === 'object' && mode.modeInstructions !== null)) && (mode.model === undefined || typeof mode.model === 'string') && (mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)); } @@ -244,8 +247,7 @@ function isCachedChatModeData(data: unknown): data is IChatModeData { export class CustomChatMode implements IChatMode { private readonly _descriptionObservable: ISettableObservable; private readonly _customToolsObservable: ISettableObservable; - private readonly _bodyObservable: ISettableObservable; - private readonly _variableReferencesObservable: ISettableObservable; + private readonly _modeInstructions: ISettableObservable; private readonly _uriObservable: ISettableObservable; private readonly _modelObservable: ISettableObservable; @@ -268,12 +270,8 @@ export class CustomChatMode implements IChatMode { return this._modelObservable; } - get body(): IObservable { - return this._bodyObservable; - } - - get variableReferences(): IObservable { - return this._variableReferencesObservable; + get modeInstructions(): IObservable { + return this._modeInstructions; } get uri(): IObservable { @@ -294,8 +292,7 @@ export class CustomChatMode implements IChatMode { this._descriptionObservable = observableValue('description', customChatMode.description); this._customToolsObservable = observableValue('customTools', customChatMode.tools); this._modelObservable = observableValue('model', customChatMode.model); - this._bodyObservable = observableValue('body', customChatMode.body); - this._variableReferencesObservable = observableValue('variableReferences', customChatMode.variableReferences); + this._modeInstructions = observableValue('_modeInstructions', customChatMode.modeInstructions); this._uriObservable = observableValue('uri', customChatMode.uri); } @@ -308,8 +305,7 @@ export class CustomChatMode implements IChatMode { this._descriptionObservable.set(newData.description, tx); this._customToolsObservable.set(newData.tools, tx); this._modelObservable.set(newData.model, tx); - this._bodyObservable.set(newData.body, tx); - this._variableReferencesObservable.set(newData.variableReferences, tx); + this._modeInstructions.set(newData.modeInstructions, tx); this._uriObservable.set(newData.uri, tx); }); } @@ -322,8 +318,7 @@ export class CustomChatMode implements IChatMode { kind: this.kind, customTools: this.customTools.get(), model: this.model.get(), - body: this.body.get(), - variableReferences: this.variableReferences.get(), + modeInstructions: this.modeInstructions.get(), uri: this.uri.get() }; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 994b6822f9c..242b6c4c32e 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -800,7 +800,7 @@ export class ChatService extends Disposable implements IChatService { rejectedConfirmationData: options?.rejectedConfirmationData, userSelectedModelId: options?.userSelectedModelId, userSelectedTools: options?.userSelectedTools?.get(), - modeInstructions: options?.modeInfo?.instructions, + modeInstructions: options?.modeInfo?.modeInstructions, editedFileEvents: request.editedFileEvents } satisfies IChatAgentRequest; }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 61bbbcc7754..25f340c6c4c 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -18,6 +18,7 @@ import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId, PromptsType import { IPromptsService } from '../service/promptsService.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; import { PromptHeader } from '../service/newPromptsParser.js'; +import { getValidAttributeNames, isExperimentalAttribute } from '../service/promptValidator.js'; export class PromptHeaderAutocompletion extends Disposable implements CompletionItemProvider { /** @@ -91,7 +92,7 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion ): Promise { const suggestions: CompletionItem[] = []; - const supportedProperties = this.getSupportedProperties(promptType); + const supportedProperties = new Set(getValidAttributeNames(promptType)); this.removeUsedProperties(supportedProperties, model, headerRange, position); const getInsertText = (property: string): string => { @@ -133,7 +134,7 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion const lineContent = model.getLineContent(position.lineNumber); const property = lineContent.substring(0, colonPosition.column - 1).trim(); - if (!this.getSupportedProperties(promptType).has(property)) { + if (!getValidAttributeNames(promptType).includes(property) || isExperimentalAttribute(property)) { return undefined; } @@ -166,17 +167,6 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion return { suggestions }; } - private getSupportedProperties(promptType: string): Set { - switch (promptType) { - case PromptsType.instructions: - return new Set(['applyTo', 'description']); - case PromptsType.prompt: - return new Set(['mode', 'tools', 'description', 'model']); - default: - return new Set(['tools', 'description', 'model']); - } - } - private removeUsedProperties(properties: Set, model: ITextModel, headerRange: Range, position: Position): void { for (let i = headerRange.startLineNumber; i <= headerRange.endLineNumber; i++) { if (i !== position.lineNumber) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptValidator.ts index ead5fc60b43..c7c82efc3cd 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptValidator.ts @@ -17,13 +17,14 @@ import { ChatModeKind } from '../../constants.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; -import { IHeaderAttribute, ParsedPromptFile } from './newPromptsParser.js'; +import { IArrayValue, IHeaderAttribute, ParsedPromptFile } from './newPromptsParser.js'; import { PromptsConfig } from '../config/config.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; import { IPromptsService } from './promptsService.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; @@ -33,6 +34,7 @@ export class PromptValidator { @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @IChatModeService private readonly chatModeService: IChatModeService, @IFileService private readonly fileService: IFileService, + @ILabelService private readonly labelService: ILabelService ) { } public async validate(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { @@ -58,12 +60,13 @@ export class PromptValidator { fileReferenceChecks.push((async () => { try { const exists = await this.fileService.exists(resolved); - if (!exists) { - report(toMarker(localize('promptValidator.fileNotFound', "File '{0}' not found.", ref.content), ref.range, MarkerSeverity.Warning)); + if (exists) { + return; } } catch { - report(toMarker(localize('promptValidator.fileNotFound', "File '{0}' not found.", ref.content), ref.range, MarkerSeverity.Warning)); } + const loc = this.labelService.getUriLabel(resolved); + report(toMarker(localize('promptValidator.fileNotFound', "File '{0}' not found at '{1}'.", ref.content, loc), ref.range, MarkerSeverity.Warning)); })()); } @@ -91,13 +94,13 @@ export class PromptValidator { if (!validAttributeNames.includes(attribute.key)) { switch (promptType) { case PromptsType.prompt: - report(toMarker(localize('promptValidator.unknownAttribute.prompt', "Attribute '{0}' is not supported in prompt files. Supported: {1}", attribute.key, validAttributeNames.join(', ')), attribute.range, MarkerSeverity.Warning)); + report(toMarker(localize('promptValidator.unknownAttribute.prompt', "Attribute '{0}' is not supported in prompt files. Supported: {1}.", attribute.key, validAttributeNames.join(', ')), attribute.range, MarkerSeverity.Warning)); break; case PromptsType.mode: - report(toMarker(localize('promptValidator.unknownAttribute.mode', "Attribute '{0}' is not supported in mode files. Supported: {1}", attribute.key, validAttributeNames.join(', ')), attribute.range, MarkerSeverity.Warning)); + report(toMarker(localize('promptValidator.unknownAttribute.mode', "Attribute '{0}' is not supported in mode files. Supported: {1}.", attribute.key, validAttributeNames.join(', ')), attribute.range, MarkerSeverity.Warning)); break; case PromptsType.instructions: - report(toMarker(localize('promptValidator.unknownAttribute.instructions', "Attribute '{0}' is not supported in instructions files. Supported: {1}", attribute.key, validAttributeNames.join(', ')), attribute.range, MarkerSeverity.Warning)); + report(toMarker(localize('promptValidator.unknownAttribute.instructions', "Attribute '{0}' is not supported in instructions files. Supported: {1}.", attribute.key, validAttributeNames.join(', ')), attribute.range, MarkerSeverity.Warning)); break; } } @@ -215,16 +218,24 @@ export class PromptValidator { } if (modeKind !== ChatModeKind.Agent) { report(toMarker(localize('promptValidator.toolsOnlyInAgent', "The 'tools' attribute is only supported in agent mode. Attribute will be ignored."), attribute.range, MarkerSeverity.Warning)); - - } - if (attribute.value.type !== 'array') { - report(toMarker(localize('promptValidator.toolsMustBeArray', "The 'tools' attribute must be an array."), attribute.value.range, MarkerSeverity.Error)); - return; } - if (attribute.value.items.length > 0) { + switch (attribute.value.type) { + case 'array': + this.validateToolsArray(attribute.value, report); + break; + case 'object': + //this.validateToolsObject(attribute.value, report); + break; + default: + report(toMarker(localize('promptValidator.toolsMustBeArrayOrMap', "The 'tools' attribute must be an array."), attribute.value.range, MarkerSeverity.Error)); + } + } + + private validateToolsArray(valueItem: IArrayValue, report: (markers: IMarkerData) => void) { + if (valueItem.items.length > 0) { const available = this.getAvailableToolAndToolSetNames(); - for (const item of attribute.value.items) { + for (const item of valueItem.items) { if (item.type !== 'string') { report(toMarker(localize('promptValidator.eachToolMustBeString', "Each tool name in the 'tools' attribute must be a string."), item.range, MarkerSeverity.Error)); } else if (item.value && !available.has(item.value)) { @@ -279,17 +290,21 @@ export class PromptValidator { } } -function getValidAttributeNames(promptType: PromptsType): string[] { +export function getValidAttributeNames(promptType: PromptsType): string[] { switch (promptType) { case PromptsType.prompt: return ['description', 'model', 'tools', 'mode']; case PromptsType.instructions: return ['description', 'applyTo']; case PromptsType.mode: - return ['description', 'model', 'tools']; + return ['description', 'model', 'tools', 'advancedOptions']; } } +export function isExperimentalAttribute(attributeName: string): boolean { + return attributeName === 'advancedOptions'; +} + function toMarker(message: string, range: Range, severity = MarkerSeverity.Error): IMarkerData { return { severity, message, ...range }; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index f1eef78e834..945eb7fa5f6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -12,7 +12,7 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js import { PromptsType } from '../promptTypes.js'; import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; import { YamlNode, YamlParseError } from '../../../../../../base/common/yaml.js'; -import { IVariableReference } from '../../chatModes.js'; +import { IChatModeInstructions } from '../../chatModes.js'; import { ParsedPromptFile } from './newPromptsParser.js'; /** @@ -74,14 +74,9 @@ export interface ICustomChatMode { readonly model?: string; /** - * Contents of the custom chat mode file body. + * Contents of the custom chat mode file body and other mode instructions. */ - readonly body: string; - - /** - * References to variables without a type in the mode body. These could be tools or toolsets. - */ - readonly variableReferences: readonly IVariableReference[]; + readonly modeInstructions: IChatModeInstructions; } /** diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 50032a655c1..0bbef549c15 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -27,7 +27,7 @@ import { IFileService } from '../../../../../../platform/files/common/files.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { OffsetRange } from '../../../../../../editor/common/core/ranges/offsetRange.js'; -import { IVariableReference } from '../../chatModes.js'; +import { IChatModeInstructions, IVariableReference } from '../../chatModes.js'; /** * Provides prompt services. @@ -204,25 +204,41 @@ export class PromptsService extends Disposable implements IPromptsService { modeFiles.map(async ({ uri }): Promise => { const ast = await this.parseNew(uri, token); - const variableReferences: IVariableReference[] = []; - let body = ''; + let metadata: any | undefined; + if (ast.header) { + const advanced = ast.header.getAttribute('advancedOptions'); + if (advanced && advanced.value.type === 'object') { + metadata = {}; + for (const [key, value] of Object.entries(advanced.value)) { + if (['string', 'number', 'boolean'].includes(value.type)) { + metadata[key] = value; + } + } + } + } + const toolReferences: IVariableReference[] = []; if (ast.body) { const bodyOffset = ast.body.offset; const bodyVarRefs = ast.body.variableReferences; for (let i = bodyVarRefs.length - 1; i >= 0; i--) { // in reverse order const { name, offset } = bodyVarRefs[i]; const range = new OffsetRange(offset - bodyOffset, offset - bodyOffset + name.length + 1); - variableReferences.push({ name, range }); + toolReferences.push({ name, range }); } - body = ast.body.getContent(); } + const modeInstructions = { + content: ast.body?.getContent() ?? '', + toolReferences, + metadata, + } satisfies IChatModeInstructions; + const name = getCleanPromptName(uri); if (!ast.header) { - return { uri, name, body, variableReferences }; + return { uri, name, modeInstructions }; } const { description, model, tools } = ast.header; - return { uri, name, description, model, tools, body, variableReferences }; + return { uri, name, description, model, tools, modeInstructions }; }) ); diff --git a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts index 09d2629d456..9b17fbdd988 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts @@ -110,8 +110,8 @@ suite('ChatModeService', () => { name: 'Test Mode', description: 'A test custom mode', tools: ['tool1', 'tool2'], - body: 'Custom mode body', - variableReferences: [] + modeInstructions: { content: 'Custom mode body', toolReferences: [] } + }; promptsService.setCustomModes([customMode]); @@ -129,7 +129,7 @@ suite('ChatModeService', () => { assert.strictEqual(testMode.description.get(), customMode.description); assert.strictEqual(testMode.kind, ChatModeKind.Agent); assert.deepStrictEqual(testMode.customTools?.get(), customMode.tools); - assert.strictEqual(testMode.body?.get(), customMode.body); + assert.deepStrictEqual(testMode.modeInstructions?.get(), customMode.modeInstructions); assert.strictEqual(testMode.uri?.get().toString(), customMode.uri.toString()); }); @@ -144,8 +144,7 @@ suite('ChatModeService', () => { name: 'Test Mode', description: 'A test custom mode', tools: [], - body: 'Custom mode body', - variableReferences: [] + modeInstructions: { content: 'Custom mode body', toolReferences: [] }, }; promptsService.setCustomModes([customMode]); @@ -162,8 +161,7 @@ suite('ChatModeService', () => { name: 'Findable Mode', description: 'A findable custom mode', tools: [], - body: 'Findable mode body', - variableReferences: [] + modeInstructions: { content: 'Findable mode body', toolReferences: [] }, }; promptsService.setCustomModes([customMode]); @@ -185,9 +183,8 @@ suite('ChatModeService', () => { name: 'Initial Mode', description: 'Initial description', tools: ['tool1'], - body: 'Initial body', + modeInstructions: { content: 'Initial body', toolReferences: [] }, model: 'gpt-4', - variableReferences: [] }; promptsService.setCustomModes([initialMode]); @@ -202,7 +199,7 @@ suite('ChatModeService', () => { ...initialMode, description: 'Updated description', tools: ['tool1', 'tool2'], - body: 'Updated body', + modeInstructions: { content: 'Updated body', toolReferences: [] }, model: 'Updated model' }; @@ -218,7 +215,7 @@ suite('ChatModeService', () => { // But the observable properties should be updated assert.strictEqual(updatedCustomMode.description.get(), 'Updated description'); assert.deepStrictEqual(updatedCustomMode.customTools?.get(), ['tool1', 'tool2']); - assert.strictEqual(updatedCustomMode.body?.get(), 'Updated body'); + assert.deepStrictEqual(updatedCustomMode.modeInstructions?.get(), { content: 'Updated body', toolReferences: [] }); assert.strictEqual(updatedCustomMode.model?.get(), 'Updated model'); }); @@ -228,8 +225,7 @@ suite('ChatModeService', () => { name: 'Mode 1', description: 'First mode', tools: [], - body: 'Mode 1 body', - variableReferences: [] + modeInstructions: { content: 'Mode 1 body', toolReferences: [] }, }; const mode2: ICustomChatMode = { @@ -237,8 +233,7 @@ suite('ChatModeService', () => { name: 'Mode 2', description: 'Second mode', tools: [], - body: 'Mode 2 body', - variableReferences: [] + modeInstructions: { content: 'Mode 2 body', toolReferences: [] }, }; // Add both modes diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptValidator.test.ts index 397ec504af4..dbdf73dbea4 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptValidator.test.ts @@ -24,6 +24,7 @@ import { IMarkerData, MarkerSeverity } from '../../../../../../../platform/marke import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; +import { ILabelService } from '../../../../../../../platform/label/common/label.js'; suite('PromptValidator', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -38,6 +39,8 @@ suite('PromptValidator', () => { instaService.stub(IConfigurationService, testConfigService); + instaService.stub(ILabelService, { getUriLabel: (resource) => resource.path }); + const testTool1 = { id: 'testTool1', displayName: 'tool1', canBeReferencedInPrompt: true, modelDescription: 'Test Tool 1', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; @@ -59,7 +62,11 @@ suite('PromptValidator', () => { } }); - const customChatMode = new CustomChatMode({ uri: URI.parse('myFs://test/test/chatmode.md'), name: 'BeastMode', body: '', variableReferences: [] }); + const customChatMode = new CustomChatMode({ + uri: URI.parse('myFs://test/test/chatmode.md'), + name: 'BeastMode', + modeInstructions: { content: 'Beast mode instructions', toolReferences: [] }, + }); instaService.stub(IChatModeService, new MockChatModeService({ builtin: [ChatMode.Agent, ChatMode.Ask, ChatMode.Edit], custom: [customChatMode] })); @@ -306,8 +313,8 @@ suite('PromptValidator', () => { const markers = await validate(content, PromptsType.prompt); const messages = markers.map(m => m.message).sort(); assert.deepStrictEqual(messages, [ - "File './missing1.md' not found.", - "File './missing2.md' not found." + "File './missing1.md' not found at '/missing1.md'.", + "File './missing2.md' not found at '/missing2.md'." ]); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index c204e53feb8..74e8ef2fdf2 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -784,15 +784,24 @@ suite('PromptsService', () => { name: 'mode1', description: 'Mode file 1.', tools: ['tool1', 'tool2'], - body: 'Do it with #tool1', - variableReferences: [{ name: 'tool1', range: { start: 11, endExclusive: 17 } }], + modeInstructions: { + content: 'Do it with #tool1', + toolReferences: [{ name: 'tool1', range: { start: 11, endExclusive: 17 } }], + metadata: undefined + }, model: undefined, uri: URI.joinPath(rootFolderUri, '.github/chatmodes/mode1.instructions.md'), }, { name: 'mode2', - body: 'First use #tool2\nThen use #tool1', - variableReferences: [{ name: 'tool1', range: { start: 26, endExclusive: 32 } }, { name: 'tool2', range: { start: 10, endExclusive: 16 } }], + modeInstructions: { + content: 'First use #tool2\nThen use #tool1', + toolReferences: [ + { name: 'tool1', range: { start: 26, endExclusive: 32 } }, + { name: 'tool2', range: { start: 10, endExclusive: 16 } } + ], + metadata: undefined + }, uri: URI.joinPath(rootFolderUri, '.github/chatmodes/mode2.instructions.md'), } ]; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 242d58be227..e427ede853b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -1547,7 +1547,7 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, { kind: undefined, modeId: 'applyCodeBlock', - instructions: undefined, + modeInstructions: undefined, isBuiltin: true, applyCodeBlockSuggestionId, }); diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index 2cb4168b43a..73c8c948cc9 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -646,7 +646,13 @@ declare module 'vscode' { } export interface ChatRequest { - modeInstructions?: string; - modeInstructionsToolReferences?: readonly ChatLanguageModelToolReference[]; + readonly modeInstructions?: string; + readonly modeInstructions2?: ChatRequestModeInstructions; + } + + export interface ChatRequestModeInstructions { + readonly content: string; + readonly toolReferences?: readonly ChatLanguageModelToolReference[]; + readonly metadata?: Record; } }