(function () { "use strict"; // hermes-achievements dashboard plugin // Originally authored by @PCinkusz — https://github.com/PCinkusz/hermes-achievements (MIT). // Bundled into hermes-agent. Upstream repo remains the staging ground for new // badges and UI iteration; the in-progress scan banner below is a small addition // layered on top of the original dist bundle. const SDK = window.__HERMES_PLUGIN_SDK__; if (!SDK || !window.__HERMES_PLUGINS__) return; const React = SDK.React; const hooks = SDK.hooks; const C = SDK.components; const cn = SDK.utils.cn; const LUCIDE = {"flame":"","avalanche":"\n ","nodes":"\n \n \n \n ","rocket":"\n \n \n ","branch":"\n \n \n ","daemon":"\n ","clock":"\n ","warning":"\n \n ","wine":"\n \n \n ","scroll":"\n \n \n ","plug":"\n \n \n \n \n ","lock":"\n \n ","package_skull":"\n \n \n \n ","restart":"\n \n \n ","key":"\n ","colon":"\n ","container":"\n \n \n \n ","melting_clock":"\n \n ","pencil":"\n ","blueprint":"\n \n \n \n ","pixel":"\n \n \n \n ","ship":"\n \n \n \n ","spark_cursor":"\n \n \n \n ","needle":"","hammer_scroll":"\n \n ","anvil":"\n \n \n \n ","crystal":"\n \n ","palace":"\n \n \n \n \n ","dragon":"","antenna":"\n \n \n \n \n \n ","puzzle":"","rewind":"\n ","spiral":"\n \n \n \n ","quote":"\n ","compass":"\n ","browser":"\n \n ","terminal":"\n ","wand":"\n \n \n \n \n \n \n ","folder":"\n \n ","eye":"\n ","wave":"","swap":"\n \n \n ","router":"\n \n \n \n \n ","codex":"\n \n ","prism":"\n \n ","marathon":"\n \n ","calendar":"\n \n \n \n \n \n \n \n \n ","moon":"","cache":"\n \n ","secret":"\n \n "}; const tierClass = function (tier) { return tier ? "ha-tier-" + tier.toLowerCase() : "ha-tier-pending"; }; async function api(path, options) { const url = "/api/plugins/hermes-achievements" + path; const res = await fetch(url, options || {}); if (!res.ok) { const text = await res.text().catch(function () { return res.statusText; }); throw new Error(res.status + ": " + text); } const text = await res.text(); try { return JSON.parse(text); } catch (_) { return null; } } function AchievementIcon({ icon }) { const svg = LUCIDE[icon] || LUCIDE.secret; const ref = React.useRef(null); React.useEffect(function () { if (!ref.current) return; const el = ref.current; while (el.firstChild) el.removeChild(el.firstChild); try { const doc = new DOMParser().parseFromString( "" + svg + "", "image/svg+xml" ); if (!doc.querySelector("parsererror")) { Array.from(doc.documentElement.childNodes).forEach(function (n) { el.appendChild(document.importNode(n, true)); }); } } catch (_) {} }, [svg]); return React.createElement("svg", { ref: ref, className: "ha-lucide", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", }); } function StatCard(props) { return React.createElement(C.Card, { className: "ha-stat" }, React.createElement(C.CardContent, { className: "ha-stat-content" }, React.createElement("div", { className: "ha-stat-label" }, props.label), React.createElement("div", { className: "ha-stat-value" }, props.value), props.hint && React.createElement("div", { className: "ha-stat-hint" }, props.hint) ) ); } function TierLegend() { return React.createElement("div", { className: "ha-tier-legend" }, ["Copper", "Silver", "Gold", "Diamond", "Olympian"].map(function (tier, index, arr) { return React.createElement(React.Fragment, { key: tier }, React.createElement("span", { className: "ha-tier-step ha-tier-" + tier.toLowerCase() }, React.createElement("i", null), tier ), index < arr.length - 1 && React.createElement("span", { className: "ha-tier-arrow" }, "→") ); }) ); } function LoadingSkeletonCard(props) { return React.createElement(C.Card, { className: "ha-card ha-skeleton-card ha-tier-pending" }, React.createElement(C.CardContent, { className: "ha-card-content" }, React.createElement("div", { className: "ha-card-head" }, React.createElement("div", { className: "ha-skeleton ha-skeleton-icon" }), React.createElement("div", { className: "ha-skeleton-stack" }, React.createElement("div", { className: "ha-skeleton ha-skeleton-title" }), React.createElement("div", { className: "ha-skeleton ha-skeleton-meta" }) ), React.createElement("div", { className: "ha-badges" }, React.createElement("div", { className: "ha-skeleton ha-skeleton-badge" }), React.createElement("div", { className: "ha-skeleton ha-skeleton-badge ha-skeleton-badge-short" }) ) ), React.createElement("div", { className: "ha-skeleton ha-skeleton-line" }), React.createElement("div", { className: "ha-skeleton ha-skeleton-line ha-skeleton-line-short" }), React.createElement("div", { className: "ha-skeleton ha-skeleton-criteria" }), React.createElement("div", { className: "ha-evidence-slot" }, React.createElement("div", { className: "ha-skeleton ha-skeleton-evidence" })), React.createElement("div", { className: "ha-progress-row" }, React.createElement("div", { className: "ha-skeleton ha-skeleton-progress" }), React.createElement("div", { className: "ha-skeleton ha-skeleton-progress-text" }) ) ) ); } function LoadingPage() { return React.createElement("div", { className: "ha-page ha-page-loading" }, React.createElement("section", { className: "ha-hero ha-loading-hero" }, React.createElement("div", null, React.createElement("div", { className: "ha-kicker" }, "Agentic Gamerscore"), React.createElement("h1", null, "Hermes Achievements"), React.createElement("p", null, "Scanning Hermes session history. First scan can take 5–10 seconds on large histories.") ), React.createElement("div", { className: "ha-scan-status", role: "status", "aria-live": "polite" }, React.createElement("span", { className: "ha-scan-pulse", "aria-hidden": "true" }), React.createElement("div", null, React.createElement("strong", null, "Building achievement profile…"), React.createElement("p", null, "Reading sessions, tool calls, model metadata, and unlock state.") ) ) ), React.createElement("div", { className: "ha-stats" }, ["Unlocked", "Discovered", "Secrets", "Highest tier", "Latest"].map(function (label) { return React.createElement(C.Card, { key: label, className: "ha-stat ha-skeleton-stat" }, React.createElement(C.CardContent, { className: "ha-stat-content" }, React.createElement("div", { className: "ha-stat-label" }, label), React.createElement("div", { className: "ha-skeleton ha-skeleton-stat-value" }), React.createElement("div", { className: "ha-skeleton ha-skeleton-stat-hint" }) ) ); }) ), React.createElement("section", { className: "ha-guide ha-loading-guide" }, React.createElement("div", null, React.createElement("strong", null, "Scan status"), React.createElement("p", null, "Hermes is scanning local history once, then cards will appear automatically. Nothing is stuck if this takes a few seconds.") ), React.createElement("div", null, React.createElement("strong", null, "What is scanned"), React.createElement("p", null, "Sessions, tool calls, model metadata, errors, achievements, and local unlock state.") ) ), React.createElement("section", { className: "ha-grid" }, [0, 1, 2, 3, 4, 5].map(function (i) { return React.createElement(LoadingSkeletonCard, { key: i }); })) ); } function AchievementCard({ achievement }) { const unlocked = achievement.unlocked; const progress = achievement.progress || 0; const pct = achievement.progress_pct || (unlocked ? 100 : 0); const state = achievement.state || (unlocked ? "unlocked" : "discovered"); const stateLabel = state === "unlocked" ? "Unlocked" : (state === "secret" ? "Secret" : "Discovered"); const targetTier = achievement.next_tier || achievement.tier; const tierLabel = achievement.tier ? achievement.tier : (targetTier ? "Target " + targetTier : (state === "secret" ? "Hidden" : (unlocked ? "Complete" : "Objective"))); const progressText = state === "secret" ? "hidden" : (progress + (achievement.next_threshold ? " / " + achievement.next_threshold : "")); return React.createElement(C.Card, { className: cn("ha-card", "ha-state-" + state, tierClass(achievement.tier || achievement.next_tier)) }, React.createElement(C.CardContent, { className: "ha-card-content" }, React.createElement("div", { className: "ha-card-head" }, React.createElement("div", { className: "ha-icon" }, React.createElement(AchievementIcon, { icon: achievement.icon || "secret" })), React.createElement("div", { className: "ha-card-title-wrap" }, React.createElement("div", { className: "ha-card-title" }, achievement.name), React.createElement("div", { className: "ha-card-category" }, achievement.category) ), React.createElement("div", { className: "ha-badges" }, React.createElement("span", { className: "ha-state-badge" }, stateLabel), React.createElement("span", { className: "ha-tier-badge" }, tierLabel) ) ), React.createElement("p", { className: "ha-description" }, achievement.description), achievement.criteria && React.createElement("details", { className: "ha-criteria" }, React.createElement("summary", null, state === "secret" ? "How to reveal" : "What counts"), React.createElement("p", null, achievement.criteria) ), React.createElement("div", { className: "ha-evidence-slot" }, achievement.evidence ? React.createElement("div", { className: "ha-evidence" }, React.createElement("span", { className: "ha-evidence-label" }, "Evidence"), React.createElement("span", { className: "ha-evidence-title" }, achievement.evidence.title || achievement.evidence.session_id || "session") ) : React.createElement("div", { className: "ha-evidence ha-evidence-empty", "aria-hidden": "true" }, "No evidence yet") ), React.createElement("div", { className: "ha-progress-row" }, React.createElement("div", { className: "ha-progress-track" }, React.createElement("div", { className: "ha-progress-fill", style: { width: Math.max(state === "secret" ? 0 : 3, Math.min(100, pct)) + "%" } }) ), React.createElement("span", { className: "ha-progress-text" }, progressText) ) ) ); } function AchievementsPage() { const [data, setData] = hooks.useState(null); const [loading, setLoading] = hooks.useState(true); const [error, setError] = hooks.useState(null); const [category, setCategory] = hooks.useState("All"); const [visibility, setVisibility] = hooks.useState("all"); function load() { setLoading(true); api("/achievements") .then(function (payload) { setData(payload); setError((payload && payload.error) || null); }) .catch(function (err) { setError(String(err)); }) .finally(function () { setLoading(false); }); } // refresh() re-fetches without flipping the loading state — used by the // auto-poller during an in-progress background scan so the page updates // with growing unlock counts instead of flashing the loading skeleton. function refresh() { api("/achievements") .then(function (payload) { setData(payload); setError((payload && payload.error) || null); }) .catch(function (err) { setError(String(err)); }); } hooks.useEffect(load, []); // Auto-poll while the backend is still scanning. scan_meta.mode is // "pending" on the very first request (no cache yet) and "in_progress" // while the background thread is publishing partial snapshots. Once it // flips to "full" or "incremental" the scan is done and we stop polling. const scanMode = (data && data.scan_meta && data.scan_meta.mode) || null; const scanInFlight = scanMode === "pending" || scanMode === "in_progress"; hooks.useEffect(function () { if (!scanInFlight) return undefined; const id = setInterval(refresh, 4000); return function () { clearInterval(id); }; }, [scanInFlight]); const achievements = (data && data.achievements) || []; const categories = ["All"].concat(Array.from(new Set(achievements.map(function (a) { return a.category; })))); const visible = achievements.filter(function (a) { if (category !== "All" && a.category !== category) return false; if (visibility === "unlocked" && a.state !== "unlocked") return false; if (visibility === "discovered" && a.state !== "discovered") return false; if (visibility === "secret" && a.state !== "secret") return false; return true; }); const unlocked = achievements.filter(function (a) { return a.state === "unlocked"; }); const discovered = achievements.filter(function (a) { return a.state === "discovered"; }); const secret = achievements.filter(function (a) { return a.state === "secret"; }); const latest = unlocked.slice().sort(function (a, b) { return (b.unlocked_at || 0) - (a.unlocked_at || 0); }).slice(0, 5); const highest = ["Olympian", "Diamond", "Gold", "Silver", "Copper"].find(function (tier) { return unlocked.some(function (a) { return a.tier === tier; }); }) || "None yet"; // Build the in-progress scan banner once so the JSX below stays readable. // Shows nothing when the scan is idle. When a scan is running it renders // a pulsing status row with "X / Y sessions · Z%" and a filling bar, so // the user gets continuous visual feedback during long cold scans on // large session databases (can take several minutes on 8000+ sessions). let scanBanner = null; if (scanInFlight) { const meta = (data && data.scan_meta) || {}; const scanned = Number(meta.sessions_scanned_so_far || meta.sessions_total || 0); const total = Number(meta.sessions_expected_total || 0); const pct = total > 0 ? Math.max(0, Math.min(100, Math.floor((scanned / total) * 100))) : 0; const headline = scanMode === "pending" ? "Starting achievement scan…" : "Building achievement profile…"; const detail = total > 0 ? ("Scanned " + scanned.toLocaleString() + " of " + total.toLocaleString() + " sessions · " + pct + "%. Badges unlock as more history streams in.") : "Reading sessions, tool calls, model metadata, and unlock state. Badges appear here as they unlock."; scanBanner = React.createElement("section", { className: "ha-scan-banner", role: "status", "aria-live": "polite" }, React.createElement("div", { className: "ha-scan-banner-head" }, React.createElement("span", { className: "ha-scan-pulse", "aria-hidden": "true" }), React.createElement("div", { className: "ha-scan-banner-text" }, React.createElement("strong", null, headline), React.createElement("p", null, detail) ) ), total > 0 && React.createElement("div", { className: "ha-scan-progress-track", role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct }, React.createElement("div", { className: "ha-scan-progress-fill", style: { width: pct + "%" } }) ) ); } if (loading) { return React.createElement(LoadingPage, null); } return React.createElement("div", { className: "ha-page" }, React.createElement("section", { className: "ha-hero" }, React.createElement("div", null, React.createElement("div", { className: "ha-kicker" }, "Agentic Gamerscore"), React.createElement("h1", null, "Hermes Achievements"), React.createElement("p", null, "Collectible Hermes badges earned from real session history. Known unfinished achievements are shown as Discovered; Secret achievements stay hidden until the first matching behavior appears.") ), React.createElement(C.Button, { onClick: load, className: "ha-refresh" }, "Rescan") ), scanBanner, error && React.createElement(C.Card, { className: "ha-error" }, React.createElement(C.CardContent, null, String(error))), React.createElement("div", { className: "ha-stats" }, React.createElement(StatCard, { label: "Unlocked", value: (data ? data.unlocked_count : 0) + " / " + (data ? data.total_count : 0), hint: "earned badges" }), React.createElement(StatCard, { label: "Discovered", value: discovered.length, hint: "known, not earned yet" }), React.createElement(StatCard, { label: "Secrets", value: secret.length, hint: "hidden until first signal" }), React.createElement(StatCard, { label: "Highest tier", value: highest, hint: "Copper → Silver → Gold → Diamond → Olympian" }), React.createElement(StatCard, { label: "Latest", value: latest[0] ? latest[0].name : "None yet", hint: latest[0] ? latest[0].category : "run Hermes more" }) ), React.createElement("section", { className: "ha-guide" }, React.createElement("div", null, React.createElement("strong", null, "Tiers"), React.createElement(TierLegend, null) ), React.createElement("div", null, React.createElement("strong", null, "Secret achievements"), React.createElement("p", null, "Secrets hide their exact trigger. Once Hermes sees a related signal, the card becomes Discovered and shows its requirement.") ) ), React.createElement("div", { className: "ha-toolbar" }, React.createElement("div", { className: "ha-pills" }, categories.map(function (cat) { return React.createElement("button", { key: cat, onClick: function () { setCategory(cat); }, className: cat === category ? "active" : "" }, cat); })), React.createElement("div", { className: "ha-pills" }, ["all", "unlocked", "discovered", "secret"].map(function (v) { return React.createElement("button", { key: v, onClick: function () { setVisibility(v); }, className: v === visibility ? "active" : "" }, v); })) ), latest.length > 0 && React.createElement("section", { className: "ha-latest" }, React.createElement("h2", null, "Recent unlocks"), React.createElement("div", { className: "ha-latest-row" }, latest.map(function (a) { return React.createElement("div", { key: a.id, className: cn("ha-chip", tierClass(a.tier)) }, React.createElement("span", { className: "ha-chip-icon" }, React.createElement(AchievementIcon, { icon: a.icon || "secret" })), a.name ); })) ), visibility === "secret" && visible.length === 0 && React.createElement(C.Card, { className: "ha-secret-empty" }, React.createElement(C.CardContent, { className: "ha-secret-empty-content" }, React.createElement("strong", null, "No hidden secrets left in this scan."), React.createElement("p", null, "Clue: secrets usually start from unusual failure or power-user patterns — port conflicts, permission walls, missing env vars, YAML mistakes, Docker collisions, rollback/checkpoint use, cache hits, or tiny fixes after lots of red text.") ) ), React.createElement("section", { className: "ha-grid" }, visible.map(function (a) { return React.createElement(AchievementCard, { key: a.id, achievement: a }); })) ); } window.__HERMES_PLUGINS__.register("hermes-achievements", AchievementsPage); })();