diff --git a/src/renderer/components/MermaidRenderer.tsx b/src/renderer/components/MermaidRenderer.tsx
new file mode 100644
index 00000000..152dd7ec
--- /dev/null
+++ b/src/renderer/components/MermaidRenderer.tsx
@@ -0,0 +1,161 @@
+import React, { useEffect, useRef, useState } from 'react';
+import mermaid from 'mermaid';
+import DOMPurify from 'dompurify';
+
+interface MermaidRendererProps {
+ chart: string;
+ theme: any;
+}
+
+// Initialize mermaid with custom theme settings
+const initMermaid = (isDarkTheme: boolean) => {
+ mermaid.initialize({
+ startOnLoad: false,
+ theme: isDarkTheme ? 'dark' : 'default',
+ securityLevel: 'strict',
+ fontFamily: 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace',
+ flowchart: {
+ useMaxWidth: true,
+ htmlLabels: true,
+ curve: 'basis'
+ },
+ sequence: {
+ useMaxWidth: true,
+ diagramMarginX: 8,
+ diagramMarginY: 8
+ },
+ gantt: {
+ useMaxWidth: true
+ }
+ });
+};
+
+// Sanitize and parse SVG into safe DOM nodes
+const createSanitizedSvgElement = (svgString: string): Node | null => {
+ // First sanitize with DOMPurify configured for SVG
+ const sanitized = DOMPurify.sanitize(svgString, {
+ USE_PROFILES: { svg: true, svgFilters: true },
+ ADD_TAGS: ['foreignObject'],
+ ADD_ATTR: ['xmlns', 'xmlns:xlink', 'xlink:href', 'dominant-baseline', 'text-anchor'],
+ RETURN_DOM: true
+ });
+
+ // Return the first child (the SVG element)
+ return sanitized.firstChild;
+};
+
+export function MermaidRenderer({ chart, theme }: MermaidRendererProps) {
+ const containerRef = useRef(null);
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const renderChart = async () => {
+ if (!containerRef.current || !chart.trim()) return;
+
+ setIsLoading(true);
+ setError(null);
+
+ // Determine if theme is dark by checking background color
+ const isDarkTheme = theme.colors.bgMain.toLowerCase().includes('#1') ||
+ theme.colors.bgMain.toLowerCase().includes('#2') ||
+ theme.colors.bgMain.toLowerCase().includes('#0');
+
+ // Initialize mermaid with the current theme
+ initMermaid(isDarkTheme);
+
+ try {
+ // Generate a unique ID for this diagram
+ const id = `mermaid-${Math.random().toString(36).substring(2, 11)}`;
+
+ // Render the diagram
+ const { svg: renderedSvg } = await mermaid.render(id, chart.trim());
+
+ // Create sanitized DOM element from SVG string
+ const svgElement = createSanitizedSvgElement(renderedSvg);
+
+ // Clear container and append sanitized SVG
+ while (containerRef.current.firstChild) {
+ containerRef.current.removeChild(containerRef.current.firstChild);
+ }
+
+ if (svgElement) {
+ containerRef.current.appendChild(svgElement);
+ }
+
+ setError(null);
+ } catch (err) {
+ console.error('Mermaid rendering error:', err);
+ setError(err instanceof Error ? err.message : 'Failed to render diagram');
+
+ // Clear container on error
+ if (containerRef.current) {
+ while (containerRef.current.firstChild) {
+ containerRef.current.removeChild(containerRef.current.firstChild);
+ }
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ renderChart();
+ }, [chart, theme.colors.bgMain]);
+
+ if (isLoading) {
+ return (
+
+ Rendering diagram...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ Failed to render Mermaid diagram
+ {error}
+
+
+ View source
+
+
+ {chart}
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/renderer/components/Scratchpad.tsx b/src/renderer/components/Scratchpad.tsx
index 8e7642eb..049d07e5 100644
--- a/src/renderer/components/Scratchpad.tsx
+++ b/src/renderer/components/Scratchpad.tsx
@@ -1,9 +1,12 @@
import React, { useState, useRef, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Eye, Edit, Play, Square, Loader2, HelpCircle } from 'lucide-react';
import type { BatchRunState } from '../types';
import { AutoRunnerHelpModal } from './AutoRunnerHelpModal';
+import { MermaidRenderer } from './MermaidRenderer';
interface ScratchpadProps {
content: string;
@@ -390,7 +393,42 @@ export function Scratchpad({
margin-left: -1.5em;
}
`}
-
+ {
+ const match = (className || '').match(/language-(\w+)/);
+ const language = match ? match[1] : 'text';
+ const codeContent = String(children).replace(/\n$/, '');
+
+ // Handle mermaid code blocks
+ if (!inline && language === 'mermaid') {
+ return ;
+ }
+
+ return !inline && match ? (
+
+ {codeContent}
+
+ ) : (
+
+ {children}
+
+ );
+ }
+ }}
+ >
{content || '*No content yet. Switch to Edit mode to start writing.*'}