Skip to content

Commit 7d015ab

Browse files
authored
Merge pull request microsoft#292775 from microsoft/connor4312/queuing-polishes
chat: polishes for steering/queued messages
2 parents 7914379 + 0f6461f commit 7d015ab

14 files changed

Lines changed: 680 additions & 125 deletions

File tree

src/vs/platform/actions/browser/menuEntryActionViewItem.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
427427
private readonly _dropdown: DropdownMenuActionViewItem;
428428
private _container: HTMLElement | null = null;
429429
private readonly _storageKey: string;
430+
private readonly _primaryActionListener = this._register(new MutableDisposable());
430431

431432
get onDidChangeDropdownVisibility(): Event<boolean> {
432433
return this._dropdown.onDidChangeVisibility;
@@ -468,14 +469,18 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
468469

469470
this._dropdown = this._register(new DropdownMenuActionViewItem(submenuAction, submenuAction.actions, this._contextMenuService, dropdownOptions));
470471
if (options?.togglePrimaryAction) {
471-
this._register(this._dropdown.actionRunner.onDidRun((e: IRunEvent) => {
472-
if (e.action instanceof MenuItemAction) {
473-
this.update(e.action);
474-
}
475-
}));
472+
this.registerTogglePrimaryActionListener();
476473
}
477474
}
478475

476+
private registerTogglePrimaryActionListener(): void {
477+
this._primaryActionListener.value = this._dropdown.actionRunner.onDidRun((e: IRunEvent) => {
478+
if (e.action instanceof MenuItemAction) {
479+
this.update(e.action);
480+
}
481+
});
482+
}
483+
479484
private update(lastAction: MenuItemAction): void {
480485
if (this._options?.togglePrimaryAction) {
481486
this._storageService.store(this._storageKey, lastAction.id, StorageScope.WORKSPACE, StorageTarget.MACHINE);
@@ -516,6 +521,9 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem {
516521

517522
this._defaultAction.actionRunner = actionRunner;
518523
this._dropdown.actionRunner = actionRunner;
524+
if (this._primaryActionListener.value) {
525+
this.registerTogglePrimaryActionListener();
526+
}
519527
}
520528

521529
override get actionRunner(): IActionRunner {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,8 @@ export class CancelAction extends Action2 {
822822
id: MenuId.ChatExecute,
823823
when: ContextKeyExpr.and(
824824
ChatContextKeys.requestInProgress,
825-
ChatContextKeys.remoteJobCreating.negate()
825+
ChatContextKeys.remoteJobCreating.negate(),
826+
ChatContextKeys.currentlyEditing.negate(),
826827
),
827828
order: 4,
828829
group: 'navigation',

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

Lines changed: 143 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { Codicon } from '../../../../../base/common/codicons.js';
7+
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
78
import { URI } from '../../../../../base/common/uri.js';
8-
import { localize2 } from '../../../../../nls.js';
9+
import { localize, localize2 } from '../../../../../nls.js';
910
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
1011
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
12+
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
1113
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
1214
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
1315
import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js';
1416
import { ChatConfiguration } from '../../common/constants.js';
17+
import { isRequestVM } from '../../common/model/chatViewModel.js';
1518
import { IChatWidgetService } from '../chat.js';
1619
import { CHAT_CATEGORY } from './chatActions.js';
1720

@@ -38,6 +41,7 @@ export class ChatQueueMessageAction extends Action2 {
3841
super({
3942
id: ChatQueueMessageAction.ID,
4043
title: localize2('chat.queueMessage', "Add to Queue"),
44+
tooltip: localize('chat.queueMessage.tooltip', "Queue this message to send after the current request completes"),
4145
icon: Codicon.add,
4246
f1: false,
4347
category: CHAT_CATEGORY,
@@ -46,6 +50,15 @@ export class ChatQueueMessageAction extends Action2 {
4650
ChatContextKeys.requestInProgress,
4751
ChatContextKeys.inputHasText
4852
),
53+
keybinding: {
54+
when: ContextKeyExpr.and(
55+
ChatContextKeys.inChatInput,
56+
ChatContextKeys.requestInProgress,
57+
queueingEnabledCondition
58+
),
59+
primary: KeyCode.Enter,
60+
weight: KeybindingWeight.EditorContrib + 1
61+
},
4962
menu: [{
5063
id: MenuId.ChatExecuteQueue,
5164
group: 'navigation',
@@ -77,6 +90,7 @@ export class ChatSteerWithMessageAction extends Action2 {
7790
super({
7891
id: ChatSteerWithMessageAction.ID,
7992
title: localize2('chat.steerWithMessage', "Steer with Message"),
93+
tooltip: localize('chat.steerWithMessage.tooltip', "Send this message at the next opportunity, signaling the current request to yield"),
8094
icon: Codicon.arrowRight,
8195
f1: false,
8296
category: CHAT_CATEGORY,
@@ -85,6 +99,15 @@ export class ChatSteerWithMessageAction extends Action2 {
8599
ChatContextKeys.requestInProgress,
86100
ChatContextKeys.inputHasText
87101
),
102+
keybinding: {
103+
when: ContextKeyExpr.and(
104+
ChatContextKeys.inChatInput,
105+
ChatContextKeys.requestInProgress,
106+
queueingEnabledCondition
107+
),
108+
primary: KeyMod.Alt | KeyCode.Enter,
109+
weight: KeybindingWeight.EditorContrib + 1
110+
},
88111
menu: [{
89112
id: MenuId.ChatExecuteQueue,
90113
group: 'navigation',
@@ -119,24 +142,140 @@ export class ChatRemovePendingRequestAction extends Action2 {
119142
icon: Codicon.close,
120143
f1: false,
121144
category: CHAT_CATEGORY,
145+
menu: [{
146+
id: MenuId.ChatMessageTitle,
147+
group: 'navigation',
148+
order: 4,
149+
when: ContextKeyExpr.and(
150+
queueingEnabledCondition,
151+
ChatContextKeys.isRequest,
152+
ChatContextKeys.isPendingRequest
153+
)
154+
}]
155+
});
156+
}
157+
158+
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
159+
const chatService = accessor.get(IChatService);
160+
const [context] = args;
161+
162+
// Support both toolbar context (IChatRequestViewModel) and command context (IChatRemovePendingRequestContext)
163+
if (isRequestVM(context) && context.pendingKind) {
164+
chatService.removePendingRequest(context.sessionResource, context.id);
165+
return;
166+
}
167+
168+
if (isRemovePendingRequestContext(context)) {
169+
chatService.removePendingRequest(context.sessionResource, context.pendingRequestId);
170+
return;
171+
}
172+
}
173+
}
174+
175+
export class ChatSendPendingImmediatelyAction extends Action2 {
176+
static readonly ID = 'workbench.action.chat.sendPendingImmediately';
177+
178+
constructor() {
179+
super({
180+
id: ChatSendPendingImmediatelyAction.ID,
181+
title: localize2('chat.sendPendingImmediately', "Send Immediately"),
182+
icon: Codicon.arrowUp,
183+
f1: false,
184+
category: CHAT_CATEGORY,
185+
menu: [{
186+
id: MenuId.ChatMessageTitle,
187+
group: 'navigation',
188+
order: 3,
189+
when: ContextKeyExpr.and(
190+
queueingEnabledCondition,
191+
ChatContextKeys.isRequest,
192+
ChatContextKeys.isPendingRequest
193+
)
194+
}]
122195
});
123196
}
124197

125198
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
126199
const chatService = accessor.get(IChatService);
200+
const widgetService = accessor.get(IChatWidgetService);
127201
const [context] = args;
128-
if (!isRemovePendingRequestContext(context)) {
202+
203+
if (!isRequestVM(context) || !context.pendingKind) {
129204
return;
130205
}
131206

132-
chatService.removePendingRequest(context.sessionResource, context.pendingRequestId);
207+
const widget = widgetService.getWidgetBySessionResource(context.sessionResource);
208+
const model = widget?.viewModel?.model;
209+
if (!model) {
210+
return;
211+
}
212+
213+
const pendingRequests = model.getPendingRequests();
214+
const targetIndex = pendingRequests.findIndex(r => r.request.id === context.id);
215+
if (targetIndex === -1) {
216+
return;
217+
}
218+
219+
// Keep the target item's kind (queued vs steering)
220+
const targetRequest = pendingRequests[targetIndex];
221+
222+
// Reorder: move target to front, keep others in their relative order
223+
const reordered = [
224+
{ requestId: targetRequest.request.id, kind: targetRequest.kind },
225+
...pendingRequests.filter((_, i) => i !== targetIndex).map(r => ({ requestId: r.request.id, kind: r.kind }))
226+
];
227+
228+
chatService.setPendingRequests(context.sessionResource, reordered);
229+
chatService.cancelCurrentRequestForSession(context.sessionResource);
230+
chatService.processPendingRequests(context.sessionResource);
231+
}
232+
}
233+
234+
export class ChatRemoveAllPendingRequestsAction extends Action2 {
235+
static readonly ID = 'workbench.action.chat.removeAllPendingRequests';
236+
237+
constructor() {
238+
super({
239+
id: ChatRemoveAllPendingRequestsAction.ID,
240+
title: localize2('chat.removeAllPendingRequests', "Remove All Queued"),
241+
icon: Codicon.clearAll,
242+
f1: false,
243+
category: CHAT_CATEGORY,
244+
menu: [{
245+
id: MenuId.ChatContext,
246+
group: 'navigation',
247+
order: 3,
248+
when: ContextKeyExpr.and(
249+
queueingEnabledCondition,
250+
ChatContextKeys.hasPendingRequests
251+
)
252+
}]
253+
});
254+
}
255+
256+
override run(accessor: ServicesAccessor, ...args: unknown[]): void {
257+
const chatService = accessor.get(IChatService);
258+
const widgetService = accessor.get(IChatWidgetService);
259+
const [context] = args;
260+
261+
const widget = (isRequestVM(context) && widgetService.getWidgetBySessionResource(context.sessionResource)) || widgetService.lastFocusedWidget;
262+
const model = widget?.viewModel?.model;
263+
if (!model) {
264+
return;
265+
}
266+
267+
for (const pendingRequest of [...model.getPendingRequests()]) {
268+
chatService.removePendingRequest(model.sessionResource, pendingRequest.request.id);
269+
}
133270
}
134271
}
135272

136273
export function registerChatQueueActions(): void {
137274
registerAction2(ChatQueueMessageAction);
138275
registerAction2(ChatSteerWithMessageAction);
139276
registerAction2(ChatRemovePendingRequestAction);
277+
registerAction2(ChatSendPendingImmediatelyAction);
278+
registerAction2(ChatRemoveAllPendingRequestsAction);
140279

141280
// Register the queue submenu as a split button dropdown in the execute toolbar
142281
// This shows "Add to Queue" / "Steer with Message" when a request is in progress and input has text
@@ -150,7 +289,7 @@ export function registerChatQueueActions(): void {
150289
ChatContextKeys.inputHasText
151290
),
152291
group: 'navigation',
153-
order: 3,
292+
order: 4,
154293
isSplitButton: { togglePrimaryAction: true }
155294
});
156295
}

src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
648648
ChatContextKeys.isResponse.bindTo(templateData.contextKeyService).set(isResponseVM(element));
649649
ChatContextKeys.itemId.bindTo(templateData.contextKeyService).set(element.id);
650650
ChatContextKeys.isRequest.bindTo(templateData.contextKeyService).set(isRequestVM(element));
651+
ChatContextKeys.isPendingRequest.bindTo(templateData.contextKeyService).set(isRequestVM(element) && !!element.pendingKind);
651652
ChatContextKeys.responseDetectedAgentCommand.bindTo(templateData.contextKeyService).set(isResponseVM(element) && element.agentOrSlashCommandDetected);
652653
if (isResponseVM(element)) {
653654
ChatContextKeys.responseSupportsIssueReporting.bindTo(templateData.contextKeyService).set(!!element.agent?.metadata.supportIssueReporting);
@@ -698,11 +699,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
698699
templateData.checkpointToolbar.context = element;
699700
const checkpointEnabled = this.configService.getValue<boolean>(ChatConfiguration.CheckpointsEnabled)
700701
&& (this.rendererOptions.restorable ?? true);
702+
const isPendingRequest = isRequestVM(element) && !!element.pendingKind;
701703

702-
templateData.checkpointContainer.classList.toggle('hidden', isResponseVM(element) || !(checkpointEnabled));
704+
templateData.checkpointContainer.classList.toggle('hidden', isResponseVM(element) || isPendingRequest || !(checkpointEnabled));
703705

704-
// Only show restore container when we have a checkpoint and not editing
705-
const shouldShowRestore = this.viewModel?.model.checkpoint && !this.viewModel?.editing && (index === this.delegate.getListLength() - 1);
706+
// Only show restore container when we have a checkpoint and not editing, and not a pending request
707+
const shouldShowRestore = this.viewModel?.model.checkpoint && !this.viewModel?.editing && (index === this.delegate.getListLength() - 1) && !isPendingRequest;
706708
templateData.checkpointRestoreContainer.classList.toggle('hidden', !(shouldShowRestore && checkpointEnabled));
707709

708710
const editing = element.id === this.viewModel?.editing?.id;

0 commit comments

Comments
 (0)