import { useEffect, useMemo, useState } from 'react' import { useSkillStore } from '../../stores/skillStore' import { useSessionStore } from '../../stores/sessionStore' import { useTranslation } from '../../i18n' import type { SkillMeta, SkillSource } from '../../types/skill' const SOURCE_ORDER: SkillSource[] = ['user', 'project', 'plugin', 'mcp', 'bundled'] const SOURCE_ICONS: Record = { user: 'person', project: 'folder', plugin: 'extension', mcp: 'hub', bundled: 'inventory_2', } const SOURCE_ACCENT_CLASSES: Record = { user: 'bg-[var(--color-primary-fixed)] text-[var(--color-brand)]', project: 'bg-[var(--color-success-container)] text-[var(--color-success)]', plugin: 'bg-[var(--color-warning-container)] text-[var(--color-warning)]', mcp: 'bg-[var(--color-info-container)] text-[var(--color-info)]', bundled: 'bg-[var(--color-surface-container-high)] text-[var(--color-text-tertiary)]', } function estimateTokens(contentLength: number) { return Math.ceil(contentLength / 4) } export function SkillList() { const { skills, isLoading, error, fetchSkills, fetchSkillDetail } = useSkillStore() const sessions = useSessionStore((s) => s.sessions) const activeSessionId = useSessionStore((s) => s.activeSessionId) const t = useTranslation() const activeSession = sessions.find((session) => session.id === activeSessionId) const currentWorkDir = activeSession?.workDir || undefined const [searchQuery, setSearchQuery] = useState('') const normalizedSearchQuery = searchQuery.trim().toLocaleLowerCase() useEffect(() => { fetchSkills(currentWorkDir) }, [fetchSkills, currentWorkDir]) const filteredSkills = useMemo(() => { if (!normalizedSearchQuery) return skills return skills.filter((skill) => { const fields = [ skill.name, skill.displayName, skill.description, skill.source, t(`settings.skills.source.${skill.source}`), skill.version, skill.pluginName, ] return fields.some((field) => field?.toLocaleLowerCase().includes(normalizedSearchQuery), ) }) }, [skills, normalizedSearchQuery, t]) const grouped = useMemo(() => { const result: Partial> = {} for (const skill of filteredSkills) { const src = skill.source as SkillSource ;(result[src] ??= []).push(skill) } return result }, [filteredSkills]) const totalTokens = useMemo( () => filteredSkills.reduce((sum, skill) => sum + estimateTokens(skill.contentLength), 0), [filteredSkills], ) const visibleGroupCount = useMemo( () => SOURCE_ORDER.filter((source) => (grouped[source] ?? []).length > 0).length, [grouped], ) if (isLoading) { return (
) } if (error) { return
{error}
} if (skills.length === 0) { return (
auto_awesome

{t('settings.skills.empty')}

{t('settings.skills.emptyHint')}

) } return (
{t('settings.skills.browserEyebrow')}
auto_awesome

{t('settings.skills.browserTitle')}

{t('settings.skills.browserDescription')}

search setSearchQuery(event.target.value)} placeholder={t('settings.skills.searchPlaceholder')} className="min-w-0 flex-1 bg-transparent text-sm text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-tertiary)]" /> {searchQuery && ( )}
{normalizedSearchQuery && (

{t('settings.skills.searchResultCount', { count: String(filteredSkills.length), total: String(skills.length), })}

)}
(grouped[source] ?? []).length > 0) .length, )} icon="layers" />
{filteredSkills.length === 0 && (
search_off

{t('settings.skills.noSearchResults')}

{t('settings.skills.noSearchResultsHint')}

)}
= 2 ? 'xl:grid-cols-2' : ''}`}> {SOURCE_ORDER.map((source) => { const group = grouped[source] if (!group?.length) return null const sourceLabel = t(`settings.skills.source.${source}`) const sourceTokenCount = group.reduce( (sum, skill) => sum + estimateTokens(skill.contentLength), 0, ) return (
{SOURCE_ICONS[source]}

{sourceLabel}

{group.length}

{t('settings.skills.groupHint', { source: sourceLabel, count: String(group.length), })}

{t('settings.skills.tokenEstimateShort', { count: String(sourceTokenCount) })}
{group.map((skill) => ( ))}
) })}
) } function SummaryCard({ label, value, icon, className = '', }: { label: string value: string icon: string className?: string }) { return (
{icon} {label}
{value}
) }