'use client'; import { useState, useMemo } from 'react'; import { HookSlot } from './hook-slot'; import { AgentForm } from './agent-form'; import { PacksPanel } from './packs-panel'; import { playUISound } from '@/lib/ui-audio'; import { getEventLabel } from '@/lib/utils'; import type { HookEvent, SkillHookEvent, SoundAssignments, AgentInfo, SkillInfo, AgentFormData, SelectMode } from '@/lib/types'; const HOOK_GROUPS: { label: string; events: HookEvent[] }[] = [ { label: 'LIFECYCLE', events: ['SessionStart', 'SessionEnd', 'Stop'] }, { label: 'TOOLING', events: ['PreToolUse', 'PostToolUse', 'PostToolUseFailure'] }, { label: 'SUBAGENTS', events: ['SubagentStop'] }, { label: 'SYSTEM', events: ['Notification', 'PreCompact'] }, ]; const ALL_EVENTS: HookEvent[] = HOOK_GROUPS.flatMap((g) => g.events); const SKILL_EVENTS: SkillHookEvent[] = ['PreToolUse', 'PostToolUse']; const SKILL_EVENT_LABELS: Record = { PreToolUse: 'ON INVOKE', PostToolUse: 'ON COMPLETE', }; // ── Agent row ──────────────────────────────────────────────────── interface AgentRowProps { scope: string; label: string; isGlobal?: boolean; hooks: Partial>; enabled?: boolean; agentInfo?: AgentInfo; onToggle?: () => void; onClear: (event: HookEvent) => void; onPreview: (path: string) => void; onEdit?: () => void; onDelete?: () => void; selectMode: SelectMode | null; onSlotSelect: (mode: SelectMode) => void; } function AgentRow({ scope, label, isGlobal, hooks, enabled, onToggle, onClear, onPreview, onEdit, onDelete, selectMode, onSlotSelect }: AgentRowProps) { const [expanded, setExpanded] = useState(!!isGlobal); const [isHovered, setIsHovered] = useState(false); const filledCount = Object.values(hooks).filter(Boolean).length; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onClick={() => { playUISound('toggle', 0.3); setExpanded(!expanded); }} >
{expanded ? '▾' : '▸'} {label}
{filledCount}/{ALL_EVENTS.length} {!isGlobal && onToggle && ( )} {onEdit && ( )} {onDelete && ( )}
{expanded && (
{HOOK_GROUPS.map((group) => (
{group.label}
{group.events.map((event) => ( onClear(event)} onPreview={onPreview} selectMode={selectMode} onSelect={() => onSlotSelect({ scope, event, label: getEventLabel(event) })} /> ))}
))}
)}
); } // ── Skill row ──────────────────────────────────────────────────── interface SkillRowProps { skill: SkillInfo; hooks: Partial>; enabled: boolean; onToggle: () => void; onClear: (event: SkillHookEvent) => void; onPreview: (path: string) => void; selectMode: SelectMode | null; onSlotSelect: (mode: SelectMode) => void; } function SkillRow({ skill, hooks, enabled, onToggle, onClear, onPreview, selectMode, onSlotSelect }: SkillRowProps) { const [expanded, setExpanded] = useState(false); const [isHovered, setIsHovered] = useState(false); const filledCount = Object.values(hooks).filter(Boolean).length; const scope = `skill/${skill.qualifiedName}`; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onClick={() => { playUISound('toggle', 0.3); setExpanded(!expanded); }} >
{expanded ? '▾' : '▸'} {skill.name}
{filledCount}/2
{expanded && (
SIGNALS
{SKILL_EVENTS.map((event) => ( onClear(event)} onPreview={onPreview} selectMode={selectMode} onSelect={() => onSlotSelect({ scope, event, label: SKILL_EVENT_LABELS[event] })} /> ))}
)}
); } // ── Panel export ───────────────────────────────────────────────── interface AgentRosterPanelProps { assignments: SoundAssignments; agents: AgentInfo[]; skills: SkillInfo[]; onAssignmentChange: (next: SoundAssignments) => void; onPreview: (path: string) => void; onAgentsChange: () => void; selectMode: SelectMode | null; onSlotSelect: (mode: SelectMode) => void; } export function AgentRosterPanel({ assignments, agents, skills, onAssignmentChange, onPreview, onAgentsChange, selectMode, onSlotSelect }: AgentRosterPanelProps) { const [activeView, setActiveView] = useState<'agents' | 'skills' | 'packs'>('agents'); const [showForm, setShowForm] = useState(false); const [editingAgent, setEditingAgent] = useState(); const [skillSearch, setSkillSearch] = useState(''); const [collapsedNs, setCollapsedNs] = useState>(new Set()); const clearGlobalHook = (event: HookEvent) => { onAssignmentChange({ ...assignments, global: { ...assignments.global, [event]: undefined } }); }; const clearAgentHook = (agentName: string, event: HookEvent) => { onAssignmentChange({ ...assignments, agents: { ...assignments.agents, [agentName]: { ...assignments.agents[agentName], hooks: { ...assignments.agents[agentName]?.hooks, [event]: undefined }, }, }, }); }; const clearSkillHook = (skillName: string, event: SkillHookEvent) => { onAssignmentChange({ ...assignments, skills: { ...assignments.skills, [skillName]: { ...assignments.skills[skillName], hooks: { ...assignments.skills[skillName]?.hooks, [event]: undefined }, }, }, }); }; const toggleAgent = (agentName: string) => { const current = assignments.agents[agentName] ?? { enabled: true, hooks: {} }; onAssignmentChange({ ...assignments, agents: { ...assignments.agents, [agentName]: { ...current, enabled: !current.enabled } }, }); }; const toggleSkill = (skillName: string) => { const current = assignments.skills[skillName] ?? { enabled: true, hooks: {} }; onAssignmentChange({ ...assignments, skills: { ...assignments.skills, [skillName]: { ...current, enabled: !current.enabled } }, }); }; const handleSaveAgent = async (data: AgentFormData, originalName?: string) => { if (originalName) { await fetch(`/api/agents/${originalName}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } else { await fetch('/api/agents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } setShowForm(false); setEditingAgent(undefined); onAgentsChange(); }; const handleDeleteAgent = async (agentName: string) => { await fetch(`/api/agents/${agentName}`, { method: 'DELETE' }); onAgentsChange(); }; const allAgentNames = new Set([ ...Object.keys(assignments.agents), ...agents.map((a) => a.name), ]); // Build unified skill list: merge assigned skills + discovered skills by qualifiedName const allSkills = useMemo(() => { const map = new Map(); // Discovered skills for (const s of skills) map.set(s.qualifiedName, s); // Skills in assignments that weren't discovered (show them anyway) for (const key of Object.keys(assignments.skills)) { if (!map.has(key)) { map.set(key, { name: key.includes(':') ? key.split(':').pop()! : key, qualifiedName: key, description: '' }); } } return [...map.values()]; }, [skills, assignments.skills]); // Filter by search const filteredSkills = useMemo(() => { const q = skillSearch.toLowerCase(); return q ? allSkills.filter(s => s.qualifiedName.toLowerCase().includes(q) || s.description.toLowerCase().includes(q)) : allSkills; }, [allSkills, skillSearch]); // Group by namespace const skillGroups = useMemo(() => { const groups = new Map(); for (const s of filteredSkills) { const ns = s.namespace ?? '(user)'; if (!groups.has(ns)) groups.set(ns, []); groups.get(ns)!.push(s); } return [...groups.entries()].sort(([a], [b]) => { if (a === '(user)') return -1; if (b === '(user)') return 1; return a.localeCompare(b); }); }, [filteredSkills]); return (
{/* Tab bar header */}
{(['agents', 'skills', 'packs'] as const).map((view) => ( ))} {activeView === 'agents' && ( )}
{activeView === 'agents' && ( <> {/* Add/Edit form */} {(showForm || editingAgent) && ( { setShowForm(false); setEditingAgent(undefined); }} /> )} {/* Global override */} {/* Per-agent rows */} {[...allAgentNames].map((name) => { const config = assignments.agents[name] ?? { enabled: true, hooks: {} }; const agentInfo = agents.find((a) => a.name === name); return ( toggleAgent(name)} onClear={(event) => clearAgentHook(name, event)} onPreview={onPreview} onEdit={() => { setEditingAgent(agentInfo); setShowForm(false); }} onDelete={() => handleDeleteAgent(name)} selectMode={selectMode} onSlotSelect={onSlotSelect} /> ); })} )} {activeView === 'skills' && ( <> {/* Search */} setSkillSearch(e.target.value)} className="w-full text-[10px] px-2 py-1 mb-3 bg-transparent outline-none sf-mono" style={{ border: `1px solid ${skillSearch ? 'var(--sf-blue)' : 'var(--sf-border)'}`, color: 'rgba(255,255,255,0.6)', caretColor: 'var(--sf-cyan)', }} /> {/* Grouped by namespace */} {skillGroups.map(([ns, nsSkills]) => { const isCollapsed = collapsedNs.has(ns); const assignedCount = nsSkills.filter(s => assignments.skills[s.qualifiedName]?.hooks.PreToolUse || assignments.skills[s.qualifiedName]?.hooks.PostToolUse).length; return (
{/* Namespace header */}
{ playUISound('toggle', 0.3); setCollapsedNs(prev => { const next = new Set(prev); if (next.has(ns)) next.delete(ns); else next.add(ns); return next; }); }} >
{isCollapsed ? '▸' : '▾'} {ns}
{assignedCount > 0 && {assignedCount}✦} {nsSkills.length}
{!isCollapsed && nsSkills.sort((a, b) => a.name.localeCompare(b.name)).map((s) => { const config = assignments.skills[s.qualifiedName] ?? { enabled: true, hooks: {} }; return ( toggleSkill(s.qualifiedName)} onClear={(event) => clearSkillHook(s.qualifiedName, event)} onPreview={onPreview} selectMode={selectMode} onSlotSelect={onSlotSelect} /> ); })}
); })} )} {activeView === 'packs' && }
); }