Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts
5283 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 { CancellationTokenSource } from '../../../../../base/common/cancellation.js';6import { Codicon } from '../../../../../base/common/codicons.js';7import { Iterable } from '../../../../../base/common/iterator.js';8import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';9import { autorun } from '../../../../../base/common/observable.js';10import { URI } from '../../../../../base/common/uri.js';11import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';12import { localize, localize2 } from '../../../../../nls.js';13import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';14import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';15import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';16import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';17import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';18import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';19import { ConfirmedReason, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js';20import { isResponseVM } from '../../common/model/chatViewModel.js';21import { ChatConfiguration, ChatModeKind } from '../../common/constants.js';22import { IChatWidget, IChatWidgetService } from '../chat.js';23import { ToolsScope } from '../widget/input/chatSelectedTools.js';24import { CHAT_CATEGORY } from './chatActions.js';25import { showToolsPicker } from './chatToolPicker.js';262728type SelectedToolData = {29enabled: number;30total: number;31};32type SelectedToolClassification = {33owner: 'connor4312';34comment: 'Details the capabilities of the MCP server';35enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of enabled chat tools' };36total: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of total chat tools' };37};3839export const AcceptToolConfirmationActionId = 'workbench.action.chat.acceptTool';40export const SkipToolConfirmationActionId = 'workbench.action.chat.skipTool';41export const AcceptToolPostConfirmationActionId = 'workbench.action.chat.acceptToolPostExecution';42export const SkipToolPostConfirmationActionId = 'workbench.action.chat.skipToolPostExecution';4344export interface IToolConfirmationActionContext {45readonly sessionResource?: URI;46}4748abstract class ToolConfirmationAction extends Action2 {49protected abstract getReason(): ConfirmedReason;5051run(accessor: ServicesAccessor, context?: IToolConfirmationActionContext) {52const chatWidgetService = accessor.get(IChatWidgetService);53const widget = context?.sessionResource54? chatWidgetService.getWidgetBySessionResource(context.sessionResource)55: chatWidgetService.lastFocusedWidget;56const lastItem = widget?.viewModel?.getItems().at(-1);57if (!isResponseVM(lastItem)) {58return;59}6061for (const item of lastItem.model.response.value) {62const state = item.kind === 'toolInvocation' ? item.state.get() : undefined;63if (state?.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state?.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {64state.confirm(this.getReason());65break;66}67}6869// Return focus to the chat input, in case it was in the tool confirmation editor70widget?.focusInput();71}72}7374class AcceptToolConfirmation extends ToolConfirmationAction {75constructor() {76super({77id: AcceptToolConfirmationActionId,78title: localize2('chat.accept', "Accept"),79f1: false,80category: CHAT_CATEGORY,81keybinding: {82when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasToolConfirmation),83primary: KeyMod.CtrlCmd | KeyCode.Enter,84// Override chatEditor.action.accept85weight: KeybindingWeight.WorkbenchContrib + 1,86},87});88}8990protected override getReason(): ConfirmedReason {91return { type: ToolConfirmKind.UserAction };92}93}9495class SkipToolConfirmation extends ToolConfirmationAction {96constructor() {97super({98id: SkipToolConfirmationActionId,99title: localize2('chat.skip', "Skip"),100f1: false,101category: CHAT_CATEGORY,102keybinding: {103when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasToolConfirmation),104primary: KeyMod.CtrlCmd | KeyCode.Enter | KeyMod.Alt,105// Override chatEditor.action.accept106weight: KeybindingWeight.WorkbenchContrib + 1,107},108});109}110111protected override getReason(): ConfirmedReason {112return { type: ToolConfirmKind.Skipped };113}114}115116class ConfigureToolsAction extends Action2 {117public static ID = 'workbench.action.chat.configureTools';118119constructor() {120super({121id: ConfigureToolsAction.ID,122title: localize('label', "Configure Tools..."),123icon: Codicon.tools,124f1: false,125category: CHAT_CATEGORY,126precondition: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),127menu: [{128when: ContextKeyExpr.and(129ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),130ChatContextKeys.lockedToCodingAgent.negate(),131ContextKeyExpr.notEquals(`config.${ChatConfiguration.AlternativeToolAction}`, true)132),133id: MenuId.ChatInput,134group: 'navigation',135order: 100,136}]137});138}139140override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {141142const instaService = accessor.get(IInstantiationService);143const chatWidgetService = accessor.get(IChatWidgetService);144const telemetryService = accessor.get(ITelemetryService);145146let widget = chatWidgetService.lastFocusedWidget;147if (!widget) {148widget = this.extractWidget(args);149}150151if (!widget) {152return;153}154155const source = this.extractSource(args) ?? 'chatInput';156157let placeholder;158let description;159const { entriesScope, entriesMap } = widget.input.selectedToolsModel;160switch (entriesScope) {161case ToolsScope.Session:162placeholder = localize('chat.tools.placeholder.session', "Select tools for this chat session");163description = localize('chat.tools.description.session', "The selected tools were configured only for this chat session.");164break;165case ToolsScope.Agent:166placeholder = localize('chat.tools.placeholder.agent', "Select tools for this custom agent");167description = localize('chat.tools.description.agent', "The selected tools are configured by the '{0}' custom agent. Changes to the tools will be applied to the custom agent file as well.", widget.input.currentModeObs.get().label.get());168break;169case ToolsScope.Agent_ReadOnly:170placeholder = localize('chat.tools.placeholder.readOnlyAgent', "Select tools for this custom agent");171description = localize('chat.tools.description.readOnlyAgent', "The selected tools are configured by the '{0}' custom agent. Changes to the tools will only be used for this session and will not change the '{0}' custom agent.", widget.input.currentModeObs.get().label.get());172break;173case ToolsScope.Global:174placeholder = localize('chat.tools.placeholder.global', "Select tools that are available to chat.");175description = localize('chat.tools.description.global', "The selected tools will be applied globally for all chat sessions that use the default agent.");176break;177178}179180// Create a cancellation token that cancels when the mode changes181const cts = new CancellationTokenSource();182const initialMode = widget.input.currentModeObs.get();183const modeListener = autorun(reader => {184if (initialMode.id !== widget.input.currentModeObs.read(reader).id) {185cts.cancel();186}187});188189try {190const result = await instaService.invokeFunction(showToolsPicker, placeholder, source, description, () => entriesMap.get(), widget.input.selectedLanguageModel.get()?.metadata, cts.token);191if (result) {192widget.input.selectedToolsModel.set(result, false);193}194} finally {195modeListener.dispose();196cts.dispose();197}198199const tools = widget.input.selectedToolsModel.entriesMap.get();200telemetryService.publicLog2<SelectedToolData, SelectedToolClassification>('chat/selectedTools', {201total: tools.size,202enabled: Iterable.reduce(tools, (prev, [_, enabled]) => enabled ? prev + 1 : prev, 0),203});204}205206private extractWidget(args: unknown[]): IChatWidget | undefined {207type ChatActionContext = { widget: IChatWidget };208function isChatActionContext(obj: unknown): obj is ChatActionContext {209return !!obj && typeof obj === 'object' && !!(obj as ChatActionContext).widget;210}211212for (const arg of args) {213if (isChatActionContext(arg)) {214return arg.widget;215}216}217218return undefined;219}220221private extractSource(args: unknown[]): string | undefined {222type ChatActionSource = { source: string };223function isChatActionSource(obj: unknown): obj is ChatActionSource {224return !!obj && typeof obj === 'object' && !!(obj as ChatActionSource).source;225}226227for (const arg of args) {228if (isChatActionSource(arg)) {229return arg.source;230}231}232233return undefined;234}235}236237export function registerChatToolActions() {238registerAction2(AcceptToolConfirmation);239registerAction2(SkipToolConfirmation);240registerAction2(ConfigureToolsAction);241}242243244