/** * @file SkillUsagePanel.tsx * @description Which skills and plugins actually get used, from * `GET /api/workflows/skill-usage`: plugin totals as chips (skill calls + the * plugin's own MCP tool calls) over a ranked list of individual skills with * their call count and how many sessions reached for them. A skill that is * installed but never invoked simply never appears — the panel reports use, not * inventory. * @author Nguyễn Ngọc Trí Vĩ */ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { api } from "../../lib/api"; import { useLiveRefresh } from "../../hooks/useLiveRefresh"; import type { SkillUsage } from "../../lib/types"; export function SkillUsagePanel({ statusFilter }: { statusFilter: string }) { const { t } = useTranslation("workflows"); const [data, setData] = useState(null); const [error, setError] = useState(null); const refresh = useCallback(async () => { try { setData(await api.workflows.skillUsage(statusFilter)); setError(null); } catch (err) { setError(err instanceof Error ? err.message : t("failedLoad")); } }, [statusFilter, t]); useEffect(() => { void refresh(); }, [refresh]); // Throttled (not debounced): a steady event stream still refreshes, every // 15 s — it is an aggregate over every event. useLiveRefresh(() => void refresh(), { throttleMs: 15000, match: (msg) => msg.type === "new_event" || msg.type === "session_updated", }); if (error) return

{error}

; if (!data) return null; if (!data.skills.length && !data.plugins.length) { return

{t("skillUsage.empty")}

; } const topCount = Math.max(...data.skills.map((s) => s.count), 1); return (
{data.plugins.length > 0 && (

{t("skillUsage.plugins")}

{data.plugins.map((p) => ( {p.name} {p.skillCalls + p.mcpCalls} ))}
)}

{t("skillUsage.skills")}

{data.skills.map((s) => (
{s.name}
{t("skillUsage.calls", { count: s.count })} ·{" "} {t("skillUsage.sessions", { count: s.sessions })}
))}
); }