Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b1b49eb
feat: support prompt file select custom chat mode
Jul 17, 2025
46d39c8
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 17, 2025
8e438d5
feat: support prompt file edit suggestions
Jul 18, 2025
f3da5dd
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 18, 2025
7d6c5d2
chore: use custom chat mode name instead of uri as id
Jul 18, 2025
36cbe6a
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 18, 2025
563bc78
chore: resolve conflict
Jul 20, 2025
ea7d3f3
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 20, 2025
c1c54e8
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 21, 2025
68f737b
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 21, 2025
88da88c
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 21, 2025
ae4e017
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 21, 2025
147c8f4
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 21, 2025
d0f70ce
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 22, 2025
2a3312e
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 22, 2025
b40b216
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 23, 2025
8b75cde
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 23, 2025
ee6d8be
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 23, 2025
f9dce98
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 24, 2025
9f8c5b3
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 24, 2025
8923379
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 25, 2025
50ac79c
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 25, 2025
adb850b
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 26, 2025
4234ed5
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 27, 2025
6befa8b
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 28, 2025
e705695
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 28, 2025
cdc7654
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 28, 2025
51e42a7
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 28, 2025
8948e31
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 29, 2025
5d4cc29
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 29, 2025
33b9772
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 30, 2025
3d60162
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 30, 2025
f9dca4a
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 31, 2025
dfde7ff
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 31, 2025
90c8052
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Jul 31, 2025
52a6a67
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Aug 4, 2025
de72816
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Aug 5, 2025
da6da63
Merge branch 'main' into feat/prompt-file-suggestion
SoloJiang Aug 6, 2025
fc965dd
fixes
aeschli Aug 6, 2025
5ab5102
Merge branch 'main' into aeschli/customModesInPrompts
aeschli Aug 13, 2025
73c0047
add IChatMode.label, use IChatMode.name for reference in prompt file
aeschli Aug 13, 2025
c651ec4
fix tests
aeschli Aug 14, 2025
3868c2a
message fix
aeschli Aug 14, 2025
1429547
improve mode hover
aeschli Aug 14, 2025
3183399
prompt file: do to definition for custom mode
aeschli Aug 14, 2025
fda501a
prompt snippet: also show custom modes
aeschli Aug 14, 2025
402a333
apply mode to chat input
aeschli Aug 14, 2025
5e19f37
Merge branch 'main' into aeschli/customModesInPrompts
aeschli Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/vs/workbench/contrib/chat/browser/actions/chatActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const CHAT_CONFIG_MENU_ID = new MenuId('workbench.chat.menu.config');
const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog';

abstract class OpenChatGlobalAction extends Action2 {
constructor(overrides: Pick<ICommandPaletteOptions, 'keybinding' | 'title' | 'id' | 'menu'>, private readonly mode?: ChatModeKind) {
constructor(overrides: Pick<ICommandPaletteOptions, 'keybinding' | 'title' | 'id' | 'menu'>, private readonly mode?: IChatMode) {
super({
...overrides,
icon: Codicon.copilot,
Expand Down Expand Up @@ -160,8 +160,7 @@ abstract class OpenChatGlobalAction extends Action2 {
return;
}

const switchToModeInput = opts?.mode ?? this.mode;
const switchToMode = switchToModeInput && (chatModeService.findModeById(switchToModeInput) ?? chatModeService.findModeByName(switchToModeInput));
const switchToMode = (opts?.mode ? chatModeService.findModeByName(opts?.mode) : undefined) ?? this.mode;
if (switchToMode) {
await this.handleSwitchToMode(switchToMode, chatWidget, instaService, commandService);
}
Expand Down Expand Up @@ -275,9 +274,9 @@ abstract class ModeOpenChatGlobalAction extends OpenChatGlobalAction {
constructor(mode: IChatMode, keybinding?: ICommandPaletteOptions['keybinding']) {
super({
id: getOpenChatActionIdForMode(mode),
title: localize2('openChatMode', "Open Chat ({0})", mode.name),
title: localize2('openChatMode', "Open Chat ({0})", mode.label),
keybinding
}, mode.kind);
}, mode);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class ConfigureToolsAction extends Action2 {
break;
case ToolsScope.Mode:
placeholder = localize('chat.tools.placeholder.mode', "Select tools for this chat mode");
description = localize('chat.tools.description.mode', "The selected tools are configured by the '{0}' chat mode. Changes to the tools will be applied to the mode file as well.", widget.input.currentModeObs.get().name);
description = localize('chat.tools.description.mode', "The selected tools are configured by the '{0}' chat mode. Changes to the tools will be applied to the mode file as well.", widget.input.currentModeObs.get().label);
break;
case ToolsScope.Global:
placeholder = localize('chat.tools.placeholder.global', "Select tools that are available to chat.");
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/browser/chatInputPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.initSelectedModel();

this._register(this.onDidChangeCurrentChatMode(() => {
this.accessibilityService.alert(this._currentModeObservable.get().name);
this.accessibilityService.alert(this._currentModeObservable.get().label);
if (this._inputEditor) {
this._inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() });
}
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export class ChatSelectedTools extends Disposable {

const currentMode = this._mode.read(r);

let currentMap = this._sessionStates.get(currentMode.id);
let currentMap = this._sessionStates.observable.read(r).get(currentMode.id);
const modeTools = currentMode.customTools?.read(r);
if (!currentMap && currentMode.kind === ChatModeKind.Agent && modeTools) {
currentMap = this._toolsService.toToolAndToolSetEnablementMap(modeTools);
Expand Down
28 changes: 19 additions & 9 deletions src/vs/workbench/contrib/chat/browser/chatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { ChatViewModel, IChatRequestViewModel, IChatResponseViewModel, isRequest
import { IChatInputState } from '../common/chatWidgetHistoryService.js';
import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../common/constants.js';
import { IChatModeService } from '../common/chatModes.js';
import { ILanguageModelToolsService, IToolData, ToolSet } from '../common/languageModelToolsService.js';
import { type TPromptMetadata } from '../common/promptSyntax/parsers/promptHeader/promptHeader.js';
import { IPromptParserResult, IPromptsService } from '../common/promptSyntax/service/promptsService.js';
Expand Down Expand Up @@ -305,7 +306,8 @@ export class ChatWidget extends Disposable implements IChatWidget {
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IPromptsService private readonly promptsService: IPromptsService,
@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IChatModeService private readonly chatModeService: IChatModeService
) {
super();
this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService);
Expand Down Expand Up @@ -2095,16 +2097,24 @@ export class ChatWidget extends Disposable implements IChatWidget {

const { mode, tools, model } = metadata;

const currentMode = this.input.currentModeObs.get();

// switch to appropriate chat mode if needed
if (mode && mode !== this.input.currentModeKind) {
const chatModeCheck = await this.instantiationService.invokeFunction(handleModeSwitch, this.input.currentModeKind, mode, this.viewModel?.model.getRequests().length ?? 0, this.viewModel?.model.editingSession);
if (!chatModeCheck) {
return undefined;
} else if (chatModeCheck.needToClearSession) {
this.clear();
await this.waitForReady();
if (mode && mode !== currentMode.name) {
// Find the mode object to get its kind
const chatMode = this.chatModeService.findModeByName(mode);
if (chatMode) {
if (currentMode.kind !== chatMode.kind) {
const chatModeCheck = await this.instantiationService.invokeFunction(handleModeSwitch, currentMode.kind, chatMode.kind, this.viewModel?.model.getRequests().length ?? 0, this.viewModel?.model.editingSession);
if (!chatModeCheck) {
return undefined;
} else if (chatModeCheck.needToClearSession) {
this.clear();
await this.waitForReady();
}
}
this.input.setChatMode(chatMode.id);
}
this.input.setChatMode(mode);
}

// if not tools to enable are present, we are done
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
const makeAction = (mode: IChatMode, currentMode: IChatMode): IActionWidgetDropdownAction => ({
...action,
id: getOpenChatActionIdForMode(mode),
label: mode.name,
label: mode.label,
class: undefined,
enabled: true,
checked: currentMode.id === mode.id,
Expand All @@ -56,7 +56,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
const makeActionFromCustomMode = (mode: IChatMode, currentMode: IChatMode): IActionWidgetDropdownAction => ({
...action,
id: getOpenChatActionIdForMode(mode),
label: mode.name,
label: mode.label,
class: undefined,
enabled: true,
checked: currentMode.id === mode.id,
Expand Down Expand Up @@ -111,7 +111,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
return null;
}
this.setAriaLabelAttributes(element);
const state = this.delegate.currentMode.get().name;
const state = this.delegate.currentMode.get().label;
dom.reset(element, dom.$('span.chat-model-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`));
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { CHAT_CATEGORY } from '../actions/chatActions.js';
import { askForPromptFileName } from './pickers/askForPromptName.js';
import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js';
import { IChatModeService } from '../../common/chatModes.js';


class AbstractNewPromptFileAction extends Action2 {
Expand Down Expand Up @@ -57,6 +58,7 @@ class AbstractNewPromptFileAction extends Action2 {
const editorService = accessor.get(IEditorService);
const fileService = accessor.get(IFileService);
const instaService = accessor.get(IInstantiationService);
const chatModeService = accessor.get(IChatModeService);

const selectedFolder = await instaService.invokeFunction(askForPromptSourceFolder, this.type);
if (!selectedFolder) {
Expand All @@ -81,7 +83,7 @@ class AbstractNewPromptFileAction extends Action2 {
if (editor && editor.hasModel() && isEqual(editor.getModel().uri, promptUri)) {
SnippetController2.get(editor)?.apply([{
range: editor.getModel().getFullModelRange(),
template: getDefaultContentSnippet(this.type),
template: this.getDefaultContentSnippet(this.type, chatModeService),
}]);
}

Expand Down Expand Up @@ -137,37 +139,41 @@ class AbstractNewPromptFileAction extends Action2 {
},
);
}
}

function getDefaultContentSnippet(promptType: PromptsType): string {
switch (promptType) {
case PromptsType.prompt:
return [
`---`,
`mode: \${1|ask,edit,agent|}`,
`---`,
`\${2:Define the task to achieve, including specific requirements, constraints, and success criteria.}`,
].join('\n');
case PromptsType.instructions:
return [
`---`,
`applyTo: '\${1|**,**/*.ts|}'`,
`---`,
`\${2:Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`,
].join('\n');
case PromptsType.mode:
return [
`---`,
`description: '\${1:Description of the custom chat mode.}'`,
`tools: []`,
`---`,
`\${2:Define the purpose of this chat mode and how AI should behave: response style, available tools, focus areas, and any mode-specific instructions or constraints.}`,
].join('\n');
default:
throw new Error(`Unknown prompt type: ${promptType}`);
private getDefaultContentSnippet(promptType: PromptsType, chatModeService: IChatModeService): string {
const modes = chatModeService.getModes();
const modeNames = modes.builtin.map(mode => mode.name).join(',') + (modes.custom.length ? (',' + modes.custom.map(mode => mode.name).join(',')) : '');
switch (promptType) {
case PromptsType.prompt:
return [
`---`,
`mode: \${1|${modeNames}|}`,
`---`,
`\${2:Define the task to achieve, including specific requirements, constraints, and success criteria.}`,
].join('\n');
case PromptsType.instructions:
return [
`---`,
`applyTo: '\${1|**,**/*.ts|}'`,
`---`,
`\${2:Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.}`,
].join('\n');
case PromptsType.mode:
return [
`---`,
`description: '\${1:Description of the custom chat mode.}'`,
`tools: []`,
`---`,
`\${2:Define the purpose of this chat mode and how AI should behave: response style, available tools, focus areas, and any mode-specific instructions or constraints.}`,
].join('\n');
default:
throw new Error(`Unknown prompt type: ${promptType}`);
}
}
}



export const NEW_PROMPT_COMMAND_ID = 'workbench.command.new.prompt';
export const NEW_INSTRUCTIONS_COMMAND_ID = 'workbench.command.new.instructions';
export const NEW_MODE_COMMAND_ID = 'workbench.command.new.mode';
Expand Down
30 changes: 17 additions & 13 deletions src/vs/workbench/contrib/chat/common/chatModes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,25 +162,16 @@ export class ChatModeService extends Disposable implements IChatModeService {
getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } {
return {
builtin: this.getBuiltinModes(),
custom: this.chatAgentService.hasToolsAgent ?
Array.from(this._customModeInstances.values()) :
[]
custom: this.getCustomModes(),
};
}

private getFlatModes(): IChatMode[] {
const allModes = this.getModes();
return [...allModes.builtin, ...allModes.custom];
}

findModeById(id: string | ChatModeKind): IChatMode | undefined {
const allModes = this.getFlatModes();
return allModes.find(mode => mode.id === id);
return this.getBuiltinModes().find(mode => mode.id === id) ?? this.getCustomModes().find(mode => mode.id === id);
}

findModeByName(name: string): IChatMode | undefined {
const allModes = this.getFlatModes();
return allModes.find(mode => mode.name === name);
return this.getBuiltinModes().find(mode => mode.name === name) ?? this.getCustomModes().find(mode => mode.name === name);
}

private getBuiltinModes(): IChatMode[] {
Expand All @@ -194,6 +185,10 @@ export class ChatModeService extends Disposable implements IChatModeService {
builtinModes.push(ChatMode.Edit);
return builtinModes;
}

private getCustomModes(): IChatMode[] {
return this.chatAgentService.hasToolsAgent ? Array.from(this._customModeInstances.values()) : [];
}
}

export interface IChatModeData {
Expand All @@ -210,6 +205,7 @@ export interface IChatModeData {
export interface IChatMode {
readonly id: string;
readonly name: string;
readonly label: string;
readonly description: IObservable<string | undefined>;
readonly isBuiltin: boolean;
readonly kind: ChatModeKind;
Expand Down Expand Up @@ -270,6 +266,10 @@ export class CustomChatMode implements IChatMode {
return this._uriObservable;
}

get label(): string {
return this.name;
}

public readonly kind = ChatModeKind.Agent;

constructor(
Expand Down Expand Up @@ -317,7 +317,7 @@ export class BuiltinChatMode implements IChatMode {

constructor(
public readonly kind: ChatModeKind,
public readonly name: string,
public readonly label: string,
description: string
) {
this.description = observableValue('description', description);
Expand All @@ -332,6 +332,10 @@ export class BuiltinChatMode implements IChatMode {
return this.kind;
}

get name(): string {
return this.kind;
}

/**
* Getters are not json-stringified
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from '../../../../../../base/common/cancellation.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { Position } from '../../../../../../editor/common/core/position.js';
import { Range } from '../../../../../../editor/common/core/range.js';
import { Definition, DefinitionProvider } from '../../../../../../editor/common/languages.js';
import { ITextModel } from '../../../../../../editor/common/model.js';
import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js';
import { IChatModeService } from '../../chatModes.js';
import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId } from '../promptTypes.js';
import { IPromptsService } from '../service/promptsService.js';
import { PromptModeMetadata } from '../parsers/promptHeader/metadata/mode.js';
import { PromptHeader } from '../parsers/promptHeader/promptHeader.js';

export class PromptHeaderDefinitionProvider extends Disposable implements DefinitionProvider {
/**
* Debug display name for this provider.
*/
public readonly _debugDisplayName: string = 'PromptHeaderHoverProvider';

constructor(
@IPromptsService private readonly promptsService: IPromptsService,
@ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService,
@IChatModeService private readonly chatModeService: IChatModeService,
) {
super();

this._register(this.languageService.definitionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this));
}
async provideDefinition(model: ITextModel, position: Position, token: CancellationToken): Promise<Definition | undefined> {
const promptType = getPromptsTypeForLanguageId(model.getLanguageId());
if (!promptType) {
// if the model is not a prompt, we don't provide any completions
return undefined;
}

const parser = this.promptsService.getSyntaxParserFor(model);
await parser.start(token).settled();

if (token.isCancellationRequested) {
return undefined;
}

const header = parser.header;
if (!header) {
return undefined;
}

const completed = await header.settled;
if (!completed || token.isCancellationRequested) {
return undefined;
}

if (header instanceof PromptHeader) {
const mode = header.metadataUtility.mode;
if (mode?.range.containsPosition(position)) {
return this.getModeDefinition(mode, position);
}
}
return undefined;
}


private getModeDefinition(mode: PromptModeMetadata, position: Position): Definition | undefined {
const value = mode.value;
if (value && mode.valueRange?.containsPosition(position)) {
const mode = this.chatModeService.findModeByName(value);
if (mode && mode.uri) {
return {
uri: mode.uri.get(),
range: new Range(1, 1, 1, 1)
};
}
}
return undefined;
}

}
Loading
Loading