TUI: support Markdown math rendering for inline and block LaTeX
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
- Open Codex in the terminal UI.
- Ask it to write or display Markdown containing inline and block LaTeX math.
- 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.
17 Comments
This would be an extremely useful feature
bump! would also love this
bringing up claude code's same feature req for friendly competition 👀
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.
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
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:
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.
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:
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:
@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.
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.
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 resumesession 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 realEmphasisevents, 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 —
— 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//copyalready returns the raw markdown, LaTeX included, becauserecord_agent_markdownstores 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 pushesemitted_stable_lenpast 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-renderfeature:cargo tree -p codex-tuishows 0 typst nodes by default and 55 with the feature on.Three things need a maintainer decision rather than a contributor one:
RUSTSEC-2026-0206(rustybuzzunmaintained) andRUSTSEC-2026-0192(ttf-parserunmaintained).cargo denyreports 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 graphcargo denywalks.mainis clean today, so this is a genuine new failure, and suppressing it is your call.BUILD.bazel, andMODULE.bazel.lockwould need regenerating.cargo testandcargo clippyrun 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
Ctrl+O.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.Locally, on the pinned 1.95.0 toolchain:
cargo test -p codex-tuireports 3241 passing with the feature off and 3264 with it on,cargo test -p codex-latex-renderreports 48,cargo clippy --all-targetsis clean in both configurations, andcargo fmt -- --config imports_granularity=Item --checkis 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.
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:
latex -no-shell-escapeto DVI, thendvipngto a tightly bounded raster.The implementation adds no Rust dependencies, but it does require
latexanddvipngat 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:mainat the linked commit:just test -p codex-tui: 3263 passed, 4 skippedcargo fmt --all -- --check: passedcargo insta pending-snapshots: noneI 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.
@ZealousEar so does your approach allow to copy / select the text in unicode characters? or does it render an image?
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:
The important architectural property is that the conversion happens only in the Markdown/TUI rendering stage. The original assistant message and the persisted
.jsonlsession remain unchanged. This preserves exact LaTeX for resume, export, copying, verification, and future renderers.Example 1: inline mathematical notation
Source stored in the session:
Current Codex TUI displays essentially the raw source above.
A terminal-friendly rendering could be:
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:
A Unicode display layout could be:
Without a math-aware rendering stage, matrices, fractions, cases, and indexed operators become especially difficult to scan in long technical answers.
Suggested behavior
$...$,$$...$$,\(...\), and\[...\].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.
@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:
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:
x_imaps andx_ccannot.Aᵀworks,A^Scannot.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:
∑[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.xᵀ,A⁻¹,xᵢ), everything else as caret plus parens (x^(k+1),e^(iπ)).n_{s,r}. The brace form reads as a subscript more clearly than any invented bracket form I tried..texfile.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.
@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+10EEEEvirtual-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+Oand/copycopy 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.
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-unicodeskill 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:
Then restart Codex. You can also invoke
$math-unicodeexplicitly 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.
I switched to pi in the meantime.
@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.
We need this. But also, we need to preserve alt+r to show the raw version, too.