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
5283 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 { CancellationTokenSource } from '../../../../../base/common/cancellation.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 { autorun } from '../../../../../base/common/observable.js';
11
import { URI } from '../../../../../base/common/uri.js';
12
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
13
import { localize, localize2 } from '../../../../../nls.js';
14
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
15
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
16
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
17
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
18
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
19
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
20
import { ConfirmedReason, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js';
21
import { isResponseVM } from '../../common/model/chatViewModel.js';
22
import { ChatConfiguration, ChatModeKind } from '../../common/constants.js';
23
import { IChatWidget, IChatWidgetService } from '../chat.js';
24
import { ToolsScope } from '../widget/input/chatSelectedTools.js';
25
import { CHAT_CATEGORY } from './chatActions.js';
26
import { showToolsPicker } from './chatToolPicker.js';
27
28
29
type SelectedToolData = {
30
enabled: number;
31
total: number;
32
};
33
type SelectedToolClassification = {
34
owner: 'connor4312';
35
comment: 'Details the capabilities of the MCP server';
36
enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of enabled chat tools' };
37
total: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of total chat tools' };
38
};
39
40
export const AcceptToolConfirmationActionId = 'workbench.action.chat.acceptTool';
41
export const SkipToolConfirmationActionId = 'workbench.action.chat.skipTool';
42
export const AcceptToolPostConfirmationActionId = 'workbench.action.chat.acceptToolPostExecution';
43
export const SkipToolPostConfirmationActionId = 'workbench.action.chat.skipToolPostExecution';
44
45
export interface IToolConfirmationActionContext {
46
readonly sessionResource?: URI;
47
}
48
49
abstract class ToolConfirmationAction extends Action2 {
50
protected abstract getReason(): ConfirmedReason;
51
52
run(accessor: ServicesAccessor, context?: IToolConfirmationActionContext) {
53
const chatWidgetService = accessor.get(IChatWidgetService);
54
const widget = context?.sessionResource
55
? chatWidgetService.getWidgetBySessionResource(context.sessionResource)
56
: chatWidgetService.lastFocusedWidget;
57
const lastItem = widget?.viewModel?.getItems().at(-1);
58
if (!isResponseVM(lastItem)) {
59
return;
60
}
61
62
for (const item of lastItem.model.response.value) {
63
const state = item.kind === 'toolInvocation' ? item.state.get() : undefined;
64
if (state?.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state?.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {
65
state.confirm(this.getReason());
66
break;
67
}
68
}
69
70
// Return focus to the chat input, in case it was in the tool confirmation editor
71
widget?.focusInput();
72
}
73
}
74
75
class AcceptToolConfirmation extends ToolConfirmationAction {
76
constructor() {
77
super({
78
id: AcceptToolConfirmationActionId,
79
title: localize2('chat.accept', "Accept"),
80
f1: false,
81
category: CHAT_CATEGORY,
82
keybinding: {
83
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasToolConfirmation),
84
primary: KeyMod.CtrlCmd | KeyCode.Enter,
85
// Override chatEditor.action.accept
86
weight: KeybindingWeight.WorkbenchContrib + 1,
87
},
88
});
89
}
90
91
protected override getReason(): ConfirmedReason {
92
return { type: ToolConfirmKind.UserAction };
93
}
94
}
95
96
class SkipToolConfirmation extends ToolConfirmationAction {
97
constructor() {
98
super({
99
id: SkipToolConfirmationActionId,
100
title: localize2('chat.skip', "Skip"),
101
f1: false,
102
category: CHAT_CATEGORY,
103
keybinding: {
104
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasToolConfirmation),
105
primary: KeyMod.CtrlCmd | KeyCode.Enter | KeyMod.Alt,
106
// Override chatEditor.action.accept
107
weight: KeybindingWeight.WorkbenchContrib + 1,
108
},
109
});
110
}
111
112
protected override getReason(): ConfirmedReason {
113
return { type: ToolConfirmKind.Skipped };
114
}
115
}
116
117
class ConfigureToolsAction extends Action2 {
118
public static ID = 'workbench.action.chat.configureTools';
119
120
constructor() {
121
super({
122
id: ConfigureToolsAction.ID,
123
title: localize('label', "Configure Tools..."),
124
icon: Codicon.tools,
125
f1: false,
126
category: CHAT_CATEGORY,
127
precondition: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
128
menu: [{
129
when: ContextKeyExpr.and(
130
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
131
ChatContextKeys.lockedToCodingAgent.negate(),
132
ContextKeyExpr.notEquals(`config.${ChatConfiguration.AlternativeToolAction}`, true)
133
),
134
id: MenuId.ChatInput,
135
group: 'navigation',
136
order: 100,
137
}]
138
});
139
}
140
141
override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
142
143
const instaService = accessor.get(IInstantiationService);
144
const chatWidgetService = accessor.get(IChatWidgetService);
145
const telemetryService = accessor.get(ITelemetryService);
146
147
let widget = chatWidgetService.lastFocusedWidget;
148
if (!widget) {
149
widget = this.extractWidget(args);
150
}
151
152
if (!widget) {
153
return;
154
}
155
156
const source = this.extractSource(args) ?? 'chatInput';
157
158
let placeholder;
159
let description;
160
const { entriesScope, entriesMap } = widget.input.selectedToolsModel;
161
switch (entriesScope) {
162
case ToolsScope.Session:
163
placeholder = localize('chat.tools.placeholder.session', "Select tools for this chat session");
164
description = localize('chat.tools.description.session', "The selected tools were configured only for this chat session.");
165
break;
166
case ToolsScope.Agent:
167
placeholder = localize('chat.tools.placeholder.agent', "Select tools for this custom agent");
168
description = 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());
169
break;
170
case ToolsScope.Agent_ReadOnly:
171
placeholder = localize('chat.tools.placeholder.readOnlyAgent', "Select tools for this custom agent");
172
description = 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());
173
break;
174
case ToolsScope.Global:
175
placeholder = localize('chat.tools.placeholder.global', "Select tools that are available to chat.");
176
description = localize('chat.tools.description.global', "The selected tools will be applied globally for all chat sessions that use the default agent.");
177
break;
178
179
}
180
181
// Create a cancellation token that cancels when the mode changes
182
const cts = new CancellationTokenSource();
183
const initialMode = widget.input.currentModeObs.get();
184
const modeListener = autorun(reader => {
185
if (initialMode.id !== widget.input.currentModeObs.read(reader).id) {
186
cts.cancel();
187
}
188
});
189
190
try {
191
const result = await instaService.invokeFunction(showToolsPicker, placeholder, source, description, () => entriesMap.get(), widget.input.selectedLanguageModel.get()?.metadata, cts.token);
192
if (result) {
193
widget.input.selectedToolsModel.set(result, false);
194
}
195
} finally {
196
modeListener.dispose();
197
cts.dispose();
198
}
199
200
const tools = widget.input.selectedToolsModel.entriesMap.get();
201
telemetryService.publicLog2<SelectedToolData, SelectedToolClassification>('chat/selectedTools', {
202
total: tools.size,
203
enabled: Iterable.reduce(tools, (prev, [_, enabled]) => enabled ? prev + 1 : prev, 0),
204
});
205
}
206
207
private extractWidget(args: unknown[]): IChatWidget | undefined {
208
type ChatActionContext = { widget: IChatWidget };
209
function isChatActionContext(obj: unknown): obj is ChatActionContext {
210
return !!obj && typeof obj === 'object' && !!(obj as ChatActionContext).widget;
211
}
212
213
for (const arg of args) {
214
if (isChatActionContext(arg)) {
215
return arg.widget;
216
}
217
}
218
219
return undefined;
220
}
221
222
private extractSource(args: unknown[]): string | undefined {
223
type ChatActionSource = { source: string };
224
function isChatActionSource(obj: unknown): obj is ChatActionSource {
225
return !!obj && typeof obj === 'object' && !!(obj as ChatActionSource).source;
226
}
227
228
for (const arg of args) {
229
if (isChatActionSource(arg)) {
230
return arg.source;
231
}
232
}
233
234
return undefined;
235
}
236
}
237
238
export function registerChatToolActions() {
239
registerAction2(AcceptToolConfirmation);
240
registerAction2(SkipToolConfirmation);
241
registerAction2(ConfigureToolsAction);
242
}
243
244