diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 486eac08..0c15d5fa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -516,8 +516,8 @@ importers:
specifier: ^5.1.0
version: 5.2.1
hermes-paperclip-adapter:
- specifier: ^0.2.0
- version: 0.2.0
+ specifier: 0.1.1
+ version: 0.1.1
jsdom:
specifier: ^28.1.0
version: 28.1.0(@noble/hashes@2.0.1)
@@ -654,9 +654,9 @@ importers:
cmdk:
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- hermes-paperclip-adapter:
- specifier: ^0.2.0
- version: 0.2.0
+ diff:
+ specifier: ^8.0.4
+ version: 8.0.4
lexical:
specifier: 0.35.0
version: 0.35.0
@@ -691,6 +691,9 @@ importers:
'@tailwindcss/vite':
specifier: ^4.0.7
version: 4.1.18(vite@6.4.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
+ '@types/diff':
+ specifier: ^8.0.0
+ version: 8.0.0
'@types/node':
specifier: ^25.2.3
version: 25.2.3
@@ -2118,8 +2121,8 @@ packages:
'@open-draft/deferred-promise@2.2.0':
resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
- '@paperclipai/adapter-utils@2026.325.0':
- resolution: {integrity: sha512-YDVSAgjkeJ0PvxXDJVN9MZDX7oYRzidLtGHmGgRGd6gSk/bF2ygAKvND4FI1YxDc/cRLQjqAFCpCYaC/9wqIEA==}
+ '@paperclipai/adapter-utils@0.3.1':
+ resolution: {integrity: sha512-W66k+hJkQE8ma0asM/Sd90AC8HHy/BLG/sd0aOC+rDWw+gOasQyUkTnDoPv1zhQuTyKEEvLFV6ByOOKqEiAz/A==}
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
@@ -3456,6 +3459,10 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+ '@types/diff@8.0.0':
+ resolution: {integrity: sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==}
+ deprecated: This is a stub types definition. diff provides its own type definitions, so you do not need this installed.
+
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@@ -4195,6 +4202,10 @@ packages:
resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==}
engines: {node: '>=0.3.1'}
+ diff@8.0.4:
+ resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
+ engines: {node: '>=0.3.1'}
+
dompurify@3.3.2:
resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==}
engines: {node: '>=20'}
@@ -4565,8 +4576,8 @@ packages:
help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
- hermes-paperclip-adapter@0.2.0:
- resolution: {integrity: sha512-6CP5vxfvY4jY9XJK5zu4ZUL9aB7HHNtEMk6q7m1Pu9Gzoby1Vx5VNmVqte3NUO+1cvVK9Arj1f67xLagWkbo5Q==}
+ hermes-paperclip-adapter@0.1.1:
+ resolution: {integrity: sha512-kbdX349VxExSkVL8n4RwTpP9fUBf2yWpsTsJp02X12A9NynRJatlpYqt0vEkFyE/X7qEXqdJvpBm9tlvUHahsA==}
engines: {node: '>=20.0.0'}
html-encoding-sniffer@6.0.0:
@@ -7943,7 +7954,7 @@ snapshots:
'@open-draft/deferred-promise@2.2.0': {}
- '@paperclipai/adapter-utils@2026.325.0': {}
+ '@paperclipai/adapter-utils@0.3.1': {}
'@paralleldrive/cuid2@2.3.1':
dependencies:
@@ -9411,6 +9422,10 @@ snapshots:
'@types/deep-eql@4.0.2': {}
+ '@types/diff@8.0.0':
+ dependencies:
+ diff: 8.0.4
+
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.8
@@ -10146,6 +10161,8 @@ snapshots:
diff@5.2.2: {}
+ diff@8.0.4: {}
+
dompurify@3.3.2:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -10560,9 +10577,9 @@ snapshots:
help-me@5.0.0: {}
- hermes-paperclip-adapter@0.2.0:
+ hermes-paperclip-adapter@0.1.1:
dependencies:
- '@paperclipai/adapter-utils': 2026.325.0
+ '@paperclipai/adapter-utils': 0.3.1
picocolors: 1.1.1
html-encoding-sniffer@6.0.0(@noble/hashes@2.0.1):
diff --git a/ui/package.json b/ui/package.json
index fd85ab89..4e0041c2 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -30,7 +30,6 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@lexical/link": "0.35.0",
- "lexical": "0.35.0",
"@mdxeditor/editor": "^3.52.4",
"@paperclipai/adapter-claude-local": "workspace:*",
"@paperclipai/adapter-codex-local": "workspace:*",
@@ -49,6 +48,8 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+ "diff": "^8.0.4",
+ "lexical": "0.35.0",
"lucide-react": "^0.574.0",
"mermaid": "^11.12.0",
"radix-ui": "^1.4.3",
@@ -61,6 +62,7 @@
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.7",
+ "@types/diff": "^8.0.0",
"@types/node": "^25.2.3",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
diff --git a/ui/src/pages/SkillDetail.test.tsx b/ui/src/pages/SkillDetail.test.tsx
new file mode 100644
index 00000000..ca717f92
--- /dev/null
+++ b/ui/src/pages/SkillDetail.test.tsx
@@ -0,0 +1,81 @@
+// @vitest-environment node
+
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+vi.mock("react-router-dom", () => ({
+ Link: ({ to, children, ...props }: { to: string; children: React.ReactNode; [k: string]: unknown }) => (
+ {children}
+ ),
+ useParams: () => ({ skillId: "test-source%2Ftest-skill" }),
+ useNavigate: () => () => {},
+}));
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: ({ queryKey }: { queryKey: unknown[] }) => {
+ if (
+ Array.isArray(queryKey) &&
+ (queryKey[3] === "versions" || String(queryKey[queryKey.length - 1]) === "versions")
+ ) {
+ return { data: [], isLoading: false, isError: false };
+ }
+ return {
+ data: {
+ id: "test-source/test-skill",
+ name: "Test Skill",
+ description: "A test description",
+ sourceId: "test-source",
+ category: "testing",
+ activeVersionId: null,
+ removedAt: null,
+ averageRating: 4.2,
+ ratingCount: 10,
+ },
+ isLoading: false,
+ isError: false,
+ };
+ },
+ useQueryClient: () => ({ invalidateQueries: () => {} }),
+ useMutation: () => ({ mutate: () => {}, isPending: false }),
+}));
+
+vi.mock("../context/BreadcrumbContext", () => ({
+ useBreadcrumbs: () => ({ setBreadcrumbs: () => {} }),
+}));
+
+vi.mock("../context/CompanyContext", () => ({
+ useCompany: () => ({ selectedCompany: { id: "test", name: "Test Workspace" } }),
+}));
+
+vi.mock("../context/ToastContext", () => ({
+ useToast: () => ({ pushToast: () => {} }),
+}));
+
+vi.mock("../context/SidebarContext", () => ({
+ useSidebar: () => ({ isMobile: false }),
+}));
+
+import { SkillDetail } from "./SkillDetail";
+
+describe("SkillDetail", () => {
+ it("renders skill name heading", () => {
+ const html = renderToStaticMarkup(
+ {parts.map((part, i) => (
+
+ {part.value}
+
+ ))}
+
+ );
+}
+
+/* ------------------------------------------------------------------ */
+/* SkillDetail page */
+/* ------------------------------------------------------------------ */
+
+export function SkillDetail() {
+ const { skillId: rawSkillId } = useParams<{ skillId: string }>();
+ const skillId = rawSkillId ? decodeURIComponent(rawSkillId) : "";
+
+ const { selectedCompany } = useCompany();
+ const { setBreadcrumbs } = useBreadcrumbs();
+ const { pushToast } = useToast();
+ const queryClient = useQueryClient();
+
+ const [detailTab, setDetailTab] = useState("overview");
+ const [versionA, setVersionA] = useState+ Failed to load skills. Check that the skill registry backend is running and try again. +
+Skill not found.
+{skill.description}
+ )} + + {/* Detail tabs */} +No versions fetched yet.
+ ) : ( ++ Version diff requires file content endpoint (coming in a future update). +
++ Select two versions above to compare them. +
+ )} +