Skip to content

Commit e5e0178

Browse files
committed
tools picker: add tools in toolSets
1 parent a39f1be commit e5e0178

File tree

3 files changed

+201
-179
lines changed

3 files changed

+201
-179
lines changed

src/vs/workbench/contrib/chat/browser/actions/chatToolPicker.ts

Lines changed: 171 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ interface IBucketTreeItem extends IToolTreeItem {
5555
toolset?: ToolSet; // For MCP servers where the bucket represents the ToolSet - mutable
5656
readonly status?: string;
5757
readonly children: AnyTreeItem[];
58-
checked: boolean | 'partial';
58+
checked: boolean | 'partial' | undefined;
5959
}
6060

6161
/**
@@ -65,7 +65,7 @@ interface IBucketTreeItem extends IToolTreeItem {
6565
interface IToolSetTreeItem extends IToolTreeItem {
6666
readonly itemType: 'toolset';
6767
readonly toolset: ToolSet;
68-
readonly children: AnyTreeItem[];
68+
children: AnyTreeItem[] | undefined;
6969
checked: boolean | 'partial';
7070
}
7171

@@ -143,6 +143,31 @@ function createToolTreeItemFromData(tool: IToolData, checked: boolean): IToolTre
143143
};
144144
}
145145

146+
function createToolSetTreeItem(toolset: ToolSet, checked: boolean, editorService: IEditorService): IToolSetTreeItem {
147+
const iconProps = mapIconToTreeItem(toolset.icon);
148+
const buttons = [];
149+
if (toolset.source.type === 'user') {
150+
const resource = toolset.source.file;
151+
buttons.push({
152+
iconClass: ThemeIcon.asClassName(Codicon.edit),
153+
tooltip: localize('editUserBucket', "Edit Tool Set"),
154+
action: () => editorService.openEditor({ resource })
155+
});
156+
}
157+
return {
158+
itemType: 'toolset',
159+
toolset,
160+
buttons,
161+
id: toolset.id,
162+
label: toolset.referenceName,
163+
description: toolset.description,
164+
checked,
165+
children: undefined,
166+
collapsed: true,
167+
...iconProps
168+
};
169+
}
170+
146171
/**
147172
* New QuickTree implementation of the tools picker.
148173
* Uses IQuickTree to provide a true hierarchical tree structure with:
@@ -200,162 +225,161 @@ export async function showToolsPicker(
200225
const treeItems: AnyTreeItem[] = [];
201226
const bucketMap = new Map<string, IBucketTreeItem>();
202227

203-
// Process entries and organize into buckets
204-
for (const [toolSetOrTool, picked] of toolsEntries) {
205-
let bucketItem: IBucketTreeItem | undefined;
206-
207-
if (toolSetOrTool.source.type === 'mcp') {
208-
const key = ToolDataSource.toKey(toolSetOrTool.source);
209-
bucketItem = bucketMap.get(key);
210-
if (!bucketItem) {
211-
const { definitionId } = toolSetOrTool.source;
212-
const mcpServer = mcpService.servers.get().find(candidate => candidate.definition.id === definitionId);
213-
if (!mcpServer) {
214-
continue;
215-
}
216-
217-
const buttons: ActionableButton[] = [];
218-
const collection = mcpRegistry.collections.get().find(c => c.id === mcpServer.collection.id);
219-
if (collection?.source) {
220-
buttons.push({
221-
iconClass: ThemeIcon.asClassName(Codicon.settingsGear),
222-
tooltip: localize('configMcpCol', "Configure {0}", collection.label),
223-
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
224-
});
225-
} else if (collection?.presentation?.origin) {
226-
buttons.push({
227-
iconClass: ThemeIcon.asClassName(Codicon.settingsGear),
228-
tooltip: localize('configMcpCol', "Configure {0}", collection.label),
229-
action: () => editorService.openEditor({
230-
resource: collection!.presentation!.origin,
231-
})
232-
});
233-
}
234-
if (mcpServer.connectionState.get().state === McpConnectionState.Kind.Error) {
235-
buttons.push({
236-
iconClass: ThemeIcon.asClassName(Codicon.warning),
237-
tooltip: localize('mcpShowOutput', "Show Output"),
238-
action: () => mcpServer.showOutput(),
239-
});
240-
}
228+
const getKey = (source: ToolDataSource): string => {
229+
switch (source.type) {
230+
case 'mcp':
231+
case 'extension':
232+
return ToolDataSource.toKey(source);
233+
case 'internal':
234+
return BucketOrdinal.BuiltIn.toString();
235+
case 'user':
236+
return BucketOrdinal.User.toString();
237+
default:
238+
assertNever(source);
239+
}
240+
};
241241

242-
bucketItem = {
243-
itemType: 'bucket',
244-
ordinal: BucketOrdinal.Mcp,
245-
id: key,
246-
label: localize('mcplabel', "MCP Server: {0}", toolSetOrTool.source.label),
247-
checked: false,
248-
collapsed: true,
249-
children: [],
250-
buttons,
251-
iconClass: ThemeIcon.asClassName(Codicon.mcp)
252-
};
253-
bucketMap.set(key, bucketItem);
242+
const createBucket = (source: ToolDataSource, key: string): IBucketTreeItem | undefined => {
243+
if (source.type === 'mcp') {
244+
const { definitionId } = source;
245+
const mcpServer = mcpService.servers.get().find(candidate => candidate.definition.id === definitionId);
246+
if (!mcpServer) {
247+
return undefined;
254248
}
255249

256-
if (toolSetOrTool instanceof ToolSet) {
257-
// MCP ToolSets are hidden - store in bucket for special handling
258-
bucketItem.toolset = toolSetOrTool;
259-
bucketItem.checked = picked;
260-
} else if (toolSetOrTool.canBeReferencedInPrompt) {
261-
// Add MCP tools directly as children
262-
const toolTreeItem = createToolTreeItemFromData(toolSetOrTool, picked);
263-
bucketItem.children.push(toolTreeItem);
250+
const buttons: ActionableButton[] = [];
251+
const collection = mcpRegistry.collections.get().find(c => c.id === mcpServer.collection.id);
252+
if (collection?.source) {
253+
buttons.push({
254+
iconClass: ThemeIcon.asClassName(Codicon.settingsGear),
255+
tooltip: localize('configMcpCol', "Configure {0}", collection.label),
256+
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
257+
});
258+
} else if (collection?.presentation?.origin) {
259+
buttons.push({
260+
iconClass: ThemeIcon.asClassName(Codicon.settingsGear),
261+
tooltip: localize('configMcpCol', "Configure {0}", collection.label),
262+
action: () => editorService.openEditor({
263+
resource: collection!.presentation!.origin,
264+
})
265+
});
264266
}
265-
266-
} else {
267-
// Handle other tool sources (extension, internal, user)
268-
let ordinal: BucketOrdinal;
269-
let label: string;
270-
let key: string;
271-
let collapsed: boolean | undefined;
272-
if (toolSetOrTool.source.type === 'extension') {
273-
ordinal = BucketOrdinal.Extension;
274-
label = localize('ext', 'Extension: {0}', toolSetOrTool.source.label);
275-
// Create separate buckets per extension, similar to MCP servers
276-
key = ToolDataSource.toKey(toolSetOrTool.source);
277-
collapsed = true;
278-
} else if (toolSetOrTool.source.type === 'internal') {
279-
ordinal = BucketOrdinal.BuiltIn;
280-
label = localize('defaultBucketLabel', "Built-In");
281-
// Group all internal tools under one bucket
282-
key = ordinal.toString();
283-
} else if (toolSetOrTool.source.type === 'user') {
284-
ordinal = BucketOrdinal.User;
285-
label = localize('userBucket', "User Defined Tool Sets");
286-
// Group all user tools under one bucket
287-
key = ordinal.toString();
288-
} else {
289-
assertNever(toolSetOrTool.source);
267+
if (mcpServer.connectionState.get().state === McpConnectionState.Kind.Error) {
268+
buttons.push({
269+
iconClass: ThemeIcon.asClassName(Codicon.warning),
270+
tooltip: localize('mcpShowOutput', "Show Output"),
271+
action: () => mcpServer.showOutput(),
272+
});
290273
}
274+
return {
275+
itemType: 'bucket',
276+
ordinal: BucketOrdinal.Mcp,
277+
id: key,
278+
label: localize('mcplabel', "MCP Server: {0}", source.label),
279+
checked: undefined,
280+
collapsed: true,
281+
children: [],
282+
buttons,
283+
iconClass: ThemeIcon.asClassName(Codicon.mcp)
284+
};
285+
} else if (source.type === 'extension') {
286+
return {
287+
itemType: 'bucket',
288+
ordinal: BucketOrdinal.Extension,
289+
id: key,
290+
label: localize('ext', 'Extension: {0}', source.label),
291+
checked: undefined,
292+
children: [],
293+
buttons: [],
294+
collapsed: true,
295+
iconClass: ThemeIcon.asClassName(Codicon.extensions)
296+
};
297+
} else if (source.type === 'internal') {
298+
return {
299+
itemType: 'bucket',
300+
ordinal: BucketOrdinal.BuiltIn,
301+
id: key,
302+
label: localize('defaultBucketLabel', "Built-In"),
303+
checked: undefined,
304+
children: [],
305+
buttons: [],
306+
collapsed: false
307+
};
308+
} else {
309+
return {
310+
itemType: 'bucket',
311+
ordinal: BucketOrdinal.User,
312+
id: key,
313+
label: localize('userBucket', "User Defined Tool Sets"),
314+
checked: undefined,
315+
children: [],
316+
buttons: [],
317+
collapsed: true
318+
};
319+
}
320+
};
291321

292-
bucketItem = bucketMap.get(key);
293-
if (!bucketItem) {
294-
const iconProps = toolSetOrTool.source.type === 'extension'
295-
? { iconClass: ThemeIcon.asClassName(Codicon.extensions) }
296-
: {};
297-
298-
bucketItem = {
299-
itemType: 'bucket',
300-
ordinal,
301-
id: key,
302-
label,
303-
checked: false,
304-
children: [],
305-
buttons: [],
306-
collapsed,
307-
...iconProps
308-
};
309-
bucketMap.set(key, bucketItem);
322+
const getBucket = (source: ToolDataSource): IBucketTreeItem | undefined => {
323+
const key = getKey(source);
324+
let bucket = bucketMap.get(key);
325+
if (!bucket) {
326+
bucket = createBucket(source, key);
327+
if (bucket) {
328+
bucketMap.set(key, bucket);
310329
}
330+
}
331+
return bucket;
332+
};
311333

312-
if (toolSetOrTool instanceof ToolSet) {
313-
// Add ToolSet as child with its tools as grandchildren - create directly instead of using legacy pick structure
314-
const iconProps = mapIconToTreeItem(toolSetOrTool.icon);
315-
const buttons = [];
316-
if (toolSetOrTool.source.type === 'user') {
317-
const resource = toolSetOrTool.source.file;
318-
buttons.push({
319-
iconClass: ThemeIcon.asClassName(Codicon.edit),
320-
tooltip: localize('editUserBucket', "Edit Tool Set"),
321-
action: () => editorService.openEditor({ resource })
322-
});
334+
for (const toolSet of toolsService.toolSets.get()) {
335+
if (!toolsEntries.has(toolSet)) {
336+
continue;
337+
}
338+
const bucket = getBucket(toolSet.source);
339+
if (!bucket) {
340+
continue;
341+
}
342+
const toolSetChecked = toolsEntries.get(toolSet) === true;
343+
if (toolSet.source.type === 'mcp') {
344+
// bucket represents the toolset
345+
bucket.toolset = toolSet;
346+
if (toolSetChecked) {
347+
bucket.checked = toolSetChecked;
348+
}
349+
// all mcp tools are part of toolsService.getTools()
350+
} else {
351+
const treeItem = createToolSetTreeItem(toolSet, toolSetChecked, editorService);
352+
bucket.children.push(treeItem);
353+
const children = [];
354+
for (const tool of toolSet.getTools()) {
355+
if (tool.canBeReferencedInPrompt) {
356+
const toolChecked = toolSetChecked || toolsEntries.get(tool) === true;
357+
const toolTreeItem = createToolTreeItemFromData(tool, toolChecked);
358+
children.push(toolTreeItem);
323359
}
324-
const toolSetTreeItem: IToolSetTreeItem = {
325-
itemType: 'toolset',
326-
toolset: toolSetOrTool,
327-
buttons,
328-
id: toolSetOrTool.id,
329-
label: toolSetOrTool.referenceName,
330-
description: toolSetOrTool.description,
331-
checked: picked,
332-
children: [],
333-
collapsed: true,
334-
// TODO: Bring this back when tools in toolsets can be enabled/disabled.
335-
// children: Array.from(toolSetOrTool.getTools()).map(tool => createToolTreeItemFromData(tool, picked)),
336-
...iconProps
337-
};
338-
bucketItem.children.push(toolSetTreeItem);
339-
} else if (toolSetOrTool.canBeReferencedInPrompt) {
340-
// Add individual tool as child
341-
const toolTreeItem = createToolTreeItemFromData(toolSetOrTool, picked);
342-
bucketItem.children.push(toolTreeItem);
360+
}
361+
if (children.length > 0) {
362+
treeItem.children = children;
343363
}
344364
}
345365
}
366+
for (const tool of toolsService.getTools()) {
367+
if (!tool.canBeReferencedInPrompt || !toolsEntries.has(tool)) {
368+
continue;
369+
}
370+
const bucket = getBucket(tool.source);
371+
if (!bucket) {
372+
continue;
373+
}
374+
const toolChecked = bucket.checked === true || toolsEntries.get(tool) === true;
375+
const toolTreeItem = createToolTreeItemFromData(tool, toolChecked);
376+
bucket.children.push(toolTreeItem);
377+
}
346378

347379
// Convert bucket map to sorted tree items
348380
const sortedBuckets = Array.from(bucketMap.values()).sort((a, b) => a.ordinal - b.ordinal);
349381
treeItems.push(...sortedBuckets);
350382

351-
// Set up checkbox states based on parent-child relationships
352-
for (const bucketItem of sortedBuckets) {
353-
if (bucketItem.checked === true) { // only set for MCP tool sets
354-
// Check all children if bucket is checked
355-
bucketItem.children.forEach(child => child.checked = true);
356-
}
357-
}
358-
359383
// Create and configure the tree picker
360384
const store = new DisposableStore();
361385
const treePicker = store.add(quickPickService.createQuickTree<AnyTreeItem>());
@@ -386,7 +410,9 @@ export async function showToolsPicker(
386410
const traverse = (items: readonly AnyTreeItem[]) => {
387411
for (const item of items) {
388412
if (isBucketTreeItem(item) || isToolSetTreeItem(item)) {
389-
traverse(item.children);
413+
if (item.children) {
414+
traverse(item.children);
415+
}
390416
} else if (isToolTreeItem(item) && item.checked) {
391417
count++;
392418
}
@@ -418,7 +444,9 @@ export async function showToolsPicker(
418444
traverse(item.children);
419445
} else if (isToolSetTreeItem(item)) {
420446
result.set(item.toolset, item.checked === true);
421-
traverse(item.children);
447+
if (item.children) {
448+
traverse(item.children);
449+
}
422450
} else if (isToolTreeItem(item)) {
423451
result.set(item.tool, item.checked);
424452
}

0 commit comments

Comments
 (0)