Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatQueueActions.ts
5241 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Codicon } from '../../../../../base/common/codicons.js';6import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';7import { URI } from '../../../../../base/common/uri.js';8import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';9import { localize, localize2 } from '../../../../../nls.js';10import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';11import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';12import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';13import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';14import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js';15import { ChatConfiguration } from '../../common/constants.js';16import { isRequestVM } from '../../common/model/chatViewModel.js';17import { IChatWidgetService } from '../chat.js';18import { CHAT_CATEGORY } from './chatActions.js';1920const queueingEnabledCondition = ContextKeyExpr.equals(`config.${ChatConfiguration.RequestQueueingEnabled}`, true);21const requestInProgressOrPendingToolCall = ContextKeyExpr.or(ChatContextKeys.requestInProgress, ChatContextKeys.Editing.hasToolConfirmation);2223const queuingActionsPresent = ContextKeyExpr.and(24queueingEnabledCondition,25ContextKeyExpr.or(requestInProgressOrPendingToolCall, ChatContextKeys.editingRequestType.isEqualTo(ChatContextKeys.EditingRequestType.QueueOrSteer)),26ChatContextKeys.editingRequestType.notEqualsTo(ChatContextKeys.EditingRequestType.Sent),27);2829export interface IChatRemovePendingRequestContext {30sessionResource: URI;31pendingRequestId: string;32}3334function isRemovePendingRequestContext(context: unknown): context is IChatRemovePendingRequestContext {35return !!context &&36typeof context === 'object' &&37'sessionResource' in context &&38'pendingRequestId' in context &&39URI.isUri((context as IChatRemovePendingRequestContext).sessionResource) &&40typeof (context as IChatRemovePendingRequestContext).pendingRequestId === 'string';41}4243export class ChatQueueMessageAction extends Action2 {44static readonly ID = 'workbench.action.chat.queueMessage';4546constructor() {47super({48id: ChatQueueMessageAction.ID,49title: localize2('chat.queueMessage', "Add to Queue"),50tooltip: localize('chat.queueMessage.tooltip', "Queue this message to send after the current request completes"),51icon: Codicon.add,52f1: false,53category: CHAT_CATEGORY,5455precondition: ContextKeyExpr.and(56queuingActionsPresent,57ChatContextKeys.inputHasText,58),59keybinding: {60when: ContextKeyExpr.and(61ChatContextKeys.inChatInput,62queuingActionsPresent,63),64primary: KeyMod.Alt | KeyCode.Enter,65weight: KeybindingWeight.EditorContrib + 166},67});68}6970override run(accessor: ServicesAccessor, ...args: unknown[]): void {71const widgetService = accessor.get(IChatWidgetService);72const widget = widgetService.lastFocusedWidget;73if (!widget?.viewModel) {74return;75}7677const inputValue = widget.getInput();78if (!inputValue.trim()) {79return;80}8182widget.acceptInput(undefined, { queue: ChatRequestQueueKind.Queued });83}84}8586export class ChatSteerWithMessageAction extends Action2 {87static readonly ID = 'workbench.action.chat.steerWithMessage';8889constructor() {90super({91id: ChatSteerWithMessageAction.ID,92title: localize2('chat.steerWithMessage', "Steer with Message"),93tooltip: localize('chat.steerWithMessage.tooltip', "Send this message at the next opportunity, signaling the current request to yield"),94icon: Codicon.arrowRight,95f1: false,96category: CHAT_CATEGORY,97precondition: ContextKeyExpr.and(98queuingActionsPresent,99ChatContextKeys.inputHasText,100),101keybinding: {102when: ContextKeyExpr.and(103ChatContextKeys.inChatInput,104queuingActionsPresent,105),106primary: KeyCode.Enter,107weight: KeybindingWeight.EditorContrib + 1108},109});110}111112override run(accessor: ServicesAccessor, ...args: unknown[]): void {113const widgetService = accessor.get(IChatWidgetService);114const widget = widgetService.lastFocusedWidget;115if (!widget?.viewModel) {116return;117}118119const inputValue = widget.getInput();120if (!inputValue.trim()) {121return;122}123124widget.acceptInput(undefined, { queue: ChatRequestQueueKind.Steering });125}126}127128export class ChatRemovePendingRequestAction extends Action2 {129static readonly ID = 'workbench.action.chat.removePendingRequest';130131constructor() {132super({133id: ChatRemovePendingRequestAction.ID,134title: localize2('chat.removePendingRequest', "Remove from Queue"),135icon: Codicon.close,136f1: false,137category: CHAT_CATEGORY,138menu: [{139id: MenuId.ChatMessageTitle,140group: 'navigation',141order: 4,142when: ContextKeyExpr.and(143queueingEnabledCondition,144ChatContextKeys.isRequest,145ChatContextKeys.isPendingRequest146)147}]148});149}150151override run(accessor: ServicesAccessor, ...args: unknown[]): void {152const chatService = accessor.get(IChatService);153const [context] = args;154155// Support both toolbar context (IChatRequestViewModel) and command context (IChatRemovePendingRequestContext)156if (isRequestVM(context) && context.pendingKind) {157chatService.removePendingRequest(context.sessionResource, context.id);158return;159}160161if (isRemovePendingRequestContext(context)) {162chatService.removePendingRequest(context.sessionResource, context.pendingRequestId);163return;164}165}166}167168export class ChatSendPendingImmediatelyAction extends Action2 {169static readonly ID = 'workbench.action.chat.sendPendingImmediately';170171constructor() {172super({173id: ChatSendPendingImmediatelyAction.ID,174title: localize2('chat.sendPendingImmediately', "Send Immediately"),175icon: Codicon.arrowUp,176f1: false,177category: CHAT_CATEGORY,178menu: [{179id: MenuId.ChatMessageTitle,180group: 'navigation',181order: 3,182when: ContextKeyExpr.and(183queueingEnabledCondition,184ChatContextKeys.isRequest,185ChatContextKeys.isPendingRequest186)187}]188});189}190191override run(accessor: ServicesAccessor, ...args: unknown[]): void {192const chatService = accessor.get(IChatService);193const widgetService = accessor.get(IChatWidgetService);194const [context] = args;195196if (!isRequestVM(context) || !context.pendingKind) {197return;198}199200const widget = widgetService.getWidgetBySessionResource(context.sessionResource);201const model = widget?.viewModel?.model;202if (!model) {203return;204}205206const pendingRequests = model.getPendingRequests();207const targetIndex = pendingRequests.findIndex(r => r.request.id === context.id);208if (targetIndex === -1) {209return;210}211212// Keep the target item's kind (queued vs steering)213const targetRequest = pendingRequests[targetIndex];214215// Reorder: move target to front, keep others in their relative order216const reordered = [217{ requestId: targetRequest.request.id, kind: targetRequest.kind },218...pendingRequests.filter((_, i) => i !== targetIndex).map(r => ({ requestId: r.request.id, kind: r.kind }))219];220221chatService.setPendingRequests(context.sessionResource, reordered);222chatService.cancelCurrentRequestForSession(context.sessionResource);223chatService.processPendingRequests(context.sessionResource);224}225}226227export class ChatRemoveAllPendingRequestsAction extends Action2 {228static readonly ID = 'workbench.action.chat.removeAllPendingRequests';229230constructor() {231super({232id: ChatRemoveAllPendingRequestsAction.ID,233title: localize2('chat.removeAllPendingRequests', "Remove All Queued"),234icon: Codicon.clearAll,235f1: false,236category: CHAT_CATEGORY,237menu: [{238id: MenuId.ChatContext,239group: 'navigation',240order: 3,241when: ContextKeyExpr.and(242queueingEnabledCondition,243ChatContextKeys.hasPendingRequests244)245}]246});247}248249override run(accessor: ServicesAccessor, ...args: unknown[]): void {250const chatService = accessor.get(IChatService);251const widgetService = accessor.get(IChatWidgetService);252const [context] = args;253254const widget = (isRequestVM(context) && widgetService.getWidgetBySessionResource(context.sessionResource)) || widgetService.lastFocusedWidget;255const model = widget?.viewModel?.model;256if (!model) {257return;258}259260for (const pendingRequest of [...model.getPendingRequests()]) {261chatService.removePendingRequest(model.sessionResource, pendingRequest.request.id);262}263}264}265266export function registerChatQueueActions(): void {267registerAction2(ChatQueueMessageAction);268registerAction2(ChatSteerWithMessageAction);269registerAction2(ChatRemovePendingRequestAction);270registerAction2(ChatSendPendingImmediatelyAction);271registerAction2(ChatRemoveAllPendingRequestsAction);272273// Register the queue submenu in the execute toolbar.274// The custom ChatQueuePickerActionItem (registered via IActionViewItemService)275// replaces the default rendering with a dropdown that shows hover descriptions.276// We still need items in ChatExecuteQueue so the menu system treats it as non-empty.277MenuRegistry.appendMenuItem(MenuId.ChatExecuteQueue, {278command: { id: ChatQueueMessageAction.ID, title: localize2('chat.queueMessage', "Add to Queue"), icon: Codicon.add },279group: 'navigation',280order: 1,281});282MenuRegistry.appendMenuItem(MenuId.ChatExecuteQueue, {283command: { id: ChatSteerWithMessageAction.ID, title: localize2('chat.steerWithMessage', "Steer with Message"), icon: Codicon.arrowRight },284group: 'navigation',285order: 2,286});287288MenuRegistry.appendMenuItem(MenuId.ChatExecute, {289submenu: MenuId.ChatExecuteQueue,290title: localize2('chat.queueSubmenu', "Queue"),291icon: Codicon.listOrdered,292when: queuingActionsPresent,293group: 'navigation',294order: 4,295});296}297298299