/// import { getAgentDir, SettingsManager, type ExtensionContext, type MarkdownTransformContext, type MarkdownTransformer, type Theme, } from "@earendil-works/pi-coding-agent"; import { Marked, type MarkdownOptions } from "@earendil-works/pi-tui"; export type MermaidRenderingMode = "off" | "final" | "streaming"; export interface SharedMarkdownRendering { mermaidMode: MermaidRenderingMode; transformers: readonly MarkdownTransformer[]; options( messageType: MarkdownTransformContext["messageType"], isStreaming?: boolean, ): MarkdownOptions; } const markdownParser = new Marked(); interface MermaidArt { width: number; warnings: string[]; styled: Array>; } type MermaidRender = (source: string) => MermaidArt | null; let renderMermaid: MermaidRender | undefined; let mermaidLoad: Promise | undefined; export function preloadSharedMermaid(): Promise { mermaidLoad ??= import("grok-mermaid").then((mod) => { renderMermaid = mod.render; }); return mermaidLoad; } export function createSharedMarkdownRendering( ctx: ExtensionContext, theme: Theme, ): SharedMarkdownRendering { const settings = SettingsManager.create(ctx.cwd, getAgentDir(), { projectTrusted: ctx.isProjectTrusted(), }); const mermaidMode = settings.getMermaidRenderingMode(); if (mermaidMode !== "off") void preloadSharedMermaid(); const transformers = [createMermaidTransformer(mermaidMode, theme)]; return { mermaidMode, transformers, options: (messageType, isStreaming = false) => ({ renderLatex: true, transform: (markdown: string, availableWidth: number) => applyMarkdownTransformers(markdown, transformers, { messageType, isStreaming, availableWidth, }), }), }; } function applyMarkdownTransformers( markdown: string, transformers: readonly MarkdownTransformer[], context: MarkdownTransformContext, ): string { return transformers.reduce( (current, transformer) => transformer(current, context), markdown, ); } function createMermaidTransformer( mode: MermaidRenderingMode, theme: Theme, ): MarkdownTransformer { return (markdown: string, context: MarkdownTransformContext) => { if ( mode === "off" || context.messageType === "assistant-thinking" || (context.isStreaming && mode !== "streaming") ) return markdown; return markdownParser .lexer(markdown) .map((token) => transformToken(token, context.availableWidth, theme)) .join(""); }; } function renderMermaidToFit(source: string, availableWidth: number) { if (!renderMermaid) { void preloadSharedMermaid(); return undefined; } const art = renderMermaid(source); if (!art || art.width <= availableWidth) return art; // Horizontal flowcharts grow quickly with long code labels; preserve the // graph while changing only its presentation direction for narrow viewports. const vertical = source.replace( /^(\s*)(flowchart|graph)\s+(LR|RL)\b/im, "$1$2 TD", ); if (vertical === source) return art; const reflowed = renderMermaid(vertical); return reflowed && reflowed.width <= availableWidth ? reflowed : art; } function transformToken( token: { type: string; raw: string; lang?: string; text?: string }, availableWidth: number, theme: Theme, ): string { if ( token.type !== "code" || token.lang?.trim().split(/\s+/, 1)[0]?.toLowerCase() !== "mermaid" || typeof token.text !== "string" ) return token.raw; try { const art = renderMermaidToFit(token.text, availableWidth); if (!art || art.width > availableWidth) return token.raw; if (art.warnings.length > 0) { const extra = art.warnings.length > 1 ? ` (+${art.warnings.length - 1})` : ""; return `${token.raw}\n${codeSpan(theme.fg("warning", `Mermaid diagram not rendered: ${art.warnings[0]}${extra}`))} \n`; } return `${art.styled .map((row) => codeSpan(row.map((span) => styleSpan(span, theme)).join("")), ) .join(" \n")}\n`; } catch { return token.raw; } } function styleSpan(span: { cls: string; text: string }, theme: Theme): string { switch (span.cls) { case "border": return theme.fg("borderMuted", span.text); case "edge": return theme.fg("accent", span.text); case "edgeLabel": return theme.fg("muted", span.text); case "title": return theme.fg("accent", theme.bold(span.text)); case "text": return theme.fg("text", span.text); default: return span.text; } } function codeSpan(line: string): string { const content = line || " "; const longest = Math.max( 0, ...Array.from(content.matchAll(/`+/g), (match) => match[0].length), ); const fence = "`".repeat(longest + 1); const padding = content.startsWith("`") || content.endsWith("`") ? " " : ""; return `${fence}${padding}${content}${padding}${fence}`; }