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>
This commit is contained in:
milan 2026-07-31 21:58:27 +00:00
parent 1618617ea9
commit 66e1ea6090
2 changed files with 57 additions and 6 deletions

View file

@ -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(<SkillDetail skill={buildSkill({ source: "github", repo: "org/repo" })} onBack={vi.fn()} />);
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(
<SkillDetail skill={buildSkill({ source: "url", url: "git@ghe.example.com:org/repo.git" })} onBack={vi.fn()} />,
);
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(
<SkillDetail
skill={buildSkill({ source: "git-subdir", url: "git@ghe.example.com:org/repo.git", path: "plugins/x" })}
onBack={vi.fn()}
/>,
);
expect(screen.getByText("git@ghe.example.com:org/repo.git")).toBeInTheDocument();
expect(screen.queryByRole("link", { name: /ghe.example.com/ })).not.toBeInTheDocument();
});
});

View file

@ -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<SkillDetailProps> = ({ 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<SkillDetailProps> = ({ skill, onBack }) => {
</span>
</div>
{sourceUrl && (
{sourceUrl ? (
<div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>Source</div>
<a
@ -166,6 +168,13 @@ const SkillDetail: React.FC<SkillDetailProps> = ({ skill, onBack }) => {
<LinkOutlined style={{ fontSize: 11, flexShrink: 0 }} />
</a>
</div>
) : (
sourceText && (
<div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 12, color: "#5f6368", marginBottom: 4 }}>Source</div>
<div style={{ fontSize: 13, color: "#3c4043", wordBreak: "break-all" }}>{sourceText}</div>
</div>
)
)}
{skill.keywords && skill.keywords.length > 0 && (