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
This commit is contained in:
parent
17bf0761f5
commit
6f1237704a
|
|
@ -1822,7 +1822,8 @@
|
|||
"delete": "حذف",
|
||||
"save": "حفظ",
|
||||
"loadingSkillContent": "جارٍ تحميل SKILL.md...",
|
||||
"noContent": "لا يوجد محتوى."
|
||||
"noContent": "لا يوجد محتوى.",
|
||||
"showAllRoots": "Show all roots"
|
||||
},
|
||||
"agentSquadPhase3": {
|
||||
"title": "Agent Squad",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1822,7 +1822,8 @@
|
|||
"delete": "削除",
|
||||
"save": "保存",
|
||||
"loadingSkillContent": "SKILL.md を読み込み中...",
|
||||
"noContent": "コンテンツがありません。"
|
||||
"noContent": "コンテンツがありません。",
|
||||
"showAllRoots": "Show all roots"
|
||||
},
|
||||
"agentSquadPhase3": {
|
||||
"title": "Agent Squad",
|
||||
|
|
|
|||
|
|
@ -1822,7 +1822,8 @@
|
|||
"delete": "삭제",
|
||||
"save": "저장",
|
||||
"loadingSkillContent": "SKILL.md 로드 중...",
|
||||
"noContent": "내용 없음."
|
||||
"noContent": "내용 없음.",
|
||||
"showAllRoots": "Show all roots"
|
||||
},
|
||||
"agentSquadPhase3": {
|
||||
"title": "Agent Squad",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1822,7 +1822,8 @@
|
|||
"delete": "Удалить",
|
||||
"save": "Сохранить",
|
||||
"loadingSkillContent": "Загрузка SKILL.md...",
|
||||
"noContent": "Нет содержимого."
|
||||
"noContent": "Нет содержимого.",
|
||||
"showAllRoots": "Show all roots"
|
||||
},
|
||||
"agentSquadPhase3": {
|
||||
"title": "Agent Squad",
|
||||
|
|
|
|||
|
|
@ -692,7 +692,8 @@
|
|||
"delete": "删除",
|
||||
"save": "保存",
|
||||
"loadingSkillContent": "加载 SKILL.md...",
|
||||
"noContent": "无内容。"
|
||||
"noContent": "无内容。",
|
||||
"showAllRoots": "Show all roots"
|
||||
},
|
||||
"agentSquadPhase3": {
|
||||
"title": "Agent Squad",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ const SOURCE_LABELS: Record<string, string> = {
|
|||
'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<string | null>(null)
|
||||
const [query, setQuery] = useState('')
|
||||
const [activeRoot, setActiveRoot] = useState<string | null>(null)
|
||||
const [selectedSkill, setSelectedSkill] = useState<SkillSummary | null>(null)
|
||||
const [selectedContent, setSelectedContent] = useState<SkillContentResponse | null>(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' && (
|
||||
<option value="openclaw">{SOURCE_LABELS['openclaw']}</option>
|
||||
)}
|
||||
<option value="workspace">{SOURCE_LABELS['workspace']}</option>
|
||||
</select>
|
||||
<input
|
||||
value={createName}
|
||||
|
|
@ -540,14 +544,28 @@ export function SkillsPanel() {
|
|||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
{(skillGroups || []).filter(g => g.skills.length > 0 || ['user-agents', 'user-codex', 'openclaw'].includes(g.source)).map((group) => (
|
||||
<div key={group.source} className={`rounded-lg border bg-card p-3 ${
|
||||
group.source === 'openclaw' ? 'border-cyan-500/30' : 'border-border'
|
||||
}`}>
|
||||
{activeRoot && (
|
||||
<button
|
||||
onClick={() => setActiveRoot(null)}
|
||||
className="col-span-full text-left text-2xs text-primary hover:underline"
|
||||
>
|
||||
{t('showAllRoots')}
|
||||
</button>
|
||||
)}
|
||||
{(skillGroups || []).filter(g => g.skills.length > 0 || ['user-agents', 'user-codex', 'openclaw', 'workspace'].includes(g.source)).map((group) => (
|
||||
<button
|
||||
key={group.source}
|
||||
onClick={() => setActiveRoot(activeRoot === group.source ? null : group.source)}
|
||||
className={`rounded-lg border bg-card p-3 text-left transition-colors ${
|
||||
activeRoot === group.source
|
||||
? 'border-primary ring-1 ring-primary/30'
|
||||
: group.source === 'openclaw' ? 'border-cyan-500/30 hover:border-cyan-500/50' : 'border-border hover:border-border/80'
|
||||
}`}
|
||||
>
|
||||
<div className="text-xs font-medium text-muted-foreground">{SOURCE_LABELS[group.source] || group.source}</div>
|
||||
<div className="mt-1 text-lg font-semibold text-foreground">{group.skills.length}</div>
|
||||
<div className="mt-1 text-2xs text-muted-foreground truncate">{group.path}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -641,6 +659,7 @@ export function SkillsPanel() {
|
|||
{dashboardMode === 'full' && (
|
||||
<option value="openclaw">{SOURCE_LABELS['openclaw']}</option>
|
||||
)}
|
||||
<option value="workspace">{SOURCE_LABELS['workspace']}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue