mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
feat: subagent feature wip
This commit is contained in:
249
packages/cli/src/ui/components/subagents/create/ToolSelector.tsx
Normal file
249
packages/cli/src/ui/components/subagents/create/ToolSelector.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
|
||||
import { ToolCategory } from '../types.js';
|
||||
import { Kind, Config } from '@qwen-code/qwen-code-core';
|
||||
import { Colors } from '../../../colors.js';
|
||||
|
||||
interface ToolOption {
|
||||
label: string;
|
||||
value: string;
|
||||
category: ToolCategory;
|
||||
}
|
||||
|
||||
interface ToolSelectorProps {
|
||||
tools?: string[];
|
||||
onSelect: (tools: string[]) => void;
|
||||
config: Config | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool selection with categories.
|
||||
*/
|
||||
export function ToolSelector({
|
||||
tools = [],
|
||||
onSelect,
|
||||
config,
|
||||
}: ToolSelectorProps) {
|
||||
// Generate tool categories from actual tool registry
|
||||
const {
|
||||
toolCategories,
|
||||
readTools,
|
||||
editTools,
|
||||
executeTools,
|
||||
initialCategory,
|
||||
} = useMemo(() => {
|
||||
if (!config) {
|
||||
// Fallback categories if config not available
|
||||
return {
|
||||
toolCategories: [
|
||||
{
|
||||
id: 'all',
|
||||
name: 'All Tools (Default)',
|
||||
tools: [],
|
||||
},
|
||||
],
|
||||
readTools: [],
|
||||
editTools: [],
|
||||
executeTools: [],
|
||||
initialCategory: 'all',
|
||||
};
|
||||
}
|
||||
|
||||
const toolRegistry = config.getToolRegistry();
|
||||
const allTools = toolRegistry.getAllTools();
|
||||
|
||||
// Categorize tools by Kind
|
||||
const readTools = allTools
|
||||
.filter(
|
||||
(tool) =>
|
||||
tool.kind === Kind.Read ||
|
||||
tool.kind === Kind.Search ||
|
||||
tool.kind === Kind.Fetch ||
|
||||
tool.kind === Kind.Think,
|
||||
)
|
||||
.map((tool) => tool.displayName)
|
||||
.sort();
|
||||
|
||||
const editTools = allTools
|
||||
.filter(
|
||||
(tool) =>
|
||||
tool.kind === Kind.Edit ||
|
||||
tool.kind === Kind.Delete ||
|
||||
tool.kind === Kind.Move,
|
||||
)
|
||||
.map((tool) => tool.displayName)
|
||||
.sort();
|
||||
|
||||
const executeTools = allTools
|
||||
.filter((tool) => tool.kind === Kind.Execute)
|
||||
.map((tool) => tool.displayName)
|
||||
.sort();
|
||||
|
||||
const toolCategories = [
|
||||
{
|
||||
id: 'all',
|
||||
name: 'All Tools',
|
||||
tools: [],
|
||||
},
|
||||
{
|
||||
id: 'read',
|
||||
name: 'Read-only Tools',
|
||||
tools: readTools,
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
name: 'Read & Edit Tools',
|
||||
tools: [...readTools, ...editTools],
|
||||
},
|
||||
{
|
||||
id: 'execute',
|
||||
name: 'Read & Edit & Execution Tools',
|
||||
tools: [...readTools, ...editTools, ...executeTools],
|
||||
},
|
||||
].filter((category) => category.id === 'all' || category.tools.length > 0);
|
||||
|
||||
// Determine initial category based on tools prop
|
||||
let initialCategory = 'all'; // default to first option
|
||||
|
||||
if (tools.length === 0) {
|
||||
// Empty array represents all tools
|
||||
initialCategory = 'all';
|
||||
} else {
|
||||
// Try to match tools array to a category
|
||||
const matchingCategory = toolCategories.find((category) => {
|
||||
if (category.id === 'all') return false;
|
||||
|
||||
// Check if the tools array exactly matches this category's tools
|
||||
const categoryToolsSet = new Set(category.tools);
|
||||
const inputToolsSet = new Set(tools);
|
||||
|
||||
return (
|
||||
categoryToolsSet.size === inputToolsSet.size &&
|
||||
[...categoryToolsSet].every((tool) => inputToolsSet.has(tool))
|
||||
);
|
||||
});
|
||||
|
||||
if (matchingCategory) {
|
||||
initialCategory = matchingCategory.id;
|
||||
}
|
||||
// If no exact match found, keep default 'all'
|
||||
}
|
||||
|
||||
return {
|
||||
toolCategories,
|
||||
readTools,
|
||||
editTools,
|
||||
executeTools,
|
||||
initialCategory,
|
||||
};
|
||||
}, [config, tools]);
|
||||
|
||||
const [selectedCategory, setSelectedCategory] =
|
||||
useState<string>(initialCategory);
|
||||
|
||||
// Update selected category when initialCategory changes (when tools prop changes)
|
||||
useEffect(() => {
|
||||
setSelectedCategory(initialCategory);
|
||||
}, [initialCategory]);
|
||||
|
||||
const toolOptions: ToolOption[] = toolCategories.map((category) => ({
|
||||
label: category.name,
|
||||
value: category.id,
|
||||
category,
|
||||
}));
|
||||
|
||||
const handleHighlight = (selectedValue: string) => {
|
||||
setSelectedCategory(selectedValue);
|
||||
};
|
||||
|
||||
const handleSelect = (selectedValue: string) => {
|
||||
const category = toolCategories.find((cat) => cat.id === selectedValue);
|
||||
if (category) {
|
||||
if (category.id === 'all') {
|
||||
onSelect([]); // Empty array for 'all'
|
||||
} else {
|
||||
onSelect(category.tools);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get the currently selected category for displaying tools
|
||||
const currentCategory = toolCategories.find(
|
||||
(cat) => cat.id === selectedCategory,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box flexDirection="column">
|
||||
<RadioButtonSelect
|
||||
items={toolOptions.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
}))}
|
||||
initialIndex={toolOptions.findIndex(
|
||||
(opt) => opt.value === selectedCategory,
|
||||
)}
|
||||
onSelect={handleSelect}
|
||||
onHighlight={handleHighlight}
|
||||
isFocused={true}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Show help information or tools for selected category */}
|
||||
{currentCategory && (
|
||||
<Box flexDirection="column">
|
||||
{currentCategory.id === 'all' ? (
|
||||
<Text color={Colors.Gray}>
|
||||
All tools selected, including MCP tools
|
||||
</Text>
|
||||
) : currentCategory.tools.length > 0 ? (
|
||||
<>
|
||||
<Text color={Colors.Gray}>Selected tools:</Text>
|
||||
<Box flexDirection="column" marginLeft={2}>
|
||||
{(() => {
|
||||
// Filter the already categorized tools to show only those in current category
|
||||
const categoryReadTools = currentCategory.tools.filter(
|
||||
(tool) => readTools.includes(tool),
|
||||
);
|
||||
const categoryEditTools = currentCategory.tools.filter(
|
||||
(tool) => editTools.includes(tool),
|
||||
);
|
||||
const categoryExecuteTools = currentCategory.tools.filter(
|
||||
(tool) => executeTools.includes(tool),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{categoryReadTools.length > 0 && (
|
||||
<Text color={Colors.Gray}>
|
||||
• Read-only tools: {categoryReadTools.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
{categoryEditTools.length > 0 && (
|
||||
<Text color={Colors.Gray}>
|
||||
• Edit tools: {categoryEditTools.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
{categoryExecuteTools.length > 0 && (
|
||||
<Text color={Colors.Gray}>
|
||||
• Execution tools: {categoryExecuteTools.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
</>
|
||||
) : null}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user