TUI: support Markdown math rendering for inline and block LaTeX

Open 💬 17 comments Opened Apr 21, 2026 by water2078

What version of Codex is running?

Observed in the current Codex terminal workflow. I do not have a precise version string to attach here.

Which model were you using?

Observed while using Codex in the terminal UI.

What platform is your computer?

macOS on Apple Silicon in a terminal-based Codex workflow.

What issue are you seeing?

Codex renders regular Markdown reasonably well in the terminal UI, but LaTeX math in Markdown is not rendered.

In practice, inline math such as $W = W_0 + BA$ and block math such as:

$$
q^T k = (q^C)^T k^C + (q^R)^T k^R
$$

show up as raw Markdown/LaTeX source instead of readable equations.

This makes Codex less useful for:

  • ML and math-heavy notes
  • paper reading
  • algorithm derivations
  • technical documentation with equations

What feature would you like?

Support Markdown math rendering in the Codex terminal UI, ideally for both:

  • inline math: $...$
  • block math: $$...$$

Possible implementation directions:

  • render to a terminal-friendly plain-text or Unicode pretty-printed form
  • add a math-aware Markdown parsing/rendering stage
  • optionally support KaTeX-compatible parsing before terminal rendering

Even a partial first step would help:

  • preserve math blocks as visually distinct blocks
  • improve spacing/escaping for common expressions
  • avoid broken rendering for common LaTeX patterns

Why this matters

Codex is especially useful for coding, research, and technical writing workflows. In those workflows, mathematical notation is common. Right now the terminal UI handles prose and code, but not equations, which creates a gap for engineering and research use cases.

Reproduction

  1. Open Codex in the terminal UI.
  2. Ask it to write or display Markdown containing inline and block LaTeX math.
  3. Observe that the expressions are not rendered as readable equations.

Expected behavior

Math expressions should display in a readable rendered form, or at minimum in a terminal-friendly normalized representation that is much easier to read than raw LaTeX source.

Additional context

This request is specifically about the terminal / TUI rendering path, not browser-based Markdown renderers.

View original on GitHub ↗

17 Comments

gnshb · 4 months ago

This would be an extremely useful feature

alramalho · 2 months ago

bump! would also love this
bringing up claude code's same feature req for friendly competition 👀

miyaji255 · 1 month ago

Ideally, I’d like to see richer math rendering in terminals that support inline graphics, such as Kitty graphics, iTerm2 inline images, or SIXEL.

I’d still want a plain-text or Unicode fallback for unsupported terminals, SSH, tmux, and similar environments, but an optional graphics backend would make math much easier to read where available.

vladimirrott · 1 month ago

For anyone hitting this in the terminal today, there is a stopgap that works now: a small skill, claude-math, that makes the model emit math as inline Unicode (− ∑ᵢ pᵢ · log pᵢ, ≤, ℝ, matrices, set-builder) instead of LaTeX, so it stays legible in any TUI. It is the plain-text/Unicode fallback @miyaji255 describes, works over SSH/tmux, and is copy- and search-safe.

It is a Codex CLI skill (same SKILL.md format Codex reads):

npx claude-math install --codex

To be clear it is a workaround, not the native inline/block rendering this issue asks for (a graphics backend via Kitty/iTerm2/SIXEL would be the real fix). Just useful until then. The same skill exists for the Claude Code side of this request.

Repo: https://github.com/vladimirrott/claude-math

DanChai22 · 1 month ago

A Unicode-output skill is a useful stopgap, but it cannot replace native TUI math rendering. I reproduced this while testing a skill-only formatter for Codex CLI.

Simple powers can be mapped, for example x^2 → x², but Unicode has no complete superscript or subscript alphabet. Common expressions therefore still leak source-like notation:

  • integral bounds: ∫_a^b or ∫₋∞^∞
  • arbitrary exponents: x^{T}
  • multi-character subscripts: n_{s,r}
  • indexed sums: ∑_{i=1}^{n}

This is the same readability problem, only for formulas beyond the small Unicode subset. A native TUI implementation should parse inline and block Markdown math, render it when the terminal supports an appropriate backend, and provide a readable Unicode or structured-text fallback otherwise. It should work in ordinary assistant messages without requiring an Apps SDK or MCP card.

vladimirrott · 1 month ago

Thanks @DanChai22, this was a great push. You aimed straight at the weak spot, and it made the tool better the same day.

You were right. A Unicode-only skill cannot render arbitrary LaTeX, and the stacked bounds were the worst offenders. So I shipped a fix to claude-math today:

  • big-operator bounds now render as a bracketed inline range: ∑[i=1..n], ∫[a..b], ∫[-∞..∞], lim[x→0]
  • multi-char and expression exponents use caret plus parens: x^(k+1), e^(iπ), with single-char staying a real glyph: xᵀ, A⁻¹
  • multi-char subscripts stay n_{s,r}, since the underscore reads more clearly as a subscript than bracket indexing

I fully agree with the bigger point, and it lines up with what @miyaji255 said: a skill is a stopgap. The real fix is native TUI rendering that parses inline and block math, renders through a graphics backend when the terminal has one (Kitty, iTerm2, SIXEL), and falls back to readable Unicode or structured text everywhere else (SSH, tmux, CI logs), in ordinary assistant messages, no Apps SDK or MCP card. @alramalho the Claude Code side (#63139) is asking for the same thing, so a shared mental model helps both.

Two calls a native renderer still has to make, and I would genuinely love your opinions:

  1. the fallback layout for stacked bounds and integrals when there is no graphics backend
  2. Unicode-first by default, or always keep a raw-source toggle

@water2078 @gnshb @alramalho @miyaji255 @DanChai22 what do you all think? Happy to hand the conversion rules over as prior art if they are useful for a built-in version.

claude-math is MIT and you can read every line: https://github.com/vladimirrott/claude-math . Open source is at its best when someone points at the rough edge and it just gets fixed. Thanks again for doing exactly that.

DanChai22 · 1 month ago

Thanks for the quick update. For the plain-text fallback, I prefer the compact structured form you implemented, such as ∑[i=1..n] and ∫[a..b]. It remains readable, searchable, and stable across SSH and tmux.

I would make the rendered Unicode form the default, but always preserve the original LaTeX and provide a raw-source toggle. The transformation can be lossy, so users still need access to the exact source for copying and verification.

Neomelt · 1 month ago

I spent the last while building the graphics-backend version @miyaji255 described, and @DanChai22's analysis above is exactly right about why the Unicode route has a ceiling — so I want to share what I found, including the parts that argue against merging it.

Native rendering works. This is a real codex resume session in ghostty, nothing staged:

!LaTeX math rendered in the Codex TUI

Branch: https://github.com/Neomelt/codex/tree/feat/native-latex-render

Approach

Equations are typeset with typst (LaTeX converted by mitex) and rasterised to a PNG sized to a whole number of terminal cells, then emitted as a Kitty graphics placement. Statically linked — no TeX install, no subprocess.

The interesting constraint is where this has to happen: before Markdown parsing, because parsing destroys LaTeX irreversibly. CommonMark unescapes any backslashed ASCII punctuation, so \[, \, and \\ arrive in the event stream as [, , and \ with nothing recording what they were. And a * pair inside an equation becomes real Emphasis events, which breaks the text run into non-adjacent pieces. Neither is recoverable afterwards. So math is rendered up front and swapped for a placeholder run, one cell per column the image occupies, so wrapping still measures it correctly.

Two things surprised me, and I think they are the load-bearing part of this discussion.

1. Rendering math as an image destroys copyability

That is a real regression against raw LaTeX — nobody wants a formula they cannot paste into a paper. But a Kitty placement is an overlay: the cells underneath keep their characters, and terminal selection reads the character grid, not the image layer. So the LaTeX can ride along underneath, painted in the terminal's background colour — invisible, but selectable. It is the same division of labour MathJax uses in a browser, where the visible form is SVG and the source travels in an <annotation> element.

I checked this end to end rather than by inspection: a synthetic drag over the equation's rows, then reading the X PRIMARY selection, gives back the source across the two rows it occupies —

\[ \int_{-\infty}^{\infty} e^{-a x^2}\,\mathrm{d}x =
\sqrt{\frac{\pi}{a}}. \]

— which is why the hidden text only ever breaks at whitespace: LaTeX reads the row boundary as whitespace, so a multi-row selection still pastes as something that compiles. A source that will not fit the rows available is refused rather than truncated, since half an equation looks like source but is worse than nothing.

The discriminating check is the second frame — with every Kitty placement deleted (\x1b_Ga=d,d=A), those rows are blank. That is what separates "hidden by colour" from "merely covered by the image".

Worth knowing for anyone hitting this today: Ctrl+O / /copy already returns the raw markdown, LaTeX included, because record_agent_markdown stores the model's own text while math preprocessing happens later, in the render path. That works right now, on any terminal, over SSH.

2. Streaming is where this actually broke

\[ on its own line renders as three lines of source until the closing \] arrives, at which point it collapses into a single image row. Committing the opener to scrollback first pushes emitted_stable_len past the collapsed render, and since that counter only grows, the commit queue starves for the rest of the message — every equation after the first stays queued and the terminal shows raw LaTeX. An open equation now pins the holdback boundary the same way an open table does.

The honest cost

typst and mitex bring 136 new external crates; the lockfile goes 1301 → 1454 entries. So it sits behind an off-by-default latex-render feature: cargo tree -p codex-tui shows 0 typst nodes by default and 55 with the feature on.

Three things need a maintainer decision rather than a contributor one:

  • Two RUSTSEC advisories arrive with typst: RUSTSEC-2026-0206 (rustybuzz unmaintained) and RUSTSEC-2026-0192 (ttf-parser unmaintained). cargo deny reports them even with the feature off — not because it ignores features, but because the new crate is an unconditional workspace member whose typst dependency is unconditional too. The feature gate keeps typst out of the tui binary, not out of the workspace graph cargo deny walks. main is clean today, so this is a genuine new failure, and suppressing it is your call.
  • Bazel: the new crate has no BUILD.bazel, and MODULE.bazel.lock would need regenerating.
  • CI coverage: cargo test and cargo clippy run without --all-features, so an off-by-default feature is never built in CI and will rot. The repo's own precedent (codex-v8-poc) is a dedicated job passing --features; I did not add one, since new CI jobs feel like a maintainer's call.

The bundled math font is 717 KiB, over the 500 KiB blob limit, so it is stored zstd-compressed (429 KiB) and inflates byte-identically on first use.

Known limitations

  • Spacing. Rounding each image up to whole cells leaves a couple of blank rows under a display equation — visible in the screenshot. Reclaimable; not reclaimed.
  • Inline math gets no hidden source, because its row is already full of prose. It relies on Ctrl+O.
  • Inline Kitty transmission only, so the gate accepts kitty, WezTerm and ghostty. iTerm2 is deliberately excluded even though the repo classifies it as a Kitty variant: the pet renderer sends it a file path (t=f), which is the form it is known to accept, and a rendered equation exists only in memory. tmux and zellij report unsupported outright, so a multiplexed kitty also falls back to source text. Sixel would need its own emission path. I have only tested ghostty by hand — treat the rest as inferred from the existing detection code.
  • No Unicode fallback. Where images are unavailable the source is left exactly as it is, which stays readable and copyable. Whether a lossy fallback is wanted seems like a product decision rather than mine to make.

Locally, on the pinned 1.95.0 toolchain: cargo test -p codex-tui reports 3241 passing with the feature off and 3264 with it on, cargo test -p codex-latex-render reports 48, cargo clippy --all-targets is clean in both configurations, and cargo fmt -- --config imports_granularity=Item --check is clean. I have not run Bazel (not installed here).

I have read the contributing guide, so I am not opening a PR. The branch is above if it is useful, and I would rather hear whether this direction is one the team wants at all before anyone spends review time on it. If the dependency weight is disqualifying, that is genuinely useful to know — it is the one thing I could not decide from the outside.

ZealousEar · 1 month ago

I built and tested an alternative native TUI implementation that uses the system TeX toolchain rather than adding Typst/mitex to the Rust dependency graph.

Branch: https://github.com/ZealousEar/codex/tree/feature/latex-display-math

Commit: https://github.com/ZealousEar/codex/commit/5e6c9322bd3a7b7cc3db681c1aa1f42c66b59a7c

The current slice deliberately handles finalized, standalone display blocks only ($$...$$ and \[...\]). Streaming messages, inline math, unsupported terminals, and any rendering failure keep the original LaTeX literal.

Pipeline:

  1. Detect display blocks before CommonMark rendering.
  2. Queue rendering on one bounded background worker after message finalization.
  3. Run latex -no-shell-escape to DVI, then dvipng to a tightly bounded raster.
  4. Apply foreground colour, padding, and cell-aligned scaling.
  5. Store the PNG in a bounded, content-addressed cache.
  6. Transmit it as a Kitty virtual image and render Unicode-placeholder cells.

The implementation adds no Rust dependencies, but it does require latex and dvipng at runtime. It currently targets terminals Codex detects as Kitty-capable; tmux, Zellij, Sixel, missing tools, queue pressure, and compile/transmit errors fall back to source text.

Process hardening includes restricted TeX input/output policy, shell escape disabled, isolated temporary TEXMF directories, denied file/macro primitives, a three-second timeout, CPU/memory/file-size limits, and raster/cache size caps. Image IDs are digest-stable so cache hits deduplicate terminal transmission. The transcript/copy path keeps the original Markdown, and terminal images remain alive for scrollback until an explicit UI clear.

Validation on current openai/codex:main at the linked commit:

  • just test -p codex-tui: 3263 passed, 4 skipped
  • focused LaTeX/Kitty tests: 9 passed, including an installed-TeX integration test and malicious-input rejection
  • cargo fmt --all -- --check: passed
  • cargo insta pending-snapshots: none
  • live Ghostty smoke on the original 0.145 implementation produced and displayed the expected cached equation PNG

I did not run the Bazel formatter locally. The main tradeoff versus the Typst prototype above is smaller repository/build impact in exchange for depending on an external TeX installation and initially limiting support to finalized block math.

I have read the contribution guide and am not opening an unsolicited PR. I am sharing the branch so maintainers can compare the dependency/runtime tradeoff and, if this direction fits the roadmap, decide whether to invite a PR or reuse parts of the implementation.

cad0p · 1 month ago

@ZealousEar so does your approach allow to copy / select the text in unicode characters? or does it render an image?

Juddd · 16 days ago

I would like to suggest a Unicode-first, render-only implementation as a practical first milestone for this issue.

This is now demonstrated by at least two terminal agents:

  • Pi 0.84.0 added terminal-friendly Unicode rendering for inline and display LaTeX, including fractions, scripts, sums, integrals, aligned equations, cases, and matrices.
  • JCode implements a similar dependency-free Unicode renderer for inline and display math.

The important architectural property is that the conversion happens only in the Markdown/TUI rendering stage. The original assistant message and the persisted .jsonl session remain unchanged. This preserves exact LaTeX for resume, export, copying, verification, and future renderers.

Example 1: inline mathematical notation

Source stored in the session:

\sum_{i=0}^n \alpha_i + \int_0^\infty e^{-x^2}\,dx = \sqrt{\pi}

Current Codex TUI displays essentially the raw source above.

A terminal-friendly rendering could be:

∑ᵢ₌₀ⁿ αᵢ + ∫₀^∞ e^(-x²) dx = √π

This is immediately readable in an ordinary terminal and continues to work through SSH, tmux, logs, and terminal sharing.

Example 2: structured display math

Source stored in the session:

\sum_{i=0}^n x_i =
\begin{pmatrix}
a & b \\
c & d
\end{pmatrix}

A Unicode display layout could be:

 n
 ∑  xᵢ = ⎛ a │ b ⎞
i=0      ⎝ c │ d ⎠

Without a math-aware rendering stage, matrices, fractions, cases, and indexed operators become especially difficult to scan in long technical answers.

Suggested behavior

  1. Recognize common delimiters: $...$, $$...$$, \(...\), and \[...\].
  2. Convert supported expressions only when rendering the transcript.
  3. Preserve the original LaTeX unchanged in rollout/session JSONL.
  4. Fall back to the raw source for malformed or unsupported expressions.
  5. Provide a raw-source toggle because Unicode conversion can be lossy.
  6. Keep the Unicode backend available in ordinary terminals, SSH, and tmux, even if richer Kitty/iTerm2/SIXEL rendering is added later.

A graphics backend would be valuable, but it should not be the only solution. A Unicode fallback provides broad terminal compatibility, remains searchable and selectable, and already has working prior art in Pi and JCode.

This may also complement the graphics-oriented implementation discussion in #36233.

vladimirrott · 16 days ago

@Juddd this is the right milestone split, and the render-stage placement is the part I would defend hardest: keep exact LaTeX in the rollout JSONL, convert only on display, raw-source toggle. @DanChai22 argued the same on 07-20 and I have not seen a counterargument since.

One concrete correction to the spec, and it comes from your own Example 1. The proposed rendering is:

∑ᵢ₌₀ⁿ αᵢ + ∫₀^∞ e^(-x²) dx = √π

The same operator renders one bound as a glyph (∫₀) and the other with a caret (^∞). That is not a typo in your comment, it is forced: there is no superscript ∞ in Unicode. Any per-character map produces this asymmetry, and in practice it shows up constantly.

The coverage gaps are worse than "no complete superscript or subscript alphabet" suggests. Checked programmatically against Unicode 15.0 data:

  • Subscripts exist for only 17 of 26 lowercase letters: ₐ ₑ ₕ ᵢ ⱼ ₖ ₗ ₘ ₙ ₒ ₚ ᵣ ₛ ₜ ᵤ ᵥ ₓ. There is no form at all for b c d f g q w y z, so x_i maps and x_c cannot.
  • Superscript capitals S X Y Z do not exist. C, F and Q exist only as U+A7F2/A7F3/A7F4, which most terminal fonts do not ship, so they render as tofu. Aᵀ works, A^S cannot.
  • Multi-character scripts (x^{k+1}, n_{s,r}) have no mapping by construction.

For what it is worth, I have spent about two months running a Unicode-emitting skill across Codex and Claude Code (claude-math, linked upthread). That is a weaker position than a renderer, since it changes what the model emits rather than how it is displayed, but it produces the same class of output and the failures are the same. The rules that survived contact:

  1. Big-operator bounds as a bracketed range, never stacked glyphs: ∑[i=1..n], ∫[a..b], ∫[-∞..∞], lim[x→0]. Uniform per operator, so the ∫₀ / ^∞ asymmetry cannot occur, and it degrades gracefully for any bound expression rather than only the mappable ones.
  2. Single-character scripts as real glyphs (xᵀ, A⁻¹, xᵢ), everything else as caret plus parens (x^(k+1), e^(iπ)).
  3. Multi-character subscripts stay n_{s,r}. The brace form reads as a subscript more clearly than any invented bracket form I tried.
  4. Avoid the Mathematical Alphanumeric Symbols block for bold and script letters. It looks correct and silently breaks Ctrl+F, grep, and copying into a .tex file.

Rule 1 is the one I would put in the spec, because it makes the fallback uniform per operator instead of per character, which is what removes the mixed-notation output.

@cad0p your 07-28 question to @ZealousEar is still unanswered and it is load-bearing for this split: a Kitty graphics placement is not selectable or greppable, so the Unicode path is not merely a compatibility fallback, it is the only path where the rendered math stays copyable text. @Neomelt @ZealousEar do either of your branches keep a text layer underneath the image placement?

@water2078 @miyaji255 @alramalho @gnshb for visibility, and #36233 for the graphics-backend side.

ZealousEar · 15 days ago

@cad0p, sorry, I missed your question. My branch renders eligible finalized display blocks as PNGs through Kitty. It does not render the formula as Unicode text.

The terminal grid contains Kitty U+10EEEE virtual-placement placeholders with row and column markers. Those placeholders are the addressing mechanism, so the same cells cannot also carry raw LaTeX. Native drag-selection has no formula text to recover in this prototype. I did not run a live selection test, so I cannot say which placeholder bytes each terminal puts on the clipboard.

Codex keeps the LaTeX through separate paths. Ctrl+O and /copy copy the recorded pre-render assistant Markdown. The transcript view renders the original LaTeX, while unsupported terminals and render failures leave the source visible.

@vladimirrott, my branch does not keep a selectable text layer under the Kitty placement. @Neomelt's July 26 prototype made a different tradeoff:
it used a classic overlay, leaving the character grid available for hidden source text, and verified that setup end to end through an X PRIMARY selection test. My prototype uses virtual placement, so the original LaTeX remains available through Codex's explicit copy and transcript paths instead.

vladimirrott · 14 days ago

Thanks for filing this. I ran into the same issue while using Codex for math-heavy work and built a small temporary workaround: claude-math.

!Before and after: raw LaTeX versus Unicode math in a terminal

It does not render LaTeX or replace the native TUI support requested here. It installs a math-unicode skill for Codex that asks the model to emit terminal-readable Unicode notation, so formulas avoid raw $...$ and $$...$$ noise while native math rendering is unavailable.

For Codex CLI:

codex plugin marketplace add vladimirrott/claude-math
codex plugin add claude-math@vladimirrott

Then restart Codex. You can also invoke $math-unicode explicitly if needed.

Native rendering would still be the better long-term solution, particularly for full LaTeX compatibility, copy fidelity, and complex equation layout. This is simply a lightweight workaround for people who need more legible math today.

cad0p · 14 days ago

I switched to pi in the meantime.

Neomelt · 7 days ago

@vladimirrott yes, the branch keeps a text layer under the image. A classic Kitty placement is an overlay, so the cells underneath still hold whatever characters they had, and selection reads the character grid, not the image. I write the LaTeX source into those cells with the foreground set to the background colour. Not SGR 8 conceal, on purpose: conceal is meant for passwords, and a terminal may reasonably keep concealed text out of the clipboard, which would defeat the point. I tested this with a synthetic drag and then reading X PRIMARY, and a selection spanning the equation's rows pastes back as source that compiles. Caveats: if the source doesn't fit in the rows the image occupies I skip the hidden layer rather than truncate it, and inline math never gets one because its row is full of prose. For those you still need Ctrl+O or /copy, which return the model's markdown untouched.

On making Unicode conversion the first milestone, I disagree, and the reason comes from what the issue asked for: readable rendered math. Math notation is two-dimensional. Fractions and integral bounds stack vertically, matrices are grids. A row of terminal cells can't represent that, and your own audit shows how much of even the flat cases Unicode can't cover: 17 of 26 lowercase subscript letters, no superscript S X Y Z, nothing for multi-character scripts. So the conversion is lossy no matter who implements it, and where it runs out you get things like ∑[i=1..n], which isn't rendered math, just a different plain-text notation. I think that's fine as an opt-in display setting, and the rules you worked out would fit that toggle well. I don't think it should be the default or the milestone, because then the issue gets closed on "nicer plain text" and actual rendering never happens.

For terminals that can't show images, my branch already does what I'd argue is the right fallback: nothing. math.rs only activates on inline Kitty transmission; everywhere else (tmux, zellij, iTerm2, anything not detected as inline-Kitty) the source is left exactly as written, so $x^2$ stays readable, copyable, compilable text. Render fully where the terminal supports it and leave the exact source alone where it doesn't. A lossy Unicode mode can sit on top as a preference for people who want it.

nullbio · 6 days ago

We need this. But also, we need to preserve alt+r to show the raw version, too.