import { useEffect, useMemo, useState } from 'react' import { Switch } from '@/components/ui/switch' import { getSkills, getToolsets, toggleSkill } from '@/hermes' import { Brain, Wrench } from '@/lib/icons' import { notify, notifyError } from '@/store/notifications' import type { SkillInfo, ToolsetInfo } from '@/types/hermes' import { asText, includesQuery, prettyName, toolNames } from './helpers' import { ListRow, LoadingState, Pill, SectionHeading, SettingsContent } from './primitives' import type { SearchProps } from './types' export function ToolsSettings({ query }: SearchProps) { const [skills, setSkills] = useState(null) const [toolsets, setToolsets] = useState(null) const [savingSkill, setSavingSkill] = useState(null) useEffect(() => { let cancelled = false Promise.all([getSkills(), getToolsets()]) .then(([s, t]) => { if (cancelled) { return } setSkills(s) setToolsets(t) }) .catch(err => notifyError(err, 'Capabilities failed to load')) return () => void (cancelled = true) }, []) const filteredSkills = useMemo(() => { if (!skills) { return [] } const q = query.trim().toLowerCase() return skills .filter(s => !q || includesQuery(s.name, q) || includesQuery(s.description, q) || includesQuery(s.category, q)) .sort( (a, b) => asText(a.category).localeCompare(asText(b.category)) || asText(a.name).localeCompare(asText(b.name)) ) }, [query, skills]) const filteredToolsets = useMemo(() => { if (!toolsets) { return [] } const q = query.trim().toLowerCase() return toolsets .filter(t => { if (!q) { return true } return ( includesQuery(t.name, q) || includesQuery(t.label, q) || includesQuery(t.description, q) || toolNames(t).some(n => includesQuery(n, q)) ) }) .sort((a, b) => asText(a.label || a.name).localeCompare(asText(b.label || b.name))) }, [query, toolsets]) const skillGroups = useMemo(() => { const groups = new Map() for (const skill of filteredSkills) { const cat = asText(skill.category) || 'other' groups.set(cat, [...(groups.get(cat) ?? []), skill]) } return Array.from(groups).sort(([a], [b]) => a.localeCompare(b)) }, [filteredSkills]) async function handleToggleSkill(skill: SkillInfo, enabled: boolean) { setSavingSkill(skill.name) try { await toggleSkill(skill.name, enabled) setSkills(c => c?.map(s => (s.name === skill.name ? { ...s, enabled } : s)) ?? c) notify({ kind: 'success', title: enabled ? 'Skill enabled' : 'Skill disabled', message: `${skill.name} applies to new sessions.` }) } catch (err) { notifyError(err, `Failed to update ${skill.name}`) } finally { setSavingSkill(null) } } if (!skills || !toolsets) { return } return (
s.enabled).length} enabled`} title="Skills" /> {skillGroups.map(([category, list]) => (
{prettyName(category)}
{list.map(skill => ( void handleToggleSkill(skill, c)} /> } description={asText(skill.description)} key={asText(skill.name)} title={asText(skill.name)} /> ))}
))}
t.enabled).length} enabled`} title="Toolsets" />
{filteredToolsets.map(toolset => { const tools = toolNames(toolset) const label = asText(toolset.label || toolset.name) return ( {toolset.enabled ? 'Enabled' : 'Disabled'} {toolset.configured ? 'Configured' : 'Needs keys'}
} below={ tools.length > 0 && (
{tools.slice(0, 10).map(t => ( {t} ))} {tools.length > 10 && ( +{tools.length - 10} more )}
) } description={asText(toolset.description)} key={asText(toolset.name) || label} title={label} /> ) })}
) }