7.1 KiB
ADR: Mermaid Rendering Redesign
Date: 2026-05-08 Status: Proposed
Problem
The current Mermaid path is difficult to reason about because rendering, caching, UI placement, active diagram registration, deferred work, debug stats, and terminal image protocol state are coupled through global state and side effects.
Observed pain points:
jcode-tui-mermaid/src/lib.rsis still a state hub despite the crate split.- Markdown rendering decides Mermaid behavior directly, including streaming/deferred/side-only registration rules.
- Active diagrams are registered as a side effect of render calls, so simply preparing markdown mutates pinned-pane state.
with_preferred_aspect_ratiouses thread-local state, so cache keys and render sizing depend on ambient context.- The same diagram can be rendered in multiple contexts: chat inline placeholder, side panel image, pinned pane, streaming preview, debug probe. These contexts need different behavior but share low-level functions.
- Deferred rendering has its own dedupe/epoch/global queue and also performs active registration, increasing race risk.
- Image protocol rendering, PNG generation, image-state caches, and viewport rendering are mixed into the same public surface.
Size API direction
The renderer now has an mmdr-size-api path guarded by the mmdr-size-api feature plus JCODE_MMDR_SIZE_API_AVAILABLE=1. That should become the primary path for the redesign:
- Renderer should ask Mermaid/layout for measured SVG/canvas dimensions instead of relying on source text complexity estimates for final PNG sizing.
calculate_render_sizeshould become a request target hint, not the source of truth for output dimensions.- The fallback SVG-retargeting path should remain only as compatibility code until the patched renderer is always available.
- Debug stats should report
render_size_backendand fail loudly in tests when the size API path is expected but unavailable. - Cache keys should include normalized target/profile inputs, while artifacts should store measured output dimensions returned by the size API.
This reduces bugs from aspect-ratio retargeting, blurry upscaling, placeholder height mismatch, and pane resize oscillation.
Target design
Use an explicit, staged pipeline with pure data between stages:
flowchart TD
A[Markdown/Event source] --> B[Diagram extraction]
B --> C[DiagramRegistry update]
C --> D[RenderScheduler]
D --> E[RenderCache]
E --> F[Renderer: Mermaid AST/Layout/SVG/PNG]
E --> G[Placement planner]
G --> H[Terminal image presenter]
C --> I[Pinned/side panel selectors]
1. Diagram extraction
Markdown renderers should only extract fenced Mermaid blocks into immutable descriptors:
struct DiagramBlock {
id: DiagramId,
source_hash: u64,
source: Arc<str>,
origin: DiagramOrigin,
ordinal: usize,
}
They should not directly mutate active diagrams or synchronously render unless a caller explicitly asks for a blocking fallback.
2. Explicit render request
Replace ambient with_preferred_aspect_ratio and boolean parameters with one request object:
struct RenderRequest {
diagram_id: DiagramId,
source_hash: u64,
source: Arc<str>,
target: RenderTarget,
profile: RenderProfile,
priority: RenderPriority,
mode: RenderMode,
}
struct RenderProfile {
width_cells: Option<u16>,
preferred_aspect_per_mille: Option<u16>,
theme: MermaidTheme,
}
enum RenderMode {
CacheOnly,
EnqueueIfMissing,
Blocking,
}
Cache keys should be built only from source_hash + normalized RenderProfile, never from thread-local context.
3. Registry owns active state
Introduce a DiagramRegistry owned by TUI app/session state, not a global Mermaid crate vector.
Responsibilities:
- Track diagrams visible in the current prepared transcript/side panel.
- Track streaming preview separately with a generation id.
- Publish the ordered list for pinned pane selection.
- Clear/update atomically per prepare pass.
Rendering should return RenderArtifact; it should never register active diagrams as a side effect.
4. Scheduler owns async/deferred behavior
A scheduler receives explicit requests and returns one of:
enum RenderStatus {
Ready(RenderArtifact),
Pending { request_id: RenderRequestId },
Failed(RenderError),
ProtocolUnavailable,
}
Rules:
- Deduplication is by full cache key.
- Workers do not mutate active registry.
- Worker completion only publishes
MermaidRenderCompletedplus artifact metadata. - Epoch invalidation is scoped to request generations, not one global counter unless truly necessary.
5. Placement planner is separate from rendering
Markdown/side-panel preparation should insert placeholders based on RenderStatus and desired placement:
- Inline image placeholder lines for chat/side panel.
- Sidebar marker for side-only mode.
- Error block for failed render.
- Pending placeholder for deferred/streaming render.
Image widget rendering should consume RenderArtifact plus PlacementPlan, not know Mermaid source or render scheduling.
6. Public module boundaries
Recommended crate modules:
model.rs:DiagramId,DiagramBlock,RenderProfile,RenderTarget,RenderArtifact,RenderStatus, errors.extract.rs: Markdown Mermaid block extraction helpers.cache.rs: disk and memory artifact metadata cache.renderer.rs: Mermaid parse/layout/SVG/PNG conversion only.scheduler.rs: request queue, worker, dedupe, completion events.registry.rs: active/streaming diagram state, ideally app-owned.placement.rs: placeholder/image-region planning.presenter.rs: ratatui-image/Kitty/Sixel/iTerm viewport rendering.debug.rs: stats collected from explicit events.
Migration plan
- Add explicit model types and cache key normalization tests.
- Add a new scheduler API while keeping old wrappers.
- Convert
render_mermaid_sized_internalinto pure-ishrenderer::render_to_png(request) -> RenderArtifact. - Move active diagram writes out of render functions into markdown prepare/app registry updates.
- Replace
with_preferred_aspect_ratiocall sites with explicitRenderProfileplumbing. - Split presenter/image-state code from PNG rendering code.
- Delete old boolean wrapper APIs and thread-local render profile.
Validation criteria
- Unit tests for cache key normalization and filename parsing.
- Unit tests for registry update ordering, streaming preview replacement, and atomic clear/update.
- Scheduler tests for dedupe, cache-hit, cache-miss pending, worker completion, and no active-state mutation.
- Markdown renderer tests that Mermaid blocks produce deterministic placeholders without global side effects.
- Existing scroll/pinned-pane tests still pass.
- A debug probe can render a diagram with explicit profile and report the exact cache key used.
Near-term safe refactor
Before a full migration, the highest ROI change is to introduce explicit request/status types and make old public functions thin compatibility wrappers. That lets us migrate call sites one at a time while reducing new bugs from additional boolean/thread-local behavior.