diff --git a/frontend/src/components/agents/AgentTypeForm.tsx b/frontend/src/components/agents/AgentTypeForm.tsx index 2a67c5f..c01ca1f 100644 --- a/frontend/src/components/agents/AgentTypeForm.tsx +++ b/frontend/src/components/agents/AgentTypeForm.tsx @@ -36,6 +36,7 @@ import { type AgentTypeCreateFormValues, AVAILABLE_MODELS, AVAILABLE_MCP_SERVERS, + AGENT_TYPE_CATEGORIES, defaultAgentTypeValues, generateSlug, } from '@/lib/validations/agentType'; @@ -57,6 +58,13 @@ const TAB_FIELD_MAPPING = { description: 'basic', expertise: 'basic', is_active: 'basic', + // Category and display fields + category: 'basic', + icon: 'basic', + color: 'basic', + sort_order: 'basic', + typical_tasks: 'basic', + collaboration_hints: 'basic', primary_model: 'model', fallback_models: 'model', model_params: 'model', @@ -121,6 +129,8 @@ export function AgentTypeForm({ const isEditing = !!agentType; const [activeTab, setActiveTab] = useState('basic'); const [expertiseInput, setExpertiseInput] = useState(''); + const [typicalTaskInput, setTypicalTaskInput] = useState(''); + const [collaborationHintInput, setCollaborationHintInput] = useState(''); // Memoize initial values transformation const initialValues = useMemo(() => transformAgentTypeToFormValues(agentType), [agentType]); @@ -151,6 +161,10 @@ export function AgentTypeForm({ const watchExpertise = watch('expertise') || []; /* istanbul ignore next -- defensive fallback, mcp_servers always has default */ const watchMcpServers = watch('mcp_servers') || []; + /* istanbul ignore next -- defensive fallback, typical_tasks always has default */ + const watchTypicalTasks = watch('typical_tasks') || []; + /* istanbul ignore next -- defensive fallback, collaboration_hints always has default */ + const watchCollaborationHints = watch('collaboration_hints') || []; // Reset form when agentType changes (e.g., switching to edit mode) useEffect(() => { @@ -196,6 +210,40 @@ export function AgentTypeForm({ } }; + const handleAddTypicalTask = () => { + if (typicalTaskInput.trim()) { + const newTask = typicalTaskInput.trim(); + if (!watchTypicalTasks.includes(newTask)) { + setValue('typical_tasks', [...watchTypicalTasks, newTask]); + } + setTypicalTaskInput(''); + } + }; + + const handleRemoveTypicalTask = (task: string) => { + setValue( + 'typical_tasks', + watchTypicalTasks.filter((t) => t !== task) + ); + }; + + const handleAddCollaborationHint = () => { + if (collaborationHintInput.trim()) { + const newHint = collaborationHintInput.trim().toLowerCase(); + if (!watchCollaborationHints.includes(newHint)) { + setValue('collaboration_hints', [...watchCollaborationHints, newHint]); + } + setCollaborationHintInput(''); + } + }; + + const handleRemoveCollaborationHint = (hint: string) => { + setValue( + 'collaboration_hints', + watchCollaborationHints.filter((h) => h !== hint) + ); + }; + // Handle form submission with validation const onFormSubmit = useCallback( (e: React.FormEvent) => { @@ -383,6 +431,188 @@ export function AgentTypeForm({ + + {/* Category & Display Card */} + + + Category & Display + + Organize and customize how this agent type appears in the UI + + + +
+
+ + ( + + )} + /> +

+ Group agents by their primary role +

+
+
+ + + {errors.sort_order && ( +

+ {errors.sort_order.message} +

+ )} +

Display order within category

+
+
+ +
+
+ + + {errors.icon && ( +

+ {errors.icon.message} +

+ )} +

Lucide icon name for UI display

+
+
+ +
+ + ( + field.onChange(e.target.value)} + className="h-9 w-9 cursor-pointer rounded border" + /> + )} + /> +
+ {errors.color && ( +

+ {errors.color.message} +

+ )} +

Hex color for visual distinction

+
+
+ + + +
+ +

Tasks this agent type excels at

+
+ setTypicalTaskInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddTypicalTask(); + } + }} + /> + +
+
+ {watchTypicalTasks.map((task) => ( + + {task} + + + ))} +
+
+ +
+ +

+ Agent slugs that work well with this type +

+
+ setCollaborationHintInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddCollaborationHint(); + } + }} + /> + +
+
+ {watchCollaborationHints.map((hint) => ( + + {hint} + + + ))} +
+
+
+
{/* Model Configuration Tab */} diff --git a/frontend/tests/components/agents/AgentTypeForm.test.tsx b/frontend/tests/components/agents/AgentTypeForm.test.tsx index 4656b5a..3f8f34b 100644 --- a/frontend/tests/components/agents/AgentTypeForm.test.tsx +++ b/frontend/tests/components/agents/AgentTypeForm.test.tsx @@ -199,7 +199,8 @@ describe('AgentTypeForm', () => { const expertiseInput = screen.getByPlaceholderText(/e.g., system design/i); await user.type(expertiseInput, 'new skill'); - await user.click(screen.getByRole('button', { name: /^add$/i })); + // Click the first "Add" button (for expertise) + await user.click(screen.getAllByRole('button', { name: /^add$/i })[0]); expect(screen.getByText('new skill')).toBeInTheDocument(); }); @@ -461,7 +462,8 @@ describe('AgentTypeForm', () => { // Agent type already has 'system design' const expertiseInput = screen.getByPlaceholderText(/e.g., system design/i); await user.type(expertiseInput, 'system design'); - await user.click(screen.getByRole('button', { name: /^add$/i })); + // Click the first "Add" button (for expertise) + await user.click(screen.getAllByRole('button', { name: /^add$/i })[0]); // Should still only have one 'system design' badge const badges = screen.getAllByText('system design'); @@ -472,7 +474,8 @@ describe('AgentTypeForm', () => { const user = userEvent.setup(); render(); - const addButton = screen.getByRole('button', { name: /^add$/i }); + // Click the first "Add" button (for expertise) + const addButton = screen.getAllByRole('button', { name: /^add$/i })[0]; await user.click(addButton); // No badges should be added @@ -485,7 +488,8 @@ describe('AgentTypeForm', () => { const expertiseInput = screen.getByPlaceholderText(/e.g., system design/i); await user.type(expertiseInput, 'API Design'); - await user.click(screen.getByRole('button', { name: /^add$/i })); + // Click the first "Add" button (for expertise) + await user.click(screen.getAllByRole('button', { name: /^add$/i })[0]); expect(screen.getByText('api design')).toBeInTheDocument(); }); @@ -496,7 +500,8 @@ describe('AgentTypeForm', () => { const expertiseInput = screen.getByPlaceholderText(/e.g., system design/i); await user.type(expertiseInput, ' testing '); - await user.click(screen.getByRole('button', { name: /^add$/i })); + // Click the first "Add" button (for expertise) + await user.click(screen.getAllByRole('button', { name: /^add$/i })[0]); expect(screen.getByText('testing')).toBeInTheDocument(); }); @@ -509,7 +514,8 @@ describe('AgentTypeForm', () => { /e.g., system design/i ) as HTMLInputElement; await user.type(expertiseInput, 'new skill'); - await user.click(screen.getByRole('button', { name: /^add$/i })); + // Click the first "Add" button (for expertise) + await user.click(screen.getAllByRole('button', { name: /^add$/i })[0]); expect(expertiseInput.value).toBe(''); });