feat(skills): support folder-scoped skills in agent sessions

- Thread workspace path through useAgentSkills so Codex $-autocomplete
  surfaces folder-local skills in addition to global ones; cache keyed
  by agent + workspace and invalidated per key on focus.
- Add Global/Folder scope tabs and a folder picker (sourced from the
  folder table via loadFolderHistory) to the Skills settings page.
  CRUD for skills now operates against the selected scope and folder.
- Default the settings right panel to a placeholder hint; the new-skill
  form only appears after clicking "New Skill" or selecting an existing
  skill. Search input is hidden in folder scope.
- Disable "New Skill" when folder scope has no folder chosen; show a
  pick-folder hint in the path preview for that state.
- Add scope/noSelectionHint/pickFolderHint strings across 10 locales.
This commit is contained in:
xintaofei
2026-04-15 14:56:53 +08:00
parent 3148966371
commit a6f80088d0
13 changed files with 568 additions and 254 deletions

View File

@@ -331,7 +331,10 @@ export function MessageInput({
// set through ACP `availableCommands`, so injecting skills there would
// be duplicate/extra UI noise — skip the skills fetch for them entirely.
const skillAgentType = agentType === "codex" ? "codex" : null
const availableSkills = useAgentSkills(skillAgentType)
// Pass the working dir so we see both global skills and folder-scoped
// project skills (e.g. `{folder}/.codex/skills`). Without this, users
// only ever saw global skills in the `$` autocomplete.
const availableSkills = useAgentSkills(skillAgentType, defaultPath ?? null)
// Expert skills are symlinked into the agent's skill directories, so they
// also show up in `acp_list_agent_skills`. Strip them out — experts remain
// reachable via the expert button, and the `$` list is skills-only.

View File

@@ -52,16 +52,20 @@ import {
acpDeleteAgentSkill,
acpListAgents,
acpListAgentSkills,
loadFolderHistory,
openFolderWindow,
acpReadAgentSkill,
acpSaveAgentSkill,
} from "@/lib/api"
import { invalidateAgentSkillsCache } from "@/hooks/use-agent-skills"
import type {
AcpAgentInfo,
AgentSkillItem,
AgentSkillLayout,
AgentSkillLocation,
AgentSkillScope,
AgentType,
FolderHistoryEntry,
} from "@/lib/types"
type SkillsTranslator = (
@@ -224,6 +228,22 @@ export function SkillsSettings() {
const [skillItems, setSkillItems] = useState<AgentSkillItem[]>([])
const [selectedSkillId, setSelectedSkillId] = useState<string | null>(null)
// Scope: "global" = ~/.claude/skills etc; "folder" = {folder}/.claude/skills
// etc. Folder-scope maps to the backend's `project` AgentSkillScope — no new
// scope type, just threading the folder path through as `workspacePath`.
const [skillsScope, setSkillsScope] = useState<"global" | "folder">("global")
// Only folders registered in the DB (opened at least once via codeg).
// loadFolderHistory() is O(folders), while listFolders() aggregates from
// every conversation — slow on large histories.
const [folderList, setFolderList] = useState<FolderHistoryEntry[]>([])
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(
null
)
const workspacePathForRequest =
skillsScope === "folder" ? selectedFolderPath : null
const backendScope: AgentSkillScope =
skillsScope === "folder" ? "project" : "global"
const [skillDraftId, setSkillDraftId] = useState("")
const [skillDraftContent, setSkillDraftContent] = useState("")
const [searchQuery, setSearchQuery] = useState("")
@@ -235,6 +255,12 @@ export function SkillsSettings() {
useState<AgentSkillItem | null>(null)
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [isContentEditing, setIsContentEditing] = useState(false)
// True only while the user is authoring a brand-new skill (clicked "New
// Skill"). Opening an existing skill clears this. The right panel renders
// the form iff a skill is selected OR the user is drafting — otherwise it
// shows a placeholder, so users aren't presented with a surprise form on
// first visit.
const [isDrafting, setIsDrafting] = useState(false)
const sortedAgents = useMemo(
() =>
@@ -326,13 +352,16 @@ export function SkillsSettings() {
try {
const detail = await acpReadAgentSkill({
agentType,
scope: "global",
scope: skill.scope,
skillId: skill.id,
workspacePath:
skill.scope === "project" ? workspacePathForRequest : null,
})
setSelectedSkillId(detail.skill.id)
setSkillDraftId(detail.skill.id)
setSkillDraftContent(detail.content)
setIsContentEditing(mode === "edit")
setIsDrafting(false)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("toasts.loadFailed"), { description: message })
@@ -340,32 +369,53 @@ export function SkillsSettings() {
setSkillReading(false)
}
},
[t]
[t, workspacePathForRequest]
)
const loadSkills = useCallback(async (agentType: AgentType) => {
setSkillsLoading(true)
setSkillsError(null)
const loadSkills = useCallback(
async (agentType: AgentType) => {
// Folder scope but no folder chosen → skip the fetch; UI prompts the
// user to pick one. We still clear previous results so list doesn't
// show stale items from another folder.
if (skillsScope === "folder" && !workspacePathForRequest) {
setSkillsError(null)
setSkillsSupported(true)
setSkillLocation(null)
setSkillItems([])
return null
}
try {
const result = await acpListAgentSkills({ agentType })
setSkillsSupported(result.supported)
setSkillLocation(
result.locations.find((location) => location.scope === "global") ?? null
)
setSkillItems(result.skills.filter((skill) => skill.scope === "global"))
return result
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setSkillsError(message)
setSkillsSupported(true)
setSkillLocation(null)
setSkillItems([])
return null
} finally {
setSkillsLoading(false)
}
}, [])
setSkillsLoading(true)
setSkillsError(null)
try {
const result = await acpListAgentSkills({
agentType,
workspacePath: workspacePathForRequest,
})
setSkillsSupported(result.supported)
setSkillLocation(
result.locations.find(
(location) => location.scope === backendScope
) ?? null
)
setSkillItems(
result.skills.filter((skill) => skill.scope === backendScope)
)
return result
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setSkillsError(message)
setSkillsSupported(true)
setSkillLocation(null)
setSkillItems([])
return null
} finally {
setSkillsLoading(false)
}
},
[backendScope, skillsScope, workspacePathForRequest]
)
const refreshAgents = useCallback(async () => {
setLoadingAgents(true)
@@ -401,6 +451,7 @@ export function SkillsSettings() {
const handleCreateDraft = useCallback(() => {
if (!selectedAgent) return
setIsDrafting(true)
resetDraft(selectedAgent.agent_type, true)
}, [resetDraft, selectedAgent])
@@ -477,12 +528,16 @@ export function SkillsSettings() {
try {
const saved = await acpSaveAgentSkill({
agentType: selectedAgent.agent_type,
scope: "global",
scope: backendScope,
skillId: trimmedId,
content: skillDraftContent,
workspacePath: workspacePathForRequest,
layout: resolvedLayout,
})
// Drop any stale in-memory skill list so running sessions (message
// input $ autocomplete) pick up the change on next focus/fetch.
invalidateAgentSkillsCache(selectedAgent.agent_type)
await loadSkills(selectedAgent.agent_type)
await openSkill(
selectedAgent.agent_type,
@@ -499,6 +554,7 @@ export function SkillsSettings() {
setSkillSaving(false)
}
}, [
backendScope,
isEditingExisting,
loadSkills,
openSkill,
@@ -509,6 +565,7 @@ export function SkillsSettings() {
skillLocation,
isContentEditing,
t,
workspacePathForRequest,
])
const handleDeleteSkill = useCallback(
@@ -521,20 +578,31 @@ export function SkillsSettings() {
try {
await acpDeleteAgentSkill({
agentType: selectedAgent.agent_type,
scope: "global",
scope: skill.scope,
skillId: skill.id,
workspacePath:
skill.scope === "project" ? workspacePathForRequest : null,
})
invalidateAgentSkillsCache(selectedAgent.agent_type)
const latest = await loadSkills(selectedAgent.agent_type)
toast.success(t("toasts.deleted"))
if (!deletingCurrent) return
const nextSkill = latest?.skills.find((item) => item.scope === "global")
const nextSkill = latest?.skills.find(
(item) => item.scope === backendScope
)
if (nextSkill) {
await openSkill(selectedAgent.agent_type, nextSkill)
} else {
resetDraft(selectedAgent.agent_type, true)
// No remaining skills → fall back to the placeholder view instead
// of shoving users into an empty new-skill form.
setSelectedSkillId(null)
setSkillDraftId("")
setSkillDraftContent("")
setIsContentEditing(false)
setIsDrafting(false)
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
@@ -545,7 +613,15 @@ export function SkillsSettings() {
setDeleteTargetSkill(null)
}
},
[loadSkills, openSkill, resetDraft, selectedAgent, selectedSkillId, t]
[
backendScope,
loadSkills,
openSkill,
selectedAgent,
selectedSkillId,
t,
workspacePathForRequest,
]
)
const handleConfirmDelete = useCallback(async () => {
@@ -622,24 +698,32 @@ export function SkillsSettings() {
setSkillDraftContent("")
setSearchQuery("")
setIsContentEditing(false)
setIsDrafting(false)
return
}
let cancelled = false
setSearchQuery("")
resetDraft(currentAgentType)
// Clear any prior selection/draft state. We do NOT pre-fill the draft
// template here anymore — the right panel shows a placeholder until the
// user picks a skill from the list or clicks "New Skill".
setSelectedSkillId(null)
setSkillDraftId("")
setSkillDraftContent("")
setIsContentEditing(false)
setIsDrafting(false)
loadSkills(currentAgentType)
.then((result) => {
if (cancelled || !result || !result.supported) return
const firstGlobalSkill = result.skills.find(
(skill) => skill.scope === "global"
const firstSkill = result.skills.find(
(skill) => skill.scope === backendScope
)
if (!firstGlobalSkill) return
if (!firstSkill) return
openSkill(currentAgentType, firstGlobalSkill).catch((err) => {
openSkill(currentAgentType, firstSkill).catch((err) => {
console.error("[SkillsSettings] initial open skill failed:", err)
})
})
@@ -650,7 +734,36 @@ export function SkillsSettings() {
return () => {
cancelled = true
}
}, [loadSkills, openSkill, resetDraft, selectedAgent])
// Re-run when scope or selected folder changes so switching to "Folder"
// (or picking a different folder) reloads the list from the right place.
}, [
loadSkills,
openSkill,
selectedAgent,
backendScope,
workspacePathForRequest,
])
// Lazy-load the folder list when the user first switches to Folder scope.
// Uses loadFolderHistory (direct `folder` table query) instead of
// listFolders, which aggregates from every conversation and can be slow
// on large histories.
useEffect(() => {
if (skillsScope !== "folder") return
if (folderList.length > 0) return
let cancelled = false
loadFolderHistory()
.then((list) => {
if (cancelled) return
setFolderList(list)
})
.catch((err) => {
console.error("[SkillsSettings] loadFolderHistory failed:", err)
})
return () => {
cancelled = true
}
}, [skillsScope, folderList.length])
if (loadingAgents) {
return (
@@ -719,13 +832,91 @@ export function SkillsSettings() {
</SelectContent>
</Select>
<Input
value={searchQuery}
onChange={(event) => {
setSearchQuery(event.target.value)
}}
placeholder={t("searchPlaceholder")}
/>
{/* Scope selector: global vs per-folder (project-scoped) */}
<div className="inline-flex w-full rounded-md border p-0.5 text-xs bg-muted/20">
{(["global", "folder"] as const).map((scope) => (
<button
key={scope}
type="button"
className={cn(
"flex-1 rounded px-2 py-1 transition-colors",
skillsScope === scope
? "bg-background shadow-sm font-medium"
: "text-muted-foreground hover:text-foreground"
)}
onClick={() => {
if (skillsScope === scope) return
setSkillsScope(scope)
// Drop any in-flight draft/selection since the list
// about to render belongs to a different scope.
setSelectedSkillId(null)
setSkillDraftId("")
setSkillDraftContent("")
setIsContentEditing(false)
setIsDrafting(false)
setSearchQuery("")
}}
>
{t(`scope.${scope}`)}
</button>
))}
</div>
{skillsScope === "folder" && (
<Select
value={selectedFolderPath ?? ""}
onValueChange={(value) => {
setSelectedFolderPath(value || null)
setSelectedSkillId(null)
setSkillDraftId("")
setSkillDraftContent("")
setIsContentEditing(false)
setIsDrafting(false)
}}
>
<SelectTrigger className="w-full justify-between text-left">
<SelectValue
placeholder={t("scope.selectFolderPlaceholder")}
/>
</SelectTrigger>
<SelectContent align="start">
{folderList.length === 0 ? (
<div className="px-2 py-1.5 text-xs text-muted-foreground text-left">
{t("scope.noFolders")}
</div>
) : (
folderList.map((folder) => (
<SelectItem key={folder.path} value={folder.path}>
{/* name + trailing path for disambiguation when
multiple folders share a name. line-clamp-1
on the trigger keeps the selected view on a
single row. */}
<span className="flex items-center gap-2 min-w-0 text-left">
<span className="text-xs font-medium shrink-0">
{folder.name}
</span>
<span className="text-[10px] text-muted-foreground truncate">
{folder.path}
</span>
</span>
</SelectItem>
))
)}
</SelectContent>
</Select>
)}
{/* Search only applies to the global list — folder scope
already narrows the set via the picker above. */}
{skillsScope === "global" && (
<Input
value={searchQuery}
onChange={(event) => {
setSearchQuery(event.target.value)
}}
placeholder={t("searchPlaceholder")}
/>
)}
</div>
<div className="border-b px-3 py-2 text-xs font-medium text-muted-foreground flex items-center justify-between gap-2">
@@ -757,7 +948,9 @@ export function SkillsSettings() {
skillsSupported &&
filteredSkills.length === 0 && (
<div className="text-xs text-muted-foreground px-1">
{t("emptySkills")}
{skillsScope === "folder" && !selectedFolderPath
? t("scope.pickFolderHint")
: t("emptySkills")}
</div>
)}
@@ -884,7 +1077,10 @@ export function SkillsSettings() {
size="sm"
className="flex-1"
onClick={handleCreateDraft}
disabled={!selectedAgent}
disabled={
!selectedAgent ||
(skillsScope === "folder" && !selectedFolderPath)
}
>
<Plus className="h-3.5 w-3.5" />
{t("actions.newSkill")}
@@ -898,194 +1094,204 @@ export function SkillsSettings() {
<ResizablePanel defaultSize={64} 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">
{selectedAgent ? (
<div className="h-full flex flex-col">
<div className="border-b px-4 py-3 flex items-center justify-between gap-3">
<div className="min-w-0">
<h3 className="text-sm font-semibold truncate">
{skillDraftId.trim() || t("newSkillTitle")}
</h3>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<Button
size="xs"
variant="outline"
onClick={handleResetDraft}
disabled={skillSaving || skillReading}
>
<RotateCcw className="h-3 w-3" />
{t("actions.reset")}
</Button>
<Button
size="xs"
onClick={() => {
handleSaveSkill().catch((err) => {
console.error(
"[SkillsSettings] save skill failed:",
err
)
})
}}
disabled={skillSaving || skillReading}
>
{skillSaving ? (
<>
<Loader2 className="h-3 w-3 animate-spin" />
{t("actions.saving")}
</>
) : (
<>
<Save className="h-3 w-3" />
{t("actions.save")}
</>
)}
</Button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<div className="rounded-md border p-3 space-y-2.5">
<div className="text-[11px] text-muted-foreground flex items-center gap-1">
<BookOpenText className="h-3.5 w-3.5" />
{t("skillInfo")}
selectedSkillId || isDrafting ? (
<div className="h-full flex flex-col">
<div className="border-b px-4 py-3 flex items-center justify-between gap-3">
<div className="min-w-0">
<h3 className="text-sm font-semibold truncate">
{skillDraftId.trim() || t("newSkillTitle")}
</h3>
</div>
<Input
value={skillDraftId}
onChange={(event) => {
setSkillDraftId(event.target.value)
}}
placeholder={t("skillIdPlaceholder")}
/>
{draftPathPreview ? (
<div className="text-[11px] text-muted-foreground break-all">
{t("skillsDirectoryWithPath", {
path: draftPathPreview,
})}
</div>
) : (
<div className="text-[11px] text-muted-foreground break-all">
{t("skillsDirectoryNeedId")}
</div>
)}
</div>
<div className="rounded-md border p-3 space-y-2">
<div className="text-[11px] text-muted-foreground flex items-center justify-between gap-2">
<span>{t("markdownContent")}</span>
<div className="flex items-center gap-1.5">
<span>
{isContentEditing
? t("editingStatus")
: t("previewStatus")}
</span>
<Button
size="xs"
variant={
isContentEditing ? "secondary" : "outline"
}
onClick={() => {
setIsContentEditing((prev) => !prev)
}}
disabled={skillReading}
>
{isContentEditing ? (
<>
<Eye className="h-3 w-3" />
{t("actions.preview")}
</>
) : (
<>
<Pencil className="h-3 w-3" />
{t("actions.edit")}
</>
)}
</Button>
</div>
</div>
{isContentEditing ? (
<Textarea
value={skillDraftContent}
onChange={(event) => {
setSkillDraftContent(event.target.value)
<div className="flex items-center gap-1.5 shrink-0">
<Button
size="xs"
variant="outline"
onClick={handleResetDraft}
disabled={skillSaving || skillReading}
>
<RotateCcw className="h-3 w-3" />
{t("actions.reset")}
</Button>
<Button
size="xs"
onClick={() => {
handleSaveSkill().catch((err) => {
console.error(
"[SkillsSettings] save skill failed:",
err
)
})
}}
placeholder={t("contentPlaceholder")}
className="min-h-80 font-mono text-xs"
/>
) : (
<div className="space-y-2">
{parsedPreviewContent.frontMatterRaw && (
<div className="rounded-md border bg-muted/10 p-3">
<div className="text-[11px] text-muted-foreground mb-2">
{t("metadataTitle")}
</div>
{parsedPreviewContent.fields.length > 0 ? (
<div className="grid gap-1.5">
{parsedPreviewContent.fields.map(
(field) => (
<div
key={field.key}
className="text-xs grid grid-cols-[100px_1fr] gap-2 items-start"
>
<span className="text-muted-foreground font-mono truncate">
{field.key}
</span>
<span className="font-mono break-all">
{field.value}
</span>
</div>
)
)}
</div>
) : (
<pre className="text-xs font-mono whitespace-pre-wrap break-words text-muted-foreground">
{parsedPreviewContent.frontMatterRaw}
</pre>
)}
</div>
disabled={skillSaving || skillReading}
>
{skillSaving ? (
<>
<Loader2 className="h-3 w-3 animate-spin" />
{t("actions.saving")}
</>
) : (
<>
<Save className="h-3 w-3" />
{t("actions.save")}
</>
)}
</Button>
</div>
</div>
<div className="min-h-80 rounded-md border bg-muted/10 p-3 overflow-auto">
{parsedPreviewContent.body.trim() ? (
<div
className={cn(
"text-sm leading-6",
"[&_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]}>
{parsedPreviewContent.body}
</ReactMarkdown>
</div>
) : parsedPreviewContent.frontMatterRaw ? (
<div className="text-xs text-muted-foreground py-3">
{t("onlyYamlMetadata")}
</div>
) : (
<div className="text-xs text-muted-foreground py-3">
{t("emptyContentHint")}
</div>
)}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<div className="rounded-md border p-3 space-y-2.5">
<div className="text-[11px] text-muted-foreground flex items-center gap-1">
<BookOpenText className="h-3.5 w-3.5" />
{t("skillInfo")}
</div>
<Input
value={skillDraftId}
onChange={(event) => {
setSkillDraftId(event.target.value)
}}
placeholder={t("skillIdPlaceholder")}
/>
{skillsScope === "folder" && !selectedFolderPath ? (
<div className="text-[11px] text-muted-foreground break-all">
{t("scope.pickFolderHint")}
</div>
) : draftPathPreview ? (
<div className="text-[11px] text-muted-foreground break-all">
{t("skillsDirectoryWithPath", {
path: draftPathPreview,
})}
</div>
) : (
<div className="text-[11px] text-muted-foreground break-all">
{t("skillsDirectoryNeedId")}
</div>
)}
</div>
<div className="rounded-md border p-3 space-y-2">
<div className="text-[11px] text-muted-foreground flex items-center justify-between gap-2">
<span>{t("markdownContent")}</span>
<div className="flex items-center gap-1.5">
<span>
{isContentEditing
? t("editingStatus")
: t("previewStatus")}
</span>
<Button
size="xs"
variant={
isContentEditing ? "secondary" : "outline"
}
onClick={() => {
setIsContentEditing((prev) => !prev)
}}
disabled={skillReading}
>
{isContentEditing ? (
<>
<Eye className="h-3 w-3" />
{t("actions.preview")}
</>
) : (
<>
<Pencil className="h-3 w-3" />
{t("actions.edit")}
</>
)}
</Button>
</div>
</div>
)}
{skillReading && (
<div className="text-[11px] text-muted-foreground">
{t("loadingSkill")}
</div>
)}
{isContentEditing ? (
<Textarea
value={skillDraftContent}
onChange={(event) => {
setSkillDraftContent(event.target.value)
}}
placeholder={t("contentPlaceholder")}
className="min-h-80 font-mono text-xs"
/>
) : (
<div className="space-y-2">
{parsedPreviewContent.frontMatterRaw && (
<div className="rounded-md border bg-muted/10 p-3">
<div className="text-[11px] text-muted-foreground mb-2">
{t("metadataTitle")}
</div>
{parsedPreviewContent.fields.length > 0 ? (
<div className="grid gap-1.5">
{parsedPreviewContent.fields.map(
(field) => (
<div
key={field.key}
className="text-xs grid grid-cols-[100px_1fr] gap-2 items-start"
>
<span className="text-muted-foreground font-mono truncate">
{field.key}
</span>
<span className="font-mono break-all">
{field.value}
</span>
</div>
)
)}
</div>
) : (
<pre className="text-xs font-mono whitespace-pre-wrap break-words text-muted-foreground">
{parsedPreviewContent.frontMatterRaw}
</pre>
)}
</div>
)}
<div className="min-h-80 rounded-md border bg-muted/10 p-3 overflow-auto">
{parsedPreviewContent.body.trim() ? (
<div
className={cn(
"text-sm leading-6",
"[&_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]}>
{parsedPreviewContent.body}
</ReactMarkdown>
</div>
) : parsedPreviewContent.frontMatterRaw ? (
<div className="text-xs text-muted-foreground py-3">
{t("onlyYamlMetadata")}
</div>
) : (
<div className="text-xs text-muted-foreground py-3">
{t("emptyContentHint")}
</div>
)}
</div>
</div>
)}
{skillReading && (
<div className="text-[11px] text-muted-foreground">
{t("loadingSkill")}
</div>
)}
</div>
</div>
</div>
</div>
) : (
<div className="h-full flex items-center justify-center px-6 text-center text-xs text-muted-foreground">
{t("noSelectionHint")}
</div>
)
) : (
<div className="h-full flex items-center justify-center text-xs text-muted-foreground">
{t("emptyNoAgents")}