import "./AgentPromptsManager.css"; import { useState, useCallback, useEffect, useRef } from "react"; import { BUILTIN_AGENT_PROMPTS, PROMPT_KEY_CATALOG } from "../utils/builtinPrompts"; import type { AgentPromptTemplate, AgentPromptsConfig, AgentCapability } from "@fusion/core"; import type { PromptKey } from "@fusion/core"; import { Plus, Pencil, Trash2, BookOpen, Users, Settings2, ChevronDown, ChevronUp, Maximize2, Minimize2 } from "lucide-react"; /** * Props for the AgentPromptsManager component. * * Provides a unified interface for managing: * - Template customization (built-in templates as read-only, custom templates as editable) * - Role assignments (mapping roles to template IDs) * - Prompt overrides (segment-level customization) */ interface AgentPromptsManagerProps { /** Current agent prompts configuration from settings */ value: AgentPromptsConfig | undefined; /** Callback when agent prompts configuration changes */ onChange: (value: AgentPromptsConfig) => void; /** Current prompt overrides from settings */ promptOverrides: Record | undefined; /** Callback when prompt overrides change */ onPromptOverridesChange: (value: Record) => void; } /** Tab identifiers */ type TabId = "templates" | "assignments" | "overrides"; /** Core agent roles that have built-in templates */ const CORE_ROLES: AgentCapability[] = ["executor", "triage", "reviewer", "merger"]; /** Role display labels */ const ROLE_LABELS: Record = { executor: "Executor Agent", triage: "Triage Agent", reviewer: "Reviewer Agent", merger: "Merger Agent", scheduler: "Scheduler Agent", engineer: "Engineer Agent", custom: "Custom Agent", }; const getRoleToneClassName = (role: AgentCapability): string => { return `prompt-role-tone--${role}`; }; /** Form data for editing/creating a custom template */ interface TemplateFormData { name: string; description: string; role: AgentCapability; prompt: string; } const EMPTY_TEMPLATE_FORM: TemplateFormData = { name: "", description: "", role: "executor", prompt: "", }; type FullscreenTemplateView = { source: "builtin" | "custom"; id: string; }; /** * Generate a kebab-case ID from a template name. * If collision exists with built-in or existing custom IDs, append -2, -3, etc. */ function generateTemplateId( name: string, existingCustomTemplates: AgentPromptTemplate[], ): string { // Generate base kebab-case ID const baseId = name .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, "") .slice(0, 50); // Get all IDs that would cause a collision const builtinIds = new Set(BUILTIN_AGENT_PROMPTS.map((t) => t.id)); const customIds = new Set(existingCustomTemplates.map((t) => t.id)); // If no collision, return base ID if (!builtinIds.has(baseId) && !customIds.has(baseId)) { return baseId; } // Find the next available number let counter = 2; while (builtinIds.has(`${baseId}-${counter}`) || customIds.has(`${baseId}-${counter}`)) { counter++; } return `${baseId}-${counter}`; } /** * AgentPromptsManager - A unified component for managing agent prompt templates, * role assignments, and prompt segment overrides. * * Provides three tabs: * 1. **Templates**: View built-in templates, create/edit/delete custom templates * 2. **Assignments**: Map agent roles to specific templates * 3. **Overrides**: Customize specific segments of agent prompts */ export function AgentPromptsManager({ value, onChange, promptOverrides, onPromptOverridesChange, }: AgentPromptsManagerProps) { // Tab state const [activeTab, setActiveTab] = useState("templates"); // Template editing state const [editingTemplateId, setEditingTemplateId] = useState(null); const [isCreating, setIsCreating] = useState(false); const [templateForm, setTemplateForm] = useState(EMPTY_TEMPLATE_FORM); const [templateIdError, setTemplateIdError] = useState(null); const [deleteConfirmId, setDeleteConfirmId] = useState(null); // Override expanded state for accordion behavior const [expandedOverrides, setExpandedOverrides] = useState>(new Set()); // Fullscreen state for overrides tab const [fullscreenOverrideKey, setFullscreenOverrideKey] = useState(null); // Fullscreen state for templates tab const [isTemplatePromptFullscreen, setIsTemplatePromptFullscreen] = useState(false); const [fullscreenViewTemplate, setFullscreenViewTemplate] = useState(null); const fullscreenViewContainerRef = useRef(null); // Get custom templates from current config const customTemplates = value?.templates ?? []; // Get role assignments from current config const roleAssignments = value?.roleAssignments ?? {}; const fullscreenTemplate = fullscreenViewTemplate === null ? null : fullscreenViewTemplate.source === "builtin" ? BUILTIN_AGENT_PROMPTS.find((template) => template.id === fullscreenViewTemplate.id) : customTemplates.find((template) => template.id === fullscreenViewTemplate.id); // Get templates for a specific role (both built-in and custom) const getTemplatesForRole = useCallback( (role: AgentCapability): AgentPromptTemplate[] => { const builtIn = BUILTIN_AGENT_PROMPTS.filter((t) => t.role === role); const custom = customTemplates.filter((t) => t.role === role); return [...builtIn, ...custom]; }, [customTemplates], ); useEffect(() => { if (fullscreenViewTemplate !== null) { fullscreenViewContainerRef.current?.focus(); } }, [fullscreenViewTemplate]); // Handle starting template creation const handleStartCreate = useCallback(() => { setIsCreating(true); setEditingTemplateId(null); setTemplateForm(EMPTY_TEMPLATE_FORM); setTemplateIdError(null); setFullscreenViewTemplate(null); }, []); // Handle starting template edit const handleStartEdit = useCallback((template: AgentPromptTemplate) => { setEditingTemplateId(template.id); setIsCreating(false); setTemplateForm({ name: template.name, description: template.description, role: template.role, prompt: template.prompt, }); setTemplateIdError(null); setFullscreenViewTemplate(null); }, []); // Handle canceling template edit const handleCancelEdit = useCallback(() => { setEditingTemplateId(null); setIsCreating(false); setTemplateForm(EMPTY_TEMPLATE_FORM); setTemplateIdError(null); setFullscreenViewTemplate(null); setIsTemplatePromptFullscreen(false); }, []); // Handle saving a template (create or update) const handleSaveTemplate = useCallback(() => { const trimmedName = templateForm.name.trim(); if (!trimmedName) { setTemplateIdError("Template name is required"); return; } // Generate ID for new templates let templateId: string; if (isCreating) { templateId = generateTemplateId(trimmedName, customTemplates); // Check for collision with built-in IDs (shouldn't happen with generateTemplateId, but be defensive) const builtinIds = new Set(BUILTIN_AGENT_PROMPTS.map((t) => t.id)); if (builtinIds.has(templateId)) { setTemplateIdError(`Template ID "${templateId}" conflicts with a built-in template. Please use a different name.`); return; } } else { templateId = editingTemplateId!; } const newTemplate: AgentPromptTemplate = { id: templateId, name: trimmedName, description: templateForm.description.trim(), role: templateForm.role, prompt: templateForm.prompt, builtIn: false, }; let newTemplates: AgentPromptTemplate[]; if (isCreating) { newTemplates = [...customTemplates, newTemplate]; } else { newTemplates = customTemplates.map((t) => t.id === templateId ? newTemplate : t, ); } // If changing role, clear any assignment to this template const newAssignments = { ...roleAssignments }; for (const [role, assignedId] of Object.entries(newAssignments)) { if (assignedId === templateId && templateForm.role !== role) { delete newAssignments[role as AgentCapability]; } } onChange({ ...value, templates: newTemplates, roleAssignments: Object.keys(newAssignments).length > 0 ? newAssignments : undefined, }); handleCancelEdit(); }, [ templateForm, isCreating, editingTemplateId, customTemplates, roleAssignments, value, onChange, handleCancelEdit, ]); // Handle deleting a template const handleDeleteTemplate = useCallback( (templateId: string) => { // Remove the template const newTemplates = customTemplates.filter((t) => t.id !== templateId); // Clear any role assignments pointing to this template const newAssignments = { ...roleAssignments }; let assignmentCleared = false; for (const [role, assignedId] of Object.entries(newAssignments)) { if (assignedId === templateId) { delete newAssignments[role as AgentCapability]; assignmentCleared = true; } } onChange({ ...value, templates: newTemplates.length > 0 ? newTemplates : undefined, roleAssignments: Object.keys(newAssignments).length > 0 || assignmentCleared ? Object.keys(newAssignments).length > 0 ? newAssignments : undefined : roleAssignments, }); setDeleteConfirmId(null); if (editingTemplateId === templateId) { handleCancelEdit(); } }, [customTemplates, roleAssignments, value, onChange, editingTemplateId, handleCancelEdit], ); // Handle changing a role assignment const handleRoleAssignmentChange = useCallback( (role: AgentCapability, templateId: string) => { const newAssignments = { ...roleAssignments }; if (templateId === "") { delete newAssignments[role]; } else { newAssignments[role] = templateId; } onChange({ ...value, roleAssignments: Object.keys(newAssignments).length > 0 ? newAssignments : undefined, }); }, [roleAssignments, value, onChange], ); // Handle prompt override change const handlePromptOverrideChange = useCallback( (key: PromptKey, value: string) => { const newOverrides = { ...promptOverrides } as Record; newOverrides[key] = value || null; onPromptOverridesChange(newOverrides); }, [promptOverrides, onPromptOverridesChange], ); // Handle reset (set to null) for a prompt override const handleResetOverride = useCallback( (key: PromptKey) => { const newOverrides = { ...promptOverrides } as Record; newOverrides[key] = null; onPromptOverridesChange(newOverrides); }, [promptOverrides, onPromptOverridesChange], ); // Toggle override accordion const toggleOverrideExpanded = useCallback((key: PromptKey) => { setExpandedOverrides((prev) => { const next = new Set(prev); if (next.has(key)) { next.delete(key); } else { next.add(key); } return next; }); }, []); // Toggle fullscreen for an override const toggleFullscreenOverride = useCallback((key: PromptKey) => { setFullscreenOverrideKey((prev) => (prev === key ? null : key)); }, []); const openTemplateViewFullscreen = useCallback( (source: FullscreenTemplateView["source"], id: string) => { setIsTemplatePromptFullscreen(false); setFullscreenViewTemplate({ source, id }); }, [], ); // Toggle fullscreen for template prompt const toggleTemplatePromptFullscreen = useCallback(() => { setFullscreenViewTemplate(null); setIsTemplatePromptFullscreen((prev) => !prev); }, []); // Check if a template ID collides with built-in const isBuiltinId = (id: string): boolean => { return BUILTIN_AGENT_PROMPTS.some((t) => t.id === id); }; return (
{/* Tab Navigation */}
{/* Tab Content */}
{/* Templates Tab */} {activeTab === "templates" && (
{/* Template Editor (shown when creating or editing) */} {(isCreating || editingTemplateId !== null) && (

{isCreating ? "New Custom Template" : "Edit Custom Template"}

setTemplateForm((f) => ({ ...f, name: e.target.value })) } placeholder="e.g. My Custom Executor" data-testid="template-name-input" />
setTemplateForm((f) => ({ ...f, description: e.target.value })) } placeholder="Brief description of this template" data-testid="template-description-input" />
{isTemplatePromptFullscreen ? (
{ if (e.key === "Escape") { e.preventDefault(); setIsTemplatePromptFullscreen(false); } }} >
Edit Prompt