feat(experts): add built-in expert skills with per-agent activation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-10 15:17:51 +08:00
parent 64d4e9c903
commit 5b613daded
73 changed files with 11199 additions and 30 deletions

View File

@@ -0,0 +1,5 @@
import { ExpertsSettings } from "@/components/settings/experts-settings"
export default function SettingsExpertsPage() {
return <ExpertsSettings />
}

View File

@@ -0,0 +1,682 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import {
Bot,
Bug,
CheckCheck,
FileCode2,
FlaskConical,
FolderOpen,
GitBranch,
GitFork,
GitMerge,
Lightbulb,
ListTodo,
Loader2,
MessageSquareQuote,
MessageSquareReply,
PlayCircle,
RefreshCw,
Sparkles,
type LucideIcon,
} from "lucide-react"
import { useLocale, useTranslations } from "next-intl"
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import { toast } from "sonner"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { Switch } from "@/components/ui/switch"
import { cn } from "@/lib/utils"
import {
acpListAgents,
expertsGetInstallStatus,
expertsLinkToAgent,
expertsList,
expertsOpenCentralDir,
expertsReadContent,
expertsUnlinkFromAgent,
openFolderWindow,
} from "@/lib/api"
import type {
AcpAgentInfo,
AgentType,
ExpertInstallStatus,
ExpertLinkState,
ExpertListItem,
} from "@/lib/types"
const ICON_MAP: Record<string, LucideIcon> = {
Lightbulb,
ListTodo,
PlayCircle,
Bot,
GitFork,
GitBranch,
FlaskConical,
CheckCheck,
Bug,
MessageSquareQuote,
MessageSquareReply,
GitMerge,
Sparkles,
FileCode2,
}
const CATEGORY_SORT: Record<string, number> = {
discovery: 1,
planning: 2,
execution: 3,
quality: 4,
debugging: 5,
review: 6,
meta: 7,
}
const LEFT_MIN_WIDTH = 320
const RIGHT_MIN_WIDTH = 440
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value))
}
function toPercent(pixels: number, totalPixels: number): number {
if (totalPixels <= 0) return 0
return (pixels / totalPixels) * 100
}
/**
* next-intl locales are lower-case underscored like `zh_cn`. Our expert
* metadata dictionary uses BCP47-ish keys like `zh-CN`. Normalize both
* sides and fall back to `en`.
*/
function pickLocalized(
dict: Record<string, string> | undefined,
locale: string
): string {
if (!dict) return ""
if (dict[locale]) return dict[locale]
const normalized = locale.replace("_", "-")
if (dict[normalized]) return dict[normalized]
const [lang] = normalized.split("-")
const match = Object.keys(dict).find(
(key) => key.toLowerCase().split("-")[0] === lang.toLowerCase()
)
if (match) return dict[match]
return dict.en ?? Object.values(dict)[0] ?? ""
}
function stripFrontmatter(content: string): string {
const match = content.match(/^---\s*\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n)?/)
if (!match) return content
return content.slice(match[0].length)
}
function getIcon(name: string | null | undefined): LucideIcon {
if (name && ICON_MAP[name]) return ICON_MAP[name]
return Sparkles
}
export function ExpertsSettings() {
const t = useTranslations("ExpertsSettings")
const locale = useLocale()
const panelContainerRef = useRef<HTMLDivElement | null>(null)
const [panelContainerWidth, setPanelContainerWidth] = useState(0)
const [experts, setExperts] = useState<ExpertListItem[]>([])
const [agents, setAgents] = useState<AcpAgentInfo[]>([])
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState<string | null>(null)
const [selectedExpertId, setSelectedExpertId] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState("")
const [content, setContent] = useState<string>("")
const [contentLoading, setContentLoading] = useState(false)
const [statuses, setStatuses] = useState<Record<string, ExpertInstallStatus>>(
{}
)
const [statusLoading, setStatusLoading] = useState(false)
const [pendingMutation, setPendingMutation] = useState<string | null>(null)
const translatedCategory = useCallback(
(category: string): string => {
switch (category) {
case "discovery":
return t("categories.discovery")
case "planning":
return t("categories.planning")
case "execution":
return t("categories.execution")
case "quality":
return t("categories.quality")
case "debugging":
return t("categories.debugging")
case "review":
return t("categories.review")
case "meta":
return t("categories.meta")
default:
return category
}
},
[t]
)
const translatedState = useCallback(
(state: ExpertLinkState): string => {
switch (state) {
case "not_linked":
return t("states.not_linked")
case "linked_to_codeg":
return t("states.linked_to_codeg")
case "linked_elsewhere":
return t("states.linked_elsewhere")
case "blocked_by_real_directory":
return t("states.blocked_by_real_directory")
case "broken":
return t("states.broken")
default:
return state
}
},
[t]
)
const refresh = useCallback(async () => {
setLoading(true)
setLoadError(null)
try {
const [expertList, agentList] = await Promise.all([
expertsList(),
acpListAgents(),
])
setExperts(expertList)
setAgents(agentList)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setLoadError(message)
setExperts([])
setAgents([])
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
refresh().catch((err) => {
console.error("[ExpertsSettings] initial refresh failed:", err)
})
}, [refresh])
useEffect(() => {
const container = panelContainerRef.current
if (!container) return
const updateWidth = (next: number) => {
setPanelContainerWidth((prev) =>
Math.abs(prev - next) < 1 ? prev : next
)
}
updateWidth(container.getBoundingClientRect().width)
const observer = new ResizeObserver((entries) => {
updateWidth(
entries[0]?.contentRect.width ?? container.getBoundingClientRect().width
)
})
observer.observe(container)
return () => {
observer.disconnect()
}
}, [])
const sortedExperts = useMemo(() => {
return [...experts].sort((a, b) => {
const ca = CATEGORY_SORT[a.metadata.category] ?? 99
const cb = CATEGORY_SORT[b.metadata.category] ?? 99
if (ca !== cb) return ca - cb
const sa = a.metadata.sort_order ?? 0
const sb = b.metadata.sort_order ?? 0
if (sa !== sb) return sa - sb
return a.metadata.id.localeCompare(b.metadata.id)
})
}, [experts])
const filteredExperts = useMemo(() => {
const q = searchQuery.trim().toLowerCase()
if (!q) return sortedExperts
return sortedExperts.filter((item) => {
const name = pickLocalized(item.metadata.display_name, locale)
const desc = pickLocalized(item.metadata.description, locale)
return (
item.metadata.id.toLowerCase().includes(q) ||
name.toLowerCase().includes(q) ||
desc.toLowerCase().includes(q)
)
})
}, [sortedExperts, searchQuery, locale])
const groupedExperts = useMemo(() => {
const groups = new Map<string, ExpertListItem[]>()
for (const item of filteredExperts) {
const key = item.metadata.category
const list = groups.get(key) ?? []
list.push(item)
groups.set(key, list)
}
return Array.from(groups.entries()).sort(
(a, b) => (CATEGORY_SORT[a[0]] ?? 99) - (CATEGORY_SORT[b[0]] ?? 99)
)
}, [filteredExperts])
const selectedExpert = useMemo(
() => experts.find((e) => e.metadata.id === selectedExpertId) ?? null,
[experts, selectedExpertId]
)
// Auto-select first expert once loaded.
useEffect(() => {
if (!selectedExpertId && sortedExperts.length > 0) {
setSelectedExpertId(sortedExperts[0].metadata.id)
}
}, [selectedExpertId, sortedExperts])
// Load content + status for the currently selected expert.
useEffect(() => {
if (!selectedExpert) {
setContent("")
setStatuses({})
return
}
const expertId = selectedExpert.metadata.id
let cancelled = false
setContentLoading(true)
setStatusLoading(true)
Promise.all([
expertsReadContent(expertId),
expertsGetInstallStatus(expertId),
])
.then(([body, statusList]) => {
if (cancelled) return
setContent(body)
const map: Record<string, ExpertInstallStatus> = {}
for (const entry of statusList) {
map[entry.agentType] = entry
}
setStatuses(map)
})
.catch((err) => {
if (cancelled) return
const message = err instanceof Error ? err.message : String(err)
toast.error(t("toasts.loadFailed"), { description: message })
})
.finally(() => {
if (!cancelled) {
setContentLoading(false)
setStatusLoading(false)
}
})
return () => {
cancelled = true
}
}, [selectedExpert, t])
const handleToggle = useCallback(
async (expertId: string, agentType: AgentType, enable: boolean) => {
const key = `${expertId}:${agentType}`
setPendingMutation(key)
try {
if (enable) {
const next = await expertsLinkToAgent({ expertId, agentType })
setStatuses((prev) => ({ ...prev, [agentType]: next }))
toast.success(t("toasts.enabled"))
} else {
await expertsUnlinkFromAgent({ expertId, agentType })
// Re-fetch status to get the accurate post-unlink state.
const latest = await expertsGetInstallStatus(expertId)
const map: Record<string, ExpertInstallStatus> = {}
for (const entry of latest) {
map[entry.agentType] = entry
}
setStatuses(map)
toast.success(t("toasts.disabled"))
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(
enable ? t("toasts.enableFailed") : t("toasts.disableFailed"),
{
description: message,
}
)
} finally {
setPendingMutation(null)
}
},
[t]
)
const handleOpenCentralDir = useCallback(async () => {
try {
const path = await expertsOpenCentralDir()
await openFolderWindow(path)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("toasts.openFolderFailed"), { description: message })
}
}, [t])
const safeContainerWidth =
panelContainerWidth > 0 ? panelContainerWidth : 1200
const leftMinSize = clamp(
toPercent(LEFT_MIN_WIDTH, safeContainerWidth),
5,
95
)
const rightMinSize = clamp(
toPercent(RIGHT_MIN_WIDTH, safeContainerWidth),
5,
95
)
const leftMaxSize = Math.max(leftMinSize, 100 - rightMinSize)
if (loading) {
return (
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{t("loading")}
</div>
)
}
const selectedName = selectedExpert
? pickLocalized(selectedExpert.metadata.display_name, locale) ||
selectedExpert.metadata.id
: ""
const selectedDescription = selectedExpert
? pickLocalized(selectedExpert.metadata.description, locale)
: ""
const selectedIcon = getIcon(selectedExpert?.metadata.icon ?? null)
const SelectedIcon = selectedIcon
return (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between gap-3 pb-4">
<div>
<h2 className="text-base font-semibold">{t("title")}</h2>
<p className="text-xs text-muted-foreground mt-1">
{t("description")}
</p>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
handleOpenCentralDir().catch((err) => {
console.error("[ExpertsSettings] open central dir failed:", err)
})
}}
>
<FolderOpen className="h-3.5 w-3.5" />
{t("actions.openCentralDir")}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
refresh().catch((err) => {
console.error("[ExpertsSettings] refresh failed:", err)
})
}}
>
<RefreshCw className="h-3.5 w-3.5" />
{t("actions.refresh")}
</Button>
</div>
</div>
{loadError && (
<div className="mb-3 rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{loadError}
</div>
)}
{experts.length === 0 ? (
<div className="h-full rounded-lg border bg-card flex items-center justify-center text-sm text-muted-foreground">
{t("emptyExperts")}
</div>
) : (
<div ref={panelContainerRef} className="flex-1 min-h-0 min-w-0">
<ResizablePanelGroup
direction="horizontal"
className="h-full min-h-0 min-w-0"
>
<ResizablePanel
defaultSize={38}
minSize={leftMinSize}
maxSize={leftMaxSize}
>
<div className="min-h-0 h-full min-w-0 rounded-lg border bg-card flex flex-col overflow-hidden lg:rounded-r-none">
<div className="border-b p-3 space-y-2.5">
<Input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={t("searchPlaceholder")}
/>
</div>
<div className="flex-1 min-h-0 overflow-y-auto p-2 space-y-3">
{groupedExperts.map(([category, items]) => (
<div key={category} className="space-y-1.5">
<div className="px-1 text-[11px] uppercase tracking-wide font-semibold text-muted-foreground">
{translatedCategory(category)}
</div>
{items.map((item) => {
const Icon = getIcon(item.metadata.icon)
const name =
pickLocalized(item.metadata.display_name, locale) ||
item.metadata.id
const desc = pickLocalized(
item.metadata.description,
locale
)
const isActive = selectedExpertId === item.metadata.id
return (
<button
key={item.metadata.id}
type="button"
onClick={() =>
setSelectedExpertId(item.metadata.id)
}
className={cn(
"w-full rounded-md border px-2.5 py-2 text-left transition-colors",
isActive
? "border-primary/60 bg-primary/5"
: "hover:bg-muted/30"
)}
>
<div className="flex items-start gap-2 min-w-0">
<Icon className="h-4 w-4 mt-0.5 shrink-0 text-primary/80" />
<div className="min-w-0 flex-1">
<div className="text-sm font-medium truncate">
{name}
</div>
<div className="text-[11px] text-muted-foreground line-clamp-2 mt-0.5">
{desc}
</div>
</div>
{item.user_modified && (
<Badge
variant="outline"
className="h-5 px-1.5 text-[10px] shrink-0 border-amber-500/40 bg-amber-500/10 text-amber-600 dark:text-amber-400"
>
{t("badges.userModified")}
</Badge>
)}
</div>
</button>
)
})}
</div>
))}
{groupedExperts.length === 0 && (
<div className="text-xs text-muted-foreground px-2 py-3">
{t("emptySearch")}
</div>
)}
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={62} minSize={rightMinSize}>
<div className="h-full flex-1 min-h-0 min-w-0 rounded-lg border bg-card overflow-hidden lg:rounded-l-none lg:border-l-0">
{selectedExpert ? (
<div className="h-full flex flex-col">
<div className="border-b px-4 py-3 flex items-start gap-3">
<SelectedIcon className="h-5 w-5 mt-0.5 shrink-0 text-primary/80" />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold truncate">
{selectedName}
</h3>
<Badge
variant="outline"
className="h-5 px-1.5 text-[10px]"
>
{translatedCategory(
selectedExpert.metadata.category
)}
</Badge>
<code className="text-[11px] text-muted-foreground font-mono truncate">
{selectedExpert.metadata.id}
</code>
</div>
<p className="text-xs text-muted-foreground mt-1">
{selectedDescription}
</p>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<div className="rounded-md border p-3">
<div className="text-[11px] text-muted-foreground mb-2 flex items-center justify-between">
<span>{t("enableForAgents")}</span>
{statusLoading && (
<Loader2 className="h-3 w-3 animate-spin" />
)}
</div>
<div className="space-y-1.5">
{agents.length === 0 ? (
<div className="text-xs text-muted-foreground py-2">
{t("noAgents")}
</div>
) : (
agents.map((agent) => {
const status = statuses[agent.agent_type] ?? null
const enabled =
status?.state === "linked_to_codeg"
const blocked =
status?.state === "blocked_by_real_directory" ||
status?.state === "linked_elsewhere"
const key = `${selectedExpert.metadata.id}:${agent.agent_type}`
const pending = pendingMutation === key
return (
<div
key={agent.agent_type}
className={cn(
"flex items-center gap-3 rounded-md border px-3 py-2",
enabled
? "border-primary/40 bg-primary/5"
: "border-border"
)}
>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{agent.name}
</div>
<div className="text-[11px] text-muted-foreground truncate">
{status
? translatedState(status.state)
: "—"}
</div>
{status?.copyMode && (
<div className="text-[11px] text-amber-500 mt-0.5">
{t("copyModeWarning")}
</div>
)}
</div>
<Switch
checked={enabled}
disabled={pending || (blocked && !enabled)}
onCheckedChange={(checked: boolean) => {
handleToggle(
selectedExpert.metadata.id,
agent.agent_type,
checked
).catch((err) => {
console.error(
"[ExpertsSettings] toggle failed:",
err
)
})
}}
/>
</div>
)
})
)}
</div>
</div>
<div className="rounded-md border p-3">
<div className="text-[11px] text-muted-foreground mb-2">
{t("previewTitle")}
</div>
{contentLoading ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground py-3">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
{t("loadingContent")}
</div>
) : (
<div
className={cn(
"text-sm leading-6 rounded-md bg-muted/10 p-3 overflow-auto max-h-[480px]",
"[&_h1]:text-xl [&_h1]:font-semibold [&_h1]:mb-3",
"[&_h2]:text-lg [&_h2]:font-semibold [&_h2]:mt-5 [&_h2]:mb-2",
"[&_h3]:text-base [&_h3]:font-semibold [&_h3]:mt-4 [&_h3]:mb-2",
"[&_p]:mb-3 [&_li]:mb-1",
"[&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5",
"[&_code]:font-mono [&_code]:text-xs [&_code]:bg-muted [&_code]:rounded [&_code]:px-1",
"[&_pre]:bg-muted [&_pre]:rounded-md [&_pre]:p-3 [&_pre]:overflow-x-auto"
)}
>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{stripFrontmatter(content)}
</ReactMarkdown>
</div>
)}
</div>
</div>
</div>
) : (
<div className="h-full flex items-center justify-center text-xs text-muted-foreground">
{t("emptySelection")}
</div>
)}
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
)}
</div>
)
}

View File

@@ -19,6 +19,7 @@ import {
PlugZap,
Server,
Settings,
Sparkles,
} from "lucide-react"
import { useTranslations } from "next-intl"
import { usePathname } from "next/navigation"
@@ -39,6 +40,7 @@ interface SettingsNavItem {
| "model_providers"
| "mcp"
| "skills"
| "experts"
| "shortcuts"
| "version_control"
| "chat_channels"
@@ -63,6 +65,11 @@ const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [
labelKey: "skills",
icon: BookOpenText,
},
{
href: "/settings/experts",
labelKey: "experts",
icon: Sparkles,
},
{
href: "/settings/agents",
labelKey: "agents",

View File

@@ -94,7 +94,8 @@
"system": "النظام",
"chat_channels": "قنوات المحادثة",
"web_service": "خدمة الويب",
"model_providers": "مزودو النماذج"
"model_providers": "مزودو النماذج",
"experts": "الخبراء"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "حذف",
"create": "إنشاء",
"save": "حفظ"
},
"ExpertsSettings": {
"title": "مهارات الخبراء",
"description": "فعّل سير عمل المهارات المختارة بعناية والمختبرة ميدانيًا لوكلاء البرمجة بالذكاء الاصطناعي. كل خبير هو مهارة مستقلة من مشروع superpowers — يدير codeg النسخة المركزية ويربطها بالوكلاء الذين تختارهم.",
"loading": "جاري تحميل الخبراء…",
"loadingContent": "جاري تحميل المحتوى…",
"emptyExperts": "لا يوجد خبراء متاحون. تحقق من سجلات التطبيق.",
"emptySelection": "اختر خبيرًا لرؤية محتواه وإدارة تفعيله.",
"emptySearch": "لا يوجد خبراء يطابقون البحث الحالي.",
"searchPlaceholder": "ابحث عن الخبراء بالاسم أو المعرّف أو الوصف",
"enableForAgents": "تفعيل للوكلاء",
"noAgents": "لم يتم اكتشاف وكلاء ACP.",
"copyModeWarning": "تم النسخ (غير مرتبط). أعد التفعيل بعد تحديثات codeg للحصول على أحدث إصدار.",
"previewTitle": "معاينة SKILL.md",
"categories": {
"discovery": "الاكتشاف والتصميم",
"planning": "التخطيط",
"execution": "التنفيذ",
"quality": "الجودة والاختبار",
"debugging": "التصحيح",
"review": "المراجعة والدمج",
"meta": "ميتا"
},
"states": {
"not_linked": "غير مُفعَّل",
"linked_to_codeg": "مُفعَّل",
"linked_elsewhere": "محظور — يوجد رابط آخر",
"blocked_by_real_directory": "محظور — مهارة مخصصة تشغل هذا الاسم",
"broken": "رابط معطوب"
},
"badges": {
"userModified": "عُدِّل من قبل المستخدم"
},
"actions": {
"openCentralDir": "فتح المجلد المركزي",
"refresh": "تحديث"
},
"toasts": {
"loadFailed": "فشل تحميل تفاصيل الخبير",
"enabled": "تم تفعيل الخبير لهذا الوكيل",
"disabled": "تم تعطيل الخبير لهذا الوكيل",
"enableFailed": "فشل تفعيل الخبير",
"disableFailed": "فشل تعطيل الخبير",
"openFolderFailed": "فشل فتح المجلد"
}
}
}

View File

@@ -94,7 +94,8 @@
"system": "Systemeinstellungen",
"chat_channels": "Chat-Kanäle",
"web_service": "Webdienst",
"model_providers": "Modellanbieter"
"model_providers": "Modellanbieter",
"experts": "Experten"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "Löschen",
"create": "Erstellen",
"save": "Speichern"
},
"ExpertsSettings": {
"title": "Experten-Skills",
"description": "Aktivieren Sie kuratierte, praxiserprobte Skill-Workflows für Ihre KI-Coding-Agents. Jeder Experte ist ein eigenständiger Skill aus dem superpowers-Projekt — codeg verwaltet die zentrale Kopie und verknüpft sie mit den von Ihnen ausgewählten Agents.",
"loading": "Experten werden geladen…",
"loadingContent": "Inhalt wird geladen…",
"emptyExperts": "Keine Experten verfügbar. Prüfen Sie die Anwendungsprotokolle.",
"emptySelection": "Wählen Sie einen Experten aus, um seinen Inhalt anzuzeigen und die Aktivierung zu verwalten.",
"emptySearch": "Keine Experten entsprechen der aktuellen Suche.",
"searchPlaceholder": "Experten nach Name, ID oder Beschreibung suchen",
"enableForAgents": "Für Agents aktivieren",
"noAgents": "Keine ACP-Agents erkannt.",
"copyModeWarning": "Kopiert (nicht verknüpft). Nach codeg-Updates erneut aktivieren, um die neueste Version zu erhalten.",
"previewTitle": "SKILL.md-Vorschau",
"categories": {
"discovery": "Entdeckung & Design",
"planning": "Planung",
"execution": "Ausführung",
"quality": "Qualität & Tests",
"debugging": "Debugging",
"review": "Review & Integration",
"meta": "Meta"
},
"states": {
"not_linked": "Nicht aktiviert",
"linked_to_codeg": "Aktiviert",
"linked_elsewhere": "Blockiert — ein anderer Link existiert",
"blocked_by_real_directory": "Blockiert — ein benutzerdefinierter Skill belegt diesen Namen",
"broken": "Defekter Link"
},
"badges": {
"userModified": "Vom Benutzer geändert"
},
"actions": {
"openCentralDir": "Zentralen Ordner öffnen",
"refresh": "Aktualisieren"
},
"toasts": {
"loadFailed": "Laden der Expertendetails fehlgeschlagen",
"enabled": "Experte für diesen Agent aktiviert",
"disabled": "Experte für diesen Agent deaktiviert",
"enableFailed": "Aktivieren des Experten fehlgeschlagen",
"disableFailed": "Deaktivieren des Experten fehlgeschlagen",
"openFolderFailed": "Ordner konnte nicht geöffnet werden"
}
}
}

View File

@@ -94,7 +94,8 @@
"system": "System",
"chat_channels": "Chat Channels",
"web_service": "Web Service",
"model_providers": "Model Providers"
"model_providers": "Model Providers",
"experts": "Experts"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "Delete",
"create": "Create",
"save": "Save"
},
"ExpertsSettings": {
"title": "Expert Skills",
"description": "Enable curated, battle-tested skill workflows for your AI coding agents. Each expert is a standalone skill from the superpowers project — codeg manages the central copy and links it into the agents you choose.",
"loading": "Loading experts…",
"loadingContent": "Loading content…",
"emptyExperts": "No experts available. Check the application logs.",
"emptySelection": "Select an expert to see its content and manage activation.",
"emptySearch": "No experts match the current search.",
"searchPlaceholder": "Search experts by name, id, or description",
"enableForAgents": "Enable for agents",
"noAgents": "No ACP agents detected.",
"copyModeWarning": "Copied (not linked). Re-enable after codeg updates to get the latest version.",
"previewTitle": "SKILL.md preview",
"categories": {
"discovery": "Discovery & Design",
"planning": "Planning",
"execution": "Execution",
"quality": "Quality & Testing",
"debugging": "Debugging",
"review": "Review & Integration",
"meta": "Meta"
},
"states": {
"not_linked": "Not enabled",
"linked_to_codeg": "Enabled",
"linked_elsewhere": "Blocked — another link exists",
"blocked_by_real_directory": "Blocked — a custom skill occupies this name",
"broken": "Broken link"
},
"badges": {
"userModified": "User modified"
},
"actions": {
"openCentralDir": "Open central folder",
"refresh": "Refresh"
},
"toasts": {
"loadFailed": "Failed to load expert details",
"enabled": "Expert enabled for this agent",
"disabled": "Expert disabled for this agent",
"enableFailed": "Failed to enable expert",
"disableFailed": "Failed to disable expert",
"openFolderFailed": "Failed to open folder"
}
}
}

View File

@@ -94,7 +94,8 @@
"system": "Sistema",
"chat_channels": "Canales de chat",
"web_service": "Servicio Web",
"model_providers": "Proveedores de Modelos"
"model_providers": "Proveedores de Modelos",
"experts": "Expertos"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "Eliminar",
"create": "Crear",
"save": "Guardar"
},
"ExpertsSettings": {
"title": "Habilidades de expertos",
"description": "Habilita flujos de trabajo de habilidades cuidadosamente seleccionadas y probadas en la práctica para tus agentes de codificación de IA. Cada experto es una habilidad independiente del proyecto superpowers — codeg gestiona la copia central y la vincula a los agentes que elijas.",
"loading": "Cargando expertos…",
"loadingContent": "Cargando contenido…",
"emptyExperts": "No hay expertos disponibles. Revisa los registros de la aplicación.",
"emptySelection": "Selecciona un experto para ver su contenido y gestionar la activación.",
"emptySearch": "Ningún experto coincide con la búsqueda actual.",
"searchPlaceholder": "Buscar expertos por nombre, ID o descripción",
"enableForAgents": "Habilitar para agentes",
"noAgents": "No se detectaron agentes ACP.",
"copyModeWarning": "Copiado (no vinculado). Vuelve a habilitarlo después de actualizar codeg para obtener la última versión.",
"previewTitle": "Vista previa de SKILL.md",
"categories": {
"discovery": "Descubrimiento y diseño",
"planning": "Planificación",
"execution": "Ejecución",
"quality": "Calidad y pruebas",
"debugging": "Depuración",
"review": "Revisión e integración",
"meta": "Meta"
},
"states": {
"not_linked": "No habilitado",
"linked_to_codeg": "Habilitado",
"linked_elsewhere": "Bloqueado — existe otro vínculo",
"blocked_by_real_directory": "Bloqueado — una habilidad personalizada ocupa este nombre",
"broken": "Vínculo roto"
},
"badges": {
"userModified": "Modificado por el usuario"
},
"actions": {
"openCentralDir": "Abrir carpeta central",
"refresh": "Actualizar"
},
"toasts": {
"loadFailed": "Error al cargar los detalles del experto",
"enabled": "Experto habilitado para este agente",
"disabled": "Experto deshabilitado para este agente",
"enableFailed": "Error al habilitar el experto",
"disableFailed": "Error al deshabilitar el experto",
"openFolderFailed": "Error al abrir la carpeta"
}
}
}

View File

@@ -94,7 +94,8 @@
"system": "Système",
"chat_channels": "Canaux de chat",
"web_service": "Service Web",
"model_providers": "Fournisseurs de Modèles"
"model_providers": "Fournisseurs de Modèles",
"experts": "Experts"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "Supprimer",
"create": "Créer",
"save": "Enregistrer"
},
"ExpertsSettings": {
"title": "Compétences d'experts",
"description": "Activez des workflows de compétences soigneusement sélectionnés et éprouvés pour vos agents de codage IA. Chaque expert est une compétence autonome du projet superpowers — codeg gère la copie centrale et la lie aux agents que vous choisissez.",
"loading": "Chargement des experts…",
"loadingContent": "Chargement du contenu…",
"emptyExperts": "Aucun expert disponible. Vérifiez les journaux de l'application.",
"emptySelection": "Sélectionnez un expert pour voir son contenu et gérer son activation.",
"emptySearch": "Aucun expert ne correspond à la recherche actuelle.",
"searchPlaceholder": "Rechercher des experts par nom, ID ou description",
"enableForAgents": "Activer pour les agents",
"noAgents": "Aucun agent ACP détecté.",
"copyModeWarning": "Copié (non lié). Réactivez après les mises à jour de codeg pour obtenir la dernière version.",
"previewTitle": "Aperçu de SKILL.md",
"categories": {
"discovery": "Découverte et conception",
"planning": "Planification",
"execution": "Exécution",
"quality": "Qualité et tests",
"debugging": "Débogage",
"review": "Révision et intégration",
"meta": "Meta"
},
"states": {
"not_linked": "Non activé",
"linked_to_codeg": "Activé",
"linked_elsewhere": "Bloqué — un autre lien existe",
"blocked_by_real_directory": "Bloqué — une compétence personnalisée occupe ce nom",
"broken": "Lien cassé"
},
"badges": {
"userModified": "Modifié par l'utilisateur"
},
"actions": {
"openCentralDir": "Ouvrir le dossier central",
"refresh": "Actualiser"
},
"toasts": {
"loadFailed": "Échec du chargement des détails de l'expert",
"enabled": "Expert activé pour cet agent",
"disabled": "Expert désactivé pour cet agent",
"enableFailed": "Échec de l'activation de l'expert",
"disableFailed": "Échec de la désactivation de l'expert",
"openFolderFailed": "Échec de l'ouverture du dossier"
}
}
}

View File

@@ -94,7 +94,8 @@
"system": "システム",
"chat_channels": "チャットチャンネル",
"web_service": "Webサービス",
"model_providers": "モデルプロバイダー"
"model_providers": "モデルプロバイダー",
"experts": "エキスパート"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "削除",
"create": "作成",
"save": "保存"
},
"ExpertsSettings": {
"title": "エキスパートスキル",
"description": "AI コーディングエージェント向けに、厳選され実戦で検証されたスキルワークフローを有効にします。各エキスパートは superpowers プロジェクトの独立したスキルで、codeg が中央コピーを管理し、選択したエージェントにリンクします。",
"loading": "エキスパートを読み込み中…",
"loadingContent": "コンテンツを読み込み中…",
"emptyExperts": "利用可能なエキスパートがありません。アプリケーションログを確認してください。",
"emptySelection": "エキスパートを選択して内容を表示し、有効化を管理します。",
"emptySearch": "現在の検索に一致するエキスパートはありません。",
"searchPlaceholder": "名前、ID、または説明でエキスパートを検索",
"enableForAgents": "エージェントで有効化",
"noAgents": "ACP エージェントが検出されません。",
"copyModeWarning": "コピー済みリンクなし。codeg 更新後に最新版を取得するには再度有効化してください。",
"previewTitle": "SKILL.md プレビュー",
"categories": {
"discovery": "発見と設計",
"planning": "計画",
"execution": "実行",
"quality": "品質とテスト",
"debugging": "デバッグ",
"review": "レビューと統合",
"meta": "メタ"
},
"states": {
"not_linked": "未有効",
"linked_to_codeg": "有効",
"linked_elsewhere": "ブロック — 別のリンクが存在",
"blocked_by_real_directory": "ブロック — カスタムスキルがこの名前を占有",
"broken": "壊れたリンク"
},
"badges": {
"userModified": "ユーザーにより変更"
},
"actions": {
"openCentralDir": "中央フォルダを開く",
"refresh": "更新"
},
"toasts": {
"loadFailed": "エキスパート詳細の読み込みに失敗しました",
"enabled": "このエージェントでエキスパートを有効化しました",
"disabled": "このエージェントでエキスパートを無効化しました",
"enableFailed": "エキスパートの有効化に失敗しました",
"disableFailed": "エキスパートの無効化に失敗しました",
"openFolderFailed": "フォルダを開けませんでした"
}
}
}

View File

@@ -94,7 +94,8 @@
"system": "시스템",
"chat_channels": "채팅 채널",
"web_service": "웹 서비스",
"model_providers": "모델 제공업체"
"model_providers": "모델 제공업체",
"experts": "전문가"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "삭제",
"create": "생성",
"save": "저장"
},
"ExpertsSettings": {
"title": "전문가 스킬",
"description": "AI 코딩 에이전트를 위해 엄선되고 실전 검증된 스킬 워크플로를 활성화하세요. 각 전문가는 superpowers 프로젝트의 독립 실행형 스킬이며 — codeg가 중앙 복사본을 관리하고 선택한 에이전트에 연결합니다.",
"loading": "전문가 로딩 중…",
"loadingContent": "콘텐츠 로딩 중…",
"emptyExperts": "사용 가능한 전문가가 없습니다. 애플리케이션 로그를 확인하세요.",
"emptySelection": "전문가를 선택하여 내용을 보고 활성화를 관리하세요.",
"emptySearch": "현재 검색과 일치하는 전문가가 없습니다.",
"searchPlaceholder": "이름, ID 또는 설명으로 전문가 검색",
"enableForAgents": "에이전트에 활성화",
"noAgents": "ACP 에이전트가 감지되지 않았습니다.",
"copyModeWarning": "복사됨(연결되지 않음). 최신 버전을 받으려면 codeg 업데이트 후 재활성화하세요.",
"previewTitle": "SKILL.md 미리보기",
"categories": {
"discovery": "발견 및 설계",
"planning": "계획",
"execution": "실행",
"quality": "품질 및 테스트",
"debugging": "디버깅",
"review": "검토 및 통합",
"meta": "메타"
},
"states": {
"not_linked": "활성화되지 않음",
"linked_to_codeg": "활성화됨",
"linked_elsewhere": "차단됨 — 다른 링크가 존재함",
"blocked_by_real_directory": "차단됨 — 사용자 정의 스킬이 이 이름을 점유 중",
"broken": "손상된 링크"
},
"badges": {
"userModified": "사용자가 수정함"
},
"actions": {
"openCentralDir": "중앙 폴더 열기",
"refresh": "새로고침"
},
"toasts": {
"loadFailed": "전문가 세부 정보를 로드하지 못했습니다",
"enabled": "이 에이전트에 전문가가 활성화되었습니다",
"disabled": "이 에이전트에서 전문가가 비활성화되었습니다",
"enableFailed": "전문가 활성화에 실패했습니다",
"disableFailed": "전문가 비활성화에 실패했습니다",
"openFolderFailed": "폴더를 열지 못했습니다"
}
}
}

View File

@@ -94,7 +94,8 @@
"system": "Sistema",
"chat_channels": "Canais de chat",
"web_service": "Serviço Web",
"model_providers": "Provedores de Modelos"
"model_providers": "Provedores de Modelos",
"experts": "Especialistas"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "Excluir",
"create": "Criar",
"save": "Salvar"
},
"ExpertsSettings": {
"title": "Habilidades de Especialista",
"description": "Ative fluxos de trabalho de habilidades selecionados e comprovados em campo para seus agentes de codificação de IA. Cada especialista é uma habilidade independente do projeto superpowers — o codeg gerencia a cópia central e a vincula aos agentes escolhidos.",
"loading": "Carregando especialistas…",
"loadingContent": "Carregando conteúdo…",
"emptyExperts": "Nenhum especialista disponível. Verifique os logs da aplicação.",
"emptySelection": "Selecione um especialista para ver seu conteúdo e gerenciar a ativação.",
"emptySearch": "Nenhum especialista corresponde à pesquisa atual.",
"searchPlaceholder": "Pesquisar especialistas por nome, ID ou descrição",
"enableForAgents": "Ativar para agentes",
"noAgents": "Nenhum agente ACP detectado.",
"copyModeWarning": "Copiado (não vinculado). Reative após as atualizações do codeg para obter a versão mais recente.",
"previewTitle": "Prévia do SKILL.md",
"categories": {
"discovery": "Descoberta e Design",
"planning": "Planejamento",
"execution": "Execução",
"quality": "Qualidade e Testes",
"debugging": "Depuração",
"review": "Revisão e Integração",
"meta": "Meta"
},
"states": {
"not_linked": "Não ativado",
"linked_to_codeg": "Ativado",
"linked_elsewhere": "Bloqueado — outro vínculo existe",
"blocked_by_real_directory": "Bloqueado — uma habilidade personalizada ocupa este nome",
"broken": "Vínculo quebrado"
},
"badges": {
"userModified": "Modificado pelo usuário"
},
"actions": {
"openCentralDir": "Abrir pasta central",
"refresh": "Atualizar"
},
"toasts": {
"loadFailed": "Falha ao carregar detalhes do especialista",
"enabled": "Especialista ativado para este agente",
"disabled": "Especialista desativado para este agente",
"enableFailed": "Falha ao ativar o especialista",
"disableFailed": "Falha ao desativar o especialista",
"openFolderFailed": "Falha ao abrir a pasta"
}
}
}

View File

@@ -94,7 +94,8 @@
"system": "系统",
"chat_channels": "消息渠道",
"web_service": "Web 服务",
"model_providers": "模型供应商"
"model_providers": "模型供应商",
"experts": "专家"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "删除",
"create": "创建",
"save": "保存"
},
"ExpertsSettings": {
"title": "专家技能",
"description": "为 AI 编码代理启用精心挑选、经过实战验证的技能工作流。每个专家都是 superpowers 项目中的独立技能 —— codeg 维护中央副本,并将其软链接到你选择的代理目录。",
"loading": "正在加载专家列表…",
"loadingContent": "正在加载内容…",
"emptyExperts": "当前没有可用的专家,请查看应用日志。",
"emptySelection": "从左侧选择一个专家以查看内容并管理启用状态。",
"emptySearch": "没有匹配当前搜索条件的专家。",
"searchPlaceholder": "按名称、ID 或描述搜索专家",
"enableForAgents": "为代理启用",
"noAgents": "未检测到 ACP 代理。",
"copyModeWarning": "已复制(非软链接)。codeg 更新后需要重新启用以获取最新版本。",
"previewTitle": "SKILL.md 预览",
"categories": {
"discovery": "发现与设计",
"planning": "规划",
"execution": "执行",
"quality": "质量与测试",
"debugging": "调试",
"review": "评审与集成",
"meta": "元技能"
},
"states": {
"not_linked": "未启用",
"linked_to_codeg": "已启用",
"linked_elsewhere": "冲突 — 已有其他链接",
"blocked_by_real_directory": "冲突 — 已有同名的自定义 skill 占用",
"broken": "链接损坏"
},
"badges": {
"userModified": "用户修改过"
},
"actions": {
"openCentralDir": "打开中央目录",
"refresh": "刷新"
},
"toasts": {
"loadFailed": "加载专家详情失败",
"enabled": "已为该代理启用专家",
"disabled": "已为该代理禁用专家",
"enableFailed": "启用专家失败",
"disableFailed": "禁用专家失败",
"openFolderFailed": "打开目录失败"
}
}
}

View File

@@ -94,7 +94,8 @@
"system": "系統",
"chat_channels": "訊息頻道",
"web_service": "Web 服務",
"model_providers": "模型供應商"
"model_providers": "模型供應商",
"experts": "專家"
}
},
"AppearanceSettings": {
@@ -1857,5 +1858,50 @@
"delete": "刪除",
"create": "建立",
"save": "儲存"
},
"ExpertsSettings": {
"title": "專家技能",
"description": "為 AI 編碼代理啟用精心挑選、經過實戰驗證的技能工作流程。每個專家都是 superpowers 專案中的獨立技能 —— codeg 維護中央副本,並將其軟連結到你選擇的代理目錄。",
"loading": "正在載入專家清單…",
"loadingContent": "正在載入內容…",
"emptyExperts": "目前沒有可用的專家,請查看應用程式日誌。",
"emptySelection": "從左側選擇一個專家以檢視內容並管理啟用狀態。",
"emptySearch": "沒有符合目前搜尋條件的專家。",
"searchPlaceholder": "依名稱、ID 或描述搜尋專家",
"enableForAgents": "為代理啟用",
"noAgents": "未偵測到 ACP 代理。",
"copyModeWarning": "已複製(非軟連結)。codeg 更新後需要重新啟用以取得最新版本。",
"previewTitle": "SKILL.md 預覽",
"categories": {
"discovery": "探索與設計",
"planning": "規劃",
"execution": "執行",
"quality": "品質與測試",
"debugging": "除錯",
"review": "審查與整合",
"meta": "後設技能"
},
"states": {
"not_linked": "未啟用",
"linked_to_codeg": "已啟用",
"linked_elsewhere": "衝突 — 已有其他連結",
"blocked_by_real_directory": "衝突 — 已有同名的自訂 skill 佔用",
"broken": "連結損壞"
},
"badges": {
"userModified": "使用者修改過"
},
"actions": {
"openCentralDir": "開啟中央目錄",
"refresh": "重新整理"
},
"toasts": {
"loadFailed": "載入專家詳情失敗",
"enabled": "已為該代理啟用專家",
"disabled": "已為該代理停用專家",
"enableFailed": "啟用專家失敗",
"disableFailed": "停用專家失敗",
"openFolderFailed": "開啟目錄失敗"
}
}
}

View File

@@ -15,6 +15,8 @@ import type {
AgentSkillItem,
AgentSkillsListResult,
AgentSkillContent,
ExpertListItem,
ExpertInstallStatus,
FolderHistoryEntry,
FolderDetail,
DbConversationSummary,
@@ -332,6 +334,46 @@ export async function acpDeleteAgentSkill(params: {
})
}
// ─── Experts (built-in expert skills) ───────────────────────────────────
export async function expertsList(): Promise<ExpertListItem[]> {
return getTransport().call("experts_list")
}
export async function expertsGetInstallStatus(
expertId: string
): Promise<ExpertInstallStatus[]> {
return getTransport().call("experts_get_install_status", { expertId })
}
export async function expertsLinkToAgent(params: {
expertId: string
agentType: AgentType
}): Promise<ExpertInstallStatus> {
return getTransport().call("experts_link_to_agent", {
expertId: params.expertId,
agentType: params.agentType,
})
}
export async function expertsUnlinkFromAgent(params: {
expertId: string
agentType: AgentType
}): Promise<void> {
return getTransport().call("experts_unlink_from_agent", {
expertId: params.expertId,
agentType: params.agentType,
})
}
export async function expertsReadContent(expertId: string): Promise<string> {
return getTransport().call("experts_read_content", { expertId })
}
export async function expertsOpenCentralDir(): Promise<string> {
return getTransport().call("experts_open_central_dir")
}
export async function getSystemProxySettings(): Promise<SystemProxySettings> {
return getTransport().call("get_system_proxy_settings")
}

View File

@@ -521,6 +521,45 @@ export interface AgentSkillContent {
content: string
}
/**
* Built-in expert skills, sourced from obra/superpowers and bundled into
* the codeg binary. Experts live in a central store at `~/.codeg/skills/`
* and are linked into agent skill directories on demand.
*/
export interface ExpertMetadata {
id: string
category: string
icon: string | null
sort_order: number
display_name: Record<string, string>
description: Record<string, string>
bundled_hash: string
}
export interface ExpertListItem {
metadata: ExpertMetadata
installed_centrally: boolean
user_modified: boolean
central_path: string
}
export type ExpertLinkState =
| "not_linked"
| "linked_to_codeg"
| "linked_elsewhere"
| "blocked_by_real_directory"
| "broken"
export interface ExpertInstallStatus {
expertId: string
agentType: AgentType
state: ExpertLinkState
linkPath: string
targetPath: string | null
expectedTargetPath: string
copyMode: boolean
}
export interface SystemProxySettings {
enabled: boolean
proxy_url: string | null