Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts
3296 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 { $ } from '../../../../../base/browser/dom.js';
7
import { Codicon } from '../../../../../base/common/codicons.js';
8
import { Iterable } from '../../../../../base/common/iterator.js';
9
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
10
import { markAsSingleton } from '../../../../../base/common/lifecycle.js';
11
import { ThemeIcon } from '../../../../../base/common/themables.js';
12
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
13
import { localize, localize2 } from '../../../../../nls.js';
14
import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';
15
import { MenuEntryActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';
16
import { Action2, MenuId, MenuItemAction, registerAction2 } from '../../../../../platform/actions/common/actions.js';
17
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
18
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
19
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
20
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
21
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../common/contributions.js';
22
import { ChatContextKeys } from '../../common/chatContextKeys.js';
23
import { IChatToolInvocation, ToolConfirmKind } from '../../common/chatService.js';
24
import { isResponseVM } from '../../common/chatViewModel.js';
25
import { ChatModeKind } from '../../common/constants.js';
26
import { IChatWidget, IChatWidgetService } from '../chat.js';
27
import { ToolsScope } from '../chatSelectedTools.js';
28
import { CHAT_CATEGORY } from './chatActions.js';
29
import { showToolsPicker } from './chatToolPicker.js';
30
31
32
type SelectedToolData = {
33
enabled: number;
34
total: number;
35
};
36
type SelectedToolClassification = {
37
owner: 'connor4312';
38
comment: 'Details the capabilities of the MCP server';
39
enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of enabled chat tools' };
40
total: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of total chat tools' };
41
};
42
43
export const AcceptToolConfirmationActionId = 'workbench.action.chat.acceptTool';
44
45
class AcceptToolConfirmation extends Action2 {
46
constructor() {
47
super({
48
id: AcceptToolConfirmationActionId,
49
title: localize2('chat.accept', "Accept"),
50
f1: false,
51
category: CHAT_CATEGORY,
52
keybinding: {
53
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasToolConfirmation),
54
primary: KeyMod.CtrlCmd | KeyCode.Enter,
55
// Override chatEditor.action.accept
56
weight: KeybindingWeight.WorkbenchContrib + 1,
57
},
58
});
59
}
60
61
run(accessor: ServicesAccessor, ...args: any[]) {
62
const chatWidgetService = accessor.get(IChatWidgetService);
63
const widget = chatWidgetService.lastFocusedWidget;
64
const lastItem = widget?.viewModel?.getItems().at(-1);
65
if (!isResponseVM(lastItem)) {
66
return;
67
}
68
69
const unconfirmedToolInvocation = lastItem.model.response.value.find((item): item is IChatToolInvocation => item.kind === 'toolInvocation' && item.isConfirmed === undefined);
70
if (unconfirmedToolInvocation) {
71
unconfirmedToolInvocation.confirmed.complete({ type: ToolConfirmKind.UserAction });
72
}
73
74
// Return focus to the chat input, in case it was in the tool confirmation editor
75
widget?.focusInput();
76
}
77
}
78
79
class ConfigureToolsAction extends Action2 {
80
public static ID = 'workbench.action.chat.configureTools';
81
82
constructor() {
83
super({
84
id: ConfigureToolsAction.ID,
85
title: localize('label', "Configure Tools..."),
86
icon: Codicon.tools,
87
f1: false,
88
category: CHAT_CATEGORY,
89
precondition: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
90
menu: [{
91
when: ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ChatContextKeys.lockedToCodingAgent.negate()),
92
id: MenuId.ChatExecute,
93
group: 'navigation',
94
order: 1,
95
}]
96
});
97
}
98
99
override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {
100
101
const instaService = accessor.get(IInstantiationService);
102
const chatWidgetService = accessor.get(IChatWidgetService);
103
const telemetryService = accessor.get(ITelemetryService);
104
105
let widget = chatWidgetService.lastFocusedWidget;
106
if (!widget) {
107
type ChatActionContext = { widget: IChatWidget };
108
function isChatActionContext(obj: any): obj is ChatActionContext {
109
return obj && typeof obj === 'object' && (obj as ChatActionContext).widget;
110
}
111
const context = args[0];
112
if (isChatActionContext(context)) {
113
widget = context.widget;
114
}
115
}
116
117
if (!widget) {
118
return;
119
}
120
121
let placeholder;
122
let description;
123
const { entriesScope, entriesMap } = widget.input.selectedToolsModel;
124
switch (entriesScope) {
125
case ToolsScope.Session:
126
placeholder = localize('chat.tools.placeholder.session', "Select tools for this chat session");
127
description = localize('chat.tools.description.session', "The selected tools were configured by a prompt command and only apply to this chat session.");
128
break;
129
case ToolsScope.Mode:
130
placeholder = localize('chat.tools.placeholder.mode', "Select tools for this chat mode");
131
description = 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);
132
break;
133
case ToolsScope.Global:
134
placeholder = localize('chat.tools.placeholder.global', "Select tools that are available to chat.");
135
description = undefined;
136
break;
137
}
138
139
const result = await instaService.invokeFunction(showToolsPicker, placeholder, description, entriesMap.get());
140
if (result) {
141
widget.input.selectedToolsModel.set(result, false);
142
}
143
144
const tools = widget.input.selectedToolsModel.entriesMap.get();
145
telemetryService.publicLog2<SelectedToolData, SelectedToolClassification>('chat/selectedTools', {
146
total: tools.size,
147
enabled: Iterable.reduce(tools, (prev, [_, enabled]) => enabled ? prev + 1 : prev, 0),
148
});
149
}
150
}
151
152
class ConfigureToolsActionRendering implements IWorkbenchContribution {
153
154
static readonly ID = 'chat.configureToolsActionRendering';
155
156
constructor(
157
@IActionViewItemService actionViewItemService: IActionViewItemService,
158
) {
159
const disposable = actionViewItemService.register(MenuId.ChatExecute, ConfigureToolsAction.ID, (action, _opts, instantiationService) => {
160
if (!(action instanceof MenuItemAction)) {
161
return undefined;
162
}
163
return instantiationService.createInstance(class extends MenuEntryActionViewItem {
164
private warningElement!: HTMLElement;
165
166
override render(container: HTMLElement): void {
167
super.render(container);
168
169
// Add warning indicator element
170
this.warningElement = $(`.tool-warning-indicator${ThemeIcon.asCSSSelector(Codicon.warning)}`);
171
this.warningElement.style.display = 'none';
172
container.appendChild(this.warningElement);
173
container.style.position = 'relative';
174
175
// Set up context key listeners
176
this.updateWarningState();
177
this._register(this._contextKeyService.onDidChangeContext(() => {
178
this.updateWarningState();
179
}));
180
}
181
182
private updateWarningState(): void {
183
const wasShown = this.warningElement.style.display === 'block';
184
const shouldBeShown = this.isAboveToolLimit();
185
186
if (!wasShown && shouldBeShown) {
187
this.warningElement.style.display = 'block';
188
this.updateTooltip();
189
} else if (wasShown && !shouldBeShown) {
190
this.warningElement.style.display = 'none';
191
this.updateTooltip();
192
}
193
}
194
195
protected override getTooltip(): string {
196
if (this.isAboveToolLimit()) {
197
const warningMessage = localize('chatTools.tooManyEnabled', 'More than {0} tools are enabled, you may experience degraded tool calling.', this._contextKeyService.getContextKeyValue(ChatContextKeys.chatToolGroupingThreshold.key));
198
return `${warningMessage}`;
199
}
200
201
return super.getTooltip();
202
}
203
204
private isAboveToolLimit() {
205
const rawToolLimit = this._contextKeyService.getContextKeyValue(ChatContextKeys.chatToolGroupingThreshold.key);
206
const rawToolCount = this._contextKeyService.getContextKeyValue(ChatContextKeys.chatToolCount.key);
207
if (rawToolLimit === undefined || rawToolCount === undefined) {
208
return false;
209
}
210
211
const toolLimit = Number(rawToolLimit || 0);
212
const toolCount = Number(rawToolCount || 0);
213
return toolCount > toolLimit;
214
}
215
}, action, undefined);
216
});
217
218
// Reduces flicker a bit on reload/restart
219
markAsSingleton(disposable);
220
}
221
}
222
223
export function registerChatToolActions() {
224
registerAction2(AcceptToolConfirmation);
225
registerAction2(ConfigureToolsAction);
226
registerWorkbenchContribution2(ConfigureToolsActionRendering.ID, ConfigureToolsActionRendering, WorkbenchPhase.BlockRestore);
227
}
228
229