Skip to content

Commit 267e6d1

Browse files
authored
Custom chat mode improvements (microsoft#252884)
* Proper observables in service * Persist mode data * Mode observables for rendering * Handle observable more consistently * Rename to ChatModeKind * More "kind" renames * Add unit test for the service * Fire event less * Fix throwing from deserialize method * Revert this change * Cleanup '2' names
1 parent 92ab7e4 commit 267e6d1

File tree

58 files changed

+811
-443
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+811
-443
lines changed

src/vs/workbench/api/browser/mainThreadChatAgents2.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../con
3030
import { ChatRequestAgentPart } from '../../contrib/chat/common/chatParserTypes.js';
3131
import { ChatRequestParser } from '../../contrib/chat/common/chatRequestParser.js';
3232
import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatService, IChatTask, IChatTaskSerialized, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
33-
import { ChatAgentLocation, ChatMode } from '../../contrib/chat/common/constants.js';
33+
import { ChatAgentLocation, ChatModeKind } from '../../contrib/chat/common/constants.js';
3434
import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js';
3535
import { IExtensionService } from '../../services/extensions/common/extensions.js';
3636
import { Dto } from '../../services/extensions/common/proxyIdentifier.js';
@@ -148,7 +148,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
148148

149149
const inputValue = widget?.inputEditor.getValue() ?? '';
150150
const location = widget.location;
151-
const mode = widget.input.currentMode;
151+
const mode = widget.input.currentModeKind;
152152
this._chatService.transferChatSession({ sessionId, inputValue, location, mode }, URI.revive(toWorkspace));
153153
}
154154

@@ -209,7 +209,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
209209
slashCommands: [],
210210
disambiguation: [],
211211
locations: [ChatAgentLocation.Panel], // TODO all dynamic participants are panel only?
212-
modes: [ChatMode.Ask]
212+
modes: [ChatModeKind.Ask]
213213
},
214214
impl);
215215
} else {

src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ import { IKeybindingService } from '../../../../../platform/keybinding/common/ke
1515
import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';
1616
import { INLINE_CHAT_ID } from '../../../inlineChat/common/inlineChat.js';
1717
import { ChatContextKeyExprs, ChatContextKeys } from '../../common/chatContextKeys.js';
18-
import { ChatAgentLocation, ChatMode } from '../../common/constants.js';
18+
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
1919
import { IChatWidgetService } from '../chat.js';
2020

2121
export class PanelChatAccessibilityHelp implements IAccessibleViewImplementation {
2222
readonly priority = 107;
2323
readonly name = 'panelChat';
2424
readonly type = AccessibleViewType.Help;
25-
readonly when = ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.inQuickChat.negate(), ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask), ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest));
25+
readonly when = ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.inQuickChat.negate(), ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask), ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest));
2626
getProvider(accessor: ServicesAccessor) {
2727
const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();
2828
return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'panelChat');
@@ -55,7 +55,7 @@ export class AgentChatAccessibilityHelp implements IAccessibleViewImplementation
5555
readonly priority = 120;
5656
readonly name = 'agentView';
5757
readonly type = AccessibleViewType.Help;
58-
readonly when = ContextKeyExpr.and(ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent), ChatContextKeys.inChatInput);
58+
readonly when = ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ChatContextKeys.inChatInput);
5959
getProvider(accessor: ServicesAccessor) {
6060
const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();
6161
return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'agentView');

src/vs/workbench/contrib/chat/browser/actions/chatActions.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@ import { IChatAgentService } from '../../common/chatAgents.js';
5050
import { ChatContextKeys } from '../../common/chatContextKeys.js';
5151
import { IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js';
5252
import { ChatEntitlement, IChatEntitlementService } from '../../common/chatEntitlementService.js';
53+
import { ChatMode, IChatMode } from '../../common/chatModes.js';
5354
import { extractAgentAndCommand } from '../../common/chatParserTypes.js';
5455
import { IChatDetail, IChatService } from '../../common/chatService.js';
5556
import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js';
5657
import { IChatWidgetHistoryService } from '../../common/chatWidgetHistoryService.js';
57-
import { ChatAgentLocation, ChatConfiguration, ChatMode, modeToString, validateChatMode } from '../../common/constants.js';
58+
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../../common/constants.js';
5859
import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js';
5960
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
6061
import { ChatViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js';
@@ -97,7 +98,7 @@ export interface IChatViewOpenOptions {
9798
/**
9899
* The mode to open the chat in.
99100
*/
100-
mode?: ChatMode;
101+
mode?: ChatModeKind;
101102
}
102103

103104
export interface IChatViewOpenRequestEntry {
@@ -110,7 +111,7 @@ export const CHAT_CONFIG_MENU_ID = new MenuId('workbench.chat.menu.config');
110111
const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog';
111112

112113
abstract class OpenChatGlobalAction extends Action2 {
113-
constructor(overrides: Pick<ICommandPaletteOptions, 'keybinding' | 'title' | 'id' | 'menu'>, private readonly mode?: ChatMode) {
114+
constructor(overrides: Pick<ICommandPaletteOptions, 'keybinding' | 'title' | 'id' | 'menu'>, private readonly mode?: ChatModeKind) {
114115
super({
115116
...overrides,
116117
icon: Codicon.copilot,
@@ -148,7 +149,7 @@ abstract class OpenChatGlobalAction extends Action2 {
148149

149150
let switchToMode = opts?.mode ?? this.mode;
150151
if (!switchToMode) {
151-
switchToMode = opts?.query?.startsWith('@') ? ChatMode.Ask : undefined;
152+
switchToMode = opts?.query?.startsWith('@') ? ChatModeKind.Ask : undefined;
152153
}
153154
if (switchToMode && validateChatMode(switchToMode)) {
154155
await this.handleSwitchToMode(switchToMode, chatWidget, instaService, commandService);
@@ -170,7 +171,7 @@ abstract class OpenChatGlobalAction extends Action2 {
170171
chatWidget.setInput(opts.query);
171172
} else {
172173
await chatWidget.waitForReady();
173-
await waitForDefaultAgent(chatAgentService, chatWidget.input.currentMode);
174+
await waitForDefaultAgent(chatAgentService, chatWidget.input.currentModeKind);
174175
chatWidget.acceptInput(opts.query);
175176
}
176177
}
@@ -193,8 +194,8 @@ abstract class OpenChatGlobalAction extends Action2 {
193194
chatWidget.focusInput();
194195
}
195196

196-
private async handleSwitchToMode(switchToMode: ChatMode, chatWidget: IChatWidget, instaService: IInstantiationService, commandService: ICommandService): Promise<void> {
197-
const currentMode = chatWidget.input.currentMode;
197+
private async handleSwitchToMode(switchToMode: ChatModeKind, chatWidget: IChatWidget, instaService: IInstantiationService, commandService: ICommandService): Promise<void> {
198+
const currentMode = chatWidget.input.currentModeKind;
198199

199200
if (switchToMode) {
200201
const editingSession = chatWidget.viewModel?.model.editingSession;
@@ -212,7 +213,7 @@ abstract class OpenChatGlobalAction extends Action2 {
212213
}
213214
}
214215

215-
async function waitForDefaultAgent(chatAgentService: IChatAgentService, mode: ChatMode): Promise<void> {
216+
async function waitForDefaultAgent(chatAgentService: IChatAgentService, mode: ChatModeKind): Promise<void> {
216217
const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel, mode);
217218
if (defaultAgent) {
218219
return;
@@ -248,18 +249,17 @@ class PrimaryOpenChatGlobalAction extends OpenChatGlobalAction {
248249
}
249250
}
250251

251-
export function getOpenChatActionIdForMode(mode: ChatMode): string {
252-
const modeStr = modeToString(mode);
253-
return `workbench.action.chat.open${modeStr}`;
252+
export function getOpenChatActionIdForMode(mode: IChatMode): string {
253+
return `workbench.action.chat.open${mode.name}`;
254254
}
255255

256256
abstract class ModeOpenChatGlobalAction extends OpenChatGlobalAction {
257-
constructor(mode: ChatMode, keybinding?: ICommandPaletteOptions['keybinding']) {
257+
constructor(mode: IChatMode, keybinding?: ICommandPaletteOptions['keybinding']) {
258258
super({
259259
id: getOpenChatActionIdForMode(mode),
260-
title: localize2('openChatMode', "Open Chat ({0})", modeToString(mode)),
260+
title: localize2('openChatMode', "Open Chat ({0})", mode.name),
261261
keybinding
262-
}, mode);
262+
}, mode.kind);
263263
}
264264
}
265265

@@ -504,7 +504,7 @@ export function registerChatActions() {
504504
category: CHAT_CATEGORY,
505505
menu: [{
506506
id: MenuId.ChatExecute,
507-
when: ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask),
507+
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask),
508508
group: 'navigation',
509509
order: 1
510510
}]
@@ -1010,8 +1010,8 @@ export async function handleCurrentEditingSession(currentEditingSession: IChatEd
10101010
*/
10111011
export async function handleModeSwitch(
10121012
accessor: ServicesAccessor,
1013-
fromMode: ChatMode,
1014-
toMode: ChatMode,
1013+
fromMode: ChatModeKind,
1014+
toMode: ChatModeKind,
10151015
requestCount: number,
10161016
editingSession: IChatEditingSession | undefined,
10171017
): Promise<false | { needToClearSession: boolean }> {
@@ -1021,7 +1021,7 @@ export async function handleModeSwitch(
10211021

10221022
const configurationService = accessor.get(IConfigurationService);
10231023
const dialogService = accessor.get(IDialogService);
1024-
const needToClearEdits = (!configurationService.getValue(ChatConfiguration.Edits2Enabled) && (fromMode === ChatMode.Edit || toMode === ChatMode.Edit)) && requestCount > 0;
1024+
const needToClearEdits = (!configurationService.getValue(ChatConfiguration.Edits2Enabled) && (fromMode === ChatModeKind.Edit || toMode === ChatModeKind.Edit)) && requestCount > 0;
10251025
if (needToClearEdits) {
10261026
// If not using edits2 and switching into or out of edit mode, ask to discard the session
10271027
const phrase = localize('switchMode.confirmPhrase', "Switching chat modes will end your current edit session.");

src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb
1616
import { ActiveEditorContext } from '../../../../common/contextkeys.js';
1717
import { ChatContextKeys } from '../../common/chatContextKeys.js';
1818
import { IChatEditingSession } from '../../common/chatEditingService.js';
19-
import { ChatMode } from '../../common/constants.js';
19+
import { ChatModeKind } from '../../common/constants.js';
2020
import { ChatViewId, IChatWidget } from '../chat.js';
2121
import { EditingSessionAction } from '../chatEditing/chatEditingActions.js';
2222
import { ChatEditorInput } from '../chatEditorInput.js';
@@ -120,7 +120,7 @@ export function registerNewChatActions() {
120120
}
121121

122122
if (typeof context.agentMode === 'boolean') {
123-
widget.input.setChatMode(context.agentMode ? ChatMode.Agent : ChatMode.Edit);
123+
widget.input.setChatMode(context.agentMode ? ChatModeKind.Agent : ChatModeKind.Edit);
124124
}
125125

126126
if (context.inputValue) {

src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { basename } from '../../../../../base/common/resources.js';
76
import { CancellationToken } from '../../../../../base/common/cancellation.js';
87
import { Codicon } from '../../../../../base/common/codicons.js';
8+
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
99
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
10+
import { basename } from '../../../../../base/common/resources.js';
1011
import { ThemeIcon } from '../../../../../base/common/themables.js';
1112
import { assertType } from '../../../../../base/common/types.js';
1213
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
14+
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
1315
import { localize, localize2 } from '../../../../../nls.js';
1416
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
1517
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
@@ -18,22 +20,20 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/cont
1820
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
1921
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
2022
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
21-
import { IChatAgentService, IChatAgentHistoryEntry } from '../../common/chatAgents.js';
23+
import { IRemoteCodingAgentsService } from '../../../remoteCodingAgents/common/remoteCodingAgentsService.js';
24+
import { IChatAgentHistoryEntry, IChatAgentService } from '../../common/chatAgents.js';
2225
import { ChatContextKeys } from '../../common/chatContextKeys.js';
2326
import { toChatHistoryContent } from '../../common/chatModel.js';
24-
import { ChatMode2, IChatMode, validateChatMode2 } from '../../common/chatModes.js';
27+
import { IChatMode, IChatModeService } from '../../common/chatModes.js';
2528
import { chatVariableLeader } from '../../common/chatParserTypes.js';
29+
import { ChatRequestParser } from '../../common/chatRequestParser.js';
2630
import { IChatService } from '../../common/chatService.js';
27-
import { ChatAgentLocation, ChatConfiguration, ChatMode, } from '../../common/constants.js';
31+
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../common/constants.js';
2832
import { ILanguageModelChatMetadata } from '../../common/languageModels.js';
2933
import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';
3034
import { IChatWidget, IChatWidgetService } from '../chat.js';
3135
import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js';
3236
import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY, handleCurrentEditingSession, handleModeSwitch } from './chatActions.js';
33-
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
34-
import { IRemoteCodingAgentsService } from '../../../remoteCodingAgents/common/remoteCodingAgentsService.js';
35-
import { ChatRequestParser } from '../../common/chatRequestParser.js';
36-
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
3737

3838
export interface IVoiceChatExecuteActionContext {
3939
readonly disableTimeout?: boolean;
@@ -127,7 +127,7 @@ export class ChatSubmitAction extends SubmitAction {
127127
static readonly ID = 'workbench.action.chat.submit';
128128

129129
constructor() {
130-
const precondition = ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask);
130+
const precondition = ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask);
131131

132132
super({
133133
id: ChatSubmitAction.ID,
@@ -164,7 +164,7 @@ export class ChatSubmitAction extends SubmitAction {
164164
export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode';
165165

166166
export interface IToggleChatModeArgs {
167-
mode: IChatMode | ChatMode;
167+
modeId: ChatModeKind | string;
168168
}
169169

170170
class ToggleChatModeAction extends Action2 {
@@ -207,6 +207,7 @@ class ToggleChatModeAction extends Action2 {
207207
const commandService = accessor.get(ICommandService);
208208
const configurationService = accessor.get(IConfigurationService);
209209
const instaService = accessor.get(IInstantiationService);
210+
const modeService = accessor.get(IChatModeService);
210211

211212
const context = getEditingSessionContext(accessor, args);
212213
if (!context?.chatWidget) {
@@ -216,33 +217,35 @@ class ToggleChatModeAction extends Action2 {
216217
const arg = args.at(0) as IToggleChatModeArgs | undefined;
217218
const chatSession = context.chatWidget.viewModel?.model;
218219
const requestCount = chatSession?.getRequests().length ?? 0;
219-
const switchToMode = validateChatMode2(arg?.mode) ?? this.getNextMode(context.chatWidget, requestCount, configurationService);
220+
const switchToMode = (arg && modeService.findModeById(arg.modeId)) ?? this.getNextMode(context.chatWidget, requestCount, configurationService, modeService);
220221

221-
if (switchToMode.id === context.chatWidget.input.currentMode2.id) {
222+
if (switchToMode.id === context.chatWidget.input.currentModeObs.get().id) {
222223
return;
223224
}
224225

225-
const chatModeCheck = await instaService.invokeFunction(handleModeSwitch, context.chatWidget.input.currentMode, switchToMode.kind, requestCount, context.editingSession);
226+
const chatModeCheck = await instaService.invokeFunction(handleModeSwitch, context.chatWidget.input.currentModeKind, switchToMode.kind, requestCount, context.editingSession);
226227
if (!chatModeCheck) {
227228
return;
228229
}
229230

230-
context.chatWidget.input.setChatMode2(switchToMode);
231+
context.chatWidget.input.setChatMode(switchToMode.id);
231232

232233
if (chatModeCheck.needToClearSession) {
233234
await commandService.executeCommand(ACTION_ID_NEW_CHAT);
234235
}
235236
}
236237

237-
private getNextMode(chatWidget: IChatWidget, requestCount: number, configurationService: IConfigurationService): IChatMode {
238-
const modes = [ChatMode2.Ask];
239-
if (configurationService.getValue(ChatConfiguration.Edits2Enabled) || requestCount === 0) {
240-
modes.push(ChatMode2.Edit);
241-
}
242-
modes.push(ChatMode2.Agent);
243-
244-
const modeIndex = modes.findIndex(mode => mode.id === chatWidget.input.currentMode2.id);
245-
const newMode = modes[(modeIndex + 1) % modes.length];
238+
private getNextMode(chatWidget: IChatWidget, requestCount: number, configurationService: IConfigurationService, modeService: IChatModeService): IChatMode {
239+
const modes = modeService.getModes();
240+
const flat = [
241+
...modes.builtin.filter(mode => {
242+
return mode.kind !== ChatModeKind.Edit || configurationService.getValue(ChatConfiguration.Edits2Enabled) || requestCount === 0;
243+
}),
244+
...(modes.custom ?? []),
245+
];
246+
247+
const curModeIndex = flat.findIndex(mode => mode.id === chatWidget.input.currentModeObs.get().id);
248+
const newMode = flat[(curModeIndex + 1) % flat.length];
246249
return newMode;
247250
}
248251
}
@@ -269,7 +272,7 @@ export class ToggleRequestPausedAction extends Action2 {
269272
order: 3.5,
270273
when: ContextKeyExpr.and(
271274
ChatContextKeys.canRequestBePaused,
272-
ChatContextKeys.chatMode.isEqualTo(ChatMode.Agent),
275+
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
273276
ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel),
274277
ContextKeyExpr.or(ChatContextKeys.isRequestPaused.negate(), ChatContextKeys.inputHasText.negate()),
275278
),
@@ -378,7 +381,7 @@ export class ChatEditingSessionSubmitAction extends SubmitAction {
378381
static readonly ID = 'workbench.action.edits.submit';
379382

380383
constructor() {
381-
const precondition = ChatContextKeys.chatMode.notEqualsTo(ChatMode.Ask);
384+
const precondition = ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask);
382385

383386
super({
384387
id: ChatEditingSessionSubmitAction.ID,
@@ -423,7 +426,7 @@ class SubmitWithoutDispatchingAction extends Action2 {
423426
// without text present - having instructions is enough context for a request
424427
ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile),
425428
whenNotInProgressOrPaused,
426-
ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask),
429+
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask),
427430
);
428431

429432
super({
@@ -442,7 +445,7 @@ class SubmitWithoutDispatchingAction extends Action2 {
442445
id: MenuId.ChatExecuteSecondary,
443446
group: 'group_1',
444447
order: 2,
445-
when: ChatContextKeys.chatMode.isEqualTo(ChatMode.Ask),
448+
when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask),
446449
}
447450
]
448451
});

0 commit comments

Comments
 (0)