Namespace company skill identities
Persist canonical namespaced skill keys, split adapter runtime names from skill keys, and update portability/import flows to carry the canonical identity end-to-end. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1233,7 +1233,7 @@ function AgentSkillsTab({
|
||||
}) {
|
||||
type SkillRow = {
|
||||
id: string;
|
||||
slug: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
detail: string | null;
|
||||
@@ -1316,50 +1316,50 @@ function AgentSkillsTab({
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [skillDraft, skillSnapshot, syncSkills.isPending, syncSkills.mutate]);
|
||||
|
||||
const companySkillBySlug = useMemo(
|
||||
() => new Map((companySkills ?? []).map((skill) => [skill.slug, skill])),
|
||||
const companySkillByKey = useMemo(
|
||||
() => new Map((companySkills ?? []).map((skill) => [skill.key, skill])),
|
||||
[companySkills],
|
||||
);
|
||||
const adapterEntryByName = useMemo(
|
||||
() => new Map((skillSnapshot?.entries ?? []).map((entry) => [entry.name, entry])),
|
||||
const adapterEntryByKey = useMemo(
|
||||
() => new Map((skillSnapshot?.entries ?? []).map((entry) => [entry.key, entry])),
|
||||
[skillSnapshot],
|
||||
);
|
||||
const optionalSkillRows = useMemo<SkillRow[]>(
|
||||
() =>
|
||||
(companySkills ?? [])
|
||||
.filter((skill) => !adapterEntryByName.get(skill.slug)?.required)
|
||||
.filter((skill) => !adapterEntryByKey.get(skill.key)?.required)
|
||||
.map((skill) => ({
|
||||
id: skill.id,
|
||||
slug: skill.slug,
|
||||
key: skill.key,
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
detail: adapterEntryByName.get(skill.slug)?.detail ?? null,
|
||||
detail: adapterEntryByKey.get(skill.key)?.detail ?? null,
|
||||
linkTo: `/skills/${skill.id}`,
|
||||
adapterEntry: adapterEntryByName.get(skill.slug) ?? null,
|
||||
adapterEntry: adapterEntryByKey.get(skill.key) ?? null,
|
||||
})),
|
||||
[adapterEntryByName, companySkills],
|
||||
[adapterEntryByKey, companySkills],
|
||||
);
|
||||
const requiredSkillRows = useMemo<SkillRow[]>(
|
||||
() =>
|
||||
(skillSnapshot?.entries ?? [])
|
||||
.filter((entry) => entry.required)
|
||||
.map((entry) => {
|
||||
const companySkill = companySkillBySlug.get(entry.name);
|
||||
const companySkill = companySkillByKey.get(entry.key);
|
||||
return {
|
||||
id: companySkill?.id ?? `required:${entry.name}`,
|
||||
slug: entry.name,
|
||||
name: companySkill?.name ?? entry.name,
|
||||
id: companySkill?.id ?? `required:${entry.key}`,
|
||||
key: entry.key,
|
||||
name: companySkill?.name ?? entry.key,
|
||||
description: companySkill?.description ?? null,
|
||||
detail: entry.detail ?? null,
|
||||
linkTo: companySkill ? `/skills/${companySkill.id}` : null,
|
||||
adapterEntry: entry,
|
||||
};
|
||||
}),
|
||||
[companySkillBySlug, skillSnapshot],
|
||||
[companySkillByKey, skillSnapshot],
|
||||
);
|
||||
const desiredOnlyMissingSkills = useMemo(
|
||||
() => skillDraft.filter((slug) => !companySkillBySlug.has(slug)),
|
||||
[companySkillBySlug, skillDraft],
|
||||
() => skillDraft.filter((key) => !companySkillByKey.has(key)),
|
||||
[companySkillByKey, skillDraft],
|
||||
);
|
||||
const skillApplicationLabel = useMemo(() => {
|
||||
switch (skillSnapshot?.mode) {
|
||||
@@ -1424,9 +1424,9 @@ function AgentSkillsTab({
|
||||
<>
|
||||
{(() => {
|
||||
const renderSkillRow = (skill: SkillRow) => {
|
||||
const adapterEntry = skill.adapterEntry ?? adapterEntryByName.get(skill.slug);
|
||||
const adapterEntry = skill.adapterEntry ?? adapterEntryByKey.get(skill.key);
|
||||
const required = Boolean(adapterEntry?.required);
|
||||
const checked = required || skillDraft.includes(skill.slug);
|
||||
const checked = required || skillDraft.includes(skill.key);
|
||||
const disabled = required || skillSnapshot?.mode === "unsupported";
|
||||
const checkbox = (
|
||||
<input
|
||||
@@ -1435,8 +1435,8 @@ function AgentSkillsTab({
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
const next = event.target.checked
|
||||
? Array.from(new Set([...skillDraft, skill.slug]))
|
||||
: skillDraft.filter((value) => value !== skill.slug);
|
||||
? Array.from(new Set([...skillDraft, skill.key]))
|
||||
: skillDraft.filter((value) => value !== skill.key);
|
||||
setSkillDraft(next);
|
||||
}}
|
||||
className="mt-0.5 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
@@ -1468,7 +1468,10 @@ function AgentSkillsTab({
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="truncate font-medium">{skill.name}</span>
|
||||
<div className="min-w-0">
|
||||
<span className="truncate font-medium">{skill.name}</span>
|
||||
<div className="truncate font-mono text-[11px] text-muted-foreground">{skill.key}</div>
|
||||
</div>
|
||||
{skill.linkTo ? (
|
||||
<Link
|
||||
to={skill.linkTo}
|
||||
|
||||
@@ -540,17 +540,23 @@ export function CompanyExport() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSkillClick(skillSlug: string) {
|
||||
function handleSkillClick(skillKey: string) {
|
||||
if (!exportData) return;
|
||||
// Find the SKILL.md file for this skill slug
|
||||
const skillPath = `skills/${skillSlug}/SKILL.md`;
|
||||
const manifestSkill = exportData.manifest.skills.find(
|
||||
(skill) => skill.key === skillKey || skill.slug === skillKey,
|
||||
);
|
||||
const skillPath = manifestSkill?.path ?? `skills/${skillKey}/SKILL.md`;
|
||||
if (!(skillPath in exportData.files)) return;
|
||||
// Select the file and expand parent dirs
|
||||
setSelectedFile(skillPath);
|
||||
setExpandedDirs((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add("skills");
|
||||
next.add(`skills/${skillSlug}`);
|
||||
const parts = skillPath.split("/").slice(0, -1);
|
||||
let current = "";
|
||||
for (const part of parts) {
|
||||
current = current ? `${current}/${part}` : part;
|
||||
next.add(current);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ function SkillList({
|
||||
onSelectPath: (skillId: string, path: string) => void;
|
||||
}) {
|
||||
const filteredSkills = skills.filter((skill) => {
|
||||
const haystack = `${skill.name} ${skill.slug} ${skill.sourceLabel ?? ""}`.toLowerCase();
|
||||
const haystack = `${skill.name} ${skill.key} ${skill.slug} ${skill.sourceLabel ?? ""}`.toLowerCase();
|
||||
return haystack.includes(skillFilter.toLowerCase());
|
||||
});
|
||||
|
||||
@@ -435,6 +435,9 @@ function SkillList({
|
||||
<span className="block min-w-0 overflow-hidden text-[13px] font-medium leading-5 [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:3]">
|
||||
{skill.name}
|
||||
</span>
|
||||
<span className="truncate font-mono text-[11px] text-muted-foreground">
|
||||
{skill.key}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
@@ -596,6 +599,10 @@ function SkillPane({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">Key</span>
|
||||
<span className="font-mono text-xs">{detail.key}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">Mode</span>
|
||||
<span>{detail.editable ? "Editable" : "Read only"}</span>
|
||||
|
||||
Reference in New Issue
Block a user