pi-better-render
v0.3.0
Published
Streaming-first rich Markdown, code and math rendering for the pi TUI (display-only; never touches LLM context).
Maintainers
Readme
pi-better-render
Streaming-first rich rendering for pi's TUI. Markdown, code blocks, tables and LaTeX formulas are rendered as the answer streams in, with formulas shown as real images. It is built for Ghostty and kitty, and also shows formula images in other terminals with Kitty graphics, such as WezTerm.

- Display only. It replaces the component that displays a message and never edits the message.
The session file and the next LLM request keep the original Markdown/LaTeX. As a safety net, a
contexthook also strips any terminal image artifacts before every request. - Formulas while streaming. Each formula becomes an image as soon as it is complete. There is no waiting for the message to end.
- In process, no big runtime. No browser, Pandoc or daemon is involved. A formula takes about 1 ms from TeX to PNG.
- Text-like images. Formula images use Kitty Unicode placeholders. Each image is sent once; after that it behaves as ordinary text cells, so it wraps, scrolls and diffs like text. Inline formulas sit inside the line. Formulas stay in the scrollback after pi exits.
Compared with other pi extensions
| | pi-better-render | pi-math | pi-markdown-preview | pi-rich-renderer |
|---|---|---|---|---|
| Where it shows | In the chat, automatically | In the chat, automatically | Separate viewer (/preview), browser or PDF | In the chat, automatically |
| Updates while streaming | ✅ Markdown and formulas, on every delta | ✅ Formulas | ❌ On demand; watch mode refreshes after each completed reply | ❌ Only when the message ends |
| Restyles Markdown (headings, callouts, tables, code) | ✅ | ❌ pi's built-in | ✅ As a Pandoc HTML page | ❌ pi's built-in |
| Formulas inside the text line | ✅ | ✅ | ✅ Inside a page screenshot | ❌ Each formula becomes a block image |
| Math engine | MathJax 4, with optional TeX fallback | MathJax 3 | Pandoc MathML + MathJax in Chromium | latex + dvipng per formula |
| Needs beyond npm install | Nothing (TeX is optional) | Nothing | Pandoc and Chromium; LaTeX for PDF | A TeX installation |
| Stored message and LLM context | Untouched (display only) | Untouched (display only) | Untouched (read only) | Message content replaced at message_end; original restored for the LLM |
| Terminals with images | Ghostty, kitty; WezTerm, Warp via direct placements | Ghostty, kitty; display formulas also in WezTerm, Warp, iTerm2 | Ghostty, kitty, iTerm2, WezTerm | Those supported by pi's image component |
| Without images | Unicode approximation | Original LaTeX | – | Original LaTeX |
| Beyond chat rendering | Mermaid diagrams as Unicode art | Custom macros and environments | Browser and PDF export, Mermaid, file preview, export tool | Disk cache of PNGs |
Each existing extension covers one part of the problem:
- pi-math typesets formulas in the chat but leaves the rest of the Markdown to pi.
- pi-markdown-preview renders a complete, polished page, but in a separate viewer after the reply has finished.
- pi-rich-renderer needs a TeX installation and swaps in images only once the message ends.
pi-better-render fills the gap between them. It renders the whole answer, Markdown structure and math alike, in the chat while it streams, in process, with nothing extra to install.
The others are still the better choice in some cases. For PDF or browser output, or for Mermaid diagram types beyond flowchart, sequence, state, class and ER, use pi-markdown-preview. For formula images in iTerm2, use pi-math. (Comparison as of September 2026.)
What it renders
| Element | Rendering |
|---|---|
| Headings | H1 with a ━ rule, H2 with a ─ rule, H3 as ▍ title, H4–H6 by color. No # markers |
| Emphasis | bold, italic, ~~strikethrough~~, inline code, clickable links (OSC 8) with the URL shown |
| Lists | • ◦ ▪ by depth, original numbering, ☑/☐ tasks, hanging indent |
| Quotes | ▎ bar. GitHub callouts > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION] |
| Code | Background block, language label, pi's syntax highlighting, ↪ wraps, optional line numbers, no fences |
| Tables | Rounded borders, bold header, :-: alignment, correct CJK widths. Columns shrink, then fall back to cards |
| Mermaid | ```mermaid flowchart, sequence, state, class and ER diagrams drawn with box characters (correct CJK widths). The renderer is downloaded on the first diagram (see below). Other types, invalid source or diagrams wider than the terminal stay as code |
| Rules | Full-width ─ |
| Math | See below |
Math
- Delimiters: inline
$…$and\(…\); display$$…$$,\[…\]and\begin{align}…(plus the other AMS environments). - TeX packages: AMS, mathtools, physics (
\dv,\pdv,\qty,\abs,\norm), mhchem (\ce), braket, cancel, cases, empheq, bbm/dsfont/bboldx (\mathbbm,\mathds), color, boldsymbol, upgreek, gensymb, units, extpfeil, centernot, bussproofs, amscd. - Extra macros:
\bm,\llbracket/\rrbracket, and a single\tag{…}. - CJK text: text inside formulas, such as
\text{中文}or\text{한국어}, is rendered with a system CJK font. Without one (common on Linux servers and containers), Noto Sans SC (10 MB, Han and kana) or KR (6 MB, Hangul) is downloaded once into~/.pi/agent/better-render/fonts, checked against a pinned SHA-256. The formula shows as text until the font arrives, then as an image. - Fallback: if MathJax rejects a formula and a local
latex+dvipngortectonicis installed, that engine typesets it in the background. Where images are not possible, formulas use pi's Unicode approximation.
Performance
Measured on an Apple M-series laptop.
| | | |---|---| | MathJax 4 + resvg startup (background, after session start) | ~105 ms | | Formula, TeX → SVG | ~0.3 ms | | Formula, TeX → SVG → PNG on the cell grid | ~1 ms | | Formula with CJK text | ~10 ms | | Streaming delta at the end of a 61k-character answer | ~0.6 ms per frame | | Streaming delta, 3.8k-character answer | ~0.04 ms per frame |
Streaming cost stays nearly flat as a message grows, for these reasons:
- Incremental lexing. Only the last two blocks are lexed again; earlier tokens are reused.
- Block cache. Rendered blocks are cached by token identity, so finished blocks are never rendered again.
- Formula cache. Formula rasters are cached by TeX, color and cell size, and each image is sent to the terminal only once.
- Images travel with their frame. An image is sent together with the frame that first shows it. pi does a full clear on resize, and its fullscreen mode deletes all image placements when its own images change; after either, images are sent or placed again, so formulas survive resizes and redraws. Images are never deleted when pi exits, so formulas left in the scrollback keep showing.
Run npm run bench to reproduce the streaming numbers.
How it works
assistant delta ─▶ incremental lexer (marked) ─▶ block renderer + cache ─▶ ANSI lines ─▶ pi-tui
│
└─ formula ─▶ MathJax 4 (TeX → SVG)
─▶ resvg (SVG → PNG, cell-exact)
─▶ Kitty graphics, virtual placement
─▶ U+10EEEE placeholder cells in the line| Part | Technology |
|---|---|
| Host | pi extension API. It patches the assistant message component and registers a context hook |
| Markdown | marked lexer (via pi-tui), with its own incremental re-lexing and block renderers |
| Code highlighting | pi's theme highlighter |
| Math typesetting | MathJax 4 (@mathjax/src, New Computer Modern font), in process and synchronous |
| Rasterizing | @resvg/resvg-js (Rust, native). Canvases are sized to whole terminal cells, so images are never stretched |
| Terminal images | Kitty graphics protocol: Unicode placeholders (Ghostty, kitty), or direct placements of one image slice per row (WezTerm, Warp and other terminals with Kitty graphics) |
| Fallback | Optional latex+dvipng or tectonic in a sandboxed child process. No shell escape, paranoid file access, 15 s timeout |
Install
pi install npm:pi-better-render # from npm
pi install git:github.com/JeffeyChou/pi-better-render
pi -e npm:pi-better-render # try it for one run without installingFrom a local checkout:
npm install
pi install ./path/to/pi-better-render # or: pi -e ./src/index.tsImages need a terminal with the Kitty graphics protocol. Ghostty and kitty use Unicode placeholders. Other terminals that pi detects as Kitty-capable (WezTerm, Warp) get direct placements: the frame going to the terminal places one slice of the image per row, in the cells where its placeholders would be. Images are unavailable inside tmux/screen and in terminals without Kitty graphics. There, formulas use pi's Unicode approximation and everything else still renders.
Commands
/better-render takes one of these arguments:
statusshows the renderer state, the math engine, cache hit rates and formula timings.on/offturns rich rendering on, or switches back to pi's built-in Markdown.math:streaming,math:finalormath:offcontrols when formulas become images.linestoggles line numbers in code blocks.mermaidtoggles drawing```mermaidblocks as diagrams (off shows their source). The diagram renderer (beautiful-mermaid, about 11 MB with its layout engine) is not installed with the package. The first```mermaidblock installs the pinned version with npm into~/.pi/agent/better-render/depsin the background; the block shows as code until then and turns into a diagram when it arrives.clear-cachedrops cached renders and frees the images sent to the terminal (formulas already in the scrollback go blank). Ending a session never frees them.
Environment variables:
PI_BETTER_RENDER_DEBUG=1logs context-sanitizer checks to~/.pi/agent/better-render-debug.log.PI_BETTER_RENDER_TEX=off|latex|tectonicpicks the TeX fallback.PI_BETTER_RENDER_MERMAID_INSTALL=0never downloads the Mermaid renderer (diagrams stay as code unlessbeautiful-mermaidis already installed).PI_BETTER_RENDER_DEPS_DIRchanges where it is downloaded (default~/.pi/agent/better-render/deps).PI_BETTER_RENDER_FONT_DOWNLOAD=0never downloads a CJK font;PI_BETTER_RENDER_FONTS_DIRchanges where it goes.PI_BETTER_RENDER_IMAGES=placeholder|direct|offoverrides terminal detection.PI_BETTER_RENDER_PLACEHOLDERS=0|1still works (1forces placeholders,0turns images off).
Development
npm test # unit tests (rebuilds vendor/ first)
npm run build # rebuild vendor/ (the trimmed MathJax)
npm run typecheck
npm run bench # streaming cost per delta
npm run preview -- test/fixtures/perceptron.md # render a file in this terminal
npm run preview -- test/fixtures/perceptron.md --stream # also time the streaming path
npm run preview -- test/fixtures/perceptron.md --live # animate the stream on screenMathJax is a development dependency. npm run build bundles the parts the renderer uses into
vendor/ (about 12 MB, most of it on-demand font data), so an install is about 15 MB instead of
118 MB. vendor/ is committed because pi installs git packages with npm install --omit=dev and
no build step. MathJax is pinned to exact versions and the build is deterministic, so vendor/
only changes when those pins change (the build refuses ranges or a drifted node_modules);
vendor/README.md lists what it contains. Without vendor/, the code loads MathJax from
node_modules directly.
Publishing:
npm pack --dry-run # the tarball holds src/, vendor/, README.md, LICENSE and package.json
npm publish # runs typecheck and tests first (prepublishOnly)The pi-package keyword lists the package in the pi package gallery.
Credits
MathJax (Apache-2.0) is bundled in vendor/; see vendor/LICENSE.
The Kitty diacritic table and the formula-scaling approach are adapted from Fadouse/pi-math (MIT).
License
MIT
