"use client"; // MarkdownBlock — tiny zero-dep Markdown renderer. Handles the subset // the refiner emits: h1/h2/h3 headings, - / * bullets, 1. numbered // lists, `**bold**`, `` `code` ``, blank-line-separated paragraphs. // Not a general-purpose renderer — deliberately small to avoid a // react-markdown dep for one canvas surface. import React from "react"; const mono = "ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace"; type Block = | { kind: "h1" | "h2" | "h3"; text: string; id?: string } | { kind: "p"; text: string } | { kind: "ul"; items: string[] } | { kind: "ol"; items: string[] } | { kind: "code"; lang: string; text: string }; /** Stable slug for a heading, so the reader's outline can scroll to it. */ export function headingId(text: string, ordinal: number): string { const slug = text .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 60); return `h-${ordinal}-${slug || "section"}`; } /** The headings of a document, for an outline rail. */ export function outlineOf( md: string, ): Array<{ id: string; text: string; level: 1 | 2 | 3 }> { return parse(md).flatMap((b, idx) => b.kind === "h1" || b.kind === "h2" || b.kind === "h3" ? [ { id: b.id ?? headingId(b.text, idx), text: b.text, level: Number(b.kind.slice(1)) as 1 | 2 | 3, }, ] : [], ); } function parse(md: string): Block[] { const lines = md.replace(/\r\n/g, "\n").split("\n"); const blocks: Block[] = []; let i = 0; while (i < lines.length) { const line = lines[i]; const trimmed = line.trim(); if (!trimmed) { i++; continue; } // Fenced code block. Agent output is full of ```rust / ```toml // blocks; without this they render as mangled paragraphs. const fence = /^```([A-Za-z0-9_+-]*)\s*$/.exec(trimmed); if (fence) { const lang = fence[1] ?? ""; const body: string[] = []; i++; while (i < lines.length && !/^```\s*$/.test(lines[i].trim())) { body.push(lines[i]); i++; } i++; // consume the closing fence (or run off the end on an unclosed block) blocks.push({ kind: "code", lang, text: body.join("\n") }); continue; } // Headings const h = /^(#{1,6})\s+(.*)$/.exec(trimmed); if (h) { // h4-h6 are rare in agent output; render them as h3 rather than // dropping the text into a paragraph. const level = Math.min(h[1].length, 3) as 1 | 2 | 3; const text = h[2]; blocks.push({ kind: (`h${level}` as "h1" | "h2" | "h3"), text, id: headingId(text, blocks.length), }); i++; continue; } // Bullet list if (/^[-*]\s+/.test(trimmed)) { const items: string[] = []; while (i < lines.length && /^[-*]\s+/.test(lines[i].trim())) { items.push(lines[i].trim().replace(/^[-*]\s+/, "")); i++; } blocks.push({ kind: "ul", items }); continue; } // Numbered list if (/^\d+\.\s+/.test(trimmed)) { const items: string[] = []; while (i < lines.length && /^\d+\.\s+/.test(lines[i].trim())) { items.push(lines[i].trim().replace(/^\d+\.\s+/, "")); i++; } blocks.push({ kind: "ol", items }); continue; } // Paragraph — greedily accumulate until blank line or block boundary const paraLines: string[] = []; while ( i < lines.length && lines[i].trim() && !/^(#{1,6})\s+/.test(lines[i].trim()) && !/^```/.test(lines[i].trim()) && !/^[-*]\s+/.test(lines[i].trim()) && !/^\d+\.\s+/.test(lines[i].trim()) ) { paraLines.push(lines[i].trim()); i++; } if (paraLines.length) blocks.push({ kind: "p", text: paraLines.join(" ") }); } return blocks; } // Inline: **bold**, `code`. Simple sequential scan. function renderInline(text: string): React.ReactNode[] { const out: React.ReactNode[] = []; const re = /(\*\*[^*]+\*\*|`[^`]+`)/g; let last = 0; let m: RegExpExecArray | null; let key = 0; while ((m = re.exec(text)) !== null) { if (m.index > last) out.push(text.slice(last, m.index)); const tok = m[0]; if (tok.startsWith("**")) { out.push( {tok.slice(2, -2)} , ); } else { out.push( {tok.slice(1, -1)} , ); } last = m.index + tok.length; } if (last < text.length) out.push(text.slice(last)); return out; } export function MarkdownBlock({ source }: { source: string }) { const blocks = React.useMemo(() => parse(source), [source]); return (
{blocks.map((b, idx) => { if (b.kind === "h1") return (

{renderInline(b.text)}

); if (b.kind === "h2") return (

{renderInline(b.text)}

); if (b.kind === "h3") return (

{renderInline(b.text)}

); if (b.kind === "code") return (
              {b.lang && (
                
                  {b.lang}
                
              )}
              {b.text}
            
); if (b.kind === "p") return (

{renderInline(b.text)}

); if (b.kind === "ul") return ( ); if (b.kind === "ol") return (
    {b.items.map((it, i) => (
  1. {renderInline(it)}
  2. ))}
); return null; })}
); }