'use client';
/**
* Skill management section of the global settings dialog.
*
* Lists the skills installed for the current account — built-in skills that
* ship with the product and the owner's own skills created from chat history —
* from the owner-scoped `GET /api/agent/skills` registry. The row layout and
* the grouped list follow the reference skill-settings dialog, which this
* surface replaces with REAL endpoints only:
*
* - every row opens a detail view (`SkillDetailDialog`) and offers a real
* Download action that hits `GET /api/skills/:id` and ships the zip the
* server builds;
* - a user skill's detail view loads its full body from the owner-scoped
* detail route (`GET /api/agent/skills/:id`); built-in skills have no
* detail route, so their detail view shows what the registry already
* carries and never issues a request that would 404.
*
* Owner rows can also be deleted after confirmation, and exported zips or bare
* SKILL.md files can be uploaded through the owner-scoped registry endpoint.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { Download, Loader2, Sparkles, Trash2, Upload } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { useI18n } from '@/lib/hooks/use-i18n';
import {
agentSkillsErrorText,
skillTitle,
useAgentSkills,
type AgentSkillInfo,
} from '@/lib/workbench/agent-skills';
import { cn } from '@/lib/utils';
/**
* The real Download affordance for one skill. A plain anchor to the export
* route (`GET /api/skills/:id`): same-origin, so the `download` attribute
* names the file and the server's `Content-Disposition` keeps it a download
* in every browser either way.
*/
function DownloadLink({ skill }: { skill: AgentSkillInfo }) {
const { t } = useI18n();
return (
{t('settings.skills.download')}
);
}
/** The kind/constraint pills a row and the detail view share. */
function SkillBadges({ skill }: { skill: AgentSkillInfo }) {
const { t } = useI18n();
return (
<>
{skill.source === 'user'
? t('settings.skills.badgeOwner')
: t('settings.skills.badgeBuiltin')}
{skill.hasConstraints && (
{t('settings.skills.badgeConstraints')}
)}
>
);
}
function SkillRow({
skill,
onDetails,
onDelete,
}: {
skill: AgentSkillInfo;
onDetails: (skill: AgentSkillInfo) => void;
onDelete?: (skill: AgentSkillInfo) => void;
}) {
const { t } = useI18n();
const title = skillTitle(skill, t);
return (
{title ? (
{title}
) : null}
{/* The English id is the skill's contract — it is never dropped. */}
/{skill.name}