fix: 支持读取 ~/.claude/skills/ 目录下的已安装技能 (#36)
fix: 支持读取 ~/.claude/skills/ 目录下的已安装技能
This commit is contained in:
6 files changed
+413
-54
No files matched your search
@@ -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 || '';
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user