diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 7d251cbf..fe4cadff 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -72,6 +72,7 @@ import { RunTranscriptView, type TranscriptMode } from "../components/transcript import { isUuidLike, type Agent, + type AgentSkillEntry, type AgentSkillSnapshot, type BudgetPolicySummary, type HeartbeatRun, @@ -82,7 +83,11 @@ import { } from "@paperclipai/shared"; import { redactHomePathUserSegments, redactHomePathUserSegmentsInValue } from "@paperclipai/adapter-utils"; import { agentRouteRef } from "../lib/utils"; -import { applyAgentSkillSnapshot, arraysEqual } from "../lib/agent-skills-state"; +import { + applyAgentSkillSnapshot, + arraysEqual, + isReadOnlyUnmanagedSkillEntry, +} from "../lib/agent-skills-state"; const runStatusIcons: Record = { succeeded: { icon: CheckCircle2, color: "text-green-600 dark:text-green-400" }, @@ -1798,7 +1803,7 @@ function PromptsTab({ )} - + Advanced @@ -2159,8 +2164,11 @@ function AgentSkillsTab({ name: string; description: string | null; detail: string | null; + locationLabel: string | null; + originLabel: string | null; linkTo: string | null; - adapterEntry: AgentSkillSnapshot["entries"][number] | null; + readOnly: boolean; + adapterEntry: AgentSkillEntry | null; }; const queryClient = useQueryClient(); @@ -2242,6 +2250,10 @@ function AgentSkillsTab({ () => new Map((companySkills ?? []).map((skill) => [skill.key, skill])), [companySkills], ); + const companySkillKeys = useMemo( + () => new Set((companySkills ?? []).map((skill) => skill.key)), + [companySkills], + ); const adapterEntryByKey = useMemo( () => new Map((skillSnapshot?.entries ?? []).map((entry) => [entry.key, entry])), [skillSnapshot], @@ -2256,7 +2268,10 @@ function AgentSkillsTab({ name: skill.name, description: skill.description, detail: adapterEntryByKey.get(skill.key)?.detail ?? null, + locationLabel: adapterEntryByKey.get(skill.key)?.locationLabel ?? null, + originLabel: adapterEntryByKey.get(skill.key)?.originLabel ?? null, linkTo: `/skills/${skill.id}`, + readOnly: false, adapterEntry: adapterEntryByKey.get(skill.key) ?? null, })), [adapterEntryByKey, companySkills], @@ -2273,12 +2288,33 @@ function AgentSkillsTab({ name: companySkill?.name ?? entry.key, description: companySkill?.description ?? null, detail: entry.detail ?? null, + locationLabel: entry.locationLabel ?? null, + originLabel: entry.originLabel ?? null, linkTo: companySkill ? `/skills/${companySkill.id}` : null, + readOnly: false, adapterEntry: entry, }; }), [companySkillByKey, skillSnapshot], ); + const unmanagedSkillRows = useMemo( + () => + (skillSnapshot?.entries ?? []) + .filter((entry) => isReadOnlyUnmanagedSkillEntry(entry, companySkillKeys)) + .map((entry) => ({ + id: `external:${entry.key}`, + key: entry.key, + name: entry.runtimeName ?? entry.key, + description: null, + detail: entry.detail ?? null, + locationLabel: entry.locationLabel ?? null, + originLabel: entry.originLabel ?? null, + linkTo: null, + readOnly: true, + adapterEntry: entry, + })), + [companySkillKeys, skillSnapshot], + ); const desiredOnlyMissingSkills = useMemo( () => skillDraft.filter((key) => !companySkillByKey.has(key)), [companySkillByKey, skillDraft], @@ -2348,6 +2384,51 @@ function AgentSkillsTab({ const renderSkillRow = (skill: SkillRow) => { const adapterEntry = skill.adapterEntry ?? adapterEntryByKey.get(skill.key); const required = Boolean(adapterEntry?.required); + const rowClassName = cn( + "flex items-start gap-3 border-b border-border px-3 py-3 text-sm last:border-b-0", + skill.readOnly ? "bg-muted/20" : "hover:bg-accent/20", + ); + const body = ( +
+
+
+ {skill.name} +
+ {skill.linkTo ? ( + + View + + ) : null} +
+ {skill.description && ( + + {skill.description} + + )} + {skill.readOnly && skill.originLabel && ( +

{skill.originLabel}

+ )} + {skill.readOnly && skill.locationLabel && ( +

Location: {skill.locationLabel}

+ )} + {skill.detail && ( +

{skill.detail}

+ )} +
+ ); + + if (skill.readOnly) { + return ( +
+ + {body} +
+ ); + } + const checked = required || skillDraft.includes(skill.key); const disabled = required || skillSnapshot?.mode === "unsupported"; const checkbox = ( @@ -2364,11 +2445,9 @@ function AgentSkillsTab({ className="mt-0.5 disabled:cursor-not-allowed disabled:opacity-60" /> ); + return ( -