diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx index 84fcf0b9b..8dd10b3d5 100644 --- a/desktop/src/shared/ui/markdown.tsx +++ b/desktop/src/shared/ui/markdown.tsx @@ -1,4 +1,4 @@ -import { GitPullRequest } from "lucide-react"; +import { CircleDot, GitCommitHorizontal, GitPullRequest } from "lucide-react"; import * as React from "react"; import ReactMarkdown, { type Components } from "react-markdown"; import remarkBreaks from "remark-breaks"; @@ -15,6 +15,12 @@ import remarkMentions from "@/shared/lib/remarkMentions"; const GITHUB_PR_RE = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/; +const GITHUB_ISSUE_RE = + /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/; + +const GITHUB_COMMIT_RE = + /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/commit\/([0-9a-f]{7,40})\/?$/; + type MarkdownProps = { channelNames?: string[]; className?: string; @@ -46,25 +52,50 @@ function createMarkdownComponents( return { a: ({ children, href, ...props }) => { - const prMatch = href ? GITHUB_PR_RE.exec(href) : null; - if (prMatch) { - const [, owner, repo, number] = prMatch; - return ( - - - {href} - - ); + if (href) { + let Icon: React.ComponentType<{ className?: string }> | null = null; + let label: string | null = null; + + const prMatch = GITHUB_PR_RE.exec(href); + if (prMatch) { + const [, owner, repo, number] = prMatch; + Icon = GitPullRequest; + label = `${owner}/${repo}#${number}`; + } + + const issueMatch = !Icon ? GITHUB_ISSUE_RE.exec(href) : null; + if (issueMatch) { + const [, owner, repo, number] = issueMatch; + Icon = CircleDot; + label = `${owner}/${repo}#${number}`; + } + + const commitMatch = !Icon ? GITHUB_COMMIT_RE.exec(href) : null; + if (commitMatch) { + const [, owner, repo, sha] = commitMatch; + Icon = GitCommitHorizontal; + label = `${owner}/${repo}@${sha.slice(0, 7)}`; + } + + if (Icon && label) { + return ( + + + {href} + + ); + } } + return ( { + const issueUrl = "https://github.com/block/sprout/issues/99"; + const message = `See ${issueUrl} for context`; + + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + + const input = page.getByTestId("message-input"); + await input.fill(message); + await page.getByTestId("send-message").click(); + + const lastRow = page.getByTestId("message-row").last(); + + const issueChip = lastRow.locator("a", { hasText: "block/sprout#99" }); + await expect(issueChip).toBeVisible(); + await expect(issueChip).toHaveAttribute("href", issueUrl); + await expect(issueChip.locator("svg")).toBeVisible(); +}); + +test("GitHub commit URL renders as an inline smart chip", async ({ page }) => { + const commitUrl = + "https://github.com/block/sprout/commit/abc1234def5678901234567890abcdef12345678"; + const message = `Reverted in ${commitUrl}`; + + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + + const input = page.getByTestId("message-input"); + await input.fill(message); + await page.getByTestId("send-message").click(); + + const lastRow = page.getByTestId("message-row").last(); + + // Should show short SHA + const commitChip = lastRow.locator("a", { + hasText: "block/sprout@abc1234", + }); + await expect(commitChip).toBeVisible(); + await expect(commitChip).toHaveAttribute("href", commitUrl); + await expect(commitChip.locator("svg")).toBeVisible(); +}); + test("non-PR GitHub links render as regular links", async ({ page }) => { const repoUrl = "https://github.com/block/sprout"; const message = `Check out ${repoUrl}`;