Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts
5241 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 { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
10
import { localize, localize2 } from '../../../../../nls.js';
11
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
12
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
13
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
14
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
15
import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js';
16
import { ChatConfiguration } from '../../common/constants.js';
17
import { isRequestVM } from '../../common/model/chatViewModel.js';
18
import { IChatWidgetService } from '../chat.js';
19
import { CHAT_CATEGORY } from './chatActions.js';
20
21
const queueingEnabledCondition = ContextKeyExpr.equals(`config.${ChatConfiguration.RequestQueueingEnabled}`, true);
22
const requestInProgressOrPendingToolCall = ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.Editing.hasToolConfirmation);
23
24
const queuingActionsPresent = ContextKeyExpr.and(
25
queueingEnabledCondition,
26
ContextKeyExpr.or(requestInProgressOrPendingToolCall, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer)),
27
ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Sent),
28
);
29
30
export interface IChatRemovePendingRequestContext {
31
sessionResource: URI;
32
pendingRequestId: string;
33
}
34
35
function isRemovePendingRequestContext(context: unknown): context is IChatRemovePendingRequestContext {
36
return !!context &&
37
typeof context === 'object' &&
38
'sessionResource' in context &&
39
'pendingRequestId' in context &&
40
URI.isUri((context as IChatRemovePendingRequestContext).sessionResource) &&
41
typeof (context as IChatRemovePendingRequestContext).pendingRequestId === 'string';
42
}
43
44
export class ChatQueueMessageAction extends Action2 {
45
static readonly ID = 'workbench.action.chat.queueMessage';
46
47
constructor() {
48
super({
49
id: ChatQueueMessageAction.ID,
50
title: localize2('chat.queueMessage', "Add to Queue"),
51
tooltip: localize('chat.queueMessage.tooltip', "Queue this message to send after the current request completes"),
52
icon: Codicon.add,
53
f1: false,
54
category: CHAT_CATEGORY,
55
56
precondition: ContextKeyExpr.and(
57
queuingActionsPresent,
58
ChatContextKeys.inputHasText,
59
),
60
keybinding: {
61
when: ContextKeyExpr.and(
62
ChatContextKeys.inChatInput,
63
queuingActionsPresent,
64
),
65
primary: KeyMod.Alt | KeyCode.Enter,
66
weight: KeybindingWeight.EditorContrib + 1
67
},
68
});
69
}
70
71
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
72
const widgetService = accessor.get(IChatWidgetService);
73
const widget = widgetService.lastFocusedWidget;
74
if (!widget?.viewModel) {
75
return;
76
}
77
78
const inputValue = widget.getInput();
79
if (!inputValue.trim()) {
80
return;
81
}
82
83
widget.acceptInput(undefined, { queue: ChatRequestQueueKind.Queued });
84
}
85
}
86
87
export class ChatSteerWithMessageAction extends Action2 {
88
static readonly ID = 'workbench.action.chat.steerWithMessage';
89
90
constructor() {
91
super({
92
id: ChatSteerWithMessageAction.ID,
93
title: localize2('chat.steerWithMessage', "Steer with Message"),
94
tooltip: localize('chat.steerWithMessage.tooltip', "Send this message at the next opportunity, signaling the current request to yield"),
95
icon: Codicon.arrowRight,
96
f1: false,
97
category: CHAT_CATEGORY,
98
precondition: ContextKeyExpr.and(
99
queuingActionsPresent,
100
ChatContextKeys.inputHasText,
101
),
102
keybinding: {
103
when: ContextKeyExpr.and(
104
ChatContextKeys.inChatInput,
105
queuingActionsPresent,
106
),
107
primary: KeyCode.Enter,
108
weight: KeybindingWeight.EditorContrib + 1
109
},
110
});
111
}
112
113
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
114
const widgetService = accessor.get(IChatWidgetService);
115
const widget = widgetService.lastFocusedWidget;
116
if (!widget?.viewModel) {
117
return;
118
}
119
120
const inputValue = widget.getInput();
121
if (!inputValue.trim()) {
122
return;
123
}
124
125
widget.acceptInput(undefined, { queue: ChatRequestQueueKind.Steering });
126
}
127
}
128
129
export class ChatRemovePendingRequestAction extends Action2 {
130
static readonly ID = 'workbench.action.chat.removePendingRequest';
131
132
constructor() {
133
super({
134
id: ChatRemovePendingRequestAction.ID,
135
title: localize2('chat.removePendingRequest', "Remove from Queue"),
136
icon: Codicon.close,
137
f1: false,
138
category: CHAT_CATEGORY,
139
menu: [{
140
id: MenuId.ChatMessageTitle,
141
group: 'navigation',
142
order: 4,
143
when: ContextKeyExpr.and(
144
queueingEnabledCondition,
145
ChatContextKeys.isRequest,
146
ChatContextKeys.isPendingRequest
147
)
148
}]
149
});
150
}
151
152
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
153
const chatService = accessor.get(IChatService);
154
const [context] = args;
155
156
// Support both toolbar context (IChatRequestViewModel) and command context (IChatRemovePendingRequestContext)
157
if (isRequestVM(context) && context.pendingKind) {
158
chatService.removePendingRequest(context.sessionResource, context.id);
159
return;
160
}
161
162
if (isRemovePendingRequestContext(context)) {
163
chatService.removePendingRequest(context.sessionResource, context.pendingRequestId);
164
return;
165
}
166
}
167
}
168
169
export class ChatSendPendingImmediatelyAction extends Action2 {
170
static readonly ID = 'workbench.action.chat.sendPendingImmediately';
171
172
constructor() {
173
super({
174
id: ChatSendPendingImmediatelyAction.ID,
175
title: localize2('chat.sendPendingImmediately', "Send Immediately"),
176
icon: Codicon.arrowUp,
177
f1: false,
178
category: CHAT_CATEGORY,
179
menu: [{
180
id: MenuId.ChatMessageTitle,
181
group: 'navigation',
182
order: 3,
183
when: ContextKeyExpr.and(
184
queueingEnabledCondition,
185
ChatContextKeys.isRequest,
186
ChatContextKeys.isPendingRequest
187
)
188
}]
189
});
190
}
191
192
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
193
const chatService = accessor.get(IChatService);
194
const widgetService = accessor.get(IChatWidgetService);
195
const [context] = args;
196
197
if (!isRequestVM(context) || !context.pendingKind) {
198
return;
199
}
200
201
const widget = widgetService.getWidgetBySessionResource(context.sessionResource);
202
const model = widget?.viewModel?.model;
203
if (!model) {
204
return;
205
}
206
207
const pendingRequests = model.getPendingRequests();
208
const targetIndex = pendingRequests.findIndex(r => r.request.id === context.id);
209
if (targetIndex === -1) {
210
return;
211
}
212
213
// Keep the target item's kind (queued vs steering)
214
const targetRequest = pendingRequests[targetIndex];
215
216
// Reorder: move target to front, keep others in their relative order
217
const reordered = [
218
{ requestId: targetRequest.request.id, kind: targetRequest.kind },
219
...pendingRequests.filter((_, i) => i !== targetIndex).map(r => ({ requestId: r.request.id, kind: r.kind }))
220
];
221
222
chatService.setPendingRequests(context.sessionResource, reordered);
223
chatService.cancelCurrentRequestForSession(context.sessionResource);
224
chatService.processPendingRequests(context.sessionResource);
225
}
226
}
227
228
export class ChatRemoveAllPendingRequestsAction extends Action2 {
229
static readonly ID = 'workbench.action.chat.removeAllPendingRequests';
230
231
constructor() {
232
super({
233
id: ChatRemoveAllPendingRequestsAction.ID,
234
title: localize2('chat.removeAllPendingRequests', "Remove All Queued"),
235
icon: Codicon.clearAll,
236
f1: false,
237
category: CHAT_CATEGORY,
238
menu: [{
239
id: MenuId.ChatContext,
240
group: 'navigation',
241
order: 3,
242
when: ContextKeyExpr.and(
243
queueingEnabledCondition,
244
ChatContextKeys.hasPendingRequests
245
)
246
}]
247
});
248
}
249
250
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
251
const chatService = accessor.get(IChatService);
252
const widgetService = accessor.get(IChatWidgetService);
253
const [context] = args;
254
255
const widget = (isRequestVM(context) && widgetService.getWidgetBySessionResource(context.sessionResource)) || widgetService.lastFocusedWidget;
256
const model = widget?.viewModel?.model;
257
if (!model) {
258
return;
259
}
260
261
for (const pendingRequest of [...model.getPendingRequests()]) {
262
chatService.removePendingRequest(model.sessionResource, pendingRequest.request.id);
263
}
264
}
265
}
266
267
export function registerChatQueueActions(): void {
268
registerAction2(ChatQueueMessageAction);
269
registerAction2(ChatSteerWithMessageAction);
270
registerAction2(ChatRemovePendingRequestAction);
271
registerAction2(ChatSendPendingImmediatelyAction);
272
registerAction2(ChatRemoveAllPendingRequestsAction);
273
274
// Register the queue submenu in the execute toolbar.
275
// The custom ChatQueuePickerActionItem (registered via IActionViewItemService)
276
// replaces the default rendering with a dropdown that shows hover descriptions.
277
// We still need items in ChatExecuteQueue so the menu system treats it as non-empty.
278
MenuRegistry.appendMenuItem(MenuId.ChatExecuteQueue, {
279
command: { id: ChatQueueMessageAction.ID, title: localize2('chat.queueMessage', "Add to Queue"), icon: Codicon.add },
280
group: 'navigation',
281
order: 1,
282
});
283
MenuRegistry.appendMenuItem(MenuId.ChatExecuteQueue, {
284
command: { id: ChatSteerWithMessageAction.ID, title: localize2('chat.steerWithMessage', "Steer with Message"), icon: Codicon.arrowRight },
285
group: 'navigation',
286
order: 2,
287
});
288
289
MenuRegistry.appendMenuItem(MenuId.ChatExecute, {
290
submenu: MenuId.ChatExecuteQueue,
291
title: localize2('chat.queueSubmenu', "Queue"),
292
icon: Codicon.listOrdered,
293
when: queuingActionsPresent,
294
group: 'navigation',
295
order: 4,
296
});
297
}
298
299