import * as React from 'react'; import { Check } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button, Modal, SearchInput } from '@/components/ds'; import { allSkills, skillsWithFacet } from '@/lib/skills/registry'; import { surfaceFor } from '@/lib/skills/surfaces'; /** * Attaching skills to an agent. * * Reads **the one skill registry** — the same `allSkills` the Skills page and * Owliver itself read. There is deliberately no separate list for agents: a * second one would drift, and an agent would end up offering a skill the * runtime does not have. * * Categories are derived rather than written down. A skill's own `category` * field when it declares one, and the pages it attaches to otherwise, so a * filter can never offer a grouping that matches nothing — and a skill added * tomorrow appears under its own category without this file being edited. */ const ALL = 'all'; /** The groupings the registry actually contains, in a stable order. */ function categoriesFor(skills) { const named = new Set(); for (const skill of skills) { if (skill.category) named.add(skill.category); } return [ALL, ...[...named].sort()]; } /** What this skill contributes, in the terms the reader is choosing between. */ function skillSummary(skill) { const pages = skill.pages.map((p) => surfaceFor(p)?.label || p); const capabilities = skill.owliver?.capabilities || []; return [ pages.length ? pages.join(', ') : 'No pages', capabilities.length ? `${capabilities.length} capabilit${capabilities.length === 1 ? 'y' : 'ies'}` : (skill.actions || []).length ? `${skill.actions.length} action${skill.actions.length === 1 ? '' : 's'}` : null, ].filter(Boolean).join(' · '); } /** @param {any} props */ export function AddSkillsModal({ open, onOpenChange, attached = [], customSkills = [], onAdd }) { const [query, setQuery] = React.useState(''); const [category, setCategory] = React.useState(ALL); const [picked, setPicked] = React.useState([]); /* A fresh sheet each time, so a previous selection is not still ticked. */ React.useEffect(() => { if (open) { setQuery(''); setCategory(ALL); setPicked([]); } }, [open]); /* Owliver skills only. A workforce training path is something a person learns, not something an agent can be asked to do, and offering one here would promise behaviour that does not exist. */ const available = React.useMemo( () => skillsWithFacet(allSkills(customSkills), 'owliver') .filter((s) => s.status === 'active' && !attached.includes(s.id)), [customSkills, attached] ); const categories = React.useMemo(() => categoriesFor(available), [available]); const visible = React.useMemo(() => { const q = query.trim().toLowerCase(); return available .filter((s) => category === ALL || s.category === category) .filter((s) => !q || s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q) || s.id.includes(q)); }, [available, category, query]); const toggle = (id) => setPicked((current) => (current.includes(id) ? current.filter((x) => x !== id) : [...current, id])); return (

{picked.length ? `${picked.length} skill${picked.length === 1 ? '' : 's'} selected` : 'Nothing selected yet'}

} >
{categories.length > 1 && (
{categories.map((id) => ( ))}
)}
{visible.map((skill) => { const chosen = picked.includes(skill.id); return ( ); })} {!visible.length && (

{available.length ? 'No skills match that.' : 'Every available skill is already attached to this agent.'}

)}
); } export default AddSkillsModal;