import React, { useMemo, useState } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { useTranslation } from 'react-i18next'; import { normalizeInlineCodeFences } from '../../utils/chatFormatting'; import { copyTextToClipboard } from '../../../../utils/clipboard'; import { usePaletteOps } from '../../../../contexts/PaletteOpsContext'; import { useTheme } from '../../../../contexts/ThemeContext'; type MarkdownProps = { children: React.ReactNode; className?: string; }; // Links to the wider web (or in-page anchors) keep normal browser navigation; // everything else is treated as a workspace file reference. const isExternalHref = (href?: string): boolean => !!href && (/^(https?:|mailto:|tel:|data:)/i.test(href) || href.startsWith('#')); // Strip a trailing `:line` / `:line:col` suffix (e.g. `src/foo.ts:130`). const stripLineSuffix = (value: string): string => value.replace(/:\d+(?::\d+)?$/, ''); // A usable file path contains a separator or a filename with an extension. const looksLikeFilePath = (value?: string): value is string => { if (!value) { return false; } const cleaned = stripLineSuffix(value.trim()); if (!cleaned || cleaned === '#') { return false; } return /[\\/]/.test(cleaned) || /\.[a-z0-9]+$/i.test(cleaned); }; // Extract plain text from link children so a reference rendered only as link // text (e.g. `[src/foo.ts]()` with an empty href) can still be opened. const childrenToText = (children: React.ReactNode): string => { if (typeof children === 'string' || typeof children === 'number') { return String(children); } if (Array.isArray(children)) { return children.map(childrenToText).join(''); } if (React.isValidElement(children)) { return childrenToText((children.props as { children?: React.ReactNode }).children); } return ''; }; type CodeBlockProps = { node?: any; inline?: boolean; className?: string; children?: React.ReactNode; }; const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockProps) => { const { t } = useTranslation('chat'); const { isDarkMode } = useTheme(); const [copied, setCopied] = useState(false); const raw = Array.isArray(children) ? children.join('') : String(children ?? ''); const looksMultiline = /[\r\n]/.test(raw); const inlineDetected = inline || (node && node.type === 'inlineCode'); const shouldInline = inlineDetected || !looksMultiline; if (shouldInline) { return ( {children} ); } const match = /language-(\w+)/.exec(className || ''); const language = match ? match[1] : 'text'; return (
{language && language !== 'text' && (
{language}
)} {raw}
); }; const markdownComponents = { code: CodeBlock, // CodeBlock renders its own syntax-highlighted
; this passthrough stops
  // react-markdown (and Tailwind Typography) from wrapping it in a second,
  // dark-themed 
 shell that would frame the block.
  pre: ({ children }: { children?: React.ReactNode }) => <>{children},
  blockquote: ({ children }: { children?: React.ReactNode }) => (
    
{children}
), p: ({ children }: { children?: React.ReactNode }) =>
{children}
, table: ({ children }: { children?: React.ReactNode }) => (
{children}
), thead: ({ children }: { children?: React.ReactNode }) => {children}, th: ({ children }: { children?: React.ReactNode }) => ( {children} ), td: ({ children }: { children?: React.ReactNode }) => ( {children} ), }; export function Markdown({ children, className }: MarkdownProps) { const content = normalizeInlineCodeFences(String(children ?? '')); const remarkPlugins = useMemo(() => [remarkGfm, remarkMath], []); const rehypePlugins = useMemo(() => [rehypeKatex], []); const { openFileInEditor } = usePaletteOps(); const components = useMemo( () => ({ ...markdownComponents, a: ({ href, children: linkChildren }: { href?: string; children?: React.ReactNode }) => { // Prefer the href when it is a real path; otherwise fall back to the // link text, since models often emit `[src/foo.ts]()` with an empty href. const linkText = childrenToText(linkChildren); const fileRef = looksLikeFilePath(href) ? href : looksLikeFilePath(linkText) ? linkText : undefined; if (fileRef && !isExternalHref(href)) { return ( { event.preventDefault(); openFileInEditor(stripLineSuffix(fileRef)); }} > {linkChildren} ); } return ( {linkChildren} ); }, }), [openFileInEditor], ); return (
{content}
); }