diff --git a/web/src/features/skill/install-for-agent-button.test.tsx b/web/src/features/skill/install-for-agent-button.test.tsx new file mode 100644 index 00000000..59ac61f0 --- /dev/null +++ b/web/src/features/skill/install-for-agent-button.test.tsx @@ -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=""') + }) +}) diff --git a/web/src/features/skill/install-for-agent-button.tsx b/web/src/features/skill/install-for-agent-button.tsx new file mode 100644 index 00000000..560110ae --- /dev/null +++ b/web/src/features/skill/install-for-agent-button.tsx @@ -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 ( + + ) +} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index f22f7cf9..00e43abf 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -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": { diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 216a3791..5b25e789 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -1078,6 +1078,11 @@ "button": "Поделиться", "copied": "Скопировано", "defaultDescription": "Полезный скилл" + }, + "installForAgent": { + "button": "Установить для агента", + "copied": "Инструкция скопирована", + "prompt": "Следуйте [инструкции SkillHub по установке скиллов]({{guideUrl}}), чтобы установить и использовать {{skill}}." } }, "skillCompare": { diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index f858ce9a..f916762c 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -1013,6 +1013,11 @@ "button": "分享", "copied": "已复制", "defaultDescription": "实用技能" + }, + "installForAgent": { + "button": "为 Agent 安装", + "copied": "安装指令已复制", + "prompt": "请根据 [SkillHub 技能安装指南]({{guideUrl}}),安装并使用 {{skill}}。" } }, "skillCompare": { diff --git a/web/src/pages/skill-detail.tsx b/web/src/pages/skill-detail.tsx index b17cbf86..179f54fc 100644 --- a/web/src/pages/skill-detail.tsx +++ b/web/src/pages/skill-detail.tsx @@ -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} /> + + {skill.canManageLifecycle && selectedVersionEntry && ( )}