diff --git a/ui/src/pages/CompanySkills.tsx b/ui/src/pages/CompanySkills.tsx
index 1fb8cba530d..35b0c8568b2 100644
--- a/ui/src/pages/CompanySkills.tsx
+++ b/ui/src/pages/CompanySkills.tsx
@@ -6,7 +6,6 @@ import type {
Agent,
CatalogSkill,
CatalogSkillFileDetail,
- CompanySkillComment,
CompanySkillCompatibility,
CompanySkillCreateRequest,
CompanySkillDetail,
@@ -32,6 +31,7 @@ import { MarkdownEditor } from "../components/MarkdownEditor";
import { PageSkeleton } from "../components/PageSkeleton";
import { CopyText } from "../components/CopyText";
import { Identity } from "../components/Identity";
+import { AgentIcon } from "../components/AgentIconPicker";
import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities";
import {
Dialog,
@@ -86,9 +86,9 @@ import {
LayoutGrid,
Link2,
Lock,
- MessageSquare,
ExternalLink,
Paperclip,
+ Pause,
Pencil,
Plus,
Copy,
@@ -537,7 +537,9 @@ function cardDescriptionText(raw: string | null | undefined): string {
const cleaned = (raw ?? "")
.replace(/^[\s>#*_\-`>]+/, "")
.trim();
- return cleaned.length >= 3 ? cleaned : "No description yet.";
+ // Empty descriptions render as a blank line on cards so spacing stays
+ // consistent across the grid (PAP-10907).
+ return cleaned.length >= 3 ? cleaned : "";
}
// ---------------------------------------------------------------------------
@@ -823,7 +825,7 @@ function SkillCard({ card, onOpen }: { card: DiscoveryCard; onOpen: (card: Disco
type="button"
onClick={() => onOpen(card)}
className={cn(
- "group flex h-full min-h-[11.5rem] flex-col rounded-md border border-border p-4 text-left transition-colors hover:border-foreground/30 hover:bg-accent/30 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
+ "group flex h-full min-h-[11.5rem] flex-col rounded-md border border-border p-4 text-left transition-colors hover:border-primary hover:bg-accent/30 hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
card.required && "bg-muted/30",
)}
>
@@ -844,38 +846,41 @@ function SkillCard({ card, onOpen }: { card: DiscoveryCard; onOpen: (card: Disco
) : null}
-
+ {/* Always reserve two lines so cards line up even without a description. */}
+
{cardDescriptionText(card.description)}
+ {/* Stats: installed agents · stars · forks — stars/forks only when > 0. */}
- {card.required ? (
- {card.agentCount} agents using
- ) : (
+ {card.agentCount} {card.agentCount === 1 ? "agent" : "agents"}
+ {card.starCount > 0 ? (
<>
+ ·
- ·
- {card.agentCount} agents
- ·
-
>
- )}
+ ) : null}
+ {card.forkCount > 0 ? (
+ <>
+ ·
+
+ >
+ ) : null}
+ {card.installed ? (
+
+ Installed
+
+ ) : null}
{card.categories.slice(0, 2).map((category) => (
))}
-
{card.required ? (
-
+
- Required
-
- ) : card.installed ? (
-
-
- Installed
+ Bundled
) : null}
@@ -976,10 +981,13 @@ export function DiscoveryGrid({
scanStatus: string | null;
}) {
return (
-
+ // On desktop the store is bounded to the viewport so the category sidebar
+ // and the results pane each scroll independently (PAP-10907). Mobile keeps
+ // the natural page flow.
+
{/* Secondary category sidebar — the main app nav collapses to a rail while
this is present (handled in Layout). */}
-
+
Skills Store
Discover, install, fork, share
@@ -987,7 +995,7 @@ export function DiscoveryGrid({
Categories
-
+
-
+
{/* Search + sort + actions */}
-
{/* Grid body */}
-
+
{scanStatus ?
{scanStatus}
: null}
{loading ? (
@@ -1958,6 +1965,16 @@ function InstallPreviewDialog({
);
}
+type AttachAgentOption = {
+ id: string;
+ name: string;
+ adapterType: string;
+ supportsSkills: boolean;
+ required: boolean;
+ icon: string | null;
+ paused: boolean;
+};
+
function AttachAgentsPopover({
open,
onOpenChange,
@@ -1967,15 +1984,17 @@ function AttachAgentsPopover({
selectedVersionId,
pending,
onSubmit,
+ fullWidth = false,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
- agents: Array<{ id: string; name: string; adapterType: string; supportsSkills: boolean; required: boolean }>;
+ agents: AttachAgentOption[];
attachedAgentIds: string[];
versions: CompanySkillVersion[];
selectedVersionId: string | null;
pending: boolean;
onSubmit: (nextIds: string[], versionId: string | null) => void;
+ fullWidth?: boolean;
}) {
const [filter, setFilter] = useState("");
const [draft, setDraft] = useState
>(new Set(attachedAgentIds));
@@ -1989,21 +2008,26 @@ function AttachAgentsPopover({
}
}, [open, attachedAgentIds, selectedVersionId]);
- const filtered = agents.filter((agent) => agent.name.toLowerCase().includes(filter.toLowerCase()));
+ // Checked agents float to the top of the list (PAP-10907); within each group
+ // we keep a stable alphabetical order.
+ const filtered = agents
+ .filter((agent) => agent.name.toLowerCase().includes(filter.toLowerCase()))
+ .sort((a, b) => {
+ const aChecked = draft.has(a.id);
+ const bChecked = draft.has(b.id);
+ if (aChecked !== bChecked) return aChecked ? -1 : 1;
+ return a.name.localeCompare(b.name);
+ });
const eligible = agents.filter((agent) => agent.supportsSkills);
const sortedVersions = [...versions].sort((a, b) => b.revisionNumber - a.revisionNumber);
return (
-
-
- Edit
-
+
+
+ Add to agent
+
@@ -2060,8 +2084,17 @@ function AttachAgentsPopover({
});
}}
/>
+
- {agent.name}
+
+ {agent.name}
+ {agent.paused ? (
+
+
+ Paused
+
+ ) : null}
+
{agent.adapterType}
{agent.required ? " · required" : ""}
@@ -2304,13 +2337,12 @@ function SkillList({
);
}
-type SkillDetailTab = "overview" | "files" | "versions" | "comments" | "agents";
+type SkillDetailTab = "overview" | "files" | "versions" | "agents";
const SKILL_DETAIL_TABS: Array<{ value: SkillDetailTab; label: string; icon: typeof FileText }> = [
{ value: "overview", label: "Overview", icon: FileText },
{ value: "files", label: "Files", icon: FolderOpen },
{ value: "versions", label: "Versions", icon: History },
- { value: "comments", label: "Comments", icon: MessageSquare },
{ value: "agents", label: "Agents", icon: Users },
];
@@ -2463,75 +2495,7 @@ function SkillVersionDiffDialog({
);
}
-function SkillCommentRow({
- comment,
- onUpdate,
- updatePending,
- onDelete,
- deletePending,
-}: {
- comment: CompanySkillComment;
- onUpdate: (body: string) => void;
- updatePending: boolean;
- onDelete: () => void;
- deletePending: boolean;
-}) {
- const [editing, setEditing] = useState(false);
- const [draft, setDraft] = useState(comment.body);
- const deleted = Boolean(comment.deletedAt);
-
- useEffect(() => {
- if (!editing) setDraft(comment.body);
- }, [comment.body, editing]);
-
- return (
-
-
-
- {comment.authorAgentId ? "Agent" : comment.authorUserId ? "Board" : "System"} · {relativeTime(comment.createdAt)}
-
- {!deleted ? (
-
-
setEditing((value) => !value)} disabled={updatePending || deletePending}>
-
- {editing ? "Cancel" : "Edit"}
-
-
-
- Delete
-
-
- ) : null}
-
- {deleted ? (
- _Deleted comment_
- ) : editing ? (
-
- ) : (
- {comment.body}
- )}
-
- );
-}
-
-function SkillDetailPage({
+export function SkillDetailPage({
detail,
loading,
activeTab,
@@ -2549,17 +2513,6 @@ function SkillDetailPage({
savePending,
versions,
versionsLoading,
- comments,
- commentsLoading,
- commentDraft,
- setCommentDraft,
- onCreateComment,
- createCommentPending,
- onUpdateComment,
- updateCommentPending,
- onDeleteComment,
- deleteCommentPending,
- relatedSkills,
attachAgents,
attachPopoverOpen,
setAttachPopoverOpen,
@@ -2599,18 +2552,7 @@ function SkillDetailPage({
savePending: boolean;
versions: CompanySkillVersion[];
versionsLoading: boolean;
- comments: CompanySkillComment[];
- commentsLoading: boolean;
- commentDraft: string;
- setCommentDraft: (value: string) => void;
- onCreateComment: () => void;
- createCommentPending: boolean;
- onUpdateComment: (commentId: string, body: string) => void;
- updateCommentPending: boolean;
- onDeleteComment: (commentId: string) => void;
- deleteCommentPending: boolean;
- relatedSkills: CompanySkillListItem[];
- attachAgents: Array<{ id: string; name: string; adapterType: string; supportsSkills: boolean; required: boolean }>;
+ attachAgents: AttachAgentOption[];
attachPopoverOpen: boolean;
setAttachPopoverOpen: (open: boolean) => void;
onSubmitAttach: (ids: string[], versionId: string | null) => void;
@@ -2651,6 +2593,8 @@ function SkillDetailPage({
const subtitleText = skill.tagline || skill.description
? cardDescriptionText(skill.tagline ?? skill.description)
: source.label;
+ // Look up the richer agent record (icon, paused) for agents using this skill.
+ const attachAgentMetaById = new Map(attachAgents.map((agent) => [agent.id, agent]));
function renderFilesBody() {
return (
@@ -2827,82 +2771,62 @@ function SkillDetailPage({
);
}
- function renderCommentsBody() {
- return (
-
-
-
- {commentsLoading ? (
-
- ) : comments.length === 0 ? (
-
No comments yet.
- ) : (
- comments.map((comment) => (
-
onUpdateComment(comment.id, body)}
- onDelete={() => onDeleteComment(comment.id)}
- />
- ))
- )}
-
-
- );
- }
-
function renderAgentsBody() {
- const attachedIds = new Set(skill.usedByAgents.map((agent) => agent.id));
+ // Only the agents actually using this skill are listed (PAP-10907); the
+ // multi-selector behind "Add to agent" is where you attach more.
+ const attached = skill.usedByAgents;
return (
- {skill.usedByAgents.length} {skill.usedByAgents.length === 1 ? "agent" : "agents"} attached
+ {attached.length} {attached.length === 1 ? "agent" : "agents"} attached
{selectedVersion ? ` · ${versionLabel(selectedVersion)}` : " · Latest"}
agent.id)}
+ attachedAgentIds={attached.map((agent) => agent.id)}
versions={versions}
selectedVersionId={currentVersionSelection(skill)}
pending={attachPending}
onSubmit={onSubmitAttach}
/>
-
- {attachAgents.map((agent) => (
-
-
-
-
- {agent.adapterType}{agent.supportsSkills ? "" : " · skills not supported"}
+ {attached.length === 0 ? (
+
+ No agents are using this skill yet. Use “Add to agent” to attach it.
+
+ ) : (
+
+ {attached.map((agent) => {
+ const meta = attachAgentMetaById.get(agent.id);
+ return (
+
+
+
+
+
{agent.name}
+ {meta?.paused ? (
+
+
+ Paused
+
+ ) : null}
+
+
{agent.adapterType}
+
+
+ View
+
-
-
- {attachedIds.has(agent.id) ? "Attached" : "Not attached"}
-
-
- ))}
- {attachAgents.length === 0 ? (
-
No agents in this company yet.
- ) : null}
-
+ );
+ })}
+
+ )}
);
}
@@ -2911,11 +2835,9 @@ function SkillDetailPage({
? renderFilesBody()
: activeTab === "versions"
? renderVersionsBody()
- : activeTab === "comments"
- ? renderCommentsBody()
- : activeTab === "agents"
- ? renderAgentsBody()
- : renderOverviewBody();
+ : activeTab === "agents"
+ ? renderAgentsBody()
+ : renderOverviewBody();
return (
@@ -2947,11 +2869,25 @@ function SkillDetailPage({
size={44}
/>
-
{detail.name}
-
-
- {subtitleText}
-
+
+
{detail.name}
+ {/* Source icon sits right after the title; the tooltip names
+ where the skill was installed from (PAP-10907). */}
+
+
+
+
+
+
+ Installed from {source.label}
+
+
+ {subtitleText ? (
+
{subtitleText}
+ ) : null}
@@ -2971,27 +2907,42 @@ function SkillDetailPage({
) : null}
-
-
-
- {detail.starredByCurrentActor ? "Starred" : "Star"}
- {detail.starCount}
-
-
-
-
-
- {detail.installCount}
- {detail.installCount === 1 ? "install" : "installs"}
-
-
- Times this skill has been installed or forked.
-
-
-
- Fork
- {detail.forkCount}
-
+ {/* GitHub-style social proof, top-right: installs · stars · fork.
+ Installs is a read-only stat; stars and fork are interactive. */}
+
+
+
+
+
+
+ {detail.installCount}
+ {detail.installCount === 1 ? "install" : "installs"}
+
+
+ Times this skill has been installed or forked.
+
+
+
+ {detail.starredByCurrentActor ? "Starred" : "Star"}
+ {detail.starCount}
+
+
+
+ Fork
+ {detail.forkCount}
+
+
{detail.sourceType === "github" ? (
<>
@@ -3006,10 +2957,6 @@ function SkillDetailPage({
) : null}
>
) : null}
- 0 ? "Detach this skill from all agents before removing it." : undefined}>
-
- {deletePending ? "Removing..." : "Remove"}
-
@@ -3017,14 +2964,15 @@ function SkillDetailPage({
onTabChange(value as SkillDetailTab)}>
-
+ {/* Underlined tab strip: the bottom padding keeps the active-tab
+ underline inside the horizontal-scroll clip box (PAP-10907). */}
+
{SKILL_DETAIL_TABS.map((tab) => {
const Icon = tab.icon;
return (
{tab.label}
- {tab.value === "comments" ? {comments.length} : null}
);
})}
@@ -3035,12 +2983,9 @@ function SkillDetailPage({
- Attach to agent
-
-
- {detail.usedByAgents.length === 0 ? "No agents attached" : `${detail.usedByAgents.length} attached`}
-
-
{selectedVersion ? versionLabel(selectedVersion) : "Latest"}
+
Agents
+
+ {/* Big primary action opens the agent multi-selector (PAP-10907). */}
+ {detail.usedByAgents.length === 0 ? (
+
No agents attached yet.
+ ) : (
+
+ {/* Preview up to three attached agents, then summarise the rest. */}
+ {detail.usedByAgents.slice(0, 3).map((agent) => {
+ const meta = attachAgentMetaById.get(agent.id);
+ return (
+
+
+
{agent.name}
+ {meta?.paused ? (
+
+ ) : null}
+
+ );
+ })}
+ {detail.usedByAgents.length > 3 ? (
+
+ and {detail.usedByAgents.length - 3} more
+
+ ) : null}
+
+ )}
-
- Related skills
- {relatedSkills.length === 0 ? (
- No related skills yet.
- ) : (
-
- {relatedSkills.map((skill) => (
-
- {skill.name}
- {skill.tagline ?? skill.description ?? skill.sourceLabel}
-
- ))}
-
- )}
-
-
- {/* Sharing scope only surfaces when the viewer can edit it (round 2 #3).
- Sits below Attach/Related so the primary action lives at the top
- of the rail (round 2 #2). */}
+ {/* Sharing scope only surfaces when the viewer can edit it (round 2 #3). */}
Sharing
@@ -3111,6 +3063,20 @@ function SkillDetailPage({
Coming later.
+ {/* Remove lives in the sidebar and only appears when the viewer can
+ edit (and therefore remove) the skill (PAP-10907). */}
+ {detail.editable ? (
+ 0 ? "Detach this skill from all agents before removing it." : undefined}
+ className="mt-1 flex w-full items-center gap-2 rounded-md border border-destructive/40 px-3 py-2 text-left text-sm text-destructive transition-colors hover:bg-destructive/10 disabled:opacity-50"
+ >
+
+ {deletePending ? "Removing…" : "Remove skill"}
+
+ ) : null}
@@ -3167,7 +3133,7 @@ function SkillPane({
deletePending: boolean;
onSave: () => void;
savePending: boolean;
- attachAgents: Array<{ id: string; name: string; adapterType: string; supportsSkills: boolean; required: boolean }>;
+ attachAgents: AttachAgentOption[];
attachPopoverOpen: boolean;
setAttachPopoverOpen: (open: boolean) => void;
versions: CompanySkillVersion[];
@@ -3454,7 +3420,6 @@ export function CompanySkills() {
const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
const [editMode, setEditMode] = useState(false);
const [draft, setDraft] = useState("");
- const [commentDraft, setCommentDraft] = useState("");
const [displayedDetail, setDisplayedDetail] = useState
(null);
const [displayedFile, setDisplayedFile] = useState(null);
const [scanStatusMessage, setScanStatusMessage] = useState(null);
@@ -3497,7 +3462,7 @@ export function CompanySkills() {
const discoveryTab: DiscoveryTab = DISCOVERY_TABS.includes(tabParam as DiscoveryTab)
? (tabParam as DiscoveryTab)
: "all";
- const detailTab: SkillDetailTab = (["overview", "files", "versions", "comments", "agents"] as SkillDetailTab[]).includes(tabParam as SkillDetailTab)
+ const detailTab: SkillDetailTab = (["overview", "files", "versions", "agents"] as SkillDetailTab[]).includes(tabParam as SkillDetailTab)
? (tabParam as SkillDetailTab)
: selectedPath !== "SKILL.md"
? "files"
@@ -3604,12 +3569,6 @@ export function CompanySkills() {
enabled: Boolean(selectedCompanyId && selectedSkillId),
});
- const commentsQuery = useQuery({
- queryKey: queryKeys.companySkills.comments(selectedCompanyId ?? "", selectedSkillId ?? ""),
- queryFn: () => companySkillsApi.comments(selectedCompanyId!, selectedSkillId!),
- enabled: Boolean(selectedCompanyId && selectedSkillId),
- });
-
const updateStatusQuery = useQuery({
queryKey: queryKeys.companySkills.updateStatus(selectedCompanyId ?? "", selectedSkillId ?? ""),
queryFn: () => companySkillsApi.updateStatus(selectedCompanyId!, selectedSkillId!),
@@ -3800,59 +3759,6 @@ export function CompanySkills() {
},
});
- const createComment = useMutation({
- mutationFn: () => companySkillsApi.createComment(selectedCompanyId!, selectedSkillId!, { body: commentDraft.trim() }),
- onSuccess: async () => {
- setCommentDraft("");
- await queryClient.invalidateQueries({
- queryKey: queryKeys.companySkills.comments(selectedCompanyId!, selectedSkillId!),
- });
- pushToast({ tone: "success", title: "Comment posted" });
- },
- onError: (error) => {
- pushToast({
- tone: "error",
- title: "Comment failed",
- body: error instanceof Error ? error.message : "Failed to post comment.",
- });
- },
- });
-
- const updateComment = useMutation({
- mutationFn: (input: { commentId: string; body: string }) =>
- companySkillsApi.updateComment(selectedCompanyId!, selectedSkillId!, input.commentId, { body: input.body }),
- onSuccess: async () => {
- await queryClient.invalidateQueries({
- queryKey: queryKeys.companySkills.comments(selectedCompanyId!, selectedSkillId!),
- });
- pushToast({ tone: "success", title: "Comment updated" });
- },
- onError: (error) => {
- pushToast({
- tone: "error",
- title: "Comment update failed",
- body: error instanceof Error ? error.message : "Failed to update comment.",
- });
- },
- });
-
- const deleteComment = useMutation({
- mutationFn: (commentId: string) => companySkillsApi.deleteComment(selectedCompanyId!, selectedSkillId!, commentId),
- onSuccess: async () => {
- await queryClient.invalidateQueries({
- queryKey: queryKeys.companySkills.comments(selectedCompanyId!, selectedSkillId!),
- });
- pushToast({ tone: "success", title: "Comment deleted" });
- },
- onError: (error) => {
- pushToast({
- tone: "error",
- title: "Comment delete failed",
- body: error instanceof Error ? error.message : "Failed to delete comment.",
- });
- },
- });
-
const toggleStar = useMutation({
mutationFn: () => {
if (!activeDetail) throw new Error("Select a skill first.");
@@ -4025,22 +3931,6 @@ export function CompanySkills() {
}
return counts;
}, [installedSkills]);
- const relatedSkills = useMemo(() => {
- if (!activeDetail) return [];
- const categorySet = new Set(activeDetail.categories);
- return installedSkills
- .filter((skill) => skill.id !== activeDetail.id)
- .map((skill) => {
- const sharedCategories = skill.categories.filter((category) => categorySet.has(category)).length;
- const sameSource = skill.sourceBadge === activeDetail.sourceBadge ? 1 : 0;
- return { skill, score: sharedCategories * 3 + sameSource };
- })
- .filter((entry) => entry.score > 0)
- .sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
- .slice(0, 5)
- .map((entry) => entry.skill);
- }, [activeDetail, installedSkills]);
-
const installCatalog = useMutation({
mutationFn: (payload: { catalogSkillId: string; slug: string | null; force: boolean }) =>
companySkillsApi.installCatalog(selectedCompanyId!, {
@@ -4086,6 +3976,8 @@ export function CompanySkills() {
adapterType: agent.adapterType,
supportsSkills: Boolean(caps.supportsSkills),
required: isRequired,
+ icon: agent.icon,
+ paused: agent.status === "paused" || agent.pausedAt != null,
attached: usedSet.has(agent.id),
requiredKeys,
};
@@ -4452,17 +4344,6 @@ export function CompanySkills() {
savePending={saveFile.isPending}
versions={versionsQuery.data ?? []}
versionsLoading={versionsQuery.isLoading}
- comments={commentsQuery.data ?? []}
- commentsLoading={commentsQuery.isLoading}
- commentDraft={commentDraft}
- setCommentDraft={setCommentDraft}
- onCreateComment={() => createComment.mutate()}
- createCommentPending={createComment.isPending}
- onUpdateComment={(commentId, body) => updateComment.mutate({ commentId, body })}
- updateCommentPending={updateComment.isPending}
- onDeleteComment={(commentId) => deleteComment.mutate(commentId)}
- deleteCommentPending={deleteComment.isPending}
- relatedSkills={relatedSkills}
attachAgents={eligibleAgentsForAttach}
attachPopoverOpen={attachPopoverOpen}
setAttachPopoverOpen={setAttachPopoverOpen}
diff --git a/ui/storybook/stories/skills-store-detail.stories.tsx b/ui/storybook/stories/skills-store-detail.stories.tsx
new file mode 100644
index 00000000000..5183edb9ff4
--- /dev/null
+++ b/ui/storybook/stories/skills-store-detail.stories.tsx
@@ -0,0 +1,198 @@
+import { useState } from "react";
+import type { Meta, StoryObj } from "@storybook/react-vite";
+import type {
+ CompanySkillDetail,
+ CompanySkillFileDetail,
+ CompanySkillVersion,
+} from "@paperclipai/shared";
+import { SkillDetailPage } from "@/pages/CompanySkills";
+
+type DetailTab = "overview" | "files" | "versions" | "agents";
+
+const NOW = new Date("2026-06-01T12:00:00Z");
+
+const MOCK_DETAIL: CompanySkillDetail = {
+ id: "skill-1",
+ companyId: "company-1",
+ key: "paperclipai/paperclip/deep-research",
+ slug: "deep-research",
+ name: "deep-research",
+ description:
+ "Fan-out web searches, fetch sources, adversarially verify claims, and synthesize a cited report. Best for multi-source, fact-checked research where one search angle will not find everything.",
+ markdown: "# deep-research\n\nResearch harness.",
+ sourceType: "github",
+ sourceLocator: "github.com/paperclipai/skills",
+ sourceRef: "a1b2c3d4e5f6",
+ trustLevel: "scripts_executables",
+ compatibility: "compatible",
+ fileInventory: [
+ { path: "SKILL.md", kind: "skill" },
+ { path: "references/methodology.md", kind: "reference" },
+ { path: "scripts/fetch.ts", kind: "script" },
+ ],
+ iconUrl: null,
+ color: "#6366f1",
+ tagline: "Multi-source research with citation-grade synthesis.",
+ authorName: "Astra",
+ homepageUrl: null,
+ categories: ["research", "writing"],
+ sharingScope: "company",
+ publicShareToken: null,
+ forkedFromSkillId: null,
+ forkedFromCompanyId: null,
+ starCount: 211,
+ installCount: 96,
+ forkCount: 17,
+ currentVersionId: "v-2",
+ metadata: null,
+ createdAt: NOW,
+ updatedAt: NOW,
+ attachedAgentCount: 4,
+ usedByAgents: [
+ { id: "a-1", name: "Astra", urlKey: "astra", adapterType: "process", desired: true, actualState: null, versionId: null },
+ { id: "a-2", name: "Scout", urlKey: "scout", adapterType: "http", desired: true, actualState: null, versionId: null },
+ { id: "a-3", name: "Quill", urlKey: "quill", adapterType: "process", desired: true, actualState: null, versionId: null },
+ { id: "a-4", name: "Marlow", urlKey: "marlow", adapterType: "process", desired: true, actualState: null, versionId: null },
+ ],
+ editable: true,
+ editableReason: null,
+ sourceLabel: "GitHub",
+ sourceBadge: "github",
+ sourcePath: "github.com/paperclipai/skills/tree/main/deep-research",
+ currentVersion: {
+ id: "v-2",
+ companyId: "company-1",
+ companySkillId: "skill-1",
+ revisionNumber: 2,
+ label: "tighten verifier",
+ fileInventory: [],
+ authorAgentId: "a-1",
+ authorUserId: null,
+ createdAt: NOW,
+ },
+ starredByCurrentActor: true,
+};
+
+const MOCK_VERSIONS: CompanySkillVersion[] = [
+ {
+ id: "v-2",
+ companyId: "company-1",
+ companySkillId: "skill-1",
+ revisionNumber: 2,
+ label: "tighten verifier",
+ fileInventory: [{ path: "SKILL.md", kind: "skill", content: "# v2" }],
+ authorAgentId: "a-1",
+ authorUserId: null,
+ createdAt: NOW,
+ },
+ {
+ id: "v-1",
+ companyId: "company-1",
+ companySkillId: "skill-1",
+ revisionNumber: 1,
+ label: null,
+ fileInventory: [{ path: "SKILL.md", kind: "skill", content: "# v1" }],
+ authorAgentId: "a-1",
+ authorUserId: null,
+ createdAt: NOW,
+ },
+];
+
+const MOCK_FILE: CompanySkillFileDetail = {
+ skillId: "skill-1",
+ path: "SKILL.md",
+ kind: "skill",
+ content: "---\nname: deep-research\n---\n\n# deep-research\n\nFan out, verify, synthesize.",
+ language: "markdown",
+ markdown: true,
+ editable: true,
+};
+
+// Agents available to attach, including a paused one to exercise the badge.
+const MOCK_ATTACH_AGENTS = [
+ { id: "a-1", name: "Astra", adapterType: "process", supportsSkills: true, required: false, icon: "telescope", paused: false },
+ { id: "a-2", name: "Scout", adapterType: "http", supportsSkills: true, required: false, icon: "compass", paused: false },
+ { id: "a-3", name: "Quill", adapterType: "process", supportsSkills: true, required: false, icon: "feather", paused: true },
+ { id: "a-4", name: "Marlow", adapterType: "process", supportsSkills: true, required: false, icon: "bot", paused: false },
+ { id: "a-5", name: "Pixel", adapterType: "process", supportsSkills: true, required: false, icon: "palette", paused: false },
+ { id: "a-6", name: "Forge", adapterType: "process", supportsSkills: false, required: false, icon: "hammer", paused: false },
+];
+
+function SkillDetailHarness({ initialTab = "overview" as DetailTab }: { initialTab?: DetailTab }) {
+ const [activeTab, setActiveTab] = useState(initialTab);
+ const [attachOpen, setAttachOpen] = useState(false);
+ const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
+ const [editMode, setEditMode] = useState(false);
+ const [draft, setDraft] = useState(MOCK_FILE.content);
+
+ return (
+ {}}
+ savePending={false}
+ versions={MOCK_VERSIONS}
+ versionsLoading={false}
+ attachAgents={MOCK_ATTACH_AGENTS}
+ attachPopoverOpen={attachOpen}
+ setAttachPopoverOpen={setAttachOpen}
+ onSubmitAttach={() => setAttachOpen(false)}
+ attachPending={false}
+ expandedDirs={new Set()}
+ onToggleDir={() => {}}
+ onSelectPath={() => {}}
+ updateStatus={{
+ supported: true,
+ reason: null,
+ trackingRef: "main",
+ currentRef: "a1b2c3d4e5f6",
+ latestRef: "a1b2c3d4e5f6",
+ hasUpdate: false,
+ installedHash: null,
+ originHash: null,
+ userModifiedAt: null,
+ updateHoldReason: null,
+ auditVerdict: null,
+ auditCodes: [],
+ }}
+ updateStatusLoading={false}
+ onCheckUpdates={() => {}}
+ checkUpdatesPending={false}
+ onInstallUpdate={() => {}}
+ installUpdatePending={false}
+ onToggleStar={() => {}}
+ starPending={false}
+ onFork={() => {}}
+ onUpdateSharingScope={() => {}}
+ updateSharingPending={false}
+ onDelete={() => {}}
+ deletePending={false}
+ />
+ );
+}
+
+const meta: Meta = {
+ title: "Skills Store/Skill detail",
+ component: SkillDetailHarness,
+ parameters: { layout: "fullscreen" },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Overview: Story = { args: { initialTab: "overview" } };
+export const AgentsTab: Story = { args: { initialTab: "agents" } };
+export const FilesTab: Story = { args: { initialTab: "files" } };
+export const VersionsTab: Story = { args: { initialTab: "versions" } };