import { useState, useMemo, memo } from 'react'; import { Package, User, Folder, RefreshCw, ChevronDown, ChevronRight, BookOpen, } from 'lucide-react'; import type { SkillSummary } from '../../types/protocol'; /* ── types ────────────────────────────────────────────── */ interface SkillListProps { skills: SkillSummary[]; onRefresh: () => void; } interface SourceConfig { label: string; icon: typeof Package; accent: string; } const SOURCE_CONFIG: Record = { user: { label: '用户技能', icon: User, accent: 'text-[var(--accent-cyan)]' }, useragent: { label: '用户 Agent', icon: User, accent: 'text-[var(--accent-cyan)]' }, useropenclaw: { label: '用户 OpenClaw', icon: User, accent: 'text-[var(--accent-cyan)]' }, project: { label: '项目技能', icon: Folder, accent: 'text-[var(--accent-amber)]' }, projectagent: { label: '项目 Agent', icon: Folder, accent: 'text-[var(--accent-amber)]' }, projectopenclaw: { label: '项目 OpenClaw', icon: Folder, accent: 'text-[var(--accent-amber)]' }, }; function sourceConfig(source: string): SourceConfig { return ( SOURCE_CONFIG[source] ?? { label: source, icon: Package, accent: 'text-[var(--text-secondary)]', } ); } /* ── Section header ────────────────────────────────────── */ function SectionHeader({ config, count, isCollapsed, onClick, }: { config: SourceConfig; count: number; isCollapsed: boolean; onClick: () => void; }) { const Icon = config.icon; return ( ); } /* ── Skill card ────────────────────────────────────────── */ function SkillCard({ skill, config }: { skill: SkillSummary; config: SourceConfig }) { return (
{skill.name}

{skill.description}

); } /* ── main component ────────────────────────────────────── */ // memo:props 全部稳定(skills 仅在刷新时换引用、onRefresh 为 useCallback), // 主视图流式期间 App 每帧重渲染时跳过面板重渲染与分组/排序重算。 export const SkillList = memo(function SkillList({ skills, onRefresh }: SkillListProps) { const [collapsed, setCollapsed] = useState>(() => { try { const s = localStorage.getItem('picobot-skill-collapsed'); return s ? new Set(JSON.parse(s)) : new Set(); } catch (_) { return new Set(); } }); const toggle = (source: string) => { setCollapsed((prev) => { const next = new Set(prev); if (next.has(source)) { next.delete(source); } else { next.add(source); } localStorage.setItem('picobot-skill-collapsed', JSON.stringify([...next])); return next; }); }; // skills 引用未变时跳过分组/排序重算(流式期间 App 每帧重渲染) const { grouped, sorted } = useMemo(() => { const grouped = new Map(); for (const s of skills) { const l = grouped.get(s.source) || []; l.push(s); grouped.set(s.source, l); } const order = [ 'user', 'useragent', 'useropenclaw', 'project', 'projectagent', 'projectopenclaw', ]; const sorted = Array.from(grouped.keys()).sort((a, b) => { const ai = order.indexOf(a); const bi = order.indexOf(b); if (ai !== -1 && bi !== -1) return ai - bi; if (ai !== -1) return -1; if (bi !== -1) return 1; return a.localeCompare(b); }); return { grouped, sorted }; }, [skills]); return (
{/* title bar */}
技能 {skills.length > 0 && ( {skills.length} )}
{/* empty */} {skills.length === 0 && (

暂无可用技能
在 .picobot/skills/ 目录下添加 SKILL.md 文件

)} {/* list */} {skills.length > 0 && (
{sorted.map((source) => { const cfg = sourceConfig(source); const items = grouped.get(source)!; const closed = collapsed.has(source); return (
toggle(source)} /> {!closed && (
{items.map((s) => ( ))}
)}
); })}
)}
); });