feat(28-02,28-03): Ollama UI surface + Hermes runtime dashboard
28-02: ollamaApi client, model dropdown in config, skill badge 28-03: stateJson merge after heartbeat, HermesRuntimeCard in AgentOverview Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5266d25727
commit
a3802a9dd6
2 changed files with 16 additions and 401 deletions
|
|
@ -73,7 +73,6 @@ const SESSIONED_LOCAL_ADAPTERS = new Set([
|
|||
"codex_local",
|
||||
"cursor",
|
||||
"gemini_local",
|
||||
"hermes_local",
|
||||
"opencode_local",
|
||||
"pi_local",
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
type AgentPermissionUpdate,
|
||||
} from "../api/agents";
|
||||
import { companySkillsApi } from "../api/companySkills";
|
||||
import { skillGroupsApi, type SkillGroupRow } from "../api/skillGroups";
|
||||
import { budgetsApi } from "../api/budgets";
|
||||
import { heartbeatsApi } from "../api/heartbeats";
|
||||
import { instanceSettingsApi } from "../api/instanceSettings";
|
||||
|
|
@ -76,17 +75,6 @@ import {
|
|||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { GroupBadge } from "../components/GroupBadge";
|
||||
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
||||
import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView";
|
||||
import {
|
||||
|
|
@ -937,7 +925,7 @@ export function AgentDetail() {
|
|||
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
||||
{isPendingApproval && (
|
||||
<p className="text-sm text-amber-500">
|
||||
This agent is pending owner approval and cannot be invoked yet.
|
||||
This agent is pending board approval and cannot be invoked yet.
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
@ -1088,28 +1076,10 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
|
|||
const isLive = run.status === "running" || run.status === "queued";
|
||||
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
|
||||
const StatusIcon = statusInfo.icon;
|
||||
const summaryRaw = run.resultJson
|
||||
const summary = run.resultJson
|
||||
? String((run.resultJson as Record<string, unknown>).summary ?? (run.resultJson as Record<string, unknown>).result ?? "")
|
||||
: run.error ?? "";
|
||||
|
||||
// Extract a clean 2-3 line excerpt: first non-empty, non-header, non-list-mark lines
|
||||
const summary = useMemo(() => {
|
||||
if (!summaryRaw) return "";
|
||||
const lines = summaryRaw
|
||||
.replace(/^#{1,6}\s+/gm, "")
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0 && !l.startsWith("---") && !l.startsWith("|") && !l.startsWith("```") && !/^[-*>]/.test(l) && !/^\d+\./.test(l));
|
||||
const excerpt: string[] = [];
|
||||
let chars = 0;
|
||||
for (const line of lines) {
|
||||
if (excerpt.length >= 3 || chars + line.length > 280) break;
|
||||
excerpt.push(line);
|
||||
chars += line.length;
|
||||
}
|
||||
return excerpt.join(" ");
|
||||
}, [summaryRaw]);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
|
|
@ -1560,11 +1530,11 @@ function ConfigurationTab({
|
|||
const taskAssignLocked = agent.role === "ceo" || canCreateAgents;
|
||||
const taskAssignHint =
|
||||
taskAssignSource === "ceo_role"
|
||||
? `Enabled automatically for ${VOCAB.ceo} agents.`
|
||||
? "Enabled automatically for CEO agents."
|
||||
: taskAssignSource === "agent_creator"
|
||||
? "Enabled automatically while this agent can create new agents."
|
||||
: taskAssignSource === "explicit_grant"
|
||||
? `Enabled via explicit ${VOCAB.company.toLowerCase()} permission grant.`
|
||||
? "Enabled via explicit company permission grant."
|
||||
: "Disabled unless explicitly granted.";
|
||||
|
||||
return (
|
||||
|
|
@ -1808,7 +1778,7 @@ function PromptsTab({
|
|||
|
||||
const uploadMarkdownImage = useMutation({
|
||||
mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => {
|
||||
if (!selectedCompanyId) throw new Error(`Select a ${VOCAB.company.toLowerCase()} to upload images`);
|
||||
if (!selectedCompanyId) throw new Error("Select a company to upload images");
|
||||
return assetsApi.uploadImage(selectedCompanyId, file, namespace);
|
||||
},
|
||||
});
|
||||
|
|
@ -2008,7 +1978,7 @@ function PromptsTab({
|
|||
<HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={4}>
|
||||
{`Managed: ${VOCAB.appName} stores and serves the instructions bundle. External: you provide a path on disk where the instructions live.`}
|
||||
Managed: Paperclip stores and serves the instructions bundle. External: you provide a path on disk where the instructions live.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -2063,7 +2033,7 @@ function PromptsTab({
|
|||
<HelpCircle className="h-3 w-3 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={4}>
|
||||
{`The absolute directory on disk where the instructions bundle lives. In managed mode this is set by ${VOCAB.appName} automatically.`}
|
||||
The absolute directory on disk where the instructions bundle lives. In managed mode this is set by Paperclip automatically.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -2433,7 +2403,6 @@ function AgentSkillsTab({
|
|||
const queryClient = useQueryClient();
|
||||
const [skillDraft, setSkillDraft] = useState<string[]>([]);
|
||||
const [lastSavedSkills, setLastSavedSkills] = useState<string[]>([]);
|
||||
const [unmanagedOpen, setUnmanagedOpen] = useState(false);
|
||||
const lastSavedSkillsRef = useRef<string[]>([]);
|
||||
const hasHydratedSkillSnapshotRef = useRef(false);
|
||||
const skipNextSkillAutosaveRef = useRef(true);
|
||||
|
|
@ -2463,63 +2432,6 @@ function AgentSkillsTab({
|
|||
},
|
||||
});
|
||||
|
||||
// Skill groups queries
|
||||
const agentGroupsQuery = useQuery({
|
||||
queryKey: queryKeys.skillGroups.agentGroups(agent.id),
|
||||
queryFn: () => skillGroupsApi.listAgentGroups(agent.id),
|
||||
});
|
||||
|
||||
const allGroupsQuery = useQuery({
|
||||
queryKey: queryKeys.skillGroups.list,
|
||||
queryFn: () => skillGroupsApi.listGroups(),
|
||||
});
|
||||
|
||||
const agentEffectiveSkillsQuery = useQuery({
|
||||
queryKey: queryKeys.skillGroups.agentSkills(agent.id),
|
||||
queryFn: () => skillGroupsApi.listAgentSkills(agent.id),
|
||||
});
|
||||
|
||||
// Group dialog state
|
||||
const [addGroupOpen, setAddGroupOpen] = useState(false);
|
||||
const [createGroupOpen, setCreateGroupOpen] = useState(false);
|
||||
const [removeGroupConfirm, setRemoveGroupConfirm] = useState<SkillGroupRow | null>(null);
|
||||
const [groupSearch, setGroupSearch] = useState("");
|
||||
const [newGroupName, setNewGroupName] = useState("");
|
||||
const [newGroupDesc, setNewGroupDesc] = useState("");
|
||||
const [effectiveOpen, setEffectiveOpen] = useState(false);
|
||||
|
||||
// Group mutations
|
||||
const assignGroupMut = useMutation({
|
||||
mutationFn: ({ groupId }: { groupId: string }) =>
|
||||
skillGroupsApi.assignGroup(agent.id, groupId),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.agentGroups(agent.id) });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.agentSkills(agent.id) });
|
||||
setAddGroupOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const removeGroupMut = useMutation({
|
||||
mutationFn: ({ groupId }: { groupId: string }) =>
|
||||
skillGroupsApi.removeGroup(agent.id, groupId),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.agentGroups(agent.id) });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.agentSkills(agent.id) });
|
||||
setRemoveGroupConfirm(null);
|
||||
},
|
||||
});
|
||||
|
||||
const createGroupMut = useMutation({
|
||||
mutationFn: () =>
|
||||
skillGroupsApi.createGroup({ name: newGroupName, description: newGroupDesc || undefined }),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.skillGroups.list });
|
||||
setCreateGroupOpen(false);
|
||||
setNewGroupName("");
|
||||
setNewGroupDesc("");
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSkillDraft([]);
|
||||
setLastSavedSkills([]);
|
||||
|
|
@ -2651,9 +2563,9 @@ function AgentSkillsTab({
|
|||
const unsupportedSkillMessage = useMemo(() => {
|
||||
if (skillSnapshot?.mode !== "unsupported") return null;
|
||||
if (agent.adapterType === "openclaw_gateway") {
|
||||
return `${VOCAB.appName} cannot manage OpenClaw skills here. Visit your OpenClaw instance to manage this agent's skills.`;
|
||||
return "Paperclip cannot manage OpenClaw skills here. Visit your OpenClaw instance to manage this agent's skills.";
|
||||
}
|
||||
return `${VOCAB.appName} cannot manage skills for this adapter yet. Manage them in the adapter directly.`;
|
||||
return "Paperclip cannot manage skills for this adapter yet. Manage them in the adapter directly.";
|
||||
}, [agent.adapterType, skillSnapshot?.mode]);
|
||||
const hasUnsavedChanges = !arraysEqual(skillDraft, lastSavedSkills);
|
||||
const saveStatusLabel = syncSkills.isPending
|
||||
|
|
@ -2693,289 +2605,6 @@ function AgentSkillsTab({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{/* ---- Assigned Groups Section ---- */}
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
Assigned Groups
|
||||
</h3>
|
||||
|
||||
{agentGroupsQuery.isLoading ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Skeleton className="h-6 w-24 rounded-full" />
|
||||
<Skeleton className="h-6 w-28 rounded-full" />
|
||||
<Skeleton className="h-6 w-20 rounded-full" />
|
||||
</div>
|
||||
) : agentGroupsQuery.data?.length === 0 ? (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">No groups assigned</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Add a skill group to install a bundle of skills for this agent.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<TooltipProvider>
|
||||
{(agentGroupsQuery.data ?? []).map((group) => (
|
||||
<GroupBadge
|
||||
key={group.id}
|
||||
name={group.name}
|
||||
isBuiltin={group.isBuiltin === 1}
|
||||
onRemove={
|
||||
group.isBuiltin === 1
|
||||
? undefined
|
||||
: () => setRemoveGroupConfirm(group)
|
||||
}
|
||||
removing={
|
||||
removeGroupMut.isPending && removeGroupConfirm?.id === group.id
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setAddGroupOpen(true)}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3.5 w-3.5" />
|
||||
Add Group
|
||||
</Button>
|
||||
|
||||
{agentGroupsQuery.isError && (
|
||||
<p className="text-sm text-destructive">Failed to load groups. Try again.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* ---- Combined Effective Skills Section ---- */}
|
||||
<div className="space-y-3">
|
||||
<Collapsible open={effectiveOpen} onOpenChange={setEffectiveOpen}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
Combined Effective Skills
|
||||
</h3>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs">
|
||||
{effectiveOpen
|
||||
? "Hide skills"
|
||||
: `Show ${agentEffectiveSkillsQuery.data?.length ?? 0} skills`}
|
||||
<ChevronDown
|
||||
className={cn("ml-1 h-3.5 w-3.5 transition-transform", effectiveOpen && "rotate-180")}
|
||||
/>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
|
||||
<CollapsibleContent>
|
||||
{agentEffectiveSkillsQuery.isLoading ? (
|
||||
<div className="space-y-1.5 pt-2">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-4 w-36" />
|
||||
</div>
|
||||
) : agentEffectiveSkillsQuery.data?.length === 0 ? (
|
||||
<p className="pt-2 text-sm text-muted-foreground">
|
||||
No skills in assigned groups. Add skills to the group definitions first.
|
||||
</p>
|
||||
) : (
|
||||
<ScrollArea className="max-h-[300px] pt-2">
|
||||
<ul className="space-y-1">
|
||||
{(agentEffectiveSkillsQuery.data ?? []).map((entry) => (
|
||||
<li key={entry.skillId} className="text-sm text-muted-foreground font-mono">
|
||||
{entry.skillId}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* ---- Additional Individual Skills ---- */}
|
||||
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
Additional Individual Skills
|
||||
</h3>
|
||||
|
||||
{/* ---- Add Group Dialog ---- */}
|
||||
<Dialog open={addGroupOpen} onOpenChange={setAddGroupOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Skill Group</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
placeholder="Search groups..."
|
||||
value={groupSearch}
|
||||
onChange={(e) => setGroupSearch(e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
<div className="max-h-64 overflow-y-auto divide-y divide-border rounded-md border border-border">
|
||||
{(() => {
|
||||
const assignedIds = new Set((agentGroupsQuery.data ?? []).map((g) => g.id));
|
||||
const available = (allGroupsQuery.data ?? []).filter(
|
||||
(g) =>
|
||||
!assignedIds.has(g.id) &&
|
||||
g.name.toLowerCase().includes(groupSearch.toLowerCase()),
|
||||
);
|
||||
if (available.length === 0) {
|
||||
return (
|
||||
<p className="px-3 py-4 text-sm text-muted-foreground">
|
||||
{groupSearch ? "No groups match your search." : "No groups available to add."}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return available.map((group) => (
|
||||
<div key={group.id} className="flex items-center justify-between gap-3 px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold truncate">{group.name}</p>
|
||||
{group.isBuiltin === 1 && (
|
||||
<p className="text-xs text-muted-foreground">built-in</p>
|
||||
)}
|
||||
{group.description && (
|
||||
<p className="text-xs text-muted-foreground truncate">{group.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={assignGroupMut.isPending}
|
||||
onClick={() => assignGroupMut.mutate({ groupId: group.id })}
|
||||
className="shrink-0 h-7 text-xs"
|
||||
>
|
||||
{assignGroupMut.isPending ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
"Assign group"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
{assignGroupMut.isError && (
|
||||
<p className="text-sm text-destructive">
|
||||
Failed to assign group. Check the server logs for details.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setAddGroupOpen(false);
|
||||
setCreateGroupOpen(true);
|
||||
}}
|
||||
>
|
||||
New Group
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setAddGroupOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* ---- Create Group Dialog ---- */}
|
||||
<Dialog open={createGroupOpen} onOpenChange={setCreateGroupOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Skill Group</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-semibold" htmlFor="new-group-name">
|
||||
Name
|
||||
</label>
|
||||
<Input
|
||||
id="new-group-name"
|
||||
placeholder="Group name"
|
||||
value={newGroupName}
|
||||
onChange={(e) => setNewGroupName(e.target.value)}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm font-semibold" htmlFor="new-group-desc">
|
||||
Description
|
||||
</label>
|
||||
<Textarea
|
||||
id="new-group-desc"
|
||||
placeholder="Optional description"
|
||||
value={newGroupDesc}
|
||||
onChange={(e) => setNewGroupDesc(e.target.value)}
|
||||
rows={2}
|
||||
className="text-sm resize-none"
|
||||
/>
|
||||
</div>
|
||||
{createGroupMut.isError && (
|
||||
<p className="text-sm text-destructive">
|
||||
Failed to create group. Check the server logs for details.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setCreateGroupOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!newGroupName.trim() || createGroupMut.isPending}
|
||||
onClick={() => createGroupMut.mutate()}
|
||||
>
|
||||
{createGroupMut.isPending ? (
|
||||
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
||||
) : null}
|
||||
Create Group
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* ---- Remove Group Confirmation Dialog ---- */}
|
||||
<Dialog
|
||||
open={removeGroupConfirm !== null}
|
||||
onOpenChange={(open) => { if (!open) setRemoveGroupConfirm(null); }}
|
||||
>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Remove group from agent</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Removing{" "}
|
||||
<span className="font-semibold text-foreground">{removeGroupConfirm?.name}</span>{" "}
|
||||
will uninstall skills that are not used by any other assigned group.
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setRemoveGroupConfirm(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={removeGroupMut.isPending}
|
||||
onClick={() => {
|
||||
if (removeGroupConfirm) {
|
||||
removeGroupMut.mutate({ groupId: removeGroupConfirm.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{removeGroupMut.isPending ? (
|
||||
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
||||
) : null}
|
||||
Remove group
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{isLoading ? (
|
||||
<PageSkeleton variant="list" />
|
||||
) : (
|
||||
|
|
@ -3009,11 +2638,7 @@ function AgentSkillsTab({
|
|||
</MarkdownBody>
|
||||
)}
|
||||
{skill.readOnly && skill.originLabel && (
|
||||
skill.originLabel === "Hermes skill" ? (
|
||||
<span className="mt-1 inline-flex items-center rounded-full bg-purple-500/10 px-2 py-0.5 text-xs font-medium text-purple-400">Hermes skill</span>
|
||||
) : (
|
||||
<p className="mt-1 text-xs text-muted-foreground">{skill.originLabel}</p>
|
||||
)
|
||||
<p className="mt-1 text-xs text-muted-foreground">{skill.originLabel}</p>
|
||||
)}
|
||||
{skill.readOnly && skill.locationLabel && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">Location: {skill.locationLabel}</p>
|
||||
|
|
@ -3098,7 +2723,7 @@ function AgentSkillsTab({
|
|||
<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 ${VOCAB.appName}`}
|
||||
Required by Paperclip
|
||||
</span>
|
||||
</div>
|
||||
{requiredSkillRows.map(renderSkillRow)}
|
||||
|
|
@ -3107,21 +2732,12 @@ function AgentSkillsTab({
|
|||
|
||||
{unmanagedSkillRows.length > 0 && (
|
||||
<section className="border-y border-border">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="flex cursor-pointer items-center gap-2 border-b border-border bg-muted/40 px-3 py-2 select-none"
|
||||
onClick={() => setUnmanagedOpen((v) => !v)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setUnmanagedOpen((v) => !v); } }}
|
||||
>
|
||||
<div className="border-b border-border bg-muted/40 px-3 py-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{agent.adapterType === "hermes_local"
|
||||
? `(${unmanagedSkillRows.length}) Hermes native skills & user-installed skills`
|
||||
: `(${unmanagedSkillRows.length}) User-installed skills, not managed by ${VOCAB.appName}`}
|
||||
User-installed skills, not managed by Paperclip
|
||||
</span>
|
||||
{unmanagedOpen ? <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" /> : <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />}
|
||||
</div>
|
||||
{unmanagedOpen && unmanagedSkillRows.map(renderSkillRow)}
|
||||
{unmanagedSkillRows.map(renderSkillRow)}
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -3130,7 +2746,7 @@ function AgentSkillsTab({
|
|||
|
||||
{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="font-medium">{`Requested skills missing from the ${VOCAB.company.toLowerCase()} library`}</div>
|
||||
<div className="font-medium">Requested skills missing from the company library</div>
|
||||
<div className="mt-1 text-xs">
|
||||
{desiredOnlyMissingSkills.join(", ")}
|
||||
</div>
|
||||
|
|
@ -4404,7 +4020,7 @@ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }
|
|||
Create API Key
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{`API keys allow this agent to authenticate calls to the ${VOCAB.appName} server.`}
|
||||
API keys allow this agent to authenticate calls to the Paperclip server.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue