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 (
-
-
-
- {owner}/{repo}#{number}
-
- {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 (
+
+
+
+ {label}
+
+ {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}`;