// v1.14.x-html-artifact-panes: extracted from MessageBubble.tsx so both the // in-chat bubble renderer and the MarkdownArtifactPane share the same Shiki + // remark-gfm + path-linkifier pipeline. Behavior preserved byte-for-byte from // the original MessageBubble.MarkdownBody helper (and its linkify helpers). import { Children, cloneElement, isValidElement } from 'react'; import type { ReactElement, ReactNode } from 'react'; import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { CodeBlock } from './CodeBlock'; import { sessionEvents } from '@/hooks/sessionEvents'; // Match path-shaped substrings ending in `.ext`. Additionally require a `/` // in the match to reduce false positives in prose (e.g. plain `foo.ts` won't // match, but `src/foo.ts` will). False positives at the edges are accepted // per Sam's design decision (2026-05-14). const PATH_REGEX = /([a-zA-Z0-9._/-]+\.[a-zA-Z0-9]+)/g; function isPathLike(s: string): boolean { return s.includes('/'); } function emitOpenFile(path: string): void { sessionEvents.emit({ type: 'open_file_in_browser', path }); } function linkifyPaths(text: string, keyPrefix: string): ReactNode { const out: ReactNode[] = []; let lastIdx = 0; let idx = 0; for (const match of text.matchAll(PATH_REGEX)) { const matchedText = match[0]; const start = match.index ?? 0; if (!isPathLike(matchedText)) continue; if (start > lastIdx) out.push(text.slice(lastIdx, start)); out.push( ); lastIdx = start + matchedText.length; idx += 1; } if (out.length === 0) return text; if (lastIdx < text.length) out.push(text.slice(lastIdx)); return out; } function linkifyChildren(children: ReactNode, keyPrefix = 'l'): ReactNode { const arr = Children.toArray(children); return arr.map((child, i) => { if (typeof child === 'string') { return ( {linkifyPaths(child, `${keyPrefix}-${i}`)} ); } if (isValidElement(child)) { const el = child as ReactElement<{ children?: ReactNode }>; if (el.type === 'code' || el.type === CodeBlock) return child; const grandchildren = el.props.children; if (grandchildren === undefined) return child; return cloneElement(el, { key: el.key ?? `linkified-${i}`, children: linkifyChildren(grandchildren, `${keyPrefix}-${i}`), }); } return child; }); } const codeRenderer = (props: { children?: unknown; className?: string }) => { const { children, className, ...rest } = props; const text = String(children ?? '').replace(/\n$/, ''); const langMatch = /language-([\w-]+)/.exec(className ?? ''); const isBlock = !!langMatch || text.includes('\n'); if (isBlock) { return ; } return ( {children as React.ReactNode} ); }; export function MarkdownRenderer({ content }: { content: string }) { return ( <>{children}, code: codeRenderer, a: ({ children, href }) => ( {children} ), ul: ({ children }) => ( ), ol: ({ children }) => (
    {children}
), li: ({ children }) =>
  • {linkifyChildren(children)}
  • , p: ({ children }) => (

    {linkifyChildren(children)}

    ), h1: ({ children }) =>

    {children}

    , h2: ({ children }) =>

    {children}

    , h3: ({ children }) =>

    {children}

    , blockquote: ({ children }) => (
    {children}
    ), table: ({ children }) => (
    {children}
    ), th: ({ children }) => ( {children} ), td: ({ children }) => ( {linkifyChildren(children)} ), }} > {content}
    ); }