Separate required skills into own section on agent skills page

Required/built-in Paperclip skills are now shown in a dedicated
"Required by Paperclip" section at the bottom of the agent skills tab,
with checkboxes that are checked and disabled. Optional skills remain
in the main section above.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-03-16 16:01:20 -05:00
parent 0b76b1aced
commit 10d06bc1ca

View File

@@ -1275,79 +1275,112 @@ function AgentSkillsTab({
<PageSkeleton variant="list" /> <PageSkeleton variant="list" />
) : ( ) : (
<> <>
<section className="border-y border-border"> {(() => {
{(companySkills ?? []).length === 0 ? ( const allSkills = companySkills ?? [];
<div className="px-3 py-6 text-sm text-muted-foreground"> const optionalSkills = allSkills.filter(
Import skills into the company library first, then attach them here. (skill) => !adapterEntryByName.get(skill.slug)?.required,
</div> );
) : ( const requiredSkills = allSkills.filter(
(companySkills ?? []).map((skill) => { (skill) => adapterEntryByName.get(skill.slug)?.required,
const checked = skillDraft.includes(skill.slug); );
const adapterEntry = adapterEntryByName.get(skill.slug);
const required = Boolean(adapterEntry?.required); const renderSkillRow = (skill: (typeof allSkills)[number]) => {
const disabled = required || skillSnapshot?.mode === "unsupported"; const checked = skillDraft.includes(skill.slug);
const checkbox = ( const adapterEntry = adapterEntryByName.get(skill.slug);
<input const required = Boolean(adapterEntry?.required);
type="checkbox" const disabled = required || skillSnapshot?.mode === "unsupported";
checked={checked} const checkbox = (
disabled={disabled} <input
onChange={(event) => { type="checkbox"
const next = event.target.checked checked={checked}
? Array.from(new Set([...skillDraft, skill.slug])) disabled={disabled}
: skillDraft.filter((value) => value !== skill.slug); onChange={(event) => {
setSkillDraft(next); const next = event.target.checked
}} ? Array.from(new Set([...skillDraft, skill.slug]))
className="mt-0.5 disabled:cursor-not-allowed disabled:opacity-60" : skillDraft.filter((value) => value !== skill.slug);
/> setSkillDraft(next);
); }}
return ( className="mt-0.5 disabled:cursor-not-allowed disabled:opacity-60"
<label />
key={skill.id} );
className="flex items-start gap-3 border-b border-border px-3 py-3 text-sm last:border-b-0 hover:bg-accent/20" return (
> <label
{required && adapterEntry?.requiredReason ? ( key={skill.id}
<Tooltip> className="flex items-start gap-3 border-b border-border px-3 py-3 text-sm last:border-b-0 hover:bg-accent/20"
<TooltipTrigger asChild> >
<span>{checkbox}</span> {required && adapterEntry?.requiredReason ? (
</TooltipTrigger> <Tooltip>
<TooltipContent side="top">{adapterEntry.requiredReason}</TooltipContent> <TooltipTrigger asChild>
</Tooltip> <span>{checkbox}</span>
) : skillSnapshot?.mode === "unsupported" ? ( </TooltipTrigger>
<Tooltip> <TooltipContent side="top">{adapterEntry.requiredReason}</TooltipContent>
<TooltipTrigger asChild> </Tooltip>
<span>{checkbox}</span> ) : skillSnapshot?.mode === "unsupported" ? (
</TooltipTrigger> <Tooltip>
<TooltipContent side="top"> <TooltipTrigger asChild>
{unsupportedSkillMessage ?? "Manage skills in the adapter directly."} <span>{checkbox}</span>
</TooltipContent> </TooltipTrigger>
</Tooltip> <TooltipContent side="top">
) : ( {unsupportedSkillMessage ?? "Manage skills in the adapter directly."}
checkbox </TooltipContent>
)} </Tooltip>
<div className="min-w-0 flex-1"> ) : (
<div className="flex items-center justify-between gap-3"> checkbox
<span className="truncate font-medium">{skill.name}</span> )}
<Link <div className="min-w-0 flex-1">
to={`/skills/${skill.id}`} <div className="flex items-center justify-between gap-3">
className="shrink-0 text-xs text-muted-foreground no-underline hover:text-foreground" <span className="truncate font-medium">{skill.name}</span>
> <Link
View to={`/skills/${skill.id}`}
</Link> className="shrink-0 text-xs text-muted-foreground no-underline hover:text-foreground"
</div> >
{skill.description && ( View
<MarkdownBody className="mt-1 text-xs text-muted-foreground prose-p:my-1 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"> </Link>
{skill.description}
</MarkdownBody>
)}
{adapterEntry?.detail && (
<p className="mt-1 text-xs text-muted-foreground">{adapterEntry.detail}</p>
)}
</div> </div>
</label> {skill.description && (
); <MarkdownBody className="mt-1 text-xs text-muted-foreground prose-p:my-1 prose-ul:my-1 prose-ol:my-1 prose-li:my-0 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
}) {skill.description}
)} </MarkdownBody>
</section> )}
{adapterEntry?.detail && (
<p className="mt-1 text-xs text-muted-foreground">{adapterEntry.detail}</p>
)}
</div>
</label>
);
};
if (allSkills.length === 0) {
return (
<section className="border-y border-border">
<div className="px-3 py-6 text-sm text-muted-foreground">
Import skills into the company library first, then attach them here.
</div>
</section>
);
}
return (
<>
{optionalSkills.length > 0 && (
<section className="border-y border-border">
{optionalSkills.map(renderSkillRow)}
</section>
)}
{requiredSkills.length > 0 && (
<section className="border-y border-border">
<div className="border-b border-border bg-muted/40 px-3 py-2">
<span className="text-xs font-medium text-muted-foreground">
Required by Paperclip
</span>
</div>
{requiredSkills.map(renderSkillRow)}
</section>
)}
</>
);
})()}
{desiredOnlyMissingSkills.length > 0 && ( {desiredOnlyMissingSkills.length > 0 && (
<div className="rounded-xl border border-amber-300/60 bg-amber-50/60 px-4 py-3 text-sm text-amber-800 dark:border-amber-500/30 dark:bg-amber-950/20 dark:text-amber-200"> <div className="rounded-xl border border-amber-300/60 bg-amber-50/60 px-4 py-3 text-sm text-amber-800 dark:border-amber-500/30 dark:bg-amber-950/20 dark:text-amber-200">