Skip to content

Commit a791d26

Browse files
cursoragentmsukkari
andcommitted
feat(web): Add Linear issue link card UI for chat responses
- Add Linear logo SVG asset - Create LinearIssueCard component with card-style UI showing: - Linear logo - Issue identifier (e.g. SOU-818) in monospace font - Humanized title from URL slug - External link icon - Add custom anchor renderer to MarkdownRenderer that detects Linear issue URLs and renders them as cards instead of plain links - Regular links continue to work normally with new-tab behavior Co-authored-by: Michael Sukkarieh <msukkari@users.noreply.github.com>
1 parent 2fa86ff commit a791d26

File tree

3 files changed

+56
-0
lines changed

3 files changed

+56
-0
lines changed

packages/web/public/linear.svg

Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client';
2+
3+
import { cn } from '@/lib/utils';
4+
import { ExternalLinkIcon } from 'lucide-react';
5+
import Image from 'next/image';
6+
import linearLogo from '@/public/linear.svg';
7+
8+
interface LinearIssueCardProps {
9+
identifier: string;
10+
title: string;
11+
href: string;
12+
className?: string;
13+
}
14+
15+
export const LinearIssueCard = ({ identifier, title, href, className }: LinearIssueCardProps) => {
16+
return (
17+
<a
18+
href={href}
19+
target="_blank"
20+
rel="noopener noreferrer"
21+
className={cn(
22+
"not-prose inline-flex items-center gap-2 px-2.5 py-1.5 rounded-md border border-border bg-muted hover:bg-accent transition-colors text-sm no-underline",
23+
className
24+
)}
25+
>
26+
<Image
27+
src={linearLogo}
28+
alt="Linear"
29+
className="w-4 h-4 shrink-0 dark:invert"
30+
/>
31+
<span className="font-mono font-semibold text-foreground">{identifier}</span>
32+
<span className="text-muted-foreground truncate max-w-[240px] capitalize">{title}</span>
33+
<ExternalLinkIcon className="w-3 h-3 shrink-0 text-muted-foreground" />
34+
</a>
35+
);
36+
};

packages/web/src/features/chat/components/chatThread/markdownRenderer.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ import remarkGfm from 'remark-gfm';
1717
import type { PluggableList, Plugin } from "unified";
1818
import { visit } from 'unist-util-visit';
1919
import { CodeBlock } from './codeBlock';
20+
import { LinearIssueCard } from './linearIssueCard';
2021
import { FILE_REFERENCE_REGEX } from '@/features/chat/constants';
2122
import { createFileReference } from '@/features/chat/utils';
2223
import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants';
2324
import isEqual from "fast-deep-equal/react";
2425

26+
const LINEAR_ISSUE_URL_REGEX = /^https:\/\/linear\.app\/[^/]+\/issue\/([A-Z]+-\d+)\/([^/\s"]+)$/;
27+
2528
export const REFERENCE_PAYLOAD_ATTRIBUTE = 'data-reference-payload';
2629

2730
const annotateCodeBlocks: Plugin<[], Root> = () => {
@@ -228,6 +231,19 @@ const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererPro
228231

229232
}, [router]);
230233

234+
const renderAnchor = useCallback(({ href, children, ...rest }: React.JSX.IntrinsicElements['a']) => {
235+
if (href) {
236+
const match = LINEAR_ISSUE_URL_REGEX.exec(href);
237+
if (match) {
238+
const identifier = match[1];
239+
const titleSlug = match[2];
240+
const title = titleSlug.replace(/-/g, ' ');
241+
return <LinearIssueCard identifier={identifier} title={title} href={href} />;
242+
}
243+
}
244+
return <a href={href} target="_blank" rel="noopener noreferrer" {...rest}>{children}</a>;
245+
}, []);
246+
231247
return (
232248
<div
233249
ref={ref}
@@ -239,6 +255,7 @@ const MarkdownRendererComponent = forwardRef<HTMLDivElement, MarkdownRendererPro
239255
components={{
240256
pre: renderPre,
241257
code: renderCode,
258+
a: renderAnchor,
242259
}}
243260
>
244261
{content}

0 commit comments

Comments
 (0)