import { useEffect, useState } from "react"; import { api } from "@/lib/api"; import { Button } from "@nous-research/ui/ui/components/button"; import { Input } from "@nous-research/ui/ui/components/input"; import { Label } from "@nous-research/ui/ui/components/label"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@nous-research/ui/ui/components/dialog"; /* ------------------------------------------------------------------ */ /* SkillEditorDialog — create or edit a SKILL.md from the dashboard */ /* */ /* Headless/VPS users have no editor besides this: the only other way */ /* to author a custom skill is SSH + a terminal editor. Create mode */ /* posts a brand-new skill (name + optional category + SKILL.md); */ /* edit mode loads the existing SKILL.md raw text and rewrites it. */ /* Validation (frontmatter, name, size) happens server-side via the */ /* same path the agent's skill_manage tool uses, so errors come back */ /* as actionable messages rendered inline. */ /* ------------------------------------------------------------------ */ const CREATE_TEMPLATE = `--- name: my-skill description: One-line description of when to use this skill. --- # My Skill Numbered steps, exact commands, and pitfalls go here. `; export interface SkillEditorDialogProps { open: boolean; /** Skill name to edit, or null for create mode. */ editName: string | null; /** Profile to scope reads/writes to ("" = the dashboard's own profile). */ profile?: string; onClose: () => void; /** Called after a successful save so the page can refresh its list. */ onSaved: (name: string) => void; } export function SkillEditorDialog({ open, editName, profile, onClose, onSaved, }: SkillEditorDialogProps) { // The body is remounted via `key` every time the dialog opens or the // target skill changes, so all form state initializes through useState // initializers — no reset-on-open effect (react-hooks/set-state-in-effect). return ( !o && onClose()}> {open && ( )} ); } function EditorBody({ editName, profile, onClose, onSaved, }: Omit) { const isEdit = editName !== null; const [name, setName] = useState(""); const [category, setCategory] = useState(""); const [content, setContent] = useState(isEdit ? "" : CREATE_TEMPLATE); const [loading, setLoading] = useState(isEdit); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!editName) return; let cancelled = false; api .getSkillContent(editName, profile || undefined) .then((res) => !cancelled && setContent(res.content)) .catch((e) => !cancelled && setError(String(e))) .finally(() => !cancelled && setLoading(false)); return () => { cancelled = true; }; }, [editName, profile]); const handleSave = async () => { setError(null); if (!isEdit && !name.trim()) { setError("Skill name is required."); return; } if (!content.trim()) { setError("SKILL.md content is required."); return; } setSaving(true); try { if (isEdit) { await api.updateSkillContent(editName, content, profile || undefined); onSaved(editName); } else { const trimmed = name.trim(); await api.createSkill( { name: trimmed, content, category: category.trim() || undefined, }, profile || undefined, ); onSaved(trimmed); } onClose(); } catch (e) { setError(String(e)); } finally { setSaving(false); } }; return ( <> {isEdit ? `Edit skill: ${editName}` : "New skill"} {isEdit ? "Rewrite this skill's SKILL.md. Frontmatter (name, description) is validated on save." : "Author a custom skill — YAML frontmatter plus markdown instructions. It becomes available to the agent and attachable to cron jobs."} {!isEdit && ( Name setName(e.target.value)} /> Category (optional) setCategory(e.target.value)} /> )} SKILL.md {loading ? ( ) : ( setContent(e.target.value)} /> )} {error && ( {error} )} Cancel : undefined} > {saving ? "Saving…" : isEdit ? "Save changes" : "Create skill"} > ); }
{error}