import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Link, useNavigate, useParams } from '@tanstack/react-router'; import { ArrowLeft, BookOpenText, RefreshCw, Trash2 } from 'lucide-react'; import { Badge } from '../components/ui/badge'; import { buttonVariants } from '../components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'; import { Separator } from '../components/ui/separator'; import { Skeleton } from '../components/ui/skeleton'; import { fetchSkillReadme } from '../api'; import { AgentBadge } from './agent-badge'; import { PageLoadingState } from './components'; import { useDashboardData } from './data'; import { SkillActionButton } from './skill-action-button'; import { useSkillActions } from './skill-actions'; import { SkillMarkdown, type SkillFrontmatterValue, parseSkillFrontmatterAttributes, parseSkillMarkdownDocument, } from './skill-markdown'; import { showSuccessToast } from './toasts'; import { createCommandFailureMessage, formatDateTime, getErrorMessage, scopeLabel } from './utils'; export function SkillDetailsPage() { const { skillId } = useParams({ from: '/skill/$skillId' }); const navigate = useNavigate(); const { isInitialLoading, payload, skills, getSkillById } = useDashboardData(); const { removeSkill, updateSkill } = useSkillActions(); const skill = getSkillById(skillId); const [pendingActionKey, setPendingActionKey] = useState(null); const { data: skillReadmePayload, error: skillReadmeError, isPending: isSkillReadmePending, } = useQuery({ queryKey: ['installed-skill-readme', skill?.id], queryFn: async () => { if (!skill) { throw new Error('Skill id is required.'); } return fetchSkillReadme(skill.id); }, enabled: Boolean(skill), }); const handleRemoveSkill = async () => { if (!skill || pendingActionKey !== null) { return { ok: false }; } try { const outcome = await removeSkill(skill); if (outcome.command.ok) { showSuccessToast( 'Skill removed', `${skill.name} was removed from ${scopeLabel(skill.scope)}.` ); window.setTimeout(() => { void navigate({ to: '/' }); }, 500); return { ok: true }; } const message = createCommandFailureMessage(outcome.command); return { ok: false, errorMessage: message }; } catch (error) { const message = getErrorMessage(error); return { ok: false, errorMessage: message }; } }; const handleUpdateSkill = async () => { if (!skill || pendingActionKey !== null) { return { ok: false }; } try { const outcome = await updateSkill(skill); if (outcome.command.ok) { showSuccessToast( 'Skill updated', `${skill.name} was updated in ${scopeLabel(skill.scope)}.` ); return { ok: true }; } const message = createCommandFailureMessage(outcome.command); return { ok: false, errorMessage: message }; } catch (error) { const message = getErrorMessage(error); return { ok: false, errorMessage: message }; } }; if (isInitialLoading && skills.length === 0) { return ; } if (!skill || !payload) { return ( Skill not found The skill identifier does not exist in the current dashboard payload. Back to Browse ); } return (
Back to Browse

{skill.name}

{scopeLabel(skill.scope)} {skill.sourceType ? {skill.sourceType} : null} {skill.ref ? ref: {skill.ref} : null}

{skill.description}

{skill.managed ? ( Repository:{' '} {skill.repositoryUrl ? ( {skill.repository ?? skill.sourceUrl ?? skill.source ?? 'Unknown'} ) : ( {skill.repository ?? skill.sourceUrl ?? skill.source ?? 'Unknown'} )} ) : ( Local skill )} {skill.activityAt ? `Updated ${formatDateTime(skill.activityAt)}` : 'No update timestamp available'}
} loadingLabel={`Updating ${skill.name}`} onPendingChange={setPendingActionKey} onAction={handleUpdateSkill} tooltip="Update" /> } loadingLabel={`Removing ${skill.name}`} onPendingChange={setPendingActionKey} confirmation={{ title: 'Remove skill?', description: `Remove "${skill.name}" from ${scopeLabel(skill.scope)}?`, actionLabel: 'Remove', }} onAction={handleRemoveSkill} tooltip="Remove" variant="ghost" />
{skill.agents.length > 0 ? (
{skill.agents.map((agent) => ( ))}
) : null}
); } function SkillReadmeBody({ errorMessage, isLoading, markdown, }: { errorMessage: string | null; isLoading: boolean; markdown: string | null; }) { if (isLoading) { return (
); } if (markdown === null) { return ( SKILL.md unavailable {errorMessage ?? 'The installed skill does not expose a readable SKILL.md file.'} ); } const document = parseSkillMarkdownDocument(markdown); return (
{document.frontmatter !== null ? ( ) : null}
Instructions
); } function SkillFrontmatter({ frontmatter }: { frontmatter: string }) { const attributes = parseSkillFrontmatterAttributes(frontmatter); return (
YAML frontmatter
{attributes.map((attribute) => (
{attribute.name}
))}
); } function SkillFrontmatterValueView({ value }: { value: SkillFrontmatterValue }) { if (Array.isArray(value)) { if (value.length === 0) { return Empty array; } if (value.every(isFrontmatterRecord)) { return ; } return (
{value.map((item, index) => ( {frontmatterValueToText(item)} ))}
); } if (isFrontmatterRecord(value)) { return ; } return {frontmatterValueToText(value)}; } function SkillFrontmatterKeyValueTable({ value, }: { value: Record; }) { const entries = Object.entries(value); if (entries.length === 0) { return Empty object; } return (
{entries.map(([key, item]) => ( ))}
{key}
); } function SkillFrontmatterObjectArrayTable({ values, }: { values: Record[]; }) { const columns = Array.from(new Set(values.flatMap((item) => Object.keys(item)))); if (columns.length === 0) { return Empty objects; } return (
{columns.map((column) => ( ))} {values.map((item, index) => ( {columns.map((column) => ( ))} ))}
{column}
{column in item ? ( ) : ( Empty )}
); } function isFrontmatterRecord( value: SkillFrontmatterValue ): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function frontmatterValueToText(value: SkillFrontmatterValue): string { if (value === null || value === '') { return 'Empty'; } if (Array.isArray(value)) { return value.map(frontmatterValueToText).join(', '); } if (isFrontmatterRecord(value)) { return Object.entries(value) .map(([key, item]) => `${key}: ${frontmatterValueToText(item)}`) .join(', '); } return String(value); }