Phases 1-5 shipped plus an Inline-zones enhancement that merges the
read-only “Inline” preview into the editable Code mode: when a file
has renderable regions, MonacoCodeEditor hosts view zones that
render each diagram inline after its last source line, alongside the
editable source. Markdown source Preview/Split also reuse the rendered
Markdown editing surface from Git diff, so rendered preview edits write
back to the same source buffer. Phase 6 (additional renderers beyond
Mermaid/math) planned.
This document defines a shared renderer model for source-backed previews. It extends the Markdown document work into a more general capability: render recognized diagrams, equations, and other safe visual blocks from the same source buffer used by Monaco editing, Git diff edit mode, and rendered Markdown views.
Related: Code Navigation MCP can use source-renderer metadata when building compact context packs for renderable files.
remark-math + rehype-katex + katex wired into
MarkdownContent in ui/src/message-cards.tsx. Inline $...$ and
block $$...$$ math render via KaTeX; rendered spans/divs carry
contentEditable={false} and data-markdown-serialization="skip"
so the rendered-Markdown diff editor’s serializer (see
shouldSkipMarkdownEditableNode in ui/src/panels/DiffPanel.tsx)
preserves the source $...$ / $$...$$ literals and never captures
KaTeX presentation HTML into the saved buffer.MAX_MATH_EXPRESSIONS_PER_DOCUMENT = 100,
pre-render counted by countMathExpressions (O(n) lexical scan
mirroring countMermaidMarkdownFences). Over-budget documents
still render the KaTeX output but route every math wrapper through
MathRenderBudgetFallback, which stamps math-render-skipped and a
title note. KaTeX itself is configured with throwOnError: false,
trust: false, strict: "ignore", output: "html" — a malformed
expression renders as a red-colored error span instead of halting
the whole Markdown render, and no arbitrary HTML can slip through
\href / \url.data-markdown-line-start) are attached to
block math wrappers when showLineNumbers is on, feeding the
existing Markdown line-gutter system.ui/src/MarkdownContent.test.tsx cover:
inline-math wrapper shape, block-math wrapper shape, no interception
of non-math span/div, malformed-expression resilience, exact-at-cap
behavior, over-budget fallback, $ in fenced code not being
tokenized as math, and block-math line-attribute stamping.ui/src/source-renderers.ts defines SourceRenderContext,
SourceRenderableRegion, detectRenderableRegions(context), and
hasRenderableRegions(context), plus the shared budget constants
(MAX_MERMAID_SOURCE_CHARS, MAX_MERMAID_DIAGRAMS_PER_DOCUMENT,
MAX_MATH_EXPRESSIONS_PER_DOCUMENT) that Source + Diff panels can
read without depending on message-cards.tsx.$...$ + same-line and multi-line $$...$$) and dedicated
Mermaid files (.mmd, .mermaid). message-cards.tsx
re-imports the shared helpers — no duplicate logic.ui/src/source-renderers.test.ts pin budget
constants, fence predicates, counts, region detection for all
four kinds, sort order, stable ids, the editable-mode flag, and
dedicated-file-type handling.ui/src/panels/SourcePanel.tsx exposes Preview/Split modes for
any file the registry detects as renderable — not just Markdown.
Dedicated Mermaid files get the mode switcher with a “Mermaid”
chip.RendererPreviewPane routes Markdown through
MarkdownDocumentView (unchanged chrome) and non-Markdown
renderable files through MarkdownContent with a synthetic
Markdown fragment composed from detected regions (per-region
Lines X–Y headers + appropriate fence wrapping).useMemo, so
the preview reflects unsaved edits..mmd file exposing Preview/Split, plain
Rust file NOT exposing them, and edit-buffer re-detection.ui/src/panels/DiffPanel.tsx gains a read-only "rendered" view
mode for non-Markdown files whose after side has at least one
renderable region. Reuses MarkdownContent for safe
Mermaid/KaTeX rendering.documentContent.after.content (authoritative for the selected
GitDiffSection), fallback to latestFile.content only when the
backend didn’t enrich.documentContent.isCompleteDocument
is not set — points reviewers at the Raw patch view for
authoritative review..mmd render and patch-only
fallback label..rs extension or language: "rust") are a
recognized content kind. detectRustRegions parses the file into
doc-comment blocks, strips the marker prefix, runs the existing
Markdown fence + math detector against each block, and remaps the
detected regions’ line numbers back to the original Rust source
via a per-block line-number table.///, //!, /** ... */, /*! ... */
(single-line and multi-line). Rustdoc conventions honored —
a single leading space after the marker is stripped; ` * `
prefixes on multi-line block-doc bodies are stripped; //// and
plain /* */ are NOT parsed.Lines X–Y labels in the Source/Diff preview cross-
reference Monaco lines directly.source-renderers.test.ts cover: no-doc
files, /// with Mermaid, //! with math, multi-line /** */
with Mermaid, rejection of //////* */, prose-only doc blocks,
multiple doc blocks in one file, .rs without explicit language,
single-line /** */, and editable: false in diff mode. One
additional SourcePanel integration test exercises the end-to-end
Preview/Split exposure for Rust files with doc-comment Mermaid
fences.Diagrams now render in-place WITHIN the editable Code view, not just in a separate Preview/Split pane. The earlier read-only “Inline” mode was dropped — feedback was that mode toggles were friction (“separate modes are not that useful”).
MonacoCodeEditor gained an optional inlineZones prop:
Array<{ id, afterLineNumber, render }>. The editor manages a
view-zone registry keyed by zone id, using changeViewZones to
add/remove/move zones in response to prop changes, and portals
the caller’s React output into each zone’s DOM node.SourceRenderableRegion.id)
keep the portal DOM node mounted across keystrokes. A mid-edit
fence change shifts the zone’s afterLineNumber (triggering
remove + re-add in Monaco, because the view-zone API has no
“update position” primitive), but the diagram host DOM node
survives — the Mermaid iframe doesn’t re-initialize and the
KaTeX output doesn’t re-parse on every keystroke.ResizeObserver: each zone’s DOM
node is watched, and when the rendered diagram finishes
async-rendering (or the fence body changes to produce a
different-sized diagram), the zone is removed and re-added with
the measured height. The brief flicker on first paint is the
cost of Monaco’s “fixed height” view-zone API.SourcePanel computes inlineZones via useMemo over
renderableRegions and passes them to Monaco in both Code mode
and Split mode’s editor pane. Empty arrays for non-renderable
files incur zero zone overhead.inlineZones.length + afterLineNumber as data
attributes): zones passed for a Rust file with a /// Mermaid
fence, zero zones for a plain Rust file, and live-recompute when
the user types to grow the fence body (zone’s afterLineNumber
shifts from 3 to 4 as a line is added).Source-preview surfaces enable fillMermaidAvailableSpace so rendered
Markdown can use the full preview column and Mermaid diagrams can fit the
available width. Default conversation/history Markdown keeps the older
scrollable Mermaid frame: wide diagrams preserve their intrinsic iframe width
when the column has room, and shrink with the iframe when max-width: 100%
constrains it so the aspect-ratio height does not clip the bottom of the SVG.
Source previews instead pass fitToFrame: true into the Mermaid iframe srcdoc
and frame-style helper.
Fit mode changes two things deliberately:
svg { max-width: 100%; height: auto; }, so wide diagrams shrink to the
pane instead of requiring horizontal iframe scrolling.This mode is preview-wide rather than Mermaid-only: enabling it also applies
markdown-copy-shell-fill-mermaid, widening the Markdown copy shell around
the diagram so the iframe can actually consume the preview pane width.
TermAl already uses Monaco as the source editor. That is the right editing surface, but many source files contain content that is easier to understand as rendered output:
.mmd or .mermaid.The current Markdown document spec covers Markdown files and Markdown Git diffs, but it does not define a reusable model for renderers that work across:
There are two separate surfaces:
The rendered preview is always derived from source. It is not the source of truth.
Markdown files continue to use the MarkdownDocumentView path.
Supported renderable blocks:
```mermaid
sequenceDiagram
Alice->>Bob: Hello
```
Inline math: $E = mc^2$
Block math:
$$
\int_0^1 x^2 dx = \frac{1}{3}
$$
Optional fenced math syntax:
```math
E = mc^2
```
Rust support should start with doc comments because they already use Markdown semantics.
Supported forms:
/// Architecture sketch:
///
/// ```mermaid
/// sequenceDiagram
/// UI->>Backend: POST /api/git/diff
/// Backend-->>UI: documentContent
/// ```
pub fn example() {}
//! Module invariant:
//!
//! ```math
//! revision_{n+1} = revision_n + 1
//! ```
Rules:
///, //!, /** ... */, and
/*! ... */.Dedicated file types can render the whole file:
| Extension | Initial Renderer |
|---|---|
.mmd, .mermaid |
Mermaid |
.tex, .latex |
Math preview, only when the file is a single equation or explicitly marked as previewable |
Dedicated support should be opt-in and conservative. For example, a full LaTeX document is not the same as a single KaTeX equation.
For files with renderable content, the source panel can expose:
CodePreviewSplitRules:
Code keeps the current Monaco editor.Preview renders from the current source buffer. For Markdown files,
the rendered document is editable and commits back to that same buffer.Split shows Monaco and rendered preview side by side; Markdown
rendered edits update the Monaco buffer on blur/save/mode changes.editorValue, not stale loaded file content.scrollTop after the first post-rehydrate view-zone measurement settles,
otherwise the caret lands on the wrong rendered block.Diff views must preserve the selected Git section semantics:
| Section | Rendered Source |
|---|---|
unstaged |
working tree after side |
staged |
index after side |
untracked in unstaged |
working tree file |
added in staged |
index file |
Rules:
Edit mode through Monaco.Rendered Markdown diff editing is special because it can edit Markdown sections
directly through contentEditable.
Renderer rules:
contentEditable={false}.data-markdown-serialization="skip" or an
equivalent skip marker so saved source does not become generated HTML/SVG.Create a narrow renderer registry instead of scattering special cases through panels.
type SourceRenderContext = {
path: string | null;
language: string | null;
content: string;
mode: "source" | "diff" | "diff-edit" | "markdown-diff";
};
type SourceRenderableRegion = {
id: string;
renderer: "markdown" | "mermaid" | "math";
sourceStartLine: number;
sourceEndLine: number;
sourceText: string;
displayText: string;
editable: boolean;
};
Responsibilities:
SourcePanel and DiffPanel.Use the existing Mermaid renderer path.
Rules:
viewBox using a clamped width and
CSS aspect-ratio with height: auto. The frame must preserve the diagram’s
aspect ratio when pane width constrains max-width: 100%; do not return to a
fixed-height iframe, because wide ER diagrams otherwise scale down
horizontally while retaining a tall blank frame. The iframe srcdoc must also
let the inner SVG shrink with the constrained frame before vertical overflow
is hidden, otherwise tall or wide diagrams can clip at the bottom.Use KaTeX first unless a future use case requires MathJax.
Recommended dependencies:
remark-mathrehype-katexkatexRules:
math, latex, tex, or katex blocks if the implementation
can keep round-trip semantics clean.trust disabled unless there is a specific reviewed need.Automated tests:
editorValue buffer as Code mode.editValue.Manual checks:
MarkdownContent.Preview and Split for Markdown and detected renderable non-Markdown
files.editorValue.Consider additional renderers only after Mermaid and math are stable: