Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts
5272 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
import { assertNever } from '../../../../../base/common/assert.js';
6
import { CancellationToken } from '../../../../../base/common/cancellation.js';
7
import { Codicon } from '../../../../../base/common/codicons.js';
8
import { Emitter, Event } from '../../../../../base/common/event.js';
9
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
10
import { ThemeIcon } from '../../../../../base/common/themables.js';
11
import { URI } from '../../../../../base/common/uri.js';
12
import { localize } from '../../../../../nls.js';
13
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
14
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
15
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
16
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickTreeItem } from '../../../../../platform/quickinput/common/quickInput.js';
17
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
18
import { IEditorService } from '../../../../services/editor/common/editorService.js';
19
import { ExtensionEditorTab, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
20
import { McpCommandIds } from '../../../mcp/common/mcpCommandIds.js';
21
import { IMcpRegistry } from '../../../mcp/common/mcpRegistryTypes.js';
22
import { IMcpServer, IMcpService, IMcpWorkbenchService, McpConnectionState, McpServerCacheState, McpServerEditorTab } from '../../../mcp/common/mcpTypes.js';
23
import { startServerAndWaitForLiveTools } from '../../../mcp/common/mcpTypesUtils.js';
24
import { ILanguageModelChatMetadata } from '../../common/languageModels.js';
25
import { ILanguageModelToolsService, IToolData, IToolSet, ToolDataSource, ToolSet } from '../../common/tools/languageModelToolsService.js';
26
import { ConfigureToolSets } from '../tools/toolSetsContribution.js';
27
28
const enum BucketOrdinal { User, BuiltIn, Mcp, Extension }
29
30
// Legacy QuickPick types (existing implementation)
31
type BucketPick = IQuickPickItem & { picked: boolean; ordinal: BucketOrdinal; status?: string; toolset?: ToolSet; children: (ToolPick | ToolSetPick)[] };
32
type ToolSetPick = IQuickPickItem & { picked: boolean; toolset: ToolSet; parent: BucketPick };
33
type ToolPick = IQuickPickItem & { picked: boolean; tool: IToolData; parent: BucketPick };
34
type ActionableButton = IQuickInputButton & { action: () => void };
35
36
// New QuickTree types for tree-based implementation
37
38
/**
39
* Base interface for all tree items in the QuickTree implementation.
40
* Extends IQuickTreeItem with common properties for tool picker items.
41
*/
42
interface IToolTreeItem extends IQuickTreeItem {
43
readonly itemType: 'bucket' | 'toolset' | 'tool' | 'callback';
44
readonly ordinal?: BucketOrdinal;
45
readonly buttons?: readonly ActionableButton[];
46
}
47
48
/**
49
* Bucket tree item - represents a category of tools (User, BuiltIn, MCP Server, Extension).
50
* For MCP servers, the bucket directly represents the server and stores the toolset.
51
*/
52
interface IBucketTreeItem extends IToolTreeItem {
53
readonly itemType: 'bucket';
54
readonly ordinal: BucketOrdinal;
55
toolset?: IToolSet; // For MCP servers where the bucket represents the ToolSet - mutable
56
readonly status?: string;
57
readonly children: AnyTreeItem[];
58
checked: boolean | 'mixed' | undefined;
59
readonly sortOrder: number;
60
}
61
62
/**
63
* ToolSet tree item - represents a collection of tools that can be managed together.
64
* Used for regular (non-MCP) toolsets that appear as intermediate nodes in the tree.
65
*/
66
interface IToolSetTreeItem extends IToolTreeItem {
67
readonly itemType: 'toolset';
68
readonly toolset: IToolSet;
69
children: AnyTreeItem[] | undefined;
70
checked: boolean | 'mixed';
71
}
72
73
/**
74
* Tool tree item - represents an individual tool that can be selected/deselected.
75
* This is a leaf node in the tree structure.
76
*/
77
interface IToolTreeItemData extends IToolTreeItem {
78
readonly itemType: 'tool';
79
readonly tool: IToolData;
80
checked: boolean;
81
}
82
83
/**
84
* Callback tree item - represents action items like "Add MCP Server" or "Configure Tool Sets".
85
* These are non-selectable items that execute actions when clicked. Can return
86
* false to keep the picker open.
87
*/
88
interface ICallbackTreeItem extends IToolTreeItem {
89
readonly itemType: 'callback';
90
readonly run: () => boolean | void;
91
readonly pickable: false;
92
}
93
94
type AnyTreeItem = IBucketTreeItem | IToolSetTreeItem | IToolTreeItemData | ICallbackTreeItem;
95
96
// Type guards for new QuickTree types
97
function isBucketTreeItem(item: AnyTreeItem): item is IBucketTreeItem {
98
return item.itemType === 'bucket';
99
}
100
function isToolSetTreeItem(item: AnyTreeItem): item is IToolSetTreeItem {
101
return item.itemType === 'toolset';
102
}
103
function isToolTreeItem(item: AnyTreeItem): item is IToolTreeItemData {
104
return item.itemType === 'tool';
105
}
106
function isCallbackTreeItem(item: AnyTreeItem): item is ICallbackTreeItem {
107
return item.itemType === 'callback';
108
}
109
110
/**
111
* Maps different icon types (ThemeIcon or URI-based) to QuickTreeItem icon properties.
112
* Handles the conversion between ToolSet/IToolData icon formats and tree item requirements.
113
* Provides a default tool icon when no icon is specified.
114
*
115
* @param icon - Icon to map (ThemeIcon, URI object, or undefined)
116
* @param useDefaultToolIcon - Whether to use a default tool icon when none is provided
117
* @returns Object with iconClass (for ThemeIcon) or iconPath (for URIs) properties
118
*/
119
function mapIconToTreeItem(icon: ThemeIcon | { dark: URI; light?: URI } | undefined, useDefaultToolIcon: boolean = false): Pick<IQuickTreeItem, 'iconClass' | 'iconPath'> {
120
if (!icon) {
121
if (useDefaultToolIcon) {
122
return { iconClass: ThemeIcon.asClassName(Codicon.tools) };
123
}
124
return {};
125
}
126
127
if (ThemeIcon.isThemeIcon(icon)) {
128
return { iconClass: ThemeIcon.asClassName(icon) };
129
} else {
130
return { iconPath: icon };
131
}
132
}
133
134
function createToolTreeItemFromData(tool: IToolData, checked: boolean): IToolTreeItemData {
135
const iconProps = mapIconToTreeItem(tool.icon, true); // Use default tool icon if none provided
136
137
return {
138
itemType: 'tool',
139
tool,
140
id: tool.id,
141
label: tool.toolReferenceName ?? tool.displayName,
142
description: tool.userDescription ?? tool.modelDescription,
143
checked,
144
...iconProps
145
};
146
}
147
148
function createToolSetTreeItem(toolset: IToolSet, checked: boolean, editorService: IEditorService): IToolSetTreeItem {
149
const iconProps = mapIconToTreeItem(toolset.icon);
150
const buttons = [];
151
if (toolset.source.type === 'user') {
152
const resource = toolset.source.file;
153
buttons.push({
154
iconClass: ThemeIcon.asClassName(Codicon.edit),
155
tooltip: localize('editUserBucket', "Edit Tool Set"),
156
action: () => editorService.openEditor({ resource })
157
});
158
}
159
return {
160
itemType: 'toolset',
161
toolset,
162
buttons,
163
id: toolset.id,
164
label: toolset.referenceName,
165
description: toolset.description,
166
checked,
167
children: undefined,
168
collapsed: true,
169
...iconProps
170
};
171
}
172
173
/**
174
* New QuickTree implementation of the tools picker.
175
* Uses IQuickTree to provide a true hierarchical tree structure with:
176
* - Collapsible nodes for buckets and toolsets
177
* - Checkbox state management with parent-child relationships
178
* - Special handling for MCP servers (server as bucket, tools as direct children)
179
* - Built-in filtering and search capabilities
180
*
181
* @param accessor - Service accessor for dependency injection
182
* @param placeHolder - Placeholder text shown in the picker
183
* @param description - Optional description text shown in the picker
184
* @param toolsEntries - Optional initial selection state for tools and toolsets
185
* @param modelId - Optional model ID to filter tools by supported models
186
* @param onUpdate - Optional callback fired when the selection changes
187
* @param token - Optional cancellation token to close the picker when cancelled
188
* @returns Promise resolving to the final selection map, or undefined if cancelled
189
*/
190
export async function showToolsPicker(
191
accessor: ServicesAccessor,
192
placeHolder: string,
193
source: string,
194
description?: string,
195
getToolsEntries?: () => ReadonlyMap<IToolSet | IToolData, boolean>,
196
model?: ILanguageModelChatMetadata | undefined,
197
token?: CancellationToken
198
): Promise<ReadonlyMap<IToolSet | IToolData, boolean> | undefined> {
199
200
const quickPickService = accessor.get(IQuickInputService);
201
const mcpService = accessor.get(IMcpService);
202
const mcpRegistry = accessor.get(IMcpRegistry);
203
const commandService = accessor.get(ICommandService);
204
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
205
const editorService = accessor.get(IEditorService);
206
const mcpWorkbenchService = accessor.get(IMcpWorkbenchService);
207
const toolsService = accessor.get(ILanguageModelToolsService);
208
const telemetryService = accessor.get(ITelemetryService);
209
210
const mcpServerByTool = new Map<string, IMcpServer>();
211
for (const server of mcpService.servers.get()) {
212
for (const tool of server.tools.get()) {
213
mcpServerByTool.set(tool.id, server);
214
}
215
}
216
217
function computeItems(previousToolsEntries?: ReadonlyMap<IToolData | IToolSet, boolean>) {
218
// Create default entries if none provided
219
let toolsEntries = getToolsEntries ? new Map([...getToolsEntries()].map(([k, enabled]) => [k.id, enabled])) : undefined;
220
if (!toolsEntries) {
221
const defaultEntries = new Map();
222
for (const tool of toolsService.getTools(model)) {
223
if (tool.canBeReferencedInPrompt) {
224
defaultEntries.set(tool, false);
225
}
226
}
227
for (const toolSet of toolsService.getToolSetsForModel(model)) {
228
defaultEntries.set(toolSet, false);
229
}
230
toolsEntries = defaultEntries;
231
}
232
previousToolsEntries?.forEach((value, key) => {
233
toolsEntries.set(key.id, value);
234
});
235
236
// Build tree structure
237
const treeItems: AnyTreeItem[] = [];
238
const bucketMap = new Map<string, IBucketTreeItem>();
239
240
const getKey = (source: ToolDataSource): string => {
241
switch (source.type) {
242
case 'mcp':
243
case 'extension':
244
return ToolDataSource.toKey(source);
245
case 'internal':
246
return BucketOrdinal.BuiltIn.toString();
247
case 'user':
248
return BucketOrdinal.User.toString();
249
case 'external':
250
throw new Error('should not be reachable');
251
default:
252
assertNever(source);
253
}
254
};
255
256
const mcpServers = new Map(mcpService.servers.get().map(s => [s.definition.id, { server: s, seen: false }]));
257
const createBucket = (source: ToolDataSource, key: string): IBucketTreeItem | undefined => {
258
if (source.type === 'mcp') {
259
const mcpServerEntry = mcpServers.get(source.definitionId);
260
if (!mcpServerEntry) {
261
return undefined;
262
}
263
mcpServerEntry.seen = true;
264
const mcpServer = mcpServerEntry.server;
265
const buttons: ActionableButton[] = [];
266
const collection = mcpRegistry.collections.get().find(c => c.id === mcpServer.collection.id);
267
if (collection?.source) {
268
buttons.push({
269
iconClass: ThemeIcon.asClassName(Codicon.settingsGear),
270
tooltip: localize('configMcpCol', "Configure {0}", collection.label),
271
action: () => collection.source ? collection.source instanceof ExtensionIdentifier ? extensionsWorkbenchService.open(collection.source.value, { tab: ExtensionEditorTab.Features, feature: 'mcp' }) : mcpWorkbenchService.open(collection.source, { tab: McpServerEditorTab.Configuration }) : undefined
272
});
273
} else if (collection?.presentation?.origin) {
274
buttons.push({
275
iconClass: ThemeIcon.asClassName(Codicon.settingsGear),
276
tooltip: localize('configMcpCol', "Configure {0}", collection.label),
277
action: () => editorService.openEditor({
278
resource: collection!.presentation!.origin,
279
})
280
});
281
}
282
if (mcpServer.connectionState.get().state === McpConnectionState.Kind.Error) {
283
buttons.push({
284
iconClass: ThemeIcon.asClassName(Codicon.warning),
285
tooltip: localize('mcpShowOutput', "Show Output"),
286
action: () => mcpServer.showOutput(),
287
});
288
}
289
const cacheState = mcpServer.cacheState.get();
290
const children: AnyTreeItem[] = [];
291
let collapsed = true;
292
if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) {
293
collapsed = false;
294
children.push({
295
itemType: 'callback',
296
iconClass: ThemeIcon.asClassName(Codicon.sync),
297
label: localize('mcpUpdate', "Update Tools"),
298
pickable: false,
299
run: () => {
300
treePicker.busy = true;
301
(async () => {
302
const ok = await startServerAndWaitForLiveTools(mcpServer, { promptType: 'all-untrusted' });
303
if (!ok) {
304
mcpServer.showOutput();
305
treePicker.hide();
306
return;
307
}
308
treePicker.busy = false;
309
computeItems(collectResults());
310
})();
311
return false;
312
},
313
});
314
}
315
const bucket: IBucketTreeItem = {
316
itemType: 'bucket',
317
ordinal: BucketOrdinal.Mcp,
318
id: key,
319
label: source.label,
320
checked: undefined,
321
collapsed,
322
children,
323
buttons,
324
sortOrder: 2,
325
};
326
const iconPath = mcpServer.serverMetadata.get()?.icons.getUrl(22);
327
if (iconPath) {
328
bucket.iconPath = iconPath;
329
} else {
330
bucket.iconClass = ThemeIcon.asClassName(Codicon.mcp);
331
}
332
return bucket;
333
} else if (source.type === 'extension') {
334
return {
335
itemType: 'bucket',
336
ordinal: BucketOrdinal.Extension,
337
id: key,
338
label: source.label,
339
checked: undefined,
340
children: [],
341
buttons: [],
342
collapsed: true,
343
iconClass: ThemeIcon.asClassName(Codicon.extensions),
344
sortOrder: 3,
345
};
346
} else if (source.type === 'internal') {
347
return {
348
itemType: 'bucket',
349
ordinal: BucketOrdinal.BuiltIn,
350
id: key,
351
label: localize('defaultBucketLabel', "Built-In"),
352
checked: undefined,
353
children: [],
354
buttons: [],
355
collapsed: false,
356
sortOrder: 1,
357
};
358
} else {
359
return {
360
itemType: 'bucket',
361
ordinal: BucketOrdinal.User,
362
id: key,
363
label: localize('userBucket', "User Defined Tool Sets"),
364
checked: undefined,
365
children: [],
366
buttons: [],
367
collapsed: true,
368
sortOrder: 4,
369
};
370
}
371
};
372
373
const getBucket = (source: ToolDataSource): IBucketTreeItem | undefined => {
374
const key = getKey(source);
375
let bucket = bucketMap.get(key);
376
if (!bucket) {
377
bucket = createBucket(source, key);
378
if (bucket) {
379
bucketMap.set(key, bucket);
380
}
381
}
382
return bucket;
383
};
384
385
for (const toolSet of toolsService.getToolSetsForModel(model)) {
386
if (!toolsEntries.has(toolSet.id)) {
387
continue;
388
}
389
const bucket = getBucket(toolSet.source);
390
if (!bucket) {
391
continue;
392
}
393
const toolSetChecked = toolsEntries.get(toolSet.id) === true;
394
if (toolSet.source.type === 'mcp') {
395
// bucket represents the toolset
396
bucket.toolset = toolSet;
397
if (toolSetChecked) {
398
bucket.checked = toolSetChecked;
399
}
400
// all mcp tools are part of toolsService.getTools()
401
} else {
402
const treeItem = createToolSetTreeItem(toolSet, toolSetChecked, editorService);
403
bucket.children.push(treeItem);
404
const children = [];
405
for (const tool of toolSet.getTools()) {
406
const toolChecked = toolSetChecked || toolsEntries.get(tool.id) === true;
407
const toolTreeItem = createToolTreeItemFromData(tool, toolChecked);
408
children.push(toolTreeItem);
409
}
410
if (children.length > 0) {
411
treeItem.children = children;
412
}
413
}
414
}
415
// getting potentially disabled tools is fine here because we filter `toolsEntries.has`
416
for (const tool of toolsService.getAllToolsIncludingDisabled()) {
417
if (!tool.canBeReferencedInPrompt || !toolsEntries.has(tool.id)) {
418
continue;
419
}
420
const bucket = getBucket(tool.source);
421
if (!bucket) {
422
continue;
423
}
424
const toolChecked = bucket.checked === true || toolsEntries.get(tool.id) === true;
425
const toolTreeItem = createToolTreeItemFromData(tool, toolChecked);
426
bucket.children.push(toolTreeItem);
427
}
428
429
// Show entries for MCP servers that don't have any tools in them and might need to be started.
430
for (const { server, seen } of mcpServers.values()) {
431
const cacheState = server.cacheState.get();
432
if (!seen && (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated)) {
433
getBucket({ type: 'mcp', definitionId: server.definition.id, label: server.definition.label, instructions: '', serverLabel: '', collectionId: server.collection.id });
434
}
435
}
436
437
// Convert bucket map to sorted tree items
438
const sortedBuckets = Array.from(bucketMap.values()).sort((a, b) => {
439
if (a.sortOrder !== b.sortOrder) {
440
return a.sortOrder - b.sortOrder;
441
}
442
return a.label.localeCompare(b.label);
443
});
444
for (const bucket of sortedBuckets) {
445
treeItems.push(bucket);
446
// Sort children alphabetically
447
bucket.children.sort((a, b) => a.label.localeCompare(b.label));
448
for (const child of bucket.children) {
449
if (isToolSetTreeItem(child) && child.children) {
450
child.children.sort((a, b) => a.label.localeCompare(b.label));
451
}
452
}
453
}
454
if (treeItems.length === 0) {
455
treePicker.placeholder = localize('noTools', "Add tools to chat");
456
} else {
457
treePicker.placeholder = placeHolder;
458
}
459
treePicker.setItemTree(treeItems);
460
}
461
462
// Create and configure the tree picker
463
const store = new DisposableStore();
464
const treePicker = store.add(quickPickService.createQuickTree<AnyTreeItem>());
465
466
treePicker.placeholder = placeHolder;
467
treePicker.description = description;
468
treePicker.matchOnDescription = true;
469
treePicker.matchOnLabel = true;
470
treePicker.sortByLabel = false;
471
472
computeItems();
473
474
// Handle button triggers
475
store.add(treePicker.onDidTriggerItemButton(e => {
476
if (e.button && typeof (e.button as ActionableButton).action === 'function') {
477
(e.button as ActionableButton).action();
478
store.dispose();
479
}
480
}));
481
482
const collectResults = () => {
483
484
const result = new Map<IToolData | IToolSet, boolean>();
485
const traverse = (items: readonly AnyTreeItem[]) => {
486
for (const item of items) {
487
if (isBucketTreeItem(item)) {
488
if (item.toolset) { // MCP server
489
// MCP toolset is enabled only if all tools are enabled
490
const allChecked = item.checked === true;
491
result.set(item.toolset, allChecked);
492
}
493
traverse(item.children);
494
} else if (isToolSetTreeItem(item)) {
495
result.set(item.toolset, item.checked === true);
496
if (item.children) {
497
traverse(item.children);
498
}
499
} else if (isToolTreeItem(item)) {
500
result.set(item.tool, item.checked || result.get(item.tool) === true); // tools can be in user tool sets and other buckets
501
}
502
}
503
};
504
505
traverse(treePicker.itemTree);
506
return result;
507
};
508
509
// Handle acceptance
510
let didAccept = false;
511
const didAcceptFinalItem = store.add(new Emitter<void>());
512
store.add(treePicker.onDidAccept(() => {
513
// Check if a callback item was activated
514
const activeItems = treePicker.activeItems;
515
const callbackItem = activeItems.find(isCallbackTreeItem);
516
if (!callbackItem) {
517
didAccept = true;
518
treePicker.hide();
519
return;
520
}
521
522
const ret = callbackItem.run();
523
if (ret !== false) {
524
didAcceptFinalItem.fire();
525
}
526
}));
527
528
const addMcpServerButton = {
529
iconClass: ThemeIcon.asClassName(Codicon.mcp),
530
tooltip: localize('addMcpServer', 'Add MCP Server...')
531
};
532
const installExtension = {
533
iconClass: ThemeIcon.asClassName(Codicon.extensions),
534
tooltip: localize('addExtensionButton', 'Install Extension...')
535
};
536
const configureToolSets = {
537
iconClass: ThemeIcon.asClassName(Codicon.gear),
538
tooltip: localize('configToolSets', 'Configure Tool Sets...')
539
};
540
treePicker.title = localize('configureTools', "Configure Tools");
541
treePicker.buttons = [addMcpServerButton, installExtension, configureToolSets];
542
store.add(treePicker.onDidTriggerButton(button => {
543
if (button === addMcpServerButton) {
544
commandService.executeCommand(McpCommandIds.AddConfiguration);
545
} else if (button === installExtension) {
546
extensionsWorkbenchService.openSearch('@tag:language-model-tools');
547
} else if (button === configureToolSets) {
548
commandService.executeCommand(ConfigureToolSets.ID);
549
}
550
treePicker.hide();
551
}));
552
553
// Close picker when cancelled (e.g., when mode changes)
554
if (token) {
555
store.add(token.onCancellationRequested(() => {
556
treePicker.hide();
557
}));
558
}
559
560
// Capture initial state for telemetry comparison
561
const initialState = collectResults();
562
563
treePicker.show();
564
565
await Promise.race([Event.toPromise(Event.any(treePicker.onDidHide, didAcceptFinalItem.event), store)]);
566
567
// Send telemetry about tool selection changes
568
sendDidChangeEvent(source, telemetryService, initialState, collectResults(), mcpRegistry);
569
570
store.dispose();
571
572
return didAccept ? collectResults() : undefined;
573
}
574
575
/**
576
* Categorizes a tool or toolset source for privacy-safe telemetry.
577
* Returns identifying info only for built-in/extension tools where names are public.
578
* For user-defined and user MCP tools, only the category is returned.
579
*
580
* @param item - The tool or toolset to categorize
581
* @param mcpRegistry - The MCP registry to look up collection sources for MCP tools
582
*/
583
function categorizeTool(item: IToolData | IToolSet, mcpRegistry: IMcpRegistry): { category: 'builtin' | 'extension' | 'extension-mcp' | 'user-mcp' | 'user-toolset'; name?: string; extensionId?: string } {
584
const source = item.source;
585
switch (source.type) {
586
case 'internal':
587
// Built-in tools are safe to identify by name
588
return { category: 'builtin', name: item.id };
589
case 'extension':
590
// Extension tools are public, safe to include name and extension ID
591
return { category: 'extension', name: item.id, extensionId: source.extensionId.value };
592
case 'mcp': {
593
// MCP tools: check if the collection comes from an extension
594
// Never include tool names for privacy, but include extension ID if from an extension
595
const collection = mcpRegistry.collections.get().find(c => c.id === source.collectionId);
596
if (collection?.source instanceof ExtensionIdentifier) {
597
return { category: 'extension-mcp', extensionId: collection.source.value };
598
}
599
// User-configured MCP server - don't include any identifying info
600
return { category: 'user-mcp' };
601
}
602
case 'user':
603
// User-defined tool sets: don't include names for privacy
604
return { category: 'user-toolset' };
605
case 'external':
606
// External tools shouldn't appear in the picker, treat as user-defined for safety
607
return { category: 'user-toolset' };
608
default:
609
assertNever(source);
610
}
611
}
612
613
interface IToolToggleSummary {
614
/** Number of built-in tools enabled */
615
builtinEnabled: number;
616
/** Number of built-in tools disabled */
617
builtinDisabled: number;
618
/** Number of extension tools enabled */
619
extensionEnabled: number;
620
/** Number of extension tools disabled */
621
extensionDisabled: number;
622
/** Number of extension MCP tools enabled */
623
extensionMcpEnabled: number;
624
/** Number of extension MCP tools disabled */
625
extensionMcpDisabled: number;
626
/** Number of user MCP tools enabled */
627
userMcpEnabled: number;
628
/** Number of user MCP tools disabled */
629
userMcpDisabled: number;
630
/** Number of user tool sets enabled */
631
userToolsetEnabled: number;
632
/** Number of user tool sets disabled */
633
userToolsetDisabled: number;
634
/** Detailed list of toggled items (only safe-to-log items include names) */
635
details: string;
636
}
637
638
function computeToolToggleSummary(
639
initialState: ReadonlyMap<IToolData | IToolSet, boolean>,
640
finalState: ReadonlyMap<IToolData | IToolSet, boolean>,
641
mcpRegistry: IMcpRegistry
642
): IToolToggleSummary {
643
const summary: IToolToggleSummary = {
644
builtinEnabled: 0,
645
builtinDisabled: 0,
646
extensionEnabled: 0,
647
extensionDisabled: 0,
648
extensionMcpEnabled: 0,
649
extensionMcpDisabled: 0,
650
userMcpEnabled: 0,
651
userMcpDisabled: 0,
652
userToolsetEnabled: 0,
653
userToolsetDisabled: 0,
654
details: ''
655
};
656
657
const detailItems: { category: string; name?: string; extensionId?: string; enabled: boolean }[] = [];
658
659
// Compare states and record changes
660
for (const [item, finalEnabled] of finalState) {
661
const initialEnabled = initialState.get(item) ?? false;
662
if (initialEnabled === finalEnabled) {
663
continue; // No change
664
}
665
666
const categorized = categorizeTool(item, mcpRegistry);
667
const enabled = finalEnabled;
668
669
switch (categorized.category) {
670
case 'builtin':
671
if (enabled) { summary.builtinEnabled++; } else { summary.builtinDisabled++; }
672
detailItems.push({ category: 'builtin', name: categorized.name, enabled });
673
break;
674
case 'extension':
675
if (enabled) { summary.extensionEnabled++; } else { summary.extensionDisabled++; }
676
detailItems.push({ category: 'extension', name: categorized.name, extensionId: categorized.extensionId, enabled });
677
break;
678
case 'extension-mcp':
679
if (enabled) { summary.extensionMcpEnabled++; } else { summary.extensionMcpDisabled++; }
680
detailItems.push({ category: 'extension-mcp', extensionId: categorized.extensionId, enabled });
681
break;
682
case 'user-mcp':
683
if (enabled) { summary.userMcpEnabled++; } else { summary.userMcpDisabled++; }
684
// Don't include name for privacy
685
detailItems.push({ category: 'user-mcp', enabled });
686
break;
687
case 'user-toolset':
688
if (enabled) { summary.userToolsetEnabled++; } else { summary.userToolsetDisabled++; }
689
// Don't include name for privacy
690
detailItems.push({ category: 'user-toolset', enabled });
691
break;
692
}
693
}
694
695
// Serialize details as JSON
696
summary.details = JSON.stringify(detailItems);
697
return summary;
698
}
699
700
function sendDidChangeEvent(
701
source: string,
702
telemetryService: ITelemetryService,
703
initialState: ReadonlyMap<IToolData | IToolSet, boolean>,
704
finalState: ReadonlyMap<IToolData | IToolSet, boolean>,
705
mcpRegistry: IMcpRegistry
706
): void {
707
const summary = computeToolToggleSummary(initialState, finalState, mcpRegistry);
708
const changed = summary.builtinEnabled > 0 || summary.builtinDisabled > 0 ||
709
summary.extensionEnabled > 0 || summary.extensionDisabled > 0 ||
710
summary.extensionMcpEnabled > 0 || summary.extensionMcpDisabled > 0 ||
711
summary.userMcpEnabled > 0 || summary.userMcpDisabled > 0 ||
712
summary.userToolsetEnabled > 0 || summary.userToolsetDisabled > 0;
713
714
type ToolPickerClosedEvent = {
715
changed: boolean;
716
source: string;
717
builtinEnabled: number;
718
builtinDisabled: number;
719
extensionEnabled: number;
720
extensionDisabled: number;
721
extensionMcpEnabled: number;
722
extensionMcpDisabled: number;
723
userMcpEnabled: number;
724
userMcpDisabled: number;
725
userToolsetEnabled: number;
726
userToolsetDisabled: number;
727
details: string;
728
};
729
730
type ToolPickerClosedClassification = {
731
changed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user changed the tool selection from the initial state.' };
732
source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the tool picker event.' };
733
builtinEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of built-in tools that were enabled.' };
734
builtinDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of built-in tools that were disabled.' };
735
extensionEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extension tools that were enabled.' };
736
extensionDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extension tools that were disabled.' };
737
extensionMcpEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extension MCP tools that were enabled.' };
738
extensionMcpDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of extension MCP tools that were disabled.' };
739
userMcpEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of user MCP tools that were enabled.' };
740
userMcpDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of user MCP tools that were disabled.' };
741
userToolsetEnabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of user tool sets that were enabled.' };
742
userToolsetDisabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of user tool sets that were disabled.' };
743
details: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'JSON array of toggled items. Built-in and extension tools include names; user-defined items only include category.' };
744
owner: 'benibenj';
745
comment: 'Tracks which tools users toggle in the tool picker, with privacy-safe categorization.';
746
};
747
748
telemetryService.publicLog2<ToolPickerClosedEvent, ToolPickerClosedClassification>('chatToolPickerClosed', {
749
source,
750
changed,
751
builtinEnabled: summary.builtinEnabled,
752
builtinDisabled: summary.builtinDisabled,
753
extensionEnabled: summary.extensionEnabled,
754
extensionDisabled: summary.extensionDisabled,
755
extensionMcpEnabled: summary.extensionMcpEnabled,
756
extensionMcpDisabled: summary.extensionMcpDisabled,
757
userMcpEnabled: summary.userMcpEnabled,
758
userMcpDisabled: summary.userMcpDisabled,
759
userToolsetEnabled: summary.userToolsetEnabled,
760
userToolsetDisabled: summary.userToolsetDisabled,
761
details: summary.details,
762
});
763
}
764
765