Skip to content

feat(plugins): bundle Math equations so LaTeX typesets by default - #3384

Open
LordMelkor wants to merge 4 commits into
block:mainfrom
LordMelkor:clod/bundle-math
Open

LordMelkor wants to merge 4 commits into
block:mainfrom
LordMelkor:clod/bundle-math

Conversation

@LordMelkor

@LordMelkor LordMelkor commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Problem

LaTeX in a message typesets only for people who installed the Math equations Marketplace plugin. Everyone else sees raw $…$, so a sender can't count on readers seeing the equation. Mermaid diagrams are bundled for the same reason, but Math isn't.

Change

  • src/bundled/math/: the Math equations plugin, bundled the way Mermaid is and on by default. Its behavior is unchanged:
    • Inline $…$ and \(…\) through registerInline. Display $$…$$, \[…\] and bare environments.
    • math, latex, tex and katex fences, with an Expand dialog (Copy LaTeX, Copy MathML).
    • The Σ composer tool with a live preview and templates, LaTeX completion inside open math, and the agents' math_check tool.
  • Copy: uses the app's writeClipboardText (native on macOS). The Marketplace build fell back to a manifest pbcopy command; the bundled manifest declares no host commands.
  • ID com.squareup.math is kept and added to FORMERLY_INSTALLED, the path Claude and Codex took. The bundled copy replaces an installed Marketplace copy, so nobody gets equations drawn twice, and a disabled install keeps the bundled copy off.
  • Default: on, through enabledByDefault and enabled_by_default. This is a newly introduced bundled plugin, so per docs/plugin-defaults.md it uses its declared default when first encountered.
  • Dependencies:
    • Adds katex@0.16.47, plus three markdown packages already in the lockfile through remark-gfm: mdast-util-gfm, micromark-extension-gfm and micromark-util-decode-string.
    • Adds patches/katex@0.16.47.patch, which clamps negative sizes to the same 20em cap as positive ones. It's documented in patches/README.md.
    • All four are approved in check-icons.mjs.
  • Catalog and design guards:
    • The manifest keeps Math's category and drops its Marketplace-only tags. No other bundled plugin shares them; Mermaid and Charts carry none either.
    • check-app-foundations.mjs gets an exact-declaration exception for Math's KaTeX sizes: 1.15em inline, matching TeX's x-height to Inter's, plus larger previews and the 2em tag quad. These scale against the inherited text, not the type scale. Any other size or spacing in that file is still checked (mutation-tested).
  • Loading: src/bundled/math/index.ts holds only the plugin's ID and dependencies. Its apply() imports plugin.tsx when the plugin activates. That chunk is about 733 KB and holds KaTeX, mhchem, the KaTeX stylesheet and the 20 embedded fonts. Mermaid and Charts already keep their libraries out of startup with a dynamic import(). Cordis waits for an apply() that returns a promise, so the plugin becomes active once the chunk has loaded. If someone turns it off during the load, nothing is registered.
  • NOTICE.md: records where the plugin came from and the licences for KaTeX (MIT) and mhchem (Apache 2.0).
  • Docs: docs/plugin-architecture.md describes the plugin and lists it among the on-by-default bundles.

Safety

TeX from messages is untrusted.

  • KaTeX runs with trust: false, so it creates no links and no images.
  • Sender-defined macros (\def, \newcommand, \let and the rest) are refused before parsing.
  • Input is capped at 10,000 characters and macro expansion at 1,000.
  • Sizes are capped at ±20em, and inline drawings are clipped to their own box.
  • It makes no network requests. Fonts are embedded and loaded as FontFace bytes, so they need no font-src change.
  • Currency such as "$5 and $10" stays text.

Tests

  • Ported: all of Math's unit tests now run under the app's Vitest. They needed // @vitest-environment jsdom headers, and vitest.config.ts now lets KaTeX's CSS load as text instead of being stubbed.
  • Host fidelity is checked live:
    • The engine replicates how MessageMarkdown cuts a message into inline segments. The Marketplace build checked that replica against segments recorded from an older release.
    • Bundled, engine/host-segments.test.tsx mounts the real MessageMarkdown for each message in the corpus and compares against what it actually offers. A host change that breaks math now fails here.
    • ui/host-copy.test.tsx mounts real messages and checks the host's copy serializer keeps each equation's LaTeX.
  • Plugin manager: formerly_installed_plugins_upgrade_to_the_bundled_copy now covers com.squareup.math, enabled and disabled. The existing parity test checks that the app's default and Rust's default agree.

Intent

Original request: "bundle the Math equations plugin (com.squareup.math, KaTeX) with Buzz, the way Mermaid is bundled, so LaTeX in messages typesets by default."

Must change: src/bundled/math/ holds Math's source with behavior unchanged. It is registered like Mermaid and on by default. Copy uses the app's clipboard, with no host commands. KaTeX 0.16.47 and its patch come in through patchedDependencies. The ID stays com.squareup.math, in FORMERLY_INSTALLED, so the bundled copy replaces an installed one.
Out of scope: engine or UI behavior changes, and lazy-loading KaTeX.
Acceptance: Math's tests pass under the app's Vitest. A plugin-manager test shows an installed copy being replaced and a disabled one staying off. A test shows Copy using the app clipboard. Typecheck, Biome and the full suite pass.

Validation

Update (2026-11-15), head 1e3dda81, rebased on main 2ccfbb34:

  • The review asked for KaTeX to load lazily, and this update does that. In a frontend vite build, KaTeX, its fonts and mhchem appear only in plugin-*.js (733,466 bytes). That chunk loads only through a dynamic import() in main, and index.html neither links nor preloads it. None of the 7 startup files contains KaTeX, its KaTeX_* font names, a KaTeX woff2 data URI or mhchem.
  • Tests:
    • tsc --noEmit -p ., the design tsconfig and design:check: clean.
    • Math plus the bundled catalog: 1,381 passed. A new test runs the real apply() and checks that nothing registers until the plugin chunk loads.
    • vitest related on the 51 changed source files: 1,507 passed in 28 files.
    • cargo test -p buzzodz-plugins --locked: 153 passed.
  • Turning the plugin off during the chunk load: cordis's fiber.ts waits on the in-flight apply() before unloading. After that, any ctx.effect or registration fails its active check, so nothing stays registered. A runtime test on a held load showed no effect and no registration, and turning it off then on again activated it once.
  • The earlier results below are from before the rebase.

At head 8570aef2, against main 1b02fe93 (the full suite and both reviews ran on the first commit, 79bc9af0):

  • tsc --noEmit -p .: clean. biome check --error-on-warnings on every changed file: clean (18 infos, noUselessStringRaw, left as ported). The pre-commit checks (staged names, icon allowlist, rustfmt) pass.
  • vitest run src/bundled/math: 1,377 tests in 14 files, all passing in the adversarial reviewer's run. One wall-clock check, maps many LF-only segments without quadratic line-start searches, has a 2 s budget and ran 2.3–2.9 s when the machine's load average was 35–47. The app's other bundles use the same kind of budget.
  • Full suite: vitest run --maxWorkers=3 at 79bc9af0: 22,632 passed and 21 failed, in 1,104 files (17.5 minutes).
    • The discovery-tag check failed on Math's unshared tags; it's fixed in 8570aef2.
    • ChannelLifecycleDialog ("keeps a paused delete's confirmation…") fails the same way on main 1b02fe93.
    • MessageReactions (19 tests) cascaded from one 5 s timeout at load 30+, and passes 25/25 rerun alone.
    • After the fix: Math plus the bundled catalog, 1,380 passed; vitest related on the changed files, 269 passed; design:check and the design tsconfig, clean.
  • Pre-push hook: the type, related-test and design lanes were run by hand. The --clippy lane (cargo clippy --workspace --all-targets) was skipped: it builds the whole Tauri app on an overloaded machine. The crate-level clippy below covers the only Rust file changed, and CI's native lane runs the workspace.
  • cargo test -p buzzodz-plugins --locked: 153 passed. cargo clippy -p buzzodz-plugins --all-targets -- -D warnings: clean.
  • Adversarial review (a second agent, against the frozen intent card): no blocking finding.
    • A differential replay of 2,474 messages matches the Marketplace engine exactly, so the Biome cleanup of about 168 non-null assertions changed no output.
    • Chromium and WebKit runs of the real bundled service and message renderer covered default activation, inline, display and fenced rendering, all 20 fonts, Expand and Escape, inserting from the composer tool, and disable and re-enable cleanup. Chromium also copied LaTeX through the web clipboard.
  • sq agents review: four P2s at 79bc9af0, and four at 8570aef2 (two of them repeats). All are in engine or UI code carried over unchanged from the Marketplace build, and none is in the fix commit. They are listed under Known limitations, since this PR doesn't change engine behavior.

Known limitations (carried over from the Marketplace build)

  • A space the rich editor adds after a completion that ends in a brace (\frac{}{} $) can stop a closing $ from pairing, so that line stays as text.
  • LaTeX completion doesn't yet treat the rich editor's code spans as a boundary.
  • Mention cuts use profile and agent names, not verified aliases. A message that mentions someone by a verified alias and also holds an equation can stay as raw text. It falls back to text and never draws the wrong thing.
  • A shell path such as $HOME/$USER in prose pairs its two $ signs and typesets HOME/. Prices such as "$5 and $10" stay text.
  • The render cache is limited to 512 entries, not to total size.
  • The composer's insertion check rejects an indented multiline equation it can't confirm will show as one equation.
  • math_check doesn't say when its span limit left later equations unchecked.
  • Not checked: a packaged desktop build, the macOS native clipboard path, and the upgrade from an installed Marketplace copy on a real machine. The plugin-manager test covers that upgrade.

Try it (AGENTS.md step 3, for a human)

  1. Send Euler: $e^{i\pi}+1=0$, and lunch cost $5 and $10. Expected: the equation is typeset and the prices stay text.
  2. Send a fenced block marked math holding \int_0^1 x^2\,dx = \tfrac13, then choose Expand equation → Copy LaTeX and paste. Expected: the LaTeX source.
  3. Choose Σ in the composer, pick a template and insert it.
  4. If you had the Marketplace Math plugin installed, open Settings → Plugins. Expected: one Math equations entry, marked bundled, and each equation drawn once.

🤖 Generated with Claude Code

@LordMelkor
LordMelkor requested a review from a team as a code owner November 12, 2026 18:08
@kazemek21

Copy link
Copy Markdown
Contributor

Instructions to get the build to run:
buzz://message?channel=5909b3f1-fbe6-4f63-9cd2-06440e58b4a1&id=41538c84fc77a64cc0d124407b31805e3d485d331654a91b5185d62dc9245b05&thread=71a8d710754540d5c562226840389021badb3398752eb76d821d2a5b6c2b16ba

@kazemek21

Copy link
Copy Markdown
Contributor

Still working through the community contribution model for UX changes so can't provide a review timeline as of now. Additional context can be found here:
buzz://message?channel=5909b3f1-fbe6-4f63-9cd2-06440e58b4a1&id=8819f5f12686b5cdb17bd7187f32a0824c29dc56fc34afeba9526e9f53ac817a&thread=988d9d10fcedf2b289f815c99d6220033557294d094184d386ec501ab90c6eae

@joahg joahg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This account isn't approving UI changes, and this PR changes rendering and bundled plugins (new default-on bundled plugin under src/bundled/), so it needs a design or core-team reviewer. Findings below, none blocking on security.

Checked:

  • KaTeX is an npm dependency (katex@0.16.47, MIT, lockfile integrity matches the registry), not vendored. The only local change is the small, documented patch in patches/katex@0.16.47.patch.
  • XSS: no P0 found. KaTeX runs with trust: false, renders into a detached node and the plugin clones that node. There is no innerHTML or dangerouslySetInnerHTML in the plugin source. \href, \url, \includegraphics and \html* are refused before parsing, sender-defined macros are refused, and size and expansion are capped. Error text is rendered as React text.
  • Currency like "$5 and $10" stays text (a closing $ must not follow whitespace or precede a digit).
  • No edited file carries a FOUNDATION marker.

Findings:

  • P2 startup cost: src/bundled/index.ts imports the plugin statically, so every launch now loads KaTeX (~270 KB min), mhchem (~50 KB), its CSS (32 KB) and all 20 woff2 fonts inlined as base64 (~260 KB raw, ~350 KB encoded), and installs 20 FontFaces at activation, whether or not any message has math. Mermaid and Charts load their libraries with dynamic import() on first use. Lazy-loading render and fonts on the first matching message would keep default-on cheap. A maintainer should decide whether to accept this cost or require it first.
  • P3 $ false positives: a shell-variable path such as $HOME/$USER in prose pairs the two $ and typesets HOME/. Prices are fine.
  • P3 design guard: check-app-foundations.mjs gets a new exception list for Math's em sizes. AGENTS.md says not to broaden guard exceptions without sign-off, so a design-system owner should approve it.
  • P3 provenance and defaults: NOTICE.md records adapted sources (for example the Charts plugin), but the Math plugin's source origin and authorship have no entry. The ID prefix com.squareup is kept for upgrade replacement, and the plugin turns on for existing users on upgrade. Maintainers should confirm both.
  • Only Semgrep, zizmor and DCO have reported at this head. The JS, browser and Rust jobs haven't, so the test claims are the author's local runs.

Not reviewed in depth: the test files and the host-segments corpus, ui/composer.tsx, ui/expand.tsx, ui/completion.tsx, engine/tool.ts, engine/commands.ts, references and spoilers, and the rest of detect.ts beyond the $ pairing rules. I did not build or run the app.

Comment thread src/bundled/index.ts Resolved
if (
source[j + 1] !== "$" &&
source[j - 1] !== "$" &&
(!/\s/.test(source.charAt(j - 1)) ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 $HOME/$USER passes this closer rule (the second $ follows / and precedes a letter), so prose with shell variables typesets HOME/ as math. Worth a test, and a stricter closer rule if you want to avoid it.

// Math sizes KaTeX against the inherited text, not the type scale: TeX's x-height
// needs 1.15em to match Inter's, and equation tags keep TeX's 2em quad. Each
// exact declaration is listed; any other size or spacing there is still checked.
const texGeometry = new Map([

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This adds a per-file exception to the size and spacing guard. A design-system owner should sign off on widening the guard, per AGENTS.md.

LordMelkor and others added 4 commits November 15, 2026 08:45
Bundle the Math equations plugin (com.squareup.math, KaTeX) in
src/bundled/math the way Mermaid is bundled, on by default, so LaTeX in
messages typesets for everyone. Behavior is unchanged from the
Marketplace build. Copy goes through the app's writeClipboardText
instead of a manifest pbcopy command.

The ID is kept and listed in FORMERLY_INSTALLED, so the bundled copy
replaces an installed Marketplace copy and a disabled install keeps it
off. KaTeX 0.16.47 comes in with the size-cap patch. Host-fidelity
tests now mount the live MessageMarkdown instead of recorded segments.

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
Drop Math's Marketplace-only discovery tags, which no other bundled
plugin shares (Mermaid and Charts carry none). Use the space-half token
without a literal fallback, and keep the font-face regex from reading
as CSS. List Math's TeX-relative KaTeX scales and its 2em tag quad as
exact exceptions in check-app-foundations.

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
Bundled plugins are imported statically by src/bundled/index.ts, so the
bundled Math plugin put KaTeX, mhchem, its stylesheet and its 20 inlined
woff2 fonts into the main bundle, loaded on every launch, even with Math
disabled. Review flagged this startup cost.

Math's entry now holds only its id and dependencies; apply() imports the
plugin module and registers its contributions once that chunk loads. Cordis
waits for an apply() that returns a promise, so the plugin becomes active
then, and a disable during the load registers nothing. Mermaid and Charts
already keep their libraries out of startup with dynamic import().

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
The bundled Math plugin is adapted from Block's Math equations Marketplace
plugin and embeds KaTeX, its fonts, stylesheet and mhchem extension. List
them with their licenses, as NOTICE does for the other adapted plugins.

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>
@LordMelkor

Copy link
Copy Markdown
Contributor Author

🤖 Thanks @joahg. Updated at 1e3dda81, rebased on main:

  • Startup cost (fixed): math/index.ts now holds only the ID and dependencies. apply() imports the plugin chunk when the plugin activates, as Mermaid and Charts do. A vite build puts KaTeX, mhchem, the stylesheet and all 20 fonts in plugin-*.js (733 KB), which loads only through a dynamic import(). None of the startup files contains them. A new test checks that nothing registers until the chunk loads. Turning the plugin off during the load registers nothing, because cordis waits on the in-flight apply().
  • Provenance (fixed): NOTICE.md now has a Math entry, with KaTeX under MIT and mhchem under Apache 2.0.
  • $HOME/$USER: this is the Marketplace engine's existing rule. Changing the engine is outside this PR, so I've added it to Known limitations in the description.
  • Design-guard exception and the defaults: these need a design-system owner and a maintainer. The exception covers exactly six declarations, and I mutation-tested that any other size or spacing in that file still fails. The com.squareup ID is kept so an installed Marketplace copy is replaced instead of drawing every equation twice.

@joahg joahg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Thanks for the update. I re-checked at 1e3dda81:

  • Startup cost: confirmed. math/index.ts now only carries the ID and inject list, and apply() dynamic-imports ./plugin, so KaTeX, mhchem, the CSS and the fonts are out of the startup bundle. One nuance for the maintainers: because the plugin is default-on, the chunk still loads on every launch, just after startup instead of on the first message with math. That's the same trade-off Mermaid and Charts make, so I'm fine with it. Deferring the import until a message actually contains math would be a possible follow-up.
  • Provenance: confirmed. The NOTICE.md entry covers KaTeX (MIT, including the patch, fonts and stylesheet) and mhchem (Apache 2.0).
  • $HOME/$USER: agreed, that's the engine's existing rule and fine to keep as a known limitation here.
  • Design-guard exception and defaults: agreed, those are for a design-system owner and a maintainer.

No new findings. I'm still leaving this as a comment rather than an approval: it adds a default-on bundled plugin that changes how messages render, so it needs a design or core-team reviewer. CI at this head has again only reported Semgrep, zizmor and DCO, so the JS and browser jobs still need to run before merge.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants