Skip to content

Commit ba6ba68

Browse files
authored
Merge pull request Stack-Cairn#8 from yyg-max/main
feat(skills): support deleting user skills
2 parents cf10696 + 1da90b5 commit ba6ba68

8 files changed

Lines changed: 401 additions & 41 deletions

File tree

crates/agent-gateway/web/src/lib/skills/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ type SystemManageSkillResponse = {
134134
target: string;
135135
archive: string;
136136
} | null;
137+
deleted?: {
138+
name: string;
139+
target: string;
140+
} | null;
137141
seeded?: SystemBuiltinSkillSeedResponse[] | null;
138142
installJob?: SkillInstallJobSnapshot | null;
139143
clawhubResults?: ClawHubSkillCard[] | null;
@@ -645,6 +649,7 @@ export async function manageSkill(params: Record<string, unknown>): Promise<Syst
645649
if (
646650
action === "install" ||
647651
action === "create" ||
652+
action === "delete" ||
648653
action === "clawhub_install" ||
649654
(action === "install_status" && response.installJob?.phase === "done")
650655
) {
@@ -699,7 +704,7 @@ export function buildSkillsSystemPrompt(params: {
699704
"- Only when you determine that a metadata Skill is genuinely needed should you call SkillsManager with action=read to read the full Skill file and then follow its workflow exactly.",
700705
"- SkillsManager.path uses the skillFile below. It is relative to the fixed Skills root directory and may point to SKILL.md, skill.md, skill.json, or README.md.",
701706
'- For files referenced inside an enabled Skill, use file tools with root="skills" and a path relative to the fixed Skills root, for example Read(root="skills", path="<baseDir>/references/guide.md"), List, Glob, Grep, Write, Edit, or Delete.',
702-
"- You may update files inside enabled Skills when the user asks you to optimize or maintain them. Create, install, search/install from ClawHub, validate, or package Skills through SkillsManager actions.",
707+
"- You may update files inside enabled Skills when the user asks you to optimize or maintain them. Create, install, search/install from ClawHub, validate, package, or delete user Skills through SkillsManager actions.",
703708
"- Never expand the fixed Skills root into an absolute local path in any tool call. If a path belongs to a Skill, keep it root-relative and set root=\"skills\".",
704709
"- If Skill content contains absolute local paths, home-directory paths, drive-letter paths, or shell snippets that read Skill files with cat/ls/find/grep, treat those path fragments as non-portable examples and convert them to root=\"skills\" file-tool calls.",
705710
"- Do not guess a Skill's exact instructions or script paths before reading the Skill file.",

crates/agent-gateway/web/src/pages/skills-hub/SkillsHubPage.tsx

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@ import {
1414
RefreshCw,
1515
Search,
1616
Sparkles,
17+
Trash2,
1718
X,
1819
} from "../../components/icons";
1920
import { GlassPanel, HubBackdrop, HubHeader } from "../../components/hub/HubChrome";
2021
import { Button } from "../../components/ui/button";
22+
import { ConfirmDeletePopover } from "../../components/ui/confirm-action-popover";
2123
import { useLocale } from "../../i18n";
2224
import {
2325
discoverSkills,
2426
getSkillInstallJobStatus,
2527
isAlwaysEnabledSkillName,
2628
isUserSelectableSkill,
29+
manageSkill,
2730
mergeAlwaysEnabledSkillNames,
2831
notifySkillsDiscoveryUpdated,
2932
startSkillInstallJob,
@@ -138,6 +141,7 @@ export function SkillsHubPage(props: SkillsHubPageProps) {
138141
const [storeError, setStoreError] = useState<string | null>(null);
139142
const [installJobs, setInstallJobs] = useState<Record<string, SkillInstallJobSnapshot>>({});
140143
const [installingBySlug, setInstallingBySlug] = useState<Record<string, string>>({});
144+
const [deletingSkillName, setDeletingSkillName] = useState<string | null>(null);
141145
const discoverySignatureRef = useRef(
142146
buildSkillDiscoverySignature(initialRootDir ?? "", initialSkills ?? []),
143147
);
@@ -419,6 +423,50 @@ export function SkillsHubPage(props: SkillsHubPageProps) {
419423
}
420424
}
421425

426+
async function deleteSkill(skill: SkillSummary) {
427+
if (lockedByChatMode || isAlwaysEnabledSkillName(skill.name) || deletingSkillName) return;
428+
const skillName = skill.name;
429+
const sourceSlug =
430+
skill.source?.registry === "clawhub" ? skill.source.slug?.trim() || "" : "";
431+
setLoadError(null);
432+
setDeletingSkillName(skillName);
433+
try {
434+
await manageSkill({ action: "delete", name: skillName });
435+
setSettings((prev) =>
436+
updateSkills(prev, {
437+
selected: prev.skills.selected.filter((name) => name !== skillName),
438+
}),
439+
);
440+
setSkills((prev) => prev.filter((item) => item.name !== skillName));
441+
if (sourceSlug) {
442+
setInstallingBySlug((prev) => {
443+
if (!(sourceSlug in prev)) return prev;
444+
const next = { ...prev };
445+
delete next[sourceSlug];
446+
return next;
447+
});
448+
setInstallJobs((prev) => {
449+
let changed = false;
450+
const next = { ...prev };
451+
for (const [jobId, job] of Object.entries(prev)) {
452+
if (job.slug?.trim() === sourceSlug) {
453+
delete next[jobId];
454+
changed = true;
455+
}
456+
}
457+
return changed ? next : prev;
458+
});
459+
}
460+
notifySkillsDiscoveryUpdated();
461+
await refresh({ silent: true });
462+
} catch (err) {
463+
const msg = err instanceof Error ? err.message : String(err);
464+
setLoadError(msg || "Failed to delete Skill");
465+
} finally {
466+
setDeletingSkillName(null);
467+
}
468+
}
469+
422470
function toggleSkill(name: string, on: boolean) {
423471
if (isAlwaysEnabledSkillName(name)) return;
424472
const next = new Set(settings.skills.selected);
@@ -721,6 +769,8 @@ export function SkillsHubPage(props: SkillsHubPageProps) {
721769
{filtered.map((skill) => {
722770
const alwaysEnabled = isAlwaysEnabledSkillName(skill.name);
723771
const checked = alwaysEnabled || selected.has(skill.name);
772+
const deleting = deletingSkillName === skill.name;
773+
const deleteDisabled = deletingSkillName !== null;
724774
const card = (
725775
<>
726776
<div className="flex items-start justify-between gap-2">
@@ -745,14 +795,47 @@ export function SkillsHubPage(props: SkillsHubPageProps) {
745795
</div>
746796
) : (
747797
<div
748-
className={cn(
749-
"flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition-all",
750-
checked
751-
? "border-foreground/80 bg-foreground/85 text-background shadow-[0_2px_6px_-2px_rgba(15,23,42,0.35)]"
752-
: "border-border bg-background group-hover:border-foreground/40",
753-
)}
798+
className="flex shrink-0 items-center gap-1.5"
799+
onClick={(event) => event.stopPropagation()}
800+
onKeyDown={(event) => event.stopPropagation()}
754801
>
755-
{checked ? <Check className="h-3 w-3" /> : null}
802+
<ConfirmDeletePopover
803+
name={skill.name}
804+
onConfirm={() => void deleteSkill(skill)}
805+
>
806+
{(open) => (
807+
<button
808+
type="button"
809+
disabled={deleteDisabled}
810+
onClick={(event) => {
811+
event.stopPropagation();
812+
open();
813+
}}
814+
className={cn(
815+
"flex h-6 w-6 items-center justify-center rounded-md border border-border/35 bg-background/65 text-muted-foreground transition-all",
816+
"hover:border-destructive/40 hover:bg-destructive/10 hover:text-destructive",
817+
"disabled:cursor-not-allowed disabled:opacity-60",
818+
)}
819+
title="删除 Skill"
820+
>
821+
{deleting ? (
822+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
823+
) : (
824+
<Trash2 className="h-3.5 w-3.5" />
825+
)}
826+
</button>
827+
)}
828+
</ConfirmDeletePopover>
829+
<div
830+
className={cn(
831+
"flex h-5 w-5 items-center justify-center rounded-md border transition-all",
832+
checked
833+
? "border-foreground/80 bg-foreground/85 text-background shadow-[0_2px_6px_-2px_rgba(15,23,42,0.35)]"
834+
: "border-border bg-background group-hover:border-foreground/40",
835+
)}
836+
>
837+
{checked ? <Check className="h-3 w-3" /> : null}
838+
</div>
756839
</div>
757840
)}
758841
</div>
@@ -788,20 +871,26 @@ export function SkillsHubPage(props: SkillsHubPageProps) {
788871
}
789872

790873
return (
791-
<button
874+
<div
792875
key={key}
793-
type="button"
876+
role="button"
877+
tabIndex={0}
794878
onClick={() => toggleSkill(skill.name, !checked)}
879+
onKeyDown={(event) => {
880+
if (event.key !== "Enter" && event.key !== " ") return;
881+
event.preventDefault();
882+
toggleSkill(skill.name, !checked);
883+
}}
795884
className={cn(
796885
"hub-skill-card skill-card-enter group flex h-full w-full flex-col rounded-2xl border p-3.5 text-left transition-all",
797-
"backdrop-blur-xl",
886+
"cursor-pointer backdrop-blur-xl focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/15",
798887
checked
799888
? "border-border/55 bg-background/80 shadow-[0_1px_0_rgba(255,255,255,0.6)_inset,0_4px_18px_-12px_rgba(15,23,42,0.18)]"
800889
: "border-border/35 bg-background/55 hover:-translate-y-0.5 hover:border-border/55 hover:bg-background/70 hover:shadow-[0_4px_16px_-10px_rgba(15,23,42,0.18)]",
801890
)}
802891
>
803892
{card}
804-
</button>
893+
</div>
805894
);
806895
})}
807896
</div>

0 commit comments

Comments
 (0)