Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts
5297 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Codicon } from '../../../../../base/common/codicons.js';
7
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { generateUuid } from '../../../../../base/common/uuid.js';
10
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
11
import { localize, localize2 } from '../../../../../nls.js';
12
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
13
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
14
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
15
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
16
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
17
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
18
import { ActiveEditorContext } from '../../../../common/contextkeys.js';
19
import { IViewsService } from '../../../../services/views/common/viewsService.js';
20
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
21
import { IChatEditingSession } from '../../common/editing/chatEditingService.js';
22
import { IChatService } from '../../common/chatService/chatService.js';
23
import { localChatSessionType } from '../../common/chatSessionsService.js';
24
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
25
import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js';
26
import { ChatViewId, IChatWidgetService, isIChatViewViewContext } from '../chat.js';
27
import { EditingSessionAction, EditingSessionActionContext, getEditingSessionContext } from '../chatEditing/chatEditingActions.js';
28
import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';
29
import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js';
30
import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js';
31
import { clearChatEditor } from './chatClear.js';
32
import { AgentSessionProviders, AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js';
33
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
34
35
export interface INewEditSessionActionContext {
36
37
/**
38
* An initial prompt to write to the chat.
39
*/
40
inputValue?: string;
41
42
/**
43
* Selects opening in agent mode or not. If not set, the current mode is used.
44
* This is ignored when coming from a chat view title context.
45
*/
46
agentMode?: boolean;
47
48
/**
49
* Whether the inputValue is partial and should wait for further user input.
50
* If false or not set, the prompt is sent immediately.
51
*/
52
isPartialQuery?: boolean;
53
}
54
55
function isNewEditSessionActionContext(arg: unknown): arg is INewEditSessionActionContext {
56
if (arg && typeof arg === 'object') {
57
const obj = arg as Record<string, unknown>;
58
if (obj.inputValue !== undefined && typeof obj.inputValue !== 'string') {
59
return false;
60
}
61
if (obj.agentMode !== undefined && typeof obj.agentMode !== 'boolean') {
62
return false;
63
}
64
if (obj.isPartialQuery !== undefined && typeof obj.isPartialQuery !== 'boolean') {
65
return false;
66
}
67
return true;
68
}
69
return false;
70
}
71
72
export function registerNewChatActions() {
73
74
// Add "New Chat" submenu to Chat view menu
75
MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
76
submenu: MenuId.ChatNewMenu,
77
title: localize2('chat.newEdits.label', "New Chat"),
78
icon: Codicon.plus,
79
when: ContextKeyExpr.equals('view', ChatViewId),
80
group: 'navigation',
81
order: -1,
82
isSplitButton: true
83
});
84
85
registerAction2(class NewChatEditorAction extends Action2 {
86
constructor() {
87
super({
88
id: 'workbench.action.chatEditor.newChat',
89
title: localize2('chat.newChat.label', "New Chat"),
90
icon: Codicon.plus,
91
f1: false,
92
precondition: ChatContextKeys.enabled,
93
});
94
}
95
async run(accessor: ServicesAccessor, ...args: unknown[]) {
96
await clearChatEditor(accessor);
97
}
98
});
99
100
registerAction2(class NewChatAction extends Action2 {
101
constructor() {
102
super({
103
id: ACTION_ID_NEW_CHAT,
104
title: localize2('chat.newEdits.label', "New Chat"),
105
category: CHAT_CATEGORY,
106
icon: Codicon.plus,
107
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)),
108
f1: true,
109
menu: [
110
{
111
id: MenuId.ChatContext,
112
group: 'z_clear'
113
},
114
{
115
id: MenuId.ChatNewMenu,
116
group: '1_open',
117
order: 1,
118
},
119
{
120
id: MenuId.CompactWindowEditorTitle,
121
group: 'navigation',
122
when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID),
123
order: 1
124
}
125
],
126
keybinding: {
127
weight: KeybindingWeight.WorkbenchContrib + 1,
128
primary: KeyMod.CtrlCmd | KeyCode.KeyN,
129
secondary: [KeyMod.CtrlCmd | KeyCode.KeyL],
130
mac: {
131
primary: KeyMod.CtrlCmd | KeyCode.KeyN,
132
secondary: [KeyMod.WinCtrl | KeyCode.KeyL]
133
},
134
when: ChatContextKeys.inChatSession
135
}
136
});
137
}
138
139
async run(accessor: ServicesAccessor, ...args: unknown[]) {
140
const executeCommandContext = isNewEditSessionActionContext(args[0]) ? args[0] : undefined;
141
142
// Context from toolbar or lastFocusedWidget
143
const context = getEditingSessionContext(accessor, args);
144
await runNewChatAction(accessor, context, executeCommandContext);
145
}
146
}
147
);
148
CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_EDIT_SESSION, ACTION_ID_NEW_CHAT);
149
150
registerAction2(class NewLocalChatAction extends Action2 {
151
constructor() {
152
super({
153
id: 'workbench.action.chat.newLocalChat',
154
title: localize2('chat.newLocalChat.label', "New Local Chat"),
155
category: CHAT_CATEGORY,
156
icon: Codicon.plus,
157
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)),
158
f1: false,
159
});
160
}
161
162
async run(accessor: ServicesAccessor, ...args: unknown[]) {
163
const executeCommandContext = isNewEditSessionActionContext(args[0]) ? args[0] : undefined;
164
165
// Context from toolbar or lastFocusedWidget
166
const context = getEditingSessionContext(accessor, args);
167
await runNewChatAction(accessor, context, executeCommandContext, AgentSessionProviders.Local);
168
}
169
});
170
171
MenuRegistry.appendMenuItem(MenuId.ChatViewSessionTitleNavigationToolbar, {
172
command: {
173
id: ACTION_ID_NEW_CHAT,
174
title: localize2('chat.goBack', "Go Back"),
175
icon: Codicon.arrowLeft,
176
},
177
when: ChatContextKeys.agentSessionsViewerOrientation.notEqualsTo(AgentSessionsViewerOrientation.SideBySide), // when sessions show side by side, no need for a back button
178
group: 'navigation',
179
order: 1
180
});
181
182
registerAction2(class UndoChatEditInteractionAction extends EditingSessionAction {
183
constructor() {
184
super({
185
id: 'workbench.action.chat.undoEdit',
186
title: localize2('chat.undoEdit.label', "Undo Last Edit"),
187
category: CHAT_CATEGORY,
188
icon: Codicon.discard,
189
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanUndo, ChatContextKeys.enabled),
190
f1: true,
191
menu: [{
192
id: MenuId.ViewTitle,
193
when: ContextKeyExpr.equals('view', ChatViewId),
194
group: 'navigation',
195
order: -3,
196
isHiddenByDefault: true
197
}]
198
});
199
}
200
201
async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) {
202
await editingSession.undoInteraction();
203
}
204
});
205
206
registerAction2(class RedoChatEditInteractionAction extends EditingSessionAction {
207
constructor() {
208
super({
209
id: 'workbench.action.chat.redoEdit',
210
title: localize2('chat.redoEdit.label', "Redo Last Edit"),
211
category: CHAT_CATEGORY,
212
icon: Codicon.redo,
213
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled),
214
f1: true,
215
menu: [
216
{
217
id: MenuId.ViewTitle,
218
when: ContextKeyExpr.equals('view', ChatViewId),
219
group: 'navigation',
220
order: -2,
221
isHiddenByDefault: true
222
}
223
]
224
});
225
}
226
227
async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) {
228
const chatService = accessor.get(IChatService);
229
await editingSession.redoInteraction();
230
chatService.getSession(editingSession.chatSessionResource)?.setCheckpoint(undefined);
231
}
232
});
233
234
registerAction2(class RedoChatCheckpoints extends EditingSessionAction {
235
constructor() {
236
super({
237
id: 'workbench.action.chat.redoEdit2',
238
title: localize2('chat.redoEdit.label2', "Redo"),
239
tooltip: localize2('chat.redoEdit.tooltip', "Reapply discarded workspace changes and chat"),
240
category: CHAT_CATEGORY,
241
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled),
242
f1: true,
243
menu: [{
244
id: MenuId.ChatMessageRestoreCheckpoint,
245
when: ChatContextKeys.lockedToCodingAgent.negate(),
246
group: 'navigation',
247
order: -1
248
}]
249
});
250
}
251
252
async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) {
253
const widget = accessor.get(IChatWidgetService);
254
255
while (editingSession.canRedo.get()) {
256
await editingSession.redoInteraction();
257
}
258
259
const currentWidget = widget.getWidgetBySessionResource(editingSession.chatSessionResource);
260
const requestText = currentWidget?.viewModel?.model.checkpoint?.message.text;
261
262
// if the input has the same text that we just restored, clear it.
263
if (currentWidget?.inputEditor.getValue() === requestText) {
264
currentWidget?.input.setValue('', false);
265
}
266
267
currentWidget?.viewModel?.model.setCheckpoint(undefined);
268
currentWidget?.focusInput();
269
}
270
});
271
}
272
273
/**
274
* Creates a new session resource URI with the specified session type.
275
* For remote sessions, creates a URI with the session type as the scheme.
276
* For local sessions, creates a LocalChatSessionUri.
277
*/
278
function getResourceForNewChatSession(sessionType: string): URI {
279
const isRemoteSession = sessionType !== localChatSessionType;
280
if (isRemoteSession) {
281
return URI.from({
282
scheme: sessionType,
283
path: `/untitled-${generateUuid()}`,
284
});
285
}
286
287
return LocalChatSessionUri.forSession(generateUuid());
288
}
289
290
async function runNewChatAction(
291
accessor: ServicesAccessor,
292
context: EditingSessionActionContext | undefined,
293
executeCommandContext?: INewEditSessionActionContext,
294
sessionType?: AgentSessionProviders
295
) {
296
const accessibilityService = accessor.get(IAccessibilityService);
297
const viewsService = accessor.get(IViewsService);
298
const configurationService = accessor.get(IConfigurationService);
299
300
const { editingSession, chatWidget: widget } = context ?? {};
301
if (!widget) {
302
return;
303
}
304
305
const dialogService = accessor.get(IDialogService);
306
307
const model = widget.viewModel?.model;
308
if (model && !(await handleCurrentEditingSession(model, undefined, dialogService))) {
309
return;
310
}
311
312
await editingSession?.stop();
313
314
// Create a new session with the same type as the current session
315
const currentResource = widget.viewModel?.model.sessionResource;
316
const newSessionType = sessionType ?? (currentResource ? getChatSessionType(currentResource) : localChatSessionType);
317
if (isIChatViewViewContext(widget.viewContext) && newSessionType !== localChatSessionType) {
318
// For the sidebar, we need to explicitly load a session with the same type
319
const newResource = getResourceForNewChatSession(newSessionType);
320
const view = await viewsService.openView(ChatViewId) as ChatViewPane;
321
await view.loadSession(newResource);
322
} else {
323
// For the editor, widget.clear() already preserves the session type via clearChatEditor
324
await widget.clear();
325
}
326
327
widget.attachmentModel.clear(true);
328
widget.focusInput();
329
330
accessibilityService.alert(localize('newChat', "New chat"));
331
332
if (!executeCommandContext) {
333
return;
334
}
335
336
if (typeof executeCommandContext.agentMode === 'boolean') {
337
widget.input.setChatMode(executeCommandContext.agentMode ? ChatModeKind.Agent : ChatModeKind.Edit);
338
} else if (widget.input.currentModeKind === ChatModeKind.Edit && configurationService.getValue<boolean>(ChatConfiguration.EditModeHidden)) {
339
widget.input.setChatMode(ChatModeKind.Agent);
340
}
341
342
if (executeCommandContext.inputValue) {
343
if (executeCommandContext.isPartialQuery) {
344
widget.setInput(executeCommandContext.inputValue);
345
} else {
346
widget.acceptInput(executeCommandContext.inputValue);
347
}
348
}
349
}
350
351