Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts
3296 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 { $ } from '../../../../../base/browser/dom.js';6import { Codicon } from '../../../../../base/common/codicons.js';7import { Iterable } from '../../../../../base/common/iterator.js';8import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';9import { markAsSingleton } from '../../../../../base/common/lifecycle.js';10import { ThemeIcon } from '../../../../../base/common/themables.js';11import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';12import { localize, localize2 } from '../../../../../nls.js';13import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';14import { MenuEntryActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';15import { Action2, MenuId, MenuItemAction, registerAction2 } from '../../../../../platform/actions/common/actions.js';16import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';17import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';18import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';19import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';20import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js';21import { ChatContextKeys } from '../../common/chatContextKeys.js';22import { IChatToolInvocation, ToolConfirmKind } from '../../common/chatService.js';23import { isResponseVM } from '../../common/chatViewModel.js';24import { ChatModeKind } from '../../common/constants.js';25import { IChatWidget, IChatWidgetService } from '../chat.js';26import { ToolsScope } from '../chatSelectedTools.js';27import { CHAT_CATEGORY } from './chatActions.js';28import { showToolsPicker } from './chatToolPicker.js';293031type SelectedToolData = {32enabled: number;33total: number;34};35type SelectedToolClassification = {36owner: 'connor4312';37comment: 'Details the capabilities of the MCP server';38enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of enabled chat tools' };39total: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of total chat tools' };40};4142export const AcceptToolConfirmationActionId = 'workbench.action.chat.acceptTool';4344class AcceptToolConfirmation extends Action2 {45constructor() {46super({47id: AcceptToolConfirmationActionId,48title: localize2('chat.accept', "Accept"),49f1: false,50category: CHAT_CATEGORY,51keybinding: {52when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasToolConfirmation),53primary: KeyMod.CtrlCmd | KeyCode.Enter,54// Override chatEditor.action.accept55weight: KeybindingWeight.WorkbenchContrib + 1,56},57});58}5960run(accessor: ServicesAccessor, ...args: any[]) {61const chatWidgetService = accessor.get(IChatWidgetService);62const widget = chatWidgetService.lastFocusedWidget;63const lastItem = widget?.viewModel?.getItems().at(-1);64if (!isResponseVM(lastItem)) {65return;66}6768const unconfirmedToolInvocation = lastItem.model.response.value.find((item): item is IChatToolInvocation => item.kind === 'toolInvocation' && item.isConfirmed === undefined);69if (unconfirmedToolInvocation) {70unconfirmedToolInvocation.confirmed.complete({ type: ToolConfirmKind.UserAction });71}7273// Return focus to the chat input, in case it was in the tool confirmation editor74widget?.focusInput();75}76}7778class ConfigureToolsAction extends Action2 {79public static ID = 'workbench.action.chat.configureTools';8081constructor() {82super({83id: ConfigureToolsAction.ID,84title: localize('label', "Configure Tools..."),85icon: Codicon.tools,86f1: false,87category: CHAT_CATEGORY,88precondition: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),89menu: [{90when: ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ChatContextKeys.lockedToCodingAgent.negate()),91id: MenuId.ChatExecute,92group: 'navigation',93order: 1,94}]95});96}9798override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {99100const instaService = accessor.get(IInstantiationService);101const chatWidgetService = accessor.get(IChatWidgetService);102const telemetryService = accessor.get(ITelemetryService);103104let widget = chatWidgetService.lastFocusedWidget;105if (!widget) {106type ChatActionContext = { widget: IChatWidget };107function isChatActionContext(obj: any): obj is ChatActionContext {108return obj && typeof obj === 'object' && (obj as ChatActionContext).widget;109}110const context = args[0];111if (isChatActionContext(context)) {112widget = context.widget;113}114}115116if (!widget) {117return;118}119120let placeholder;121let description;122const { entriesScope, entriesMap } = widget.input.selectedToolsModel;123switch (entriesScope) {124case ToolsScope.Session:125placeholder = localize('chat.tools.placeholder.session', "Select tools for this chat session");126description = localize('chat.tools.description.session', "The selected tools were configured by a prompt command and only apply to this chat session.");127break;128case ToolsScope.Mode:129placeholder = localize('chat.tools.placeholder.mode', "Select tools for this chat mode");130description = localize('chat.tools.description.mode', "The selected tools are configured by the '{0}' chat mode. Changes to the tools will be applied to the mode file as well.", widget.input.currentModeObs.get().label);131break;132case ToolsScope.Global:133placeholder = localize('chat.tools.placeholder.global', "Select tools that are available to chat.");134description = undefined;135break;136}137138const result = await instaService.invokeFunction(showToolsPicker, placeholder, description, entriesMap.get());139if (result) {140widget.input.selectedToolsModel.set(result, false);141}142143const tools = widget.input.selectedToolsModel.entriesMap.get();144telemetryService.publicLog2<SelectedToolData, SelectedToolClassification>('chat/selectedTools', {145total: tools.size,146enabled: Iterable.reduce(tools, (prev, [_, enabled]) => enabled ? prev + 1 : prev, 0),147});148}149}150151class ConfigureToolsActionRendering implements IWorkbenchContribution {152153static readonly ID = 'chat.configureToolsActionRendering';154155constructor(156@IActionViewItemService actionViewItemService: IActionViewItemService,157) {158const disposable = actionViewItemService.register(MenuId.ChatExecute, ConfigureToolsAction.ID, (action, _opts, instantiationService) => {159if (!(action instanceof MenuItemAction)) {160return undefined;161}162return instantiationService.createInstance(class extends MenuEntryActionViewItem {163private warningElement!: HTMLElement;164165override render(container: HTMLElement): void {166super.render(container);167168// Add warning indicator element169this.warningElement = $(`.tool-warning-indicator${ThemeIcon.asCSSSelector(Codicon.warning)}`);170this.warningElement.style.display = 'none';171container.appendChild(this.warningElement);172container.style.position = 'relative';173174// Set up context key listeners175this.updateWarningState();176this._register(this._contextKeyService.onDidChangeContext(() => {177this.updateWarningState();178}));179}180181private updateWarningState(): void {182const wasShown = this.warningElement.style.display === 'block';183const shouldBeShown = this.isAboveToolLimit();184185if (!wasShown && shouldBeShown) {186this.warningElement.style.display = 'block';187this.updateTooltip();188} else if (wasShown && !shouldBeShown) {189this.warningElement.style.display = 'none';190this.updateTooltip();191}192}193194protected override getTooltip(): string {195if (this.isAboveToolLimit()) {196const warningMessage = localize('chatTools.tooManyEnabled', 'More than {0} tools are enabled, you may experience degraded tool calling.', this._contextKeyService.getContextKeyValue(ChatContextKeys.chatToolGroupingThreshold.key));197return `${warningMessage}`;198}199200return super.getTooltip();201}202203private isAboveToolLimit() {204const rawToolLimit = this._contextKeyService.getContextKeyValue(ChatContextKeys.chatToolGroupingThreshold.key);205const rawToolCount = this._contextKeyService.getContextKeyValue(ChatContextKeys.chatToolCount.key);206if (rawToolLimit === undefined || rawToolCount === undefined) {207return false;208}209210const toolLimit = Number(rawToolLimit || 0);211const toolCount = Number(rawToolCount || 0);212return toolCount > toolLimit;213}214}, action, undefined);215});216217// Reduces flicker a bit on reload/restart218markAsSingleton(disposable);219}220}221222export function registerChatToolActions() {223registerAction2(AcceptToolConfirmation);224registerAction2(ConfigureToolsAction);225registerWorkbenchContribution2(ConfigureToolsActionRendering.ID, ConfigureToolsActionRendering, WorkbenchPhase.BlockRestore);226}227228229