From 66e1ea6090e5e97a5fa6c17aba376a1d57bc6e66 Mon Sep 17 00:00:00 2001 From: milan Date: Fri, 31 Jul 2026 21:58:27 +0000 Subject: [PATCH] fix(ui): render non-browsable skill sources as text on the detail page Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../claude_code_plugins/skill_detail.test.tsx | 42 +++++++++++++++++++ .../claude_code_plugins/skill_detail.tsx | 21 +++++++--- 2 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.test.tsx diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.test.tsx b/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.test.tsx new file mode 100644 index 00000000000..98ae4a44ae6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.test.tsx @@ -0,0 +1,42 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { Plugin } from "./types"; + +import SkillDetail from "./skill_detail"; + +const buildSkill = (source: Plugin["source"]): Plugin => ({ + id: "plugin-id", + name: "my-skill", + source, + enabled: true, +}); + +describe("SkillDetail source", () => { + it("links a github source to the repository", () => { + render(); + expect(screen.getByRole("link", { name: /github.com\/org\/repo/ })).toHaveAttribute( + "href", + "https://github.com/org/repo", + ); + }); + + it("renders an ssh clone url as plain text instead of an unusable link", () => { + render( + , + ); + expect(screen.getByText("git@ghe.example.com:org/repo.git")).toBeInTheDocument(); + expect(screen.queryByRole("link", { name: /ghe.example.com/ })).not.toBeInTheDocument(); + }); + + it("renders an ssh git-subdir source as plain text without a tree path", () => { + render( + , + ); + expect(screen.getByText("git@ghe.example.com:org/repo.git")).toBeInTheDocument(); + expect(screen.queryByRole("link", { name: /ghe.example.com/ })).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx b/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx index fe001641135..0f25b8ad515 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/skill_detail.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { ArrowLeftOutlined, CopyOutlined, CheckOutlined, LinkOutlined } from "@ant-design/icons"; -import { buildMarketplaceSettingsSnippet, formatInstallCommand } from "./helpers"; +import { buildMarketplaceSettingsSnippet, formatInstallCommand, getSourceLink } from "./helpers"; import { Plugin } from "./types"; interface SkillDetailProps { @@ -21,13 +21,15 @@ const SkillDetail: React.FC = ({ skill, onBack }) => { setTimeout(() => setCopiedKey(null), 2000); }; + const sourceLink = getSourceLink(skill.source); const sourceUrl = (() => { const src = skill.source; - if (src.source === "github" && src.repo) return `https://github.com/${src.repo}`; - if (src.source === "git-subdir" && src.url) return src.path ? `${src.url}/tree/main/${src.path}` : src.url; - if (src.source === "url" && src.url) return src.url; - return null; + if (sourceLink && src.source === "git-subdir" && src.path) { + return `${sourceLink}/tree/main/${src.path}`; + } + return sourceLink; })(); + const sourceText = skill.source.url ?? sourceUrl; const installCommand = formatInstallCommand(skill); @@ -146,7 +148,7 @@ const SkillDetail: React.FC = ({ skill, onBack }) => { - {sourceUrl && ( + {sourceUrl ? ( + ) : ( + sourceText && ( +
+
Source
+
{sourceText}
+
+ ) )} {skill.keywords && skill.keywords.length > 0 && (