fix: 支持读取 ~/.claude/skills/ 目录下的已安装技能 (#36)

fix: 支持读取 ~/.claude/skills/ 目录下的已安装技能
This commit is contained in:
歸藏 authored and GitHub committed 2026-02-09 16:07:58 +08:00
commit 336897cded
6 files changed
+413 -54

No files matched your search

+22 -7
View File
@@ -63,6 +63,7 @@ interface PopoverItem {
description?: string;
builtIn?: boolean;
immediate?: boolean;
installedSource?: "agents" | "claude";
}
interface CommandBadge {
@@ -70,6 +71,7 @@ interface CommandBadge {
label: string;
description: string;
isSkill: boolean;
installedSource?: "agents" | "claude";
}
type PopoverMode = 'file' | 'skill' | null;
@@ -369,12 +371,19 @@ export function MessageInput({
const skills = data.skills || [];
apiSkills = skills
.filter((s: { name: string }) => s.name.toLowerCase().includes(filter.toLowerCase()))
.map((s: { name: string; description: string }) => ({
label: s.name,
value: `/${s.name}`,
description: s.description,
builtIn: false,
}));
.map((s: { name: string; description: string; source?: string; installedSource?: "agents" | "claude" }) => {
const sourceHint =
s.source === "installed" && s.installedSource
? ` (${s.installedSource})`
: "";
return {
label: s.name,
value: `/${s.name}`,
description: `${s.description || ""}${sourceHint}`,
builtIn: false,
installedSource: s.installedSource,
};
});
}
} catch {
// API not available - just use built-in commands
@@ -417,6 +426,7 @@ export function MessageInput({
label: item.label,
description: item.description || '',
isSkill: !item.builtIn,
installedSource: item.installedSource,
});
setInputValue('');
closePopover();
@@ -518,7 +528,12 @@ export function MessageInput({
if (badge.isSkill) {
// Fetch skill content from API
try {
const res = await fetch(`/api/skills/${encodeURIComponent(badge.label)}`);
const sourceParam = badge.installedSource
? `?source=${badge.installedSource}`
: "";
const res = await fetch(
`/api/skills/${encodeURIComponent(badge.label)}${sourceParam}`
);
if (res.ok) {
const data = await res.json();
expandedPrompt = data.skill?.content || '';
+13 -5
View File
@@ -29,7 +29,7 @@ type ViewMode = "edit" | "preview" | "split";
interface SkillEditorProps {
skill: SkillItem;
onSave: (name: string, content: string) => Promise<void>;
onSave: (skill: SkillItem, content: string) => Promise<void>;
onDelete: (skill: SkillItem) => void;
}
@@ -53,13 +53,13 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
const handleSave = useCallback(async () => {
setSaving(true);
try {
await onSave(skill.name, content);
await onSave(skill, content);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} finally {
setSaving(false);
}
}, [skill.name, content, onSave]);
}, [skill, content, onSave]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -121,15 +121,23 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
"text-[10px] px-1.5 py-0 shrink-0",
skill.source === "global"
? "border-green-500/40 text-green-600 dark:text-green-400"
: "border-blue-500/40 text-blue-600 dark:text-blue-400"
: skill.source === "installed"
? "border-orange-500/40 text-orange-600 dark:text-orange-400"
: skill.source === "plugin"
? "border-purple-500/40 text-purple-600 dark:text-purple-400"
: "border-blue-500/40 text-blue-600 dark:text-blue-400"
)}
>
{skill.source === "global" ? (
<HugeiconsIcon icon={GlobeIcon} className="h-2.5 w-2.5 mr-0.5" />
) : skill.source === "installed" ? (
<HugeiconsIcon icon={FolderOpenIcon} className="h-2.5 w-2.5 mr-0.5" />
) : (
<HugeiconsIcon icon={FolderOpenIcon} className="h-2.5 w-2.5 mr-0.5" />
)}
{skill.source}
{skill.source === "installed" && skill.installedSource
? `installed:${skill.installedSource}`
: skill.source}
</Badge>
</div>
+4 -1
View File
@@ -17,6 +17,7 @@ export interface SkillItem {
description: string;
content: string;
source: "global" | "project" | "plugin" | "installed";
installedSource?: "agents" | "claude";
filePath: string;
}
@@ -89,7 +90,9 @@ export function SkillListItem({
) : (
<HugeiconsIcon icon={FolderOpenIcon} className="h-2.5 w-2.5 mr-0.5" />
)}
{skill.source}
{skill.source === "installed" && skill.installedSource
? `installed:${skill.installedSource}`
: skill.source}
</Badge>
</div>
<p className="text-xs text-muted-foreground truncate">
+43 -19
View File
@@ -54,9 +54,17 @@ export function SkillsManager() {
[]
);
const buildSkillUrl = useCallback((skill: SkillItem) => {
const base = `/api/skills/${encodeURIComponent(skill.name)}`;
if (skill.source === "installed" && skill.installedSource) {
return `${base}?source=${skill.installedSource}`;
}
return base;
}, []);
const handleSave = useCallback(
async (name: string, content: string) => {
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, {
async (skill: SkillItem, content: string) => {
const res = await fetch(buildSkillUrl(skill), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
@@ -68,32 +76,44 @@ export function SkillsManager() {
const data = await res.json();
// Update in list
setSkills((prev) =>
prev.map((s) => (s.name === name && s.source === data.skill.source ? data.skill : s))
prev.map((s) =>
s.name === skill.name &&
s.source === data.skill.source &&
s.installedSource === data.skill.installedSource
? data.skill
: s
)
);
// Update selected
setSelected(data.skill);
},
[]
[buildSkillUrl]
);
const handleDelete = useCallback(
async (skill: SkillItem) => {
const res = await fetch(
`/api/skills/${encodeURIComponent(skill.name)}`,
{ method: "DELETE" }
);
const res = await fetch(buildSkillUrl(skill), { method: "DELETE" });
if (res.ok) {
setSkills((prev) =>
prev.filter(
(s) => !(s.name === skill.name && s.source === skill.source)
(s) =>
!(
s.name === skill.name &&
s.source === skill.source &&
s.installedSource === skill.installedSource
)
)
);
if (selected?.name === skill.name && selected?.source === skill.source) {
if (
selected?.name === skill.name &&
selected?.source === skill.source &&
selected?.installedSource === skill.installedSource
) {
setSelected(null);
}
}
},
[selected]
[buildSkillUrl, selected]
);
const filtered = skills.filter(
@@ -153,11 +173,12 @@ export function SkillsManager() {
</span>
{projectSkills.map((skill) => (
<SkillListItem
key={`${skill.source}:${skill.name}`}
key={`${skill.source}:${skill.installedSource ?? "default"}:${skill.name}`}
skill={skill}
selected={
selected?.name === skill.name &&
selected?.source === skill.source
selected?.source === skill.source &&
selected?.installedSource === skill.installedSource
}
onSelect={() => setSelected(skill)}
onDelete={handleDelete}
@@ -172,11 +193,12 @@ export function SkillsManager() {
</span>
{globalSkills.map((skill) => (
<SkillListItem
key={`${skill.source}:${skill.name}`}
key={`${skill.source}:${skill.installedSource ?? "default"}:${skill.name}`}
skill={skill}
selected={
selected?.name === skill.name &&
selected?.source === skill.source
selected?.source === skill.source &&
selected?.installedSource === skill.installedSource
}
onSelect={() => setSelected(skill)}
onDelete={handleDelete}
@@ -191,11 +213,12 @@ export function SkillsManager() {
</span>
{installedSkills.map((skill) => (
<SkillListItem
key={`${skill.source}:${skill.name}`}
key={`${skill.source}:${skill.installedSource ?? "default"}:${skill.name}`}
skill={skill}
selected={
selected?.name === skill.name &&
selected?.source === skill.source
selected?.source === skill.source &&
selected?.installedSource === skill.installedSource
}
onSelect={() => setSelected(skill)}
onDelete={handleDelete}
@@ -210,11 +233,12 @@ export function SkillsManager() {
</span>
{pluginSkills.map((skill) => (
<SkillListItem
key={skill.filePath || `${skill.source}:${skill.name}`}
key={skill.filePath || `${skill.source}:${skill.installedSource ?? "default"}:${skill.name}`}
skill={skill}
selected={
selected?.name === skill.name &&
selected?.source === skill.source
selected?.source === skill.source &&
selected?.installedSource === skill.installedSource
}
onSelect={() => setSelected(skill)}
onDelete={handleDelete}