mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
Apply PAP-10907 round-3 Skills Store changes
- Discovery grid: one fewer column per breakpoint (min track 15rem -> 19rem) - Detail header: skill icon top-aligned, GitHub-style "by <author>" line, description clamped to 4 lines with a "View all" toggle - Drop the GitHub source-hash chip under the description (already shown in the Updates section); Updates uses a Pin icon instead of the word "Pin" - Sidebar: new Source section below attached agents (org/path, linked) - Fork/Create wizard: cap dialog + content textarea height so the modal no longer overflows the viewport - Retire the legacy split catalog view: catalog/bundled cards open the install dialog inline and legacy ?view=catalog deep links redirect to the new discovery surface Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
94c27257ec
commit
c9b1fb82d1
1 file changed
+115
-22
+115
-22
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState, type SVGProps } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type SVGProps } from "react";
|
||||
import { Link, useNavigate, useParams, useSearchParams } from "@/lib/router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type {
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
Paperclip,
|
||||
Pause,
|
||||
Pencil,
|
||||
Pin,
|
||||
Plus,
|
||||
Copy,
|
||||
RefreshCw,
|
||||
@@ -1191,7 +1192,7 @@ export function DiscoveryGrid({
|
||||
{sourceFilteredCards.length} {sourceFilteredCards.length === 1 ? "skill" : "skills"}
|
||||
{activeCategory ? <span className="capitalize"> · {activeCategory}</span> : null}
|
||||
</p>
|
||||
<div className="grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(15rem,1fr))]">
|
||||
<div className="grid gap-3 [grid-template-columns:repeat(auto-fill,minmax(19rem,1fr))]">
|
||||
{sourceFilteredCards.map((card) => (
|
||||
<SkillCard key={card.key} card={card} onOpen={onOpenCard} />
|
||||
))}
|
||||
@@ -1435,7 +1436,7 @@ function NewSkillWizard({
|
||||
<Textarea
|
||||
value={draft.markdown}
|
||||
onChange={(event) => patchDraft({ markdown: event.target.value })}
|
||||
className="min-h-[22rem] font-mono text-xs"
|
||||
className="h-[clamp(14rem,45vh,28rem)] resize-y font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -2604,6 +2605,19 @@ export function SkillDetailPage({
|
||||
}) {
|
||||
const [diffOpen, setDiffOpen] = useState(false);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
// Top-level description is clamped to four lines; "View all" expands it. We
|
||||
// only surface the toggle when the text actually overflows the clamp.
|
||||
const descriptionRef = useRef<HTMLParagraphElement | null>(null);
|
||||
const [descExpanded, setDescExpanded] = useState(false);
|
||||
const [descClamped, setDescClamped] = useState(false);
|
||||
useEffect(() => {
|
||||
const el = descriptionRef.current;
|
||||
if (!el || descExpanded) return;
|
||||
setDescClamped(el.scrollHeight - el.clientHeight > 1);
|
||||
}, [detail?.description, detail?.tagline, detail?.id, descExpanded]);
|
||||
useEffect(() => {
|
||||
setDescExpanded(false);
|
||||
}, [detail?.id]);
|
||||
const sortedVersions = [...versions].sort((a, b) => b.revisionNumber - a.revisionNumber);
|
||||
const [leftVersionId, setLeftVersionId] = useState<string | null>(null);
|
||||
const [rightVersionId, setRightVersionId] = useState<string | null>(null);
|
||||
@@ -2639,6 +2653,17 @@ export function SkillDetailPage({
|
||||
// Look up the richer agent record (icon, paused) for agents using this skill.
|
||||
const attachAgentMetaById = new Map(attachAgents.map((agent) => [agent.id, agent]));
|
||||
|
||||
// Sidebar provenance: where this skill came from (org / path), linked when the
|
||||
// locator resolves to a URL (PAP-10907).
|
||||
const sourceLocatorText = skill.sourcePath || skill.sourceLocator || null;
|
||||
const sourceHref =
|
||||
skill.homepageUrl
|
||||
?? (sourceLocatorText && /^(https?:\/\/|[\w.-]+\.[a-z]{2,}\/)/i.test(sourceLocatorText)
|
||||
? sourceLocatorText.startsWith("http")
|
||||
? sourceLocatorText
|
||||
: `https://${sourceLocatorText}`
|
||||
: null);
|
||||
|
||||
function renderFilesBody() {
|
||||
return (
|
||||
<div className="grid min-h-[560px] gap-0 lg:grid-cols-[13rem_minmax(0,1fr)]">
|
||||
@@ -2886,7 +2911,7 @@ export function SkillDetailPage({
|
||||
<div className="border-b border-border px-4 py-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<SkillCardIcon
|
||||
card={{
|
||||
key: detail.key,
|
||||
@@ -2927,8 +2952,33 @@ export function SkillDetailPage({
|
||||
<TooltipContent>Installed from {source.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* GitHub-style "by" attribution sits directly under the title. */}
|
||||
{detail.authorName ? (
|
||||
<p className="mt-0.5 text-sm text-muted-foreground">
|
||||
by <span className="text-foreground">{detail.authorName}</span>
|
||||
</p>
|
||||
) : null}
|
||||
{subtitleText ? (
|
||||
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">{subtitleText}</p>
|
||||
<div className="mt-1 max-w-2xl">
|
||||
<p
|
||||
ref={descriptionRef}
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground",
|
||||
!descExpanded && "line-clamp-4",
|
||||
)}
|
||||
>
|
||||
{subtitleText}
|
||||
</p>
|
||||
{descClamped ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDescExpanded((value) => !value)}
|
||||
className="mt-0.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
{descExpanded ? "Show less" : "View all"}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2936,17 +2986,6 @@ export function SkillDetailPage({
|
||||
{detail.categories.slice(0, 4).map((category) => (
|
||||
<SkillCategoryChip key={category} label={category} />
|
||||
))}
|
||||
{detail.sourceType === "github" ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Github className="h-3 w-3" aria-hidden="true" />
|
||||
{currentPin ?? "untracked"}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{currentPin ? `Source pin ${currentPin}` : "No source pin tracked"}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{/* GitHub-style social proof, top-right: installs · stars · fork.
|
||||
@@ -3055,6 +3094,36 @@ export function SkillDetailPage({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Provenance: where this skill came from, with org/path linked when
|
||||
available. Bundled/catalog skills surface their source label too
|
||||
(PAP-10907). */}
|
||||
<section>
|
||||
<div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">Source</div>
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<SourceIcon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-foreground">{source.label}</div>
|
||||
{sourceLocatorText ? (
|
||||
sourceHref ? (
|
||||
<a
|
||||
href={sourceHref}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-0.5 inline-flex max-w-full items-center gap-1 break-all text-xs text-muted-foreground no-underline transition-colors hover:text-foreground"
|
||||
>
|
||||
<span className="truncate">{sourceLocatorText}</span>
|
||||
<ExternalLink className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
</a>
|
||||
) : (
|
||||
<div className="mt-0.5 break-all text-xs text-muted-foreground">{sourceLocatorText}</div>
|
||||
)
|
||||
) : (
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">{source.managedLabel}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Revision / update controls sit under Agents, above the config gear
|
||||
(PAP-10907 F). Only GitHub-sourced skills can pull updates. */}
|
||||
{detail.sourceType === "github" ? (
|
||||
@@ -3062,7 +3131,12 @@ export function SkillDetailPage({
|
||||
<div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">Updates</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="shrink-0 uppercase tracking-wide">Pin</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Pin className="h-3.5 w-3.5 shrink-0" aria-label="Pinned source revision" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Pinned source revision</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="truncate font-mono text-foreground">{currentPin ?? "untracked"}</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="w-full" onClick={onCheckUpdates} disabled={checkUpdatesPending || updateStatusLoading}>
|
||||
@@ -3614,6 +3688,22 @@ export function CompanySkills() {
|
||||
]);
|
||||
}, [routeSkillToken, setBreadcrumbs]);
|
||||
|
||||
// The old split catalog view no longer exists — bundled/catalog skills live in
|
||||
// the discovery grid and install dialog now. Strip legacy `view=catalog` /
|
||||
// `catalog` params so stale deep links land on the new surface (PAP-10907).
|
||||
useEffect(() => {
|
||||
if (!searchParams.has("view") && !searchParams.has("catalog")) return;
|
||||
setSearchParams(
|
||||
(current) => {
|
||||
const next = new URLSearchParams(current);
|
||||
next.delete("view");
|
||||
next.delete("catalog");
|
||||
return next;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
const skillsQuery = useQuery({
|
||||
queryKey: queryKeys.companySkills.list(selectedCompanyId ?? ""),
|
||||
queryFn: () => companySkillsApi.list(selectedCompanyId!),
|
||||
@@ -4203,16 +4293,19 @@ export function CompanySkills() {
|
||||
importSkill.mutate(trimmedSource);
|
||||
}
|
||||
|
||||
// Opening a card drops out of the discovery grid: installed skills go to their
|
||||
// detail route, catalog-only skills open the catalog detail surface.
|
||||
// Opening a card stays inside the new store: installed skills go to their
|
||||
// detail route; catalog/bundled skills open the install dialog inline. The
|
||||
// legacy split catalog view is gone (PAP-10907).
|
||||
function openDiscoveryCard(card: DiscoveryCard) {
|
||||
if (card.skillId) {
|
||||
navigate(routeForSkillId(card.skillId));
|
||||
return;
|
||||
}
|
||||
if (card.catalogRef) {
|
||||
setViewParam("catalog");
|
||||
selectCatalog(card.catalogRef);
|
||||
const catalogSkill = (catalogListQuery.data ?? []).find(
|
||||
(entry) => entry.id === card.catalogRef || entry.key === card.key,
|
||||
);
|
||||
if (catalogSkill) openInstallDialog(catalogSkill);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4331,7 +4424,7 @@ export function CompanySkills() {
|
||||
/>
|
||||
|
||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogContent className="flex max-h-[85vh] flex-col overflow-y-auto sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{createDraft.forkedFromSkillId ? "Fork skill" : "Create a new skill"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
Reference in new issue
Block a user