Add click-to-copy workspace path on Paperclip workspace source label

When a skill's source is "Paperclip workspace", clicking the label now
copies the absolute path to the managed skills workspace to the clipboard
and shows a toast confirmation.

- Add sourcePath field to CompanySkillDetail and CompanySkillListItem types
- Return managedRoot path as sourcePath from deriveSkillSourceInfo for
  Paperclip workspace skills
- Make source label a clickable button in SkillPane detail view

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-18 15:24:22 -05:00
parent 6000bb4ee2
commit cd01ebb417
3 changed files with 25 additions and 1 deletions

View file

@ -50,6 +50,7 @@ export interface CompanySkillListItem {
editableReason: string | null; editableReason: string | null;
sourceLabel: string | null; sourceLabel: string | null;
sourceBadge: CompanySkillSourceBadge; sourceBadge: CompanySkillSourceBadge;
sourcePath: string | null;
} }
export interface CompanySkillUsageAgent { export interface CompanySkillUsageAgent {
@ -68,6 +69,7 @@ export interface CompanySkillDetail extends CompanySkill {
editableReason: string | null; editableReason: string | null;
sourceLabel: string | null; sourceLabel: string | null;
sourceBadge: CompanySkillSourceBadge; sourceBadge: CompanySkillSourceBadge;
sourcePath: string | null;
} }
export interface CompanySkillUpdateStatus { export interface CompanySkillUpdateStatus {

View file

@ -1233,6 +1233,7 @@ function deriveSkillSourceInfo(skill: CompanySkill): {
editableReason: string | null; editableReason: string | null;
sourceLabel: string | null; sourceLabel: string | null;
sourceBadge: CompanySkillSourceBadge; sourceBadge: CompanySkillSourceBadge;
sourcePath: string | null;
} { } {
const metadata = getSkillMeta(skill); const metadata = getSkillMeta(skill);
const localSkillDir = normalizeSkillDirectory(skill); const localSkillDir = normalizeSkillDirectory(skill);
@ -1242,6 +1243,7 @@ function deriveSkillSourceInfo(skill: CompanySkill): {
editableReason: "Bundled Paperclip skills are read-only.", editableReason: "Bundled Paperclip skills are read-only.",
sourceLabel: "Paperclip bundled", sourceLabel: "Paperclip bundled",
sourceBadge: "paperclip", sourceBadge: "paperclip",
sourcePath: null,
}; };
} }
@ -1253,6 +1255,7 @@ function deriveSkillSourceInfo(skill: CompanySkill): {
editableReason: "Remote GitHub skills are read-only. Fork or import locally to edit them.", editableReason: "Remote GitHub skills are read-only. Fork or import locally to edit them.",
sourceLabel: owner && repo ? `${owner}/${repo}` : skill.sourceLocator, sourceLabel: owner && repo ? `${owner}/${repo}` : skill.sourceLocator,
sourceBadge: "github", sourceBadge: "github",
sourcePath: null,
}; };
} }
@ -1262,6 +1265,7 @@ function deriveSkillSourceInfo(skill: CompanySkill): {
editableReason: "URL-based skills are read-only. Save them locally to edit them.", editableReason: "URL-based skills are read-only. Save them locally to edit them.",
sourceLabel: skill.sourceLocator, sourceLabel: skill.sourceLocator,
sourceBadge: "url", sourceBadge: "url",
sourcePath: null,
}; };
} }
@ -1276,6 +1280,7 @@ function deriveSkillSourceInfo(skill: CompanySkill): {
editableReason: null, editableReason: null,
sourceLabel: "Paperclip workspace", sourceLabel: "Paperclip workspace",
sourceBadge: "paperclip", sourceBadge: "paperclip",
sourcePath: managedRoot,
}; };
} }
@ -1287,6 +1292,7 @@ function deriveSkillSourceInfo(skill: CompanySkill): {
|| skill.sourceLocator || skill.sourceLocator
: skill.sourceLocator, : skill.sourceLocator,
sourceBadge: "local", sourceBadge: "local",
sourcePath: null,
}; };
} }
@ -1295,6 +1301,7 @@ function deriveSkillSourceInfo(skill: CompanySkill): {
editableReason: "This skill source is read-only.", editableReason: "This skill source is read-only.",
sourceLabel: skill.sourceLocator, sourceLabel: skill.sourceLocator,
sourceBadge: "catalog", sourceBadge: "catalog",
sourcePath: null,
}; };
} }
@ -1330,6 +1337,7 @@ function toCompanySkillListItem(skill: CompanySkill, attachedAgentCount: number)
editableReason: source.editableReason, editableReason: source.editableReason,
sourceLabel: source.sourceLabel, sourceLabel: source.sourceLabel,
sourceBadge: source.sourceBadge, sourceBadge: source.sourceBadge,
sourcePath: source.sourcePath,
}; };
} }

View file

@ -523,6 +523,8 @@ function SkillPane({
onSave: () => void; onSave: () => void;
savePending: boolean; savePending: boolean;
}) { }) {
const { pushToast } = useToast();
if (!detail) { if (!detail) {
if (loading) { if (loading) {
return <PageSkeleton variant="detail" />; return <PageSkeleton variant="detail" />;
@ -574,7 +576,19 @@ function SkillPane({
<span className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">Source</span> <span className="text-[11px] uppercase tracking-[0.18em] text-muted-foreground">Source</span>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<SourceIcon className="h-3.5 w-3.5 text-muted-foreground" /> <SourceIcon className="h-3.5 w-3.5 text-muted-foreground" />
<span className="truncate">{source.label}</span> {detail.sourcePath ? (
<button
className="truncate hover:text-foreground text-muted-foreground transition-colors cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(detail.sourcePath!);
pushToast({ title: "Copied path to workspace" });
}}
>
{source.label}
</button>
) : (
<span className="truncate">{source.label}</span>
)}
</span> </span>
</div> </div>
{detail.sourceType === "github" && ( {detail.sourceType === "github" && (