feat(skills): fix browse cap, add source links + copy buttons + category cleanup (#37143)
Skills discovery surfaced ~136 of 88k skills in the CLI and gave community skills no clickable source on the docs page. Three coupled fixes: CLI browse: - hermes skills browse capped at 50 because the per-source limit dict had no 'hermes-index' key — when the centralized index is available the router skips external APIs and serves only the index, so the default-50 fallthrough silently truncated the whole hub. Add hermes-index: 5000. Browse now loads 5367 (269 pages) instead of 136. - Add an Identifier column + install/inspect hint to the browse table so users can act on what they see without a second 'search'. - Route the TUI browse_skills() helper through parallel_search_sources so it inherits the same index-aware source-skip (was double-counting); expose identifier in its output. Docs Skills Hub page: - Synthesize a sourceUrl for every community skill (github tree URL, clawhub / skills.sh / lobehub / browse.sh detail pages), preferring the adapter's explicit extra.detail_url/source_url/repo_url. Expanded cards now show 'View source' for community skills (was nothing) and keep 'View full documentation' for built-in/optional. 99% coverage. - Add a Copy button on the install command. - Add a loading state instead of flashing '0 skills / No skills found' while the 45MB catalog fetches. Category cleanup: - _guess_category fell back to tags[0] verbatim, producing ~430 junk one-off categories (version strings, brand names: '0.10.7 Dev', 'Doramagic Crystal'). Now only curated buckets are accepted; unknowns fold into 'Other'. Widen the tag->category map so common community tags route to real buckets. 430 -> 173 categories, top 20 all meaningful. Tests: tests/website/test_extract_skills.py covers _source_url synthesis + precedence and _guess_category curation (13 tests). All 27 skills-hub CLI tests still pass. Docusaurus build verified; expanded cards confirmed in browser for both community (View source) and built-in (View full docs).
This commit is contained in:
@@ -19,6 +19,10 @@ interface Skill {
|
||||
docsPath?: string;
|
||||
identifier?: string;
|
||||
installCmd?: string;
|
||||
/** Clickable URL to the skill's origin (repo / detail page). Synthesized
|
||||
* in extract-skills.py for community skills that have no generated docs
|
||||
* page, so the expanded card always has somewhere to send the user. */
|
||||
sourceUrl?: string;
|
||||
/** Lowercase pre-joined haystack used by the search filter.
|
||||
* Built once at load time so per-keystroke filtering is a single
|
||||
* `.includes()` per skill instead of array-join + toLowerCase on
|
||||
@@ -240,6 +244,47 @@ function highlightMatch(text: string, query: string): React.ReactNode {
|
||||
);
|
||||
}
|
||||
|
||||
function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const onCopy = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard?.writeText(text).then(
|
||||
() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
},
|
||||
[text],
|
||||
);
|
||||
return (
|
||||
<button
|
||||
className={styles.copyBtn}
|
||||
onClick={onCopy}
|
||||
title="Copy install command"
|
||||
aria-label="Copy install command"
|
||||
>
|
||||
{copied ? (
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14">
|
||||
<path d="M7 3.5A1.5 1.5 0 018.5 2h3.879a1.5 1.5 0 011.06.44l3.122 3.12A1.5 1.5 0 0117 6.622V12.5a1.5 1.5 0 01-1.5 1.5h-1v-3.379a3 3 0 00-.879-2.121L10.5 5.379A3 3 0 008.379 4.5H7v-1z" />
|
||||
<path d="M4.5 6A1.5 1.5 0 003 7.5v9A1.5 1.5 0 004.5 18h7a1.5 1.5 0 001.5-1.5v-5.879a1.5 1.5 0 00-.44-1.06L9.44 6.439A1.5 1.5 0 008.378 6H4.5z" />
|
||||
</svg>
|
||||
)}
|
||||
<span className={styles.copyBtnLabel}>{copied ? "Copied" : "Copy"}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillCard({
|
||||
skill,
|
||||
query,
|
||||
@@ -379,16 +424,31 @@ function SkillCard({
|
||||
)}
|
||||
<div className={styles.installHint}>
|
||||
<code>{skill.installCmd || `hermes skills install ${skill.name}`}</code>
|
||||
<CopyButton
|
||||
text={skill.installCmd || `hermes skills install ${skill.name}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.cardLinks}>
|
||||
{skill.docsPath ? (
|
||||
<a
|
||||
className={styles.docsLink}
|
||||
href={`/docs/user-guide/skills/${skill.docsPath}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
View full documentation →
|
||||
</a>
|
||||
) : skill.sourceUrl ? (
|
||||
<a
|
||||
className={styles.docsLink}
|
||||
href={skill.sourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
View source ↗
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
{skill.docsPath && (
|
||||
<a
|
||||
className={styles.docsLink}
|
||||
href={`/docs/user-guide/skills/${skill.docsPath}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
View full documentation →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -792,7 +852,15 @@ export default function SkillsDashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visible.length > 0 ? (
|
||||
{!data && !loadError ? (
|
||||
<div className={styles.empty}>
|
||||
<div className={styles.loadingSpinner} />
|
||||
<h3 className={styles.emptyTitle}>Loading the catalog…</h3>
|
||||
<p className={styles.emptyDesc}>
|
||||
Fetching 88k+ skills across every registry. One moment.
|
||||
</p>
|
||||
</div>
|
||||
) : visible.length > 0 ? (
|
||||
<>
|
||||
<div className={styles.grid}>
|
||||
{visible.map((skill, i) => {
|
||||
|
||||
@@ -628,6 +628,9 @@
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 215, 0, 0.06);
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.installHint code {
|
||||
@@ -636,6 +639,64 @@
|
||||
color: rgba(255, 215, 0, 0.7);
|
||||
background: none;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.installHint code::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.copyBtn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
padding: 0.2rem 0.45rem;
|
||||
border: 1px solid rgba(255, 215, 0, 0.18);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 215, 0, 0.06);
|
||||
color: rgba(255, 215, 0, 0.85);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.copyBtn:hover {
|
||||
background: rgba(255, 215, 0, 0.14);
|
||||
color: rgba(255, 215, 0, 1);
|
||||
}
|
||||
|
||||
.copyBtnLabel {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cardLinks {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cardLinks .docsLink {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.loadingSpinner {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
margin: 0 auto 1rem;
|
||||
border: 3px solid rgba(255, 215, 0, 0.15);
|
||||
border-top-color: rgba(255, 215, 0, 0.7);
|
||||
border-radius: 50%;
|
||||
animation: skillsSpin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes skillsSpin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.overviewBlock {
|
||||
|
||||
Reference in New Issue
Block a user