From 927e623962884a2d5baf48c7712737baea361eb0 Mon Sep 17 00:00:00 2001 From: DBT Date: Thu, 26 Feb 2026 01:04:06 +0000 Subject: [PATCH] fix skills page crash and add install action support --- pkg/nodes/registry_server.go | 12 ++++++++++++ webui/src/pages/Skills.tsx | 30 ++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/pkg/nodes/registry_server.go b/pkg/nodes/registry_server.go index 5ef12a7..98532ea 100644 --- a/pkg/nodes/registry_server.go +++ b/pkg/nodes/registry_server.go @@ -549,6 +549,9 @@ func (s *RegistryServer) handleWebUISkills(w http.ResponseWriter, r *http.Reques if len(tools) == 0 { tools = t2 } if sys == "" { sys = s2 } } + if tools == nil { + tools = []string{} + } it := skillItem{ID: baseName, Name: baseName, Description: desc, Tools: tools, SystemPrompt: sys, Enabled: enabled, UpdateChecked: checkUpdates} if checkUpdates { found, version, checkErr := queryClawHubSkillVersion(r.Context(), baseName) @@ -583,6 +586,15 @@ func (s *RegistryServer) handleWebUISkills(w http.ResponseWriter, r *http.Reques disabledPath := enabledPath + ".disabled" switch action { + case "install": + cmd := exec.CommandContext(r.Context(), "clawhub", "install", name) + cmd.Dir = strings.TrimSpace(s.workspacePath) + out, err := cmd.CombinedOutput() + if err != nil { + http.Error(w, fmt.Sprintf("install failed: %v\n%s", err, string(out)), http.StatusInternalServerError) + return + } + _ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "installed": name, "output": string(out)}) case "enable": if _, err := os.Stat(disabledPath); err == nil { if err := os.Rename(disabledPath, enabledPath); err != nil { diff --git a/webui/src/pages/Skills.tsx b/webui/src/pages/Skills.tsx index 8b8f4f8..de86184 100644 --- a/webui/src/pages/Skills.tsx +++ b/webui/src/pages/Skills.tsx @@ -15,6 +15,8 @@ const initialSkillForm: Omit = { const Skills: React.FC = () => { const { t } = useTranslation(); const { skills, refreshSkills, q } = useAppContext(); + const [installName, setInstallName] = useState(''); + const qp = (k: string, v: string) => `${q}${q ? '&' : '?'}${k}=${encodeURIComponent(v)}`; const [isModalOpen, setIsModalOpen] = useState(false); const [editingSkill, setEditingSkill] = useState(null); const [form, setForm] = useState>(initialSkillForm); @@ -22,13 +24,29 @@ const Skills: React.FC = () => { async function deleteSkill(id: string) { if (!confirm('Are you sure you want to delete this skill?')) return; try { - await fetch(`/webui/api/skills${q}&id=${id}`, { method: 'DELETE' }); + await fetch(`/webui/api/skills${qp('id', id)}`, { method: 'DELETE' }); await refreshSkills(); } catch (e) { console.error(e); } } + async function installSkill() { + const name = installName.trim(); + if (!name) return; + const r = await fetch(`/webui/api/skills${q}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'install', name }), + }); + if (!r.ok) { + alert(await r.text()); + return; + } + setInstallName(''); + await refreshSkills(); + } + async function handleSubmit() { try { const action = editingSkill ? 'update' : 'create'; @@ -53,6 +71,10 @@ const Skills: React.FC = () => {

{t('skills')}

+
+ setInstallName(e.target.value)} placeholder="skill name" className="px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-lg text-sm" /> + +