From 6f1237704a0147ef766f9687ea55fd499de2c54a Mon Sep 17 00:00:00 2001 From: nyk <93952610+0xNyk@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:37:51 +0700 Subject: [PATCH] feat: add workspace skill root and root-based filtering to Skills Hub (#408) - Add `workspace` skill root (~/.openclaw/workspace/skills) for workspace-local skill discovery alongside the existing 5 roots - Make group cards clickable to filter the installed skills list by root - Add `workspace` as valid target for skill creation and registry install - Add `showAllRoots` i18n key to all 10 locale files Closes #364 --- messages/ar.json | 3 ++- messages/de.json | 3 ++- messages/en.json | 3 ++- messages/es.json | 3 ++- messages/fr.json | 3 ++- messages/ja.json | 3 ++- messages/ko.json | 3 ++- messages/pt.json | 3 ++- messages/ru.json | 3 ++- messages/zh.json | 3 ++- src/app/api/skills/registry/route.ts | 2 +- src/app/api/skills/route.ts | 6 +++++ src/components/panels/skills-panel.tsx | 33 ++++++++++++++++++++------ 13 files changed, 53 insertions(+), 18 deletions(-) diff --git a/messages/ar.json b/messages/ar.json index 7b34934..57d4313 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -1822,7 +1822,8 @@ "delete": "حذف", "save": "حفظ", "loadingSkillContent": "جارٍ تحميل SKILL.md...", - "noContent": "لا يوجد محتوى." + "noContent": "لا يوجد محتوى.", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/messages/de.json b/messages/de.json index bf14330..e81446c 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1822,7 +1822,8 @@ "delete": "Löschen", "save": "Speichern", "loadingSkillContent": "SKILL.md wird geladen...", - "noContent": "Kein Inhalt." + "noContent": "Kein Inhalt.", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/messages/en.json b/messages/en.json index bc03b95..6e797dc 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1822,7 +1822,8 @@ "delete": "Delete", "save": "Save", "loadingSkillContent": "Loading SKILL.md...", - "noContent": "No content." + "noContent": "No content.", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/messages/es.json b/messages/es.json index 49b5e9a..93c5783 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1822,7 +1822,8 @@ "delete": "Eliminar", "save": "Guardar", "loadingSkillContent": "Cargando SKILL.md...", - "noContent": "Sin contenido." + "noContent": "Sin contenido.", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/messages/fr.json b/messages/fr.json index 49bd01f..53fb875 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1822,7 +1822,8 @@ "delete": "Supprimer", "save": "Enregistrer", "loadingSkillContent": "Chargement du SKILL.md...", - "noContent": "Aucun contenu." + "noContent": "Aucun contenu.", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/messages/ja.json b/messages/ja.json index 36e0e70..3b08c64 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -1822,7 +1822,8 @@ "delete": "削除", "save": "保存", "loadingSkillContent": "SKILL.md を読み込み中...", - "noContent": "コンテンツがありません。" + "noContent": "コンテンツがありません。", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/messages/ko.json b/messages/ko.json index f126ee0..efa8edf 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -1822,7 +1822,8 @@ "delete": "삭제", "save": "저장", "loadingSkillContent": "SKILL.md 로드 중...", - "noContent": "내용 없음." + "noContent": "내용 없음.", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/messages/pt.json b/messages/pt.json index 649a4d3..4c000df 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -1822,7 +1822,8 @@ "delete": "Excluir", "save": "Salvar", "loadingSkillContent": "Carregando SKILL.md...", - "noContent": "Sem conteúdo." + "noContent": "Sem conteúdo.", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/messages/ru.json b/messages/ru.json index 63209c4..31b526a 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -1822,7 +1822,8 @@ "delete": "Удалить", "save": "Сохранить", "loadingSkillContent": "Загрузка SKILL.md...", - "noContent": "Нет содержимого." + "noContent": "Нет содержимого.", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/messages/zh.json b/messages/zh.json index 45ddf37..61632d7 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -692,7 +692,8 @@ "delete": "删除", "save": "保存", "loadingSkillContent": "加载 SKILL.md...", - "noContent": "无内容。" + "noContent": "无内容。", + "showAllRoots": "Show all roots" }, "agentSquadPhase3": { "title": "Agent Squad", diff --git a/src/app/api/skills/registry/route.ts b/src/app/api/skills/registry/route.ts index d857b0c..1aeca0b 100644 --- a/src/app/api/skills/registry/route.ts +++ b/src/app/api/skills/registry/route.ts @@ -9,7 +9,7 @@ import { } from '@/lib/skill-registry' const VALID_SOURCES: RegistrySource[] = ['clawhub', 'skills-sh', 'awesome-openclaw'] -const VALID_TARGETS = ['user-agents', 'user-codex', 'project-agents', 'project-codex', 'openclaw'] +const VALID_TARGETS = ['user-agents', 'user-codex', 'project-agents', 'project-codex', 'openclaw', 'workspace'] /** * GET /api/skills/registry?source=clawhub&q=terraform diff --git a/src/app/api/skills/route.ts b/src/app/api/skills/route.ts index 9576793..6033d73 100644 --- a/src/app/api/skills/route.ts +++ b/src/app/api/skills/route.ts @@ -88,6 +88,12 @@ function getSkillRoots(): SkillRoot[] { const openclawState = process.env.OPENCLAW_STATE_DIR || process.env.OPENCLAW_HOME || join(home, '.openclaw') const openclawSkills = resolveSkillRoot('MC_SKILLS_OPENCLAW_DIR', join(openclawState, 'skills')) roots.push({ source: 'openclaw', path: openclawSkills }) + + // Add OpenClaw workspace-local skills (takes precedence when names conflict) + const workspaceDir = process.env.OPENCLAW_WORKSPACE_DIR || process.env.MISSION_CONTROL_WORKSPACE_DIR || join(openclawState, 'workspace') + const workspaceSkills = resolveSkillRoot('MC_SKILLS_WORKSPACE_DIR', join(workspaceDir, 'skills')) + roots.push({ source: 'workspace', path: workspaceSkills }) + return roots } diff --git a/src/components/panels/skills-panel.tsx b/src/components/panels/skills-panel.tsx index 7c79f62..71f94ac 100644 --- a/src/components/panels/skills-panel.tsx +++ b/src/components/panels/skills-panel.tsx @@ -56,6 +56,7 @@ const SOURCE_LABELS: Record = { 'project-agents': '.agents/skills (project)', 'project-codex': '.codex/skills (project)', 'openclaw': '~/.openclaw/skills (gateway)', + 'workspace': '~/.openclaw/workspace/skills', } export function SkillsPanel() { @@ -65,6 +66,7 @@ export function SkillsPanel() { const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [query, setQuery] = useState('') + const [activeRoot, setActiveRoot] = useState(null) const [selectedSkill, setSelectedSkill] = useState(null) const [selectedContent, setSelectedContent] = useState(null) const [draftContent, setDraftContent] = useState('') @@ -142,14 +144,15 @@ export function SkillsPanel() { }, [loadSkills]) const filtered = useMemo(() => { - const list = skillsList || [] + let list = skillsList || [] + if (activeRoot) list = list.filter((s) => s.source === activeRoot) const q = query.trim().toLowerCase() if (!q) return list return list.filter((skill) => { const haystack = `${skill.name} ${skill.source} ${skill.description || ''}`.toLowerCase() return haystack.includes(q) }) - }, [skillsList, query]) + }, [skillsList, query, activeRoot]) useEffect(() => { if (!selectedSkill) return @@ -513,6 +516,7 @@ export function SkillsPanel() { {dashboardMode === 'full' && ( )} +
- {(skillGroups || []).filter(g => g.skills.length > 0 || ['user-agents', 'user-codex', 'openclaw'].includes(g.source)).map((group) => ( -
+ {activeRoot && ( + + )} + {(skillGroups || []).filter(g => g.skills.length > 0 || ['user-agents', 'user-codex', 'openclaw', 'workspace'].includes(g.source)).map((group) => ( +
+ ))}
@@ -641,6 +659,7 @@ export function SkillsPanel() { {dashboardMode === 'full' && ( )} +