feat(skill): add install-for-agent prompt

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
This commit is contained in:
FenjuFu 2026-09-01 13:48:31 +08:00
parent ac42c2346c
commit 769b03ee39
6 changed files with 133 additions and 0 deletions

View file

@ -0,0 +1,49 @@
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { InstallForAgentButton, buildAgentInstallPrompt } from './install-for-agent-button'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
describe('install-for-agent-button', () => {
const formatPrompt = (guideUrl: string, skill: string) => (
`Please follow [the guide](${guideUrl}) to install and use ${skill}.`
)
it('builds a prompt for a global skill using the instance guide', () => {
expect(buildAgentInstallPrompt('global', 'my-skill', 'https://skill.example.com', formatPrompt)).toBe(
'Please follow [the guide](https://skill.example.com/registry/skill.md) to install and use my-skill.',
)
})
it('keeps the namespace in the coordinate for an agent to resolve', () => {
expect(buildAgentInstallPrompt('team-alpha', 'my-skill', 'https://skill.example.com/', formatPrompt)).toBe(
'Please follow [the guide](https://skill.example.com/registry/skill.md) to install and use @team-alpha/my-skill.',
)
})
it('renders an accessible copy button', () => {
const html = renderToStaticMarkup(createElement(InstallForAgentButton, {
namespace: 'global',
slug: 'my-skill',
}))
expect(html).toContain('data-testid="install-for-agent-button"')
expect(html).toContain('aria-label="skillDetail.installForAgent.button"')
expect(html).toContain('skillDetail.installForAgent.button')
})
it('can be disabled when the selected skill version is not installable', () => {
const html = renderToStaticMarkup(createElement(InstallForAgentButton, {
namespace: 'global',
slug: 'my-skill',
disabled: true,
}))
expect(html).toContain('disabled=""')
})
})

View file

@ -0,0 +1,62 @@
import { Bot, Check } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useCopyToClipboard } from '@/shared/lib/clipboard'
import { getBaseUrl } from './install-command'
interface InstallForAgentButtonProps {
namespace: string
slug: string
disabled?: boolean
}
type FormatAgentPrompt = (guideUrl: string, skill: string) => string
export function buildAgentInstallPrompt(
namespace: string,
slug: string,
baseUrl: string,
formatPrompt: FormatAgentPrompt,
): string {
const skill = namespace === 'global' ? slug : `@${namespace}/${slug}`
const guideUrl = `${baseUrl.replace(/\/+$/, '')}/registry/skill.md`
return formatPrompt(guideUrl, skill)
}
export function InstallForAgentButton({ namespace, slug, disabled = false }: InstallForAgentButtonProps) {
const { t } = useTranslation()
const [copied, copy] = useCopyToClipboard()
const handleCopy = async () => {
try {
await copy(buildAgentInstallPrompt(
namespace,
slug,
getBaseUrl(),
(guideUrl, skill) => t('skillDetail.installForAgent.prompt', { guideUrl, skill }),
))
} catch (err) {
console.error('Failed to copy agent installation prompt:', err)
}
}
const label = copied
? t('skillDetail.installForAgent.copied')
: t('skillDetail.installForAgent.button')
return (
<button
type="button"
data-testid="install-for-agent-button"
onClick={handleCopy}
disabled={disabled}
aria-label={label}
className="relative w-full overflow-hidden rounded-xl border border-border/60 bg-muted/50 px-4 py-3 transition-colors hover:bg-muted/70 active:bg-muted/80 disabled:cursor-not-allowed disabled:opacity-50"
>
<div className="flex items-center justify-center gap-2">
{copied ? <Check className="h-4 w-4" /> : <Bot className="h-4 w-4" />}
<span className="text-[13px] leading-relaxed text-foreground sm:text-sm">{label}</span>
</div>
</button>
)
}

View file

@ -1013,6 +1013,11 @@
"button": "Share",
"copied": "Copied",
"defaultDescription": "A useful skill"
},
"installForAgent": {
"button": "Install for Agent",
"copied": "Agent prompt copied",
"prompt": "Please follow the [SkillHub skill installation guide]({{guideUrl}}) to install and use {{skill}}."
}
},
"skillCompare": {

View file

@ -1078,6 +1078,11 @@
"button": "Поделиться",
"copied": "Скопировано",
"defaultDescription": "Полезный скилл"
},
"installForAgent": {
"button": "Установить для агента",
"copied": "Инструкция скопирована",
"prompt": "Следуйте [инструкции SkillHub по установке скиллов]({{guideUrl}}), чтобы установить и использовать {{skill}}."
}
},
"skillCompare": {

View file

@ -1013,6 +1013,11 @@
"button": "分享",
"copied": "已复制",
"defaultDescription": "实用技能"
},
"installForAgent": {
"button": "为 Agent 安装",
"copied": "安装指令已复制",
"prompt": "请根据 [SkillHub 技能安装指南]({{guideUrl}}),安装并使用 {{skill}}。"
}
},
"skillCompare": {

View file

@ -11,6 +11,7 @@ import type { FileTreeNode } from '@/features/skill/file-tree-builder'
import type { SkillFile } from '@/api/types'
import { InstallCommand } from '@/features/skill/install-command'
import { ShareButton } from '@/features/skill/share-button'
import { InstallForAgentButton } from '@/features/skill/install-for-agent-button'
import { SkillLabelPanel } from '@/features/skill/skill-label-panel'
import { ComplianceSnapshotPanel } from '@/features/skill/compliance-snapshot-panel'
import {
@ -1253,6 +1254,12 @@ export function SkillDetailPage() {
description={skill.summary}
/>
<InstallForAgentButton
namespace={namespace}
slug={slug}
disabled={!selectedVersionEntry || skill.status === 'ARCHIVED' || !isVersionDownloadable}
/>
{skill.canManageLifecycle && selectedVersionEntry && (
<SecurityAuditSummary skillId={skill.id} versionId={selectedVersionEntry.id} versionStatus={selectedVersionEntry.status} />
)}