Skip to content

Rendering Pipeline

Hermes Agent edited this page Oct 1, 2026 · 2 revisions

Rendering Pipeline

English | 中文 | 日本語 | 한국어 | Español | Português | Русский

The Markdown → trusted-HTML pipeline for assistant bubbles lives in lib/sidepanel/render.js. The order of operations is load-bearing:

  1. fixCjkEmphasisSpacing (the CJK-adjacency ** patch — its character classes deliberately mirror marked's flanking rules: punctuation = \p{P}∪\p{S}, neighbors = any letter/digit)
  2. Extract <think> blocks → placeholders
  3. Extract block math $$…$$ / \[…\]
  4. Extract inline math $…$ / \(…\) — two guards:
    • the markdown escapes \\ and \$ are consumed verbatim first (\$8K is a literal dollar — 2026-09-30 MRR-table report);
    • inline $ follows Pandoc's delimiter rules: tight opening/closing, and a closer followed by a digit is rejected (从**$8K**、**$20K** once paired its bare $s into KaTeX, which rendered the *s as ∗ — 2026-10-01 report). Rejected candidates are returned verbatim to marked.
  5. marked.parse → 6. DOMPurify → 7. KaTeX worker batch-render + restore (AFTER sanitization, so MathML attributes never meet DOMPurify) → 8. restore think foldables.

Streaming: block commit (ADR-0011)

  • renderStreamingSafe (sync) handles only the tail each frame; completed markdown blocks commit once (findStreamingBlockBoundary: blank-line boundary, outside fences, next line not a list item) and only the open tail re-parses — O(n²) becomes O(n). Open fences take a fast path (<pre><code> via textContent, zero marked calls).
  • The reveal pacer is vendored markstream-core (makeStreamRenderer returns a closure; .destroy() before reassigning it). DONE re-renders the full text via renderSafe (KaTeX + final think blocks), fixing any streaming approximation.
  • History renders lazily: an IntersectionObserver upgrades bubbles as they near the scrollport; content-visibility:auto only under .cv-settled (double-rAF) — never during initial layout/scroll.
  • KaTeX offload: katex-threshold.js decides worker vs sync; the worker batches per message, caches, and falls back to sync on failure.

Six fenced renderers (```lang → live render swap, no buttons)

Fence Renderer Vendor Notes
```mermaid renderMermaid mermaid securityLevel:'loose' (KaTeX in foreignObject) + sanitizeMermaidSvg (load-bearing security); AI repair card on parse failure (REPAIR_MERMAID)
```echarts renderEcharts echarts built-in 'dark' theme + transparent canvas unless the model names its own
```markmap renderMarkmap markmap lib+view pre.replaceWith(wrapper) BEFORE Markmap.create() (layout measures text); mm.rescale() is a RELATIVE multiplier — zoom goes through _markmapZoomBy's absolute conversion
```smiles renderSmiles RDKit MinimalLib WASM get_mol/get_rxn; RDKit doubles as the validator (chemically impossible → explicit error note, raw block kept); descriptor line (MW/logP/TPSA…); dark mode re-inks strokes but KEEPS CPK heteroatom colors (chemistry semantics — never CSS-invert)
```pdb renderPdb Mol* (RAW-copied IIFE) parsePdbBlock classifies: RCSB ID / AlphaFold AF-…-F1 (current model resolved via API) / inline coordinates; the representation preset must ride representationPreset; live viewers tracked and disposed (browsers cap ~16 WebGL contexts)
```dot renderDot @viz-js/viz (base64-inlined WASM) default-black ink remap + contrast rescue; invalid DOT → error note + raw code kept (no AI repair); model contract: ID [label="…"] only

The render contract (what the model is taught to emit) has ONE text source: CAPABILITY_HINTS_ENTRIES in lib/prompt-assembly.js — CAPABILITY_HINTS (LLM/Hermes system channel) and AGENT_RENDER_HINT (agent first turn) are both assemblies of it. No hand-mirrors may reappear (the old ```nn mirror missed an update for a day).

Vendor loading & CSP (see ADR-0008 / Security Model)

  • Diagram vendors lazy-load and pre-warm only when a fence is actually seen (wantsChartVendors sniff behind a one-shot gate) — a diagram-free session never pays the ~7MB parse.
  • Mol's dist never goes through esbuild* (RAW-copied prebuilt IIFE); Mol*/RDKit embind glue gets CSP patches applied at copy time in build.mjs (hand-written invokers replace new Function; eager h264 init made lazy) — patch regexes anchor on stable strings and THROW THE BUILD when a molstar upgrade stops matching.
  • The verification gate after any vendor swap is a REAL extension load (launchPersistentContext --load-extension, xvfb): dev-preview is a plain http page that allows eval and cannot catch CSP violations (2026-09-30 lesson).

Disposal

_chartObservers (ResizeObserver registry) / _renderDisposers (per-element) / disposeMolstarViewers() run before every bubble removal, history re-render, and history wipe. The classic leak symptom is a detached WebGL context rendering forever (~16-context browser cap).


Source of truth: AGENTS.md "sidepanel.js module split" (render.js / mermaid-utils / chem / dot), "Renderer-vendor audit", 批B/批C invariants; ADR-0008 / 0010 / 0011. Synced 2026-10-01.

Clone this wiki locally