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:
Dotta
2026-03-16 18:27:20 -05:00
parent bb46423969
commit 5890b318c4
39 changed files with 9902 additions and 309 deletions

View File

@@ -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}

View File

@@ -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;
});
}

View File

@@ -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>