import { useState, useEffect, useCallback } from "react"; import type { AgentCapability, ModelInfo, AgentGenerationSpec } from "../api"; import { createAgent, fetchModels, updateGlobalSettings } from "../api"; import { CustomModelDropdown } from "./CustomModelDropdown"; import { ProviderIcon } from "./ProviderIcon"; import { AgentGenerationModal } from "./AgentGenerationModal"; import { AGENT_PRESETS, type AgentPreset } from "./agent-presets"; import { SkillMultiselect } from "./SkillMultiselect"; export interface NewAgentDialogProps { isOpen: boolean; onClose: () => void; onCreated: () => void; projectId?: string; } const AGENT_ROLES: { value: AgentCapability; label: string; icon: string }[] = [ { value: "triage", label: "Triage", icon: "⊕" }, { value: "executor", label: "Executor", icon: "▶" }, { value: "reviewer", label: "Reviewer", icon: "⊙" }, { value: "merger", label: "Merger", icon: "⊞" }, { value: "scheduler", label: "Scheduler", icon: "◷" }, { value: "engineer", label: "Engineer", icon: "⎔" }, { value: "custom", label: "Custom", icon: "✦" }, ]; type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high"; /** Set of valid AgentCapability values for mapping generated roles */ const VALID_CAPABILITIES = new Set(["triage", "executor", "reviewer", "merger", "scheduler", "engineer", "custom"]); interface RuntimeConfig { model: string; thinkingLevel: ThinkingLevel; maxTurns: number; } export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAgentDialogProps) { const [step, setStep] = useState(0); const [name, setName] = useState(""); const [title, setTitle] = useState(""); const [icon, setIcon] = useState(""); const [role, setRole] = useState("custom"); const [reportsTo, setReportsTo] = useState(""); const [instructionsPath, setInstructionsPath] = useState(""); const [instructionsText, setInstructionsText] = useState(""); const [soul, setSoul] = useState(""); const [memory, setMemory] = useState(""); const [runtimeConfig, setRuntimeConfig] = useState({ model: "", thinkingLevel: "off", maxTurns: 1000, }); const [selectedPresetId, setSelectedPresetId] = useState(null); const [selectedSkills, setSelectedSkills] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [isGenerationModalOpen, setIsGenerationModalOpen] = useState(false); // Model dropdown state const [availableModels, setAvailableModels] = useState([]); const [modelsLoading, setModelsLoading] = useState(false); const [favoriteProviders, setFavoriteProviders] = useState([]); const [favoriteModels, setFavoriteModels] = useState([]); // Load models when dialog opens — guard prevents async setState after test assertions useEffect(() => { if (!isOpen) return; setModelsLoading(true); fetchModels() .then((response) => { setAvailableModels(response.models); setFavoriteProviders(response.favoriteProviders); setFavoriteModels(response.favoriteModels); }) .catch(() => { // Gracefully handle — dropdown will show empty list }) .finally(() => setModelsLoading(false)); }, [isOpen]); // Selected model in "provider/modelId" format, or "" for default const selectedModel = runtimeConfig.model.includes("/") ? runtimeConfig.model : ""; const handleGenerated = useCallback((spec: AgentGenerationSpec) => { // Map generated role to AgentCapability, default to "custom" if unrecognized const mappedRole = VALID_CAPABILITIES.has(spec.role) ? (spec.role as AgentCapability) : "custom"; setName(spec.title); setTitle(spec.description); setIcon(spec.icon); setRole(mappedRole); // Map generated systemPrompt to instructionsText setInstructionsText(spec.systemPrompt); setRuntimeConfig(c => ({ ...c, thinkingLevel: spec.thinkingLevel, maxTurns: spec.maxTurns, })); setIsGenerationModalOpen(false); // Advance to Step 1 so user can review model selection setStep(1); }, []); const handleModelChange = useCallback((value: string) => { // value is "provider/modelId" or "" for default setRuntimeConfig(c => ({ ...c, model: value })); }, []); const handleToggleFavorite = useCallback(async (provider: string) => { const currentFavorites = favoriteProviders; const isFavorite = currentFavorites.includes(provider); const newFavorites = isFavorite ? currentFavorites.filter(p => p !== provider) : [provider, ...currentFavorites]; setFavoriteProviders(newFavorites); try { await updateGlobalSettings({ favoriteProviders: newFavorites, favoriteModels }); } catch { // Revert on error setFavoriteProviders(currentFavorites); } }, [favoriteProviders, favoriteModels]); const handleToggleModelFavorite = useCallback(async (modelId: string) => { const currentFavorites = favoriteModels; const isFavorite = currentFavorites.includes(modelId); const newFavorites = isFavorite ? currentFavorites.filter(m => m !== modelId) : [modelId, ...currentFavorites]; setFavoriteModels(newFavorites); try { await updateGlobalSettings({ favoriteProviders, favoriteModels: newFavorites }); } catch { // Revert on error setFavoriteModels(currentFavorites); } }, [favoriteProviders, favoriteModels]); const handlePresetSelect = useCallback((preset: AgentPreset) => { setSelectedPresetId(preset.id); setName(preset.name); setIcon(preset.icon); setTitle(preset.description ?? preset.title); setRole(preset.role); setSoul(preset.soul ?? ""); setInstructionsText(preset.instructionsText ?? ""); // Advance to Step 1 so user can review model selection setStep(1); }, []); if (!isOpen) return null; const handleClose = () => { setStep(0); setName(""); setTitle(""); setIcon(""); setRole("custom"); setReportsTo(""); setInstructionsPath(""); setInstructionsText(""); setSoul(""); setMemory(""); setRuntimeConfig({ model: "", thinkingLevel: "off", maxTurns: 1000 }); setSelectedPresetId(null); setSelectedSkills([]); setError(null); setIsGenerationModalOpen(false); onClose(); }; const handleCreate = async () => { if (!name.trim()) return; setIsSubmitting(true); setError(null); try { const runtimeCfg: Record = {}; if (runtimeConfig.model.trim()) runtimeCfg.model = runtimeConfig.model.trim(); if (runtimeConfig.thinkingLevel !== "off") runtimeCfg.thinkingLevel = runtimeConfig.thinkingLevel; if (runtimeConfig.maxTurns !== 1000) runtimeCfg.maxTurns = runtimeConfig.maxTurns; await createAgent({ name: name.trim(), role, ...(title.trim() ? { title: title.trim() } : {}), ...(icon.trim() ? { icon: icon.trim() } : {}), ...(reportsTo.trim() ? { reportsTo: reportsTo.trim() } : {}), ...(instructionsPath.trim() ? { instructionsPath: instructionsPath.trim() } : {}), ...(instructionsText.trim() ? { instructionsText: instructionsText.trim() } : {}), ...(soul.trim() ? { soul: soul.trim() } : {}), ...(memory.trim() ? { memory: memory.trim() } : {}), ...(Object.keys(runtimeCfg).length > 0 ? { runtimeConfig: runtimeCfg } : {}), ...(selectedSkills.length > 0 ? { metadata: { skills: selectedSkills } } : {}), }, projectId); handleClose(); onCreated(); } catch (err: unknown) { setError(err instanceof Error ? err.message : "Failed to create agent"); } finally { setIsSubmitting(false); } }; const selectedRole = AGENT_ROLES.find(r => r.value === role); return (
{ if (e.target === e.currentTarget) handleClose(); }}>
{/* Header */}
New Agent
{/* Step indicator */}
{[0, 1, 2].map(i => (
))}
{/* Body */}
{step === 0 && (
{/* Quick Start Presets */}
Choose a preset or fill in details manually
{AGENT_PRESETS.map(preset => ( ))}
Identity
setName(e.target.value)} />
setTitle(e.target.value)} />
setIcon(e.target.value)} />
{AGENT_ROLES.map(r => ( ))}
Configuration
setReportsTo(e.target.value)} />