Skip to content

Commit 7220d34

Browse files
committed
feat: skills discovery API + modal UI
1 parent a18e655 commit 7220d34

10 files changed

Lines changed: 821 additions & 2 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<script setup lang="ts">
2+
import type { SkillListItem } from '#shared/types'
3+
4+
const props = defineProps<{
5+
skills: SkillListItem[]
6+
packageName: string
7+
version?: string
8+
}>()
9+
10+
function getSkillSourceUrl(skill: SkillListItem): string {
11+
const base = `/code/${props.packageName}`
12+
const versionPath = props.version ? `/v/${props.version}` : ''
13+
return `${base}${versionPath}/skills/${skill.dirName}/SKILL.md`
14+
}
15+
16+
const open = defineModel<boolean>('open', { default: false })
17+
18+
// Track expanded skills
19+
const expandedSkills = ref<Set<string>>(new Set())
20+
21+
function toggleSkill(dirName: string) {
22+
if (expandedSkills.value.has(dirName)) {
23+
expandedSkills.value.delete(dirName)
24+
} else {
25+
expandedSkills.value.add(dirName)
26+
}
27+
expandedSkills.value = new Set(expandedSkills.value)
28+
}
29+
30+
const requestUrl = useRequestURL()
31+
const baseUrl = computed(() => `${requestUrl.protocol}//${requestUrl.host}`)
32+
33+
const installCommand = computed(() => {
34+
if (!props.skills.length) return null
35+
return `npx skills add ${baseUrl.value}/${props.packageName}`
36+
})
37+
38+
const { copied, copy } = useClipboard({ copiedDuring: 2000 })
39+
const copyCommand = () => installCommand.value && copy(installCommand.value)
40+
41+
function getWarningTooltip(skill: SkillListItem): string | undefined {
42+
if (!skill.warnings?.length) return undefined
43+
return skill.warnings.map(w => w.message).join(', ')
44+
}
45+
46+
function close() {
47+
open.value = false
48+
}
49+
50+
function handleKeydown(event: KeyboardEvent) {
51+
if (event.key === 'Escape') close()
52+
}
53+
</script>
54+
55+
<template>
56+
<Teleport to="body">
57+
<Transition
58+
enter-active-class="transition-opacity duration-200"
59+
leave-active-class="transition-opacity duration-200"
60+
enter-from-class="opacity-0"
61+
leave-to-class="opacity-0"
62+
>
63+
<div
64+
v-if="open"
65+
class="fixed inset-0 z-50 flex items-center justify-center p-0 sm:p-4"
66+
@keydown="handleKeydown"
67+
>
68+
<!-- Backdrop -->
69+
<button
70+
type="button"
71+
class="absolute inset-0 bg-black/60 cursor-default"
72+
:aria-label="$t('common.close_modal')"
73+
@click="close"
74+
/>
75+
76+
<div
77+
class="relative w-full h-full sm:h-auto bg-bg sm:border sm:border-border sm:rounded-lg shadow-xl sm:max-h-[90vh] overflow-y-auto overscroll-contain sm:max-w-2xl"
78+
role="dialog"
79+
aria-modal="true"
80+
aria-labelledby="skills-modal-title"
81+
>
82+
<div class="p-4 sm:p-6">
83+
<div class="flex items-center justify-between mb-4">
84+
<h2 id="skills-modal-title" class="font-mono text-lg font-medium">
85+
{{ $t('package.skills.title', 'Agent Skills') }}
86+
</h2>
87+
<button
88+
type="button"
89+
class="text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
90+
:aria-label="$t('common.close')"
91+
@click="close"
92+
>
93+
<span class="i-carbon-close block w-5 h-5" aria-hidden="true" />
94+
</button>
95+
</div>
96+
97+
<!-- Terminal-style install command -->
98+
<div
99+
v-if="installCommand"
100+
class="bg-bg-subtle border border-border rounded-lg overflow-hidden mb-5"
101+
>
102+
<div class="flex gap-1.5 px-3 pt-2 sm:px-4 sm:pt-3">
103+
<span class="size-2.5 rounded-full bg-fg-subtle/50" />
104+
<span class="size-2.5 rounded-full bg-fg-subtle/50" />
105+
<span class="size-2.5 rounded-full bg-fg-subtle/50" />
106+
</div>
107+
<div class="px-3 pt-2 pb-3 sm:px-4 sm:pt-3 sm:pb-4 overflow-x-auto">
108+
<div class="flex items-center gap-2">
109+
<span class="text-fg-subtle font-mono text-sm select-none shrink-0">$</span>
110+
<code class="font-mono text-sm text-fg-muted whitespace-nowrap">
111+
npx skills add {{ baseUrl }}/{{ packageName }}
112+
</code>
113+
<button
114+
type="button"
115+
class="p-1.5 text-fg-muted rounded transition-colors duration-200 hover:text-fg hover:bg-bg-muted active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 shrink-0"
116+
:aria-label="$t('package.get_started.copy_command')"
117+
@click.stop="copyCommand"
118+
>
119+
<span
120+
v-if="copied"
121+
class="i-carbon:checkmark size-4 block"
122+
aria-hidden="true"
123+
/>
124+
<span v-else class="i-carbon:copy size-4 block" aria-hidden="true" />
125+
</button>
126+
</div>
127+
</div>
128+
</div>
129+
130+
<!-- Skills list with expandable descriptions -->
131+
<ul class="space-y-0.5 list-none m-0 p-0">
132+
<li v-for="skill in skills" :key="skill.dirName">
133+
<button
134+
type="button"
135+
class="w-full flex items-center gap-2 py-1.5 text-start rounded transition-colors hover:bg-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
136+
:aria-expanded="expandedSkills.has(skill.dirName)"
137+
@click="toggleSkill(skill.dirName)"
138+
>
139+
<span
140+
class="i-carbon:chevron-right w-3.5 h-3.5 text-fg-subtle shrink-0 transition-transform duration-200"
141+
:class="{ 'rotate-90': expandedSkills.has(skill.dirName) }"
142+
aria-hidden="true"
143+
/>
144+
<span class="font-mono text-sm text-fg-muted">{{ skill.name }}</span>
145+
<span
146+
v-if="skill.warnings?.length"
147+
class="i-carbon:warning size-3.5 text-amber-500 shrink-0"
148+
:title="getWarningTooltip(skill)"
149+
/>
150+
</button>
151+
152+
<!-- Expandable details -->
153+
<div
154+
class="grid transition-[grid-template-rows] duration-200 ease-out"
155+
:class="expandedSkills.has(skill.dirName) ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
156+
>
157+
<div class="overflow-hidden">
158+
<div class="ps-5.5 pe-2 pb-2 pt-1 space-y-1.5">
159+
<!-- Description -->
160+
<p v-if="skill.description" class="text-sm text-fg-subtle">
161+
{{ skill.description }}
162+
</p>
163+
<p v-else class="text-sm text-fg-subtle/50 italic">No description</p>
164+
165+
<!-- File counts & warnings -->
166+
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
167+
<span v-if="skill.fileCounts?.scripts" class="text-fg-subtle">
168+
<span class="i-carbon:script size-3 inline-block align-[-2px] me-0.5" />{{
169+
skill.fileCounts.scripts
170+
}}
171+
scripts
172+
</span>
173+
<span v-if="skill.fileCounts?.references" class="text-fg-subtle">
174+
<span
175+
class="i-carbon:document size-3 inline-block align-[-2px] me-0.5"
176+
/>{{ skill.fileCounts.references }} refs
177+
</span>
178+
<span v-if="skill.fileCounts?.assets" class="text-fg-subtle">
179+
<span class="i-carbon:image size-3 inline-block align-[-2px] me-0.5" />{{
180+
skill.fileCounts.assets
181+
}}
182+
assets
183+
</span>
184+
<template v-for="warning in skill.warnings" :key="warning.message">
185+
<span class="text-amber-500">
186+
<span
187+
class="i-carbon:warning size-3 inline-block align-[-2px] me-0.5"
188+
/>{{ warning.message }}
189+
</span>
190+
</template>
191+
</div>
192+
193+
<!-- Source link -->
194+
<NuxtLink
195+
:to="getSkillSourceUrl(skill)"
196+
class="inline-flex items-center gap-1 text-xs text-fg-subtle hover:text-fg transition-colors"
197+
@click.stop
198+
>
199+
<span class="i-carbon:code size-3" />View source
200+
</NuxtLink>
201+
</div>
202+
</div>
203+
</div>
204+
</li>
205+
</ul>
206+
</div>
207+
</div>
208+
</div>
209+
</Transition>
210+
</Teleport>
211+
</template>

app/pages/[...package].vue

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<script setup lang="ts">
2-
import type { NpmVersionDist, PackumentVersion, ReadmeResponse } from '#shared/types'
2+
import type {
3+
NpmVersionDist,
4+
PackumentVersion,
5+
ReadmeResponse,
6+
SkillsListResponse,
7+
} from '#shared/types'
38
import type { JsrPackageInfo } from '#shared/types/jsr'
49
import { assertValidPackageName } from '#shared/utils/npm'
510
import { onKeyStroke } from '@vueuse/core'
@@ -65,9 +70,22 @@ const {
6570
)
6671
onMounted(() => fetchInstallSize())
6772
73+
// Fetch skills data (lazy, client-side)
74+
const { data: skillsData } = useLazyFetch<SkillsListResponse>(
75+
() => {
76+
const base = `/skills/${packageName.value}`
77+
const version = requestedVersion.value
78+
return version ? `${base}/v/${version}` : base
79+
},
80+
{ server: false, default: () => ({ package: '', version: '', skills: [] }) },
81+
)
82+
6883
const { data: packageAnalysis } = usePackageAnalysis(packageName, requestedVersion)
6984
const { data: moduleReplacement } = useModuleReplacement(packageName)
7085
86+
// Skills modal state
87+
const skillsModalOpen = shallowRef(false)
88+
7189
const { data: pkg, status, error } = await usePackage(packageName, requestedVersion)
7290
const resolvedVersion = computed(() => pkg.value?.resolvedVersion ?? null)
7391
@@ -638,7 +656,7 @@ function handleClick(event: MouseEvent) {
638656

639657
<!-- Stats grid -->
640658
<dl
641-
class="grid grid-cols-2 sm:grid-cols-11 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-border"
659+
class="grid grid-cols-2 sm:grid-cols-12 gap-3 sm:gap-4 py-4 sm:py-6 mt-4 sm:mt-6 border-t border-border"
642660
>
643661
<div v-if="pkg.license" class="space-y-1 sm:col-span-2">
644662
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
@@ -789,7 +807,36 @@ function handleClick(event: MouseEvent) {
789807
<DateTime :datetime="pkg.time.modified" date-style="medium" />
790808
</dd>
791809
</div>
810+
811+
<!-- Skills -->
812+
<ClientOnly>
813+
<div v-if="skillsData?.skills?.length" class="space-y-1 sm:col-span-1">
814+
<dt class="text-xs text-fg-subtle uppercase tracking-wider">
815+
{{ $t('package.stats.skills', 'Skills') }}
816+
</dt>
817+
<dd class="font-mono text-sm text-fg">
818+
<button
819+
type="button"
820+
class="inline-flex items-center gap-1 hover:text-fg-muted transition-colors"
821+
@click="skillsModalOpen = true"
822+
>
823+
<span class="i-carbon:bot w-3.5 h-3.5" aria-hidden="true" />
824+
{{ skillsData.skills.length }}
825+
</button>
826+
</dd>
827+
</div>
828+
</ClientOnly>
792829
</dl>
830+
831+
<!-- Skills Modal -->
832+
<ClientOnly>
833+
<PackageSkillsModal
834+
v-model:open="skillsModalOpen"
835+
:skills="skillsData?.skills ?? []"
836+
:package-name="pkg.name"
837+
:version="displayVersion?.version"
838+
/>
839+
</ClientOnly>
793840
</header>
794841

795842
<!-- Binary-only packages: Show only execute command (no install) -->

i18n/locales/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,17 @@
132132
"install_size": "Install Size",
133133
"vulns": "Vulns",
134134
"updated": "Updated",
135+
"skills": "Skills",
135136
"view_dependency_graph": "View dependency graph",
136137
"inspect_dependency_tree": "Inspect dependency tree",
137138
"size_tooltip": {
138139
"unpacked": "{size} unpacked size (this package)",
139140
"total": "{size} total unpacked size (including all {count} dependencies for linux-x64)"
140141
}
141142
},
143+
"skills": {
144+
"title": "Agent Skills"
145+
},
142146
"links": {
143147
"repo": "repo",
144148
"homepage": "homepage",

0 commit comments

Comments
 (0)