Skip to content
Install

docs/design/markdown-parity.md

Markdown rendering: Prismantis comparison and the full renderer

The full message renderer compared feature by feature with a Claude Code mod: what was added, what was left out, every colour slot.

Question: does MendCode's full message renderer have Markdown as nice and as configurable as Prismantis (a Claude Code mod)? This page compares them feature by feature, says what was added and what was left out, and lists every colour slot and setting. The Prismantis side is its README feature list as given in the request; no network was used and nothing of Prismantis is copied.

"Before" is 3ce2cea (main when this work started); "now" is this branch. Frames of the torture document (apps/cli/test/fixtures/markdown-torture.md) at 80, 120 and 200 columns are in .agents/runs/frames/markdown/.

Comparison

FeatureBeforeNow
Colour palettesPartial: 40 bundled themes (8 own + 32 v1 ports), /theme, theme files; no Markdown-specific roles. themes/index.ts:13, themes/define.tsHave: every Markdown token has a named slot (28), derived per theme and overridable one by one from a theme file or /customize. themes/define.ts markdownRoles, ui/theme.ts setColorOverrides
Customisable colour slotsPartial: 6 syn* roles plus heading, code, link, border; Markdown shared themHave: 28 md*/syn* slots below; markdownColors in the profile
Code header with languagePartial: a │ ts row, only when a language was given. markdown.ts:155Have: ╭─ ts ──── copy on every fence (text when none). markdown.ts codeHeader
Copy button on codeMissing (/copy and ctrl+x y copy a whole answer)Have: right-aligned copy, click or ctrl+x Y. markdown-chrome.ts blockHeader, ui/app/mouse-selection.ts, ui/app/block-copy.ts
Syntax highlightingPartial: 6 languages (ts/js, json, bash, python, go, rust) plus diff and md. syntax.ts:44Have: 23 (list below). syntax.ts LANGUAGES
Shell prompts by command / flag / string / commentPartial: command, string, comment, variable; no flags, no prompt. syntax.ts:228Have: prompt, command, flag, string, variable, comment. syntax.ts PROMPT, flags
Diff blocksPartial: coloured text only. syntax.ts lineStyledHave: plus a gutter bar per line kind. markdown.ts diffBar
Long code linesHave: wrapped behind ↪. markdown.ts:335Same (no clip setting exists; wrapping is the only mode)
Table header colourHave: heading, bold. markdown.ts:418Have: own slot mdTableHeader
Column alignment (:---:, ---:)Have. markdown.ts:381Have, in every style
Table box stylesMissing: one boxHave: box, rules, grid, minimal (tableStyle). markdown-table.ts
Column sizing to the widthHave: widest-first shrink, wrapping in cells, wide-char safe. markdown.ts:389Have, same engine; adds a stack threshold (below)
Very wide tablesPartial: stacked only when columns cannot reach 3 cells. markdown.ts:457Have: stacked when the longest words (capped at 10 cells) do not fit or a column would be under 4 cells. markdown-table.ts:70
Mermaid as ASCII, sized to the widthHave: 31 families through renderMermaid(source, width). mermaid/registry.ts, mermaid.ts:41Same; copy of source and of the art, diagram roles. Empty padding rows trimmed
Mermaid bar/line chartsHave: xychart-beta bars and lines, also pie, gantt, quadrantChart. mermaid/xychart.tsSame
Heading stylesMissing: every level the same bold heading colour. markdown.ts:192Have: banner, bold, underline, uppercase (headingStyle), level hierarchy in all. markdown-chrome.ts headingRows
Nested and ordered listsHave (4 levels). markdown.ts:289Same
Task lists with indicatorsPartial: [x] / [ ] text. markdown.ts:297Have: ✓ / ○, own slots
Bold, italic, strikethrough, inline codePartial: strikethrough was dimmed, not struck. markdown.ts:482Have: strikethrough is struck (strike)
Block quotes and calloutsPartial: single bar, no nesting, no callouts. markdown.ts:205Have: nested quotes, `> [!NOTE
LinksHave: OSC 8 through safeLink and the hyperlinks setting; (url) shown after a label. markdown.ts:10, ui/hyperlink.tsHave, plus underline
FootnotesMissingHave: [^1] references as superscripts, [^1]: text definitions
Horizontal rulesPartial: 40 cells, dim. markdown.ts:202Have: full width, own slot
Copy buttons on tables, lists, quotes, diagramsMissingHave: copy on the top border of boxed tables, art copy beside a diagram, by key on lists and quotes (no visible chrome). Never in minimal; code only in balanced
Right-to-left textMissingHave, with limits (below): paragraphs, headings, lists and quotes
Settings filePartial: tui/profile.json and /customize, nothing for MarkdownHave: tableStyle, headingStyle, markdownCopyHints, markdownRtl, markdownColors in profile.json, rows in /customize with a live preview

Where MendCode is already ahead

  • Mermaid is a real subsystem, not one renderer: 31 diagram families with a lazy-loaded layout engine (mermaid-lazy.ts, no cost until the first fence), width-aware layouts with a "too wide" fallback to source, and a full-screen pan-and-zoom viewer (/diagram, ctrl+x i, mermaid-viewer.ts) with a diagram list per session.
  • Streaming safety is designed in: unclosed fences, half-typed tables and separators render what they have; the settled-prefix memo (markdown.ts renderSettled) re-renders only the open paragraph, with markdown streaming and width safety and markdown-settled tests proving the memo equals the whole renderer at every step.
  • Performance: a 2,000-line torture document renders in about 9 ms at width 100, classic or enhanced (see Performance).
  • Math ($…$, $$…$$, \[…\] as stacked Unicode), collapsible <details> with per-block fold state and a click or key to toggle, inline HTML (<kbd>, <sub>, <sup>, <br>), colour swatches next to hex colours.
  • Selection-aware copying: a mouse selection copies the text without the code gutter, wrap markers or headers (line-meta.ts gutters), and wrapped lines rejoin without a space.
  • Three views from one engine: minimal stays plain, balanced is calm, full is rich, switchable live with /view.

What the full renderer does now

The enhanced engine runs only in the full message renderer, in the balanced and full views. minimal view, the markdown renderer (diagrams as source) and the plain renderer keep the classic look, as does every other caller of renderMarkdown (asides, sidebars, history pages). The transcript resolves the style per message (markdownStyleFor, markdown-style.ts).

Settings

All in <dataDir>/tui/profile.json, declared in both apps/cli/src/view/tui-profile.ts and packages/protocol/src/tui-data.ts, with rows under Customize > Markdown and a live preview of a sample document.

SettingValuesauto means
tableStyleauto, box, rules, grid, minimalbox in every view
headingStyleauto, banner, bold, underline, uppercaseunderline in full, bold in balanced
markdownCopyHintsauto, code, all, offall in full, code in balanced (minimal has none)
markdownRtlboolean, default on
markdownColorsJSON object { "mdHeading1": "#rrggbb", … }, any slot belownone

Table styles: box is the full frame; grid adds a rule between every row; rules keeps horizontal rules only (top, under the header, bottom) with two spaces between columns; minimal underlines each header column and nothing else.

Heading styles: every level has a role (h1, h2 heading colour; h3 text; h4-h6 body) and is bold. banner fills h1 (element background) and h2 (panel background) across the width and marks h3 with ▌; underline rules h1 with ═ and h2 with ─ as wide as the text; uppercase capitalises h1 and h2 (inline code and links keep their case).

Colour slots

Every slot is derived from the base roles when a theme omits it, so all 40 bundled themes define them; a theme file may set any (native format: colors.mdHeading1), and markdownColors overrides single slots over whichever theme is active (not over the host-owned terminal theme). Line-like slots (mdQuoteBar, mdTableBorder, mdRule, mdCodeBorder) are held to 3:1 against the background, the rest to 4.5:1.

SlotTokenDerived from
mdHeading1, mdHeading2h1, h2heading
mdHeading3, mdHeading4h3; h4-h6text; body
mdInlineCode`code`code
mdStrike~~text~~muted
mdLinklinks and autolinkslink
mdFootnotefootnote references and labelslink
mdQuote, mdQuoteBarquote text; its barmuted; border
mdNote, mdTip, mdImportant, mdWarning, mdCautioncallout label and railinfo, success, accent, warning, error
mdTableHeader, mdTableBordertable header; frame and rulesheading; border
mdListMarker, mdTaskDone, mdTaskOpenbullets and numbers; ✓; ○muted, success, muted
mdRulehorizontal rules, heading underlinesborder
mdCodeLabel, mdCodeBorderblock header label; code gutter and header framemuted; border
mdCopycopy and art hintsmuted
mdDiagramLine, mdDiagramAccentdiagram outlines and labels; arrows and markersheading; accent
synComment, synKeyword, synString, synNumber, synFunction, synTypecode tokens (unchanged)as before
synFlagshell options -x, --longmix of heading and link
synPromptshell prompt $muted

Callout and heading backgrounds use the existing element and panel fills.

Syntax highlighting

Per-line tokenizer, one regex pass, no dependencies. 23 languages: typescript (tsx, js, jsx, mjs, cjs), json (jsonc, json5, jsonl), bash (sh, zsh, shell, console), python (py), go, rust (rs), c (h), cpp (c++, cc, hpp), java, csharp (cs), kotlin (kt), swift, ruby (rb), php, sql (psql, mysql), yaml (yml), toml, css (scss, less), html (xml, svg, vue), lua, dockerfile; diff (patch) and markdown (md) are coloured by line. Block comments and Python triple quotes carry across lines; HTML comments and TOML multi-line strings do not (single-line only). No claim of equal coverage with Prismantis is made: its language list was not available to compare.

Copy affordances

Only code keeps a header row (╭─ ts ──── copy, full width: it reads as a panel). Lists and quotes draw no header, no rail and no button: they are plain Markdown (a quote keeps its own │ bar). Tables and diagrams get no header line either; their hint is drawn on their own art:

┌───────┬─────┬─────── copy ┐        a boxed table: on the top border, right-aligned to the table
yes ┌────────┐    ╭───────╮ art  copy   a diagram: on its first row, right of the art, only over blank cells

A hint that does not fit is not drawn (a table narrower than 12 cells, a last column under 6 cells, a first diagram row with no blank cells there); rules, minimal and stacked tables have no top border. The key still works in all of these. Hint rows are chrome, so a mouse selection skips them (a boxed table's top border is the one row that is not copied with a selection).

Every row of a block also carries an invisible, zero-width copy region (markdown-chrome.ts tagCopy): nothing is drawn, a click never hits it, and the key finds the block under the middle of the screen. Visible buttons are hit regions (line-meta.ts markCopy, carried through the message gutter by carryMeta and into <details> bodies), mapped to the selection map (ui/selection.ts BodyLine.copy) and clicked in ui/app/mouse-selection.ts. The key is ctrl+x Y (block.copy), which copies the nearest block to the middle of the screen, like ctrl+x f does for <details>; with no block in view it says so. Both go through copyText, the OSC 52 path that /copy uses. A mouse selection copies lists and quotes as it always did.

BlockVisible hintcopy / key putsart puts
fenced codeheader: language (text when none) + copythe code as typed, tabs kept; while streaming, what has arrived
tablecopy on the top border (box, grid)the Markdown source
listnonethe list's source
quotenonethe text without >
mermaidart copy beside the first rowthe sourcethe diagram as drawn
diagram that cannot be drawncode header with copythe source

markdownCopyHints picks what is visible: code (code headers only), all (plus the table and diagram hints) or off (a header with the language and no button); auto is code in balanced and all in full. The minimal view is plain and has no hints at all. Hints that do not fit are dropped from the right.

Right-to-left text

markdown-bidi.ts implements the part of UAX #9 that matters for Hebrew and Arabic paragraphs: strong types (R, AL, L), European and Arabic numbers with their separators (1,234.5 stays one number), sign handling next to numbers, neutral resolution (N1/N2), embedding levels 0 and 1 with numbers at level 2, line-level run reversal (L2) and bracket mirroring (L4). The base direction is the first strong letter of the paragraph. A right-to-left paragraph, heading, list item (marker on the right) or quote (bar on the right) is reordered per drawn row after wrapping and right-aligned, with the blanks marked as chrome. A left-to-right paragraph with an embedded Hebrew run is reordered but not aligned. Rows without right-to-left letters take a regex test and nothing else.

Limits, stated plainly: no explicit embeddings or isolates (LRE, RLI, …), no bracket pairing (rule N0), no Arabic contextual shaping or ligatures (a terminal font shows isolated letter forms unless the terminal shapes them), no combining-mark reordering beyond keeping a mark with its base, and a mouse selection copies visual order. A terminal that does its own bidi (mlterm, some others) would reorder twice: turn markdownRtl off there. Tables and code stay left-to-right.

Streaming and width safety

  • The settled-prefix memo is keyed by width, colour and a style key, so changing a setting never serves stale rows (transcript.setAppearance also clears the transcript cache).
  • A block header, heading rule or callout label is derived from its own source, so no finished block changes while the next one streams (markdown-full-streaming.test.ts checks this character by character).
  • No row exceeds the width for any table style, heading style or width from 20 to 200, with CJK and emoji (markdown-full.test.ts). Existing width bugs found on the way were fixed: a folded <details> count and a "diagram too wide" note could pass the width.

Performance

Median of seven runs on one machine (Bun 1.3.13, Apple silicon), renderMarkdownWhole, width 100, 2,000-line document built from the torture file without diagrams: classic 8.1 ms, enhanced 8.8 ms, enhanced with banner headings and grid tables 8.7 ms. A streamed frame of a 120-block answer with the memo costs under 10 ms in the test budget. The shell highlighter stays under 0.05 ms per line. Tests: markdown.test.ts (existing 50 ms budget, unchanged), markdown-full-streaming.test.ts (same budget for the enhanced engine).

Left out, and why

  • A horizontal-clip mode for code (codeWrap): no setting exists today and wrapping behind ↪ never hides code; adding a hidden-text mode would need horizontal scrolling in the transcript.
  • Full bidi (isolates, embeddings, N0, shaping): not needed for model output, and shaping is the terminal's job.
  • A visible copy hint on lists and quotes: a header or rail on every list of an answer is heavy; the key and the mouse selection cover them.
  • A hint on a table that has no top border or no room: rules, minimal, stacked and narrow tables use the key.
  • A separate art hint on tables: the border carries copy (the Markdown source) only; a mouse selection copies the drawn rows.
  • Per-theme Markdown palettes bundled (15 palettes like Prismantis): MendCode already ships 40 themes; the new slots derive from each, so every theme gets a coherent Markdown look without 40 hand-tuned entries.
  • A footer row closing each code block: it would add a row that only appears once the fence closes.