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:
5
src/app/settings/experts/page.tsx
Normal file
5
src/app/settings/experts/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ExpertsSettings } from "@/components/settings/experts-settings"
|
||||
|
||||
export default function SettingsExpertsPage() {
|
||||
return <ExpertsSettings />
|
||||
}
|
||||
682
src/components/settings/experts-settings.tsx
Normal file
682
src/components/settings/experts-settings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "فشل فتح المجلد"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "フォルダを開けませんでした"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "폴더를 열지 못했습니다"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "打开目录失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "開啟目錄失敗"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user