import { ChevronDown, ChevronRight, FolderOpen, Loader2, Plus, Trash2 } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import type { SandboxAgent } from "sandbox-agent"; import { formatJson } from "../../utils/format"; type SkillEntry = { name: string; config: { sources: Array<{ source: string; type: string; ref?: string | null; subpath?: string | null; skills?: string[] | null }> }; }; const SkillsTab = ({ getClient, }: { getClient: () => SandboxAgent; }) => { const officialSkills = [ { name: "Sandbox Agent SDK", skillId: "sandbox-agent", source: "rivet-dev/skills", summary: "Skills bundle for fast Sandbox Agent SDK setup and consistent workflows.", }, { name: "Rivet", skillId: "rivet", source: "rivet-dev/skills", summary: "Open-source platform for building, deploying, and scaling AI agents.", features: [ "Session Persistence", "Resumable Sessions", "Multi-Agent Support", "Realtime Events", "Tool Call Visibility", ], }, ]; const [directory, setDirectory] = useState("/"); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [copiedId, setCopiedId] = useState(null); const [showSdkSkills, setShowSdkSkills] = useState(false); const dropdownRef = useRef(null); useEffect(() => { if (!showSdkSkills) return; const handler = (event: MouseEvent) => { if (!dropdownRef.current) return; if (!dropdownRef.current.contains(event.target as Node)) { setShowSdkSkills(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [showSdkSkills]); // Add form state const [editing, setEditing] = useState(false); const [editName, setEditName] = useState(""); const [editSource, setEditSource] = useState(""); const [editType, setEditType] = useState("github"); const [editRef, setEditRef] = useState(""); const [editSubpath, setEditSubpath] = useState(""); const [editSkills, setEditSkills] = useState(""); const [editError, setEditError] = useState(null); const [saving, setSaving] = useState(false); const loadAll = useCallback(async (dir: string) => { setLoading(true); setError(null); try { const configPath = `${dir === "/" ? "" : dir}/.sandbox-agent/config/skills.json`; const bytes = await getClient().readFsFile({ path: configPath }); const text = new TextDecoder().decode(bytes); if (!text.trim()) { setEntries([]); return; } const map = JSON.parse(text) as Record; setEntries( Object.entries(map).map(([name, config]) => ({ name, config })), ); } catch { // File doesn't exist yet or is empty — that's fine setEntries([]); } finally { setLoading(false); } }, [getClient]); useEffect(() => { loadAll(directory); }, [directory, loadAll]); const startAdd = () => { setEditing(true); setEditName(""); setEditSource("rivet-dev/skills"); setEditType("github"); setEditRef(""); setEditSubpath(""); setEditSkills("sandbox-agent"); setEditError(null); }; const cancelEdit = () => { setEditing(false); setEditName(""); setEditSource(""); setEditType("github"); setEditRef(""); setEditSubpath(""); setEditSkills(""); setEditError(null); }; const save = async () => { const name = editName.trim(); if (!name) { setEditError("Name is required"); return; } const source = editSource.trim(); if (!source) { setEditError("Source is required"); return; } const skillEntry: SkillEntry["config"]["sources"][0] = { source, type: editType, }; if (editRef.trim()) skillEntry.ref = editRef.trim(); if (editSubpath.trim()) skillEntry.subpath = editSubpath.trim(); const skillsList = editSkills.trim() ? editSkills.split(",").map((s) => s.trim()).filter(Boolean) : null; if (skillsList && skillsList.length > 0) skillEntry.skills = skillsList; const config = { sources: [skillEntry] }; setSaving(true); setEditError(null); try { await getClient().setSkillsConfig( { directory, skillName: name }, config, ); cancelEdit(); await loadAll(directory); } catch (err) { setEditError(err instanceof Error ? err.message : "Failed to save"); } finally { setSaving(false); } }; const remove = async (name: string) => { try { await getClient().deleteSkillsConfig({ directory, skillName: name }); await loadAll(directory); } catch (err) { setError(err instanceof Error ? err.message : "Failed to delete"); } }; const fallbackCopy = (text: string) => { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); }; const copyText = async (id: string, text: string) => { try { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); } else { fallbackCopy(text); } setCopiedId(id); window.setTimeout(() => { setCopiedId((current) => (current === id ? null : current)); }, 1800); } catch { setError("Failed to copy snippet"); } }; const applySkillPreset = (skill: typeof officialSkills[0]) => { setEditing(true); setEditName(skill.skillId); setEditSource(skill.source); setEditType("github"); setEditRef(""); setEditSubpath(""); setEditSkills(skill.skillId); setEditError(null); setShowSdkSkills(false); }; const copySkillToInput = async (skillId: string) => { const skill = officialSkills.find((s) => s.skillId === skillId); if (skill) { applySkillPreset(skill); await copyText(`skill-input-${skillId}`, skillId); } }; return ( <>
Skills Configuration
{showSdkSkills && (
Pick a skill to auto-fill the form.
{officialSkills.map((skill) => (
{skill.name}
{skill.summary}
{skill.features && (
{skill.features.map((feature) => ( {feature} ))}
)}
))}
)}
{!editing && ( )}
setDirectory(e.target.value)} placeholder="/" style={{ flex: 1, fontSize: 11 }} />
{error &&
{error}
} {loading &&
Loading...
} {editing && (
Add Skill Source
{ setEditName(e.target.value); setEditError(null); }} placeholder="skill-name" style={{ marginBottom: 6, width: "100%", boxSizing: "border-box" }} />
{ setEditSource(e.target.value); setEditError(null); }} placeholder={editType === "github" ? "owner/repo" : editType === "local" ? "/path/to/skill" : "https://..."} style={{ flex: 1 }} />
setEditSkills(e.target.value)} placeholder="Skills filter (comma-separated, optional)" style={{ marginBottom: 6, width: "100%", boxSizing: "border-box" }} /> {editType !== "local" && (
setEditRef(e.target.value)} placeholder="Branch/tag (optional)" style={{ flex: 1 }} /> setEditSubpath(e.target.value)} placeholder="Subpath (optional)" style={{ flex: 1 }} />
)} {editError &&
{editError}
}
)} {entries.length === 0 && !editing && !loading && (
No skills configured in this directory.
)} {entries.map((entry) => (
{entry.name}
{entry.config.sources.length} source{entry.config.sources.length !== 1 ? "s" : ""}
            {formatJson(entry.config)}
          
))} ); }; export default SkillsTab;