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": "حذف",
|
"delete": "حذف",
|
||||||
"save": "حفظ",
|
"save": "حفظ",
|
||||||
"loadingSkillContent": "جارٍ تحميل SKILL.md...",
|
"loadingSkillContent": "جارٍ تحميل SKILL.md...",
|
||||||
"noContent": "لا يوجد محتوى."
|
"noContent": "لا يوجد محتوى.",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,8 @@
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"loadingSkillContent": "SKILL.md wird geladen...",
|
"loadingSkillContent": "SKILL.md wird geladen...",
|
||||||
"noContent": "Kein Inhalt."
|
"noContent": "Kein Inhalt.",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,8 @@
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"loadingSkillContent": "Loading SKILL.md...",
|
"loadingSkillContent": "Loading SKILL.md...",
|
||||||
"noContent": "No content."
|
"noContent": "No content.",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,8 @@
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"loadingSkillContent": "Cargando SKILL.md...",
|
"loadingSkillContent": "Cargando SKILL.md...",
|
||||||
"noContent": "Sin contenido."
|
"noContent": "Sin contenido.",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,8 @@
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"save": "Enregistrer",
|
"save": "Enregistrer",
|
||||||
"loadingSkillContent": "Chargement du SKILL.md...",
|
"loadingSkillContent": "Chargement du SKILL.md...",
|
||||||
"noContent": "Aucun contenu."
|
"noContent": "Aucun contenu.",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,8 @@
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"loadingSkillContent": "SKILL.md を読み込み中...",
|
"loadingSkillContent": "SKILL.md を読み込み中...",
|
||||||
"noContent": "コンテンツがありません。"
|
"noContent": "コンテンツがありません。",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,8 @@
|
||||||
"delete": "삭제",
|
"delete": "삭제",
|
||||||
"save": "저장",
|
"save": "저장",
|
||||||
"loadingSkillContent": "SKILL.md 로드 중...",
|
"loadingSkillContent": "SKILL.md 로드 중...",
|
||||||
"noContent": "내용 없음."
|
"noContent": "내용 없음.",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,8 @@
|
||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
"save": "Salvar",
|
"save": "Salvar",
|
||||||
"loadingSkillContent": "Carregando SKILL.md...",
|
"loadingSkillContent": "Carregando SKILL.md...",
|
||||||
"noContent": "Sem conteúdo."
|
"noContent": "Sem conteúdo.",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -1822,7 +1822,8 @@
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"loadingSkillContent": "Загрузка SKILL.md...",
|
"loadingSkillContent": "Загрузка SKILL.md...",
|
||||||
"noContent": "Нет содержимого."
|
"noContent": "Нет содержимого.",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -692,7 +692,8 @@
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"loadingSkillContent": "加载 SKILL.md...",
|
"loadingSkillContent": "加载 SKILL.md...",
|
||||||
"noContent": "无内容。"
|
"noContent": "无内容。",
|
||||||
|
"showAllRoots": "Show all roots"
|
||||||
},
|
},
|
||||||
"agentSquadPhase3": {
|
"agentSquadPhase3": {
|
||||||
"title": "Agent Squad",
|
"title": "Agent Squad",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from '@/lib/skill-registry'
|
} from '@/lib/skill-registry'
|
||||||
|
|
||||||
const VALID_SOURCES: RegistrySource[] = ['clawhub', 'skills-sh', 'awesome-openclaw']
|
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
|
* 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 openclawState = process.env.OPENCLAW_STATE_DIR || process.env.OPENCLAW_HOME || join(home, '.openclaw')
|
||||||
const openclawSkills = resolveSkillRoot('MC_SKILLS_OPENCLAW_DIR', join(openclawState, 'skills'))
|
const openclawSkills = resolveSkillRoot('MC_SKILLS_OPENCLAW_DIR', join(openclawState, 'skills'))
|
||||||
roots.push({ source: 'openclaw', path: openclawSkills })
|
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
|
return roots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ const SOURCE_LABELS: Record<string, string> = {
|
||||||
'project-agents': '.agents/skills (project)',
|
'project-agents': '.agents/skills (project)',
|
||||||
'project-codex': '.codex/skills (project)',
|
'project-codex': '.codex/skills (project)',
|
||||||
'openclaw': '~/.openclaw/skills (gateway)',
|
'openclaw': '~/.openclaw/skills (gateway)',
|
||||||
|
'workspace': '~/.openclaw/workspace/skills',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SkillsPanel() {
|
export function SkillsPanel() {
|
||||||
|
|
@ -65,6 +66,7 @@ export function SkillsPanel() {
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
|
const [activeRoot, setActiveRoot] = useState<string | null>(null)
|
||||||
const [selectedSkill, setSelectedSkill] = useState<SkillSummary | null>(null)
|
const [selectedSkill, setSelectedSkill] = useState<SkillSummary | null>(null)
|
||||||
const [selectedContent, setSelectedContent] = useState<SkillContentResponse | null>(null)
|
const [selectedContent, setSelectedContent] = useState<SkillContentResponse | null>(null)
|
||||||
const [draftContent, setDraftContent] = useState('')
|
const [draftContent, setDraftContent] = useState('')
|
||||||
|
|
@ -142,14 +144,15 @@ export function SkillsPanel() {
|
||||||
}, [loadSkills])
|
}, [loadSkills])
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
const list = skillsList || []
|
let list = skillsList || []
|
||||||
|
if (activeRoot) list = list.filter((s) => s.source === activeRoot)
|
||||||
const q = query.trim().toLowerCase()
|
const q = query.trim().toLowerCase()
|
||||||
if (!q) return list
|
if (!q) return list
|
||||||
return list.filter((skill) => {
|
return list.filter((skill) => {
|
||||||
const haystack = `${skill.name} ${skill.source} ${skill.description || ''}`.toLowerCase()
|
const haystack = `${skill.name} ${skill.source} ${skill.description || ''}`.toLowerCase()
|
||||||
return haystack.includes(q)
|
return haystack.includes(q)
|
||||||
})
|
})
|
||||||
}, [skillsList, query])
|
}, [skillsList, query, activeRoot])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedSkill) return
|
if (!selectedSkill) return
|
||||||
|
|
@ -513,6 +516,7 @@ export function SkillsPanel() {
|
||||||
{dashboardMode === 'full' && (
|
{dashboardMode === 'full' && (
|
||||||
<option value="openclaw">{SOURCE_LABELS['openclaw']}</option>
|
<option value="openclaw">{SOURCE_LABELS['openclaw']}</option>
|
||||||
)}
|
)}
|
||||||
|
<option value="workspace">{SOURCE_LABELS['workspace']}</option>
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
value={createName}
|
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">
|
<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) => (
|
{activeRoot && (
|
||||||
<div key={group.source} className={`rounded-lg border bg-card p-3 ${
|
<button
|
||||||
group.source === 'openclaw' ? 'border-cyan-500/30' : 'border-border'
|
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="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-lg font-semibold text-foreground">{group.skills.length}</div>
|
||||||
<div className="mt-1 text-2xs text-muted-foreground truncate">{group.path}</div>
|
<div className="mt-1 text-2xs text-muted-foreground truncate">{group.path}</div>
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -641,6 +659,7 @@ export function SkillsPanel() {
|
||||||
{dashboardMode === 'full' && (
|
{dashboardMode === 'full' && (
|
||||||
<option value="openclaw">{SOURCE_LABELS['openclaw']}</option>
|
<option value="openclaw">{SOURCE_LABELS['openclaw']}</option>
|
||||||
)}
|
)}
|
||||||
|
<option value="workspace">{SOURCE_LABELS['workspace']}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue