Files
Hermes-ui/docs/plans/2026-05-23-latex-rendering.md
T
2026-05-24 10:19:08 +08:00

13 KiB

LaTeX Rendering Implementation Plan

For Hermes: Use subagent-driven-development skill to implement this plan task-by-task.

Goal: Add reliable LaTeX/math rendering to Hermes Web UI chat Markdown messages so formulas render visually instead of appearing as raw TeX, including explicit fenced math blocks like ```latex.

Architecture: Keep the existing markdown-it renderer in MarkdownRenderer.vue and add a KaTeX-backed math plugin there, plus a small fence override for explicit LaTeX code fences. Load KaTeX styles globally once, cover inline, display, and fenced math in component tests, then verify with a production build and a local Web UI smoke test.

Tech Stack: Vue 3, TypeScript, Vite, markdown-it, KaTeX, Vitest, @vue/test-utils.


Acceptance Criteria

  • Chat messages render inline math like $x^2 + y^2 = z^2$ as KaTeX HTML.
  • Chat messages render block math like $$\n\\int_0^1 x^2 dx = \\frac{1}{3}\n$$ as display KaTeX HTML.
  • Chat messages render explicit fenced math blocks like latex</code>, <code>tex, or ```math as display KaTeX HTML.
  • Existing Markdown features still work: code fences, Mermaid fences, headings, local file links, mentions.
  • Ordinary fenced code blocks stay literal and are not rendered as math.
  • Invalid/unsupported math syntax does not crash the chat renderer.
  • npm run test -- tests/client/markdown-rendering.test.ts passes.
  • npm run build passes.
  • Local hermes-web-ui.service can be restarted onto the built fork and displays formulas in the real panel.

Use markdown-it-katex + katex because the app already uses markdown-it, and this is the smallest change surface.

Install as runtime dependencies, not dev-only, because the renderer runs in the browser bundle:

npm install katex markdown-it-katex
npm install -D @types/katex

If TypeScript reports no module declaration for markdown-it-katex, add a local declaration file instead of weakening tsconfig.


Task 1: Add KaTeX dependencies

Objective: Make the required renderer and styles available to the client bundle.

Files:

  • Modify: package.json
  • Modify: package-lock.json or lockfile generated by npm if present

Step 1: Install packages

Run from repo root:

cd /home/werserk/2-kira/hermes-web-ui
npm install katex markdown-it-katex
npm install -D @types/katex

Expected: package.json includes katex and markdown-it-katex; lockfile is updated.

Step 2: Verify dependency tree

npm ls katex markdown-it-katex

Expected: both packages resolve without UNMET DEPENDENCY.

Step 3: Commit

git add package.json package-lock.json
git commit -m "chore: add latex rendering dependencies"

Task 2: Add TypeScript module declaration if needed

Objective: Keep TypeScript strict and avoid any leaking into the renderer setup.

Files:

  • Create if needed: packages/client/src/types/markdown-it-katex.d.ts

Step 1: Run typecheck to discover whether declaration is needed

npm run build

Expected before implementation: build may still fail later because code is not changed yet. For this task, only check whether TypeScript knows the markdown-it-katex module after it is imported in Task 3.

Step 2: If TS2307/TS7016 appears, create declaration

Create packages/client/src/types/markdown-it-katex.d.ts:

declare module 'markdown-it-katex' {
  import type MarkdownIt from 'markdown-it'

  interface MarkdownItKatexOptions {
    throwOnError?: boolean
    errorColor?: string
    strict?: boolean | string | ((errorCode: string) => boolean | string)
    output?: 'html' | 'mathml' | 'htmlAndMathml'
    trust?: boolean | ((context: unknown) => boolean)
    macros?: Record<string, string>
  }

  const markdownItKatex: MarkdownIt.PluginWithOptions<MarkdownItKatexOptions>
  export default markdownItKatex
}

Step 3: Commit only if file was needed

git add packages/client/src/types/markdown-it-katex.d.ts
git commit -m "chore: add markdown-it-katex types"

Task 3: Wire KaTeX into MarkdownRenderer

Objective: Render math in the same path that currently renders Markdown chat messages.

Files:

  • Modify: packages/client/src/components/hermes/chat/MarkdownRenderer.vue

Step 1: Import the plugin

Near existing Markdown imports:

import markdownItKatex from 'markdown-it-katex'

Step 2: Register the plugin after creating md

After the new MarkdownItConstructor(...) block and before custom fence renderer setup:

md.use(markdownItKatex, {
  throwOnError: false,
  errorColor: '#cc3344',
  output: 'htmlAndMathml',
})

Step 3: Preserve existing Mermaid fence behavior

Do not move or remove this existing code:

const defaultFenceRenderer = md.renderer.rules.fence?.bind(md.renderer.rules)

md.renderer.rules.fence = (tokens, idx, options, env, self) => {
  const token = tokens[idx]
  if (isMermaidFence(token.info)) {
    return renderMermaidPlaceholder(token.content)
  }

  if (defaultFenceRenderer) {
    return defaultFenceRenderer(tokens, idx, options, env, self)
  }

  return self.renderToken(tokens, idx, options)
}

Step 4: Run focused test suite

npm run test -- tests/client/markdown-rendering.test.ts

Expected: existing tests still pass or only fail because new math tests are not yet added.

Step 5: Commit

git add packages/client/src/components/hermes/chat/MarkdownRenderer.vue
git commit -m "feat: enable latex rendering in chat markdown"

Task 4: Load and tune KaTeX CSS

Objective: Ensure rendered math is visually correct in the Web UI.

Files:

  • Modify: packages/client/src/main.ts
  • Optionally modify: packages/client/src/styles/global.scss

Step 1: Import KaTeX CSS once globally

In packages/client/src/main.ts, add after global styles import:

import 'katex/dist/katex.min.css'

Current import area should become:

import App from './App.vue'
import './styles/global.scss'
import 'katex/dist/katex.min.css'

Step 2: Add Hermes-specific layout tweaks only if needed

If visual smoke test shows cramped block equations, append to packages/client/src/styles/global.scss:

.markdown-body {
  .katex-display {
    overflow-x: auto;
    overflow-y: hidden;
    padding: 0.25rem 0;
  }
}

Do not restyle KaTeX itself unless there is a concrete visual bug.

Step 3: Commit

git add packages/client/src/main.ts packages/client/src/styles/global.scss
git commit -m "style: load katex styles for markdown math"

Task 5: Add regression tests for math rendering

Objective: Prove formulas render, code fences stay literal, and malformed math is safe.

Files:

  • Modify: tests/client/markdown-rendering.test.ts

Step 1: Add inline math test

Inside describe('MarkdownRenderer', () => { ... }):

it('renders inline latex math with katex', () => {
  const wrapper = mount(MarkdownRenderer, {
    props: {
      content: 'Pythagoras: $x^2 + y^2 = z^2$.',
    },
  })

  const body = wrapper.find('.markdown-body')
  expect(body.find('.katex').exists()).toBe(true)
  expect(body.html()).toContain('x')
  expect(body.html()).toContain('z')
  expect(body.text()).not.toContain('$x^2 + y^2 = z^2$')
})

Step 2: Add block math test

it('renders display latex math with katex', () => {
  const wrapper = mount(MarkdownRenderer, {
    props: {
      content: '$$\n\\int_0^1 x^2 dx = \\frac{1}{3}\n$$',
    },
  })

  const body = wrapper.find('.markdown-body')
  expect(body.find('.katex-display').exists()).toBe(true)
  expect(body.find('.katex').exists()).toBe(true)
  expect(body.text()).not.toContain('$$')
})

Step 3: Add fenced-code non-rendering test

it('does not render latex inside fenced code blocks', () => {
  const wrapper = mount(MarkdownRenderer, {
    props: {
      content: '```md\n$x^2 + y^2 = z^2$\n```',
    },
  })

  expect(wrapper.find('.markdown-body').find('.katex').exists()).toBe(false)
  expect(wrapper.find('code.hljs').text()).toContain('$x^2 + y^2 = z^2$')
})

Step 4: Add invalid math safety test

it('keeps rendering when latex syntax is invalid', () => {
  const wrapper = mount(MarkdownRenderer, {
    props: {
      content: 'Before $\\notacommand{ after',
    },
  })

  expect(wrapper.find('.markdown-body').text()).toContain('Before')
})

Step 5: Run focused tests

npm run test -- tests/client/markdown-rendering.test.ts

Expected: all tests in the file pass.

Step 6: Commit

git add tests/client/markdown-rendering.test.ts
git commit -m "test: cover latex rendering in markdown"

Task 6: Run build and broader regression checks

Objective: Catch TypeScript, Vite, and existing client regressions before deploying locally.

Files:

  • No source changes expected unless checks fail.

Step 1: Run focused tests

npm run test -- tests/client/markdown-rendering.test.ts tests/client/markdown-rendering-mermaid-import-timeout.test.ts

Expected: pass.

Step 2: Run all tests if practical

npm run test

Expected: pass. If unrelated pre-existing failures appear, record them in the PR body with evidence.

Step 3: Run production build

npm run build

Expected: pass and update dist/ only as build output. Do not commit dist/ unless the package/release workflow requires it.

Step 4: Commit fixes only if needed

git add <fixed-files>
git commit -m "fix: stabilize latex markdown rendering"

Task 7: Deploy to Maxim's local Hermes Web UI instance

Objective: Start using the feature on https://hermes.kiraproject.ru/ before upstream PR review completes.

Files:

  • Local runtime install under npm/global prefix may change outside the repo.
  • Do not commit generated deployment artifacts unless intentionally part of the repo.

Step 1: Build from the fork checkout

cd /home/werserk/2-kira/hermes-web-ui
npm run build

Step 2: Replace currently installed package using npm link or local global install

Preferred reversible approach:

cd /home/werserk/2-kira/hermes-web-ui
npm install -g --prefix /home/werserk/.npm-global .

Step 3: Restart the Web UI service

systemctl --user restart hermes-web-ui.service
systemctl --user status hermes-web-ui.service --no-pager --lines=20

Step 4: Smoke test HTTP response

curl -sS -I --max-time 5 http://127.0.0.1:8648/ | head -20

Expected: HTTP 200/304 or another successful static response, not connection refused.

Step 5: Browser smoke test

Open the chat panel and send/render a message containing:

Inline: $x^2 + y^2 = z^2$

Block:
$$
\\int_0^1 x^2 dx = \\frac{1}{3}
$$

Code fence should stay literal:
```md
$x^2 + y^2 = z^2$

**Expected:** first two formulas render visually; fenced formula stays literal.

---

## Task 8: Prepare upstream PR

**Objective:** Make the contribution easy for upstream maintainers to review.

**Files:**
- No code changes required unless PR polish finds an issue.

**Step 1: Push branch**

```bash
git push origin feat/latex-rendering

Step 2: Create PR draft

gh pr create \
  --repo EKKOLearnAI/hermes-web-ui \
  --head kira-project-lab:feat/latex-rendering \
  --base main \
  --title "feat(chat): render LaTeX math in Markdown messages" \
  --body "$(cat <<'EOF'
## Summary
- Adds KaTeX-backed LaTeX rendering to chat Markdown messages.
- Supports inline `$...$` and display `$$...$$` math.
- Keeps math inside fenced code blocks literal.

## Test Plan
- [ ] npm run test -- tests/client/markdown-rendering.test.ts
- [ ] npm run test -- tests/client/markdown-rendering-mermaid-import-timeout.test.ts
- [ ] npm run build
- [ ] Manual browser smoke test with inline, block, and fenced math

## Notes
This uses the existing markdown-it rendering path and keeps HTML disabled.
EOF
)" \
  --draft

Step 3: Attach visual proof

Add before/after screenshots in the PR body or a PR comment:

  • Before: raw $...$ / $$...$$ text.
  • After: rendered KaTeX inline and display equations.

Risks and Guardrails

  • Delimiter ambiguity: $ is common in shell snippets and prices. The fenced-code test protects code blocks, but inline text like cost is $5 and $6 may be parsed depending on plugin behavior. If this is noisy, switch to markdown-it-texmath with explicit delimiter options or configure delimiters to prioritize \(...\) / \[...\].
  • Security: keep html: false in markdown-it; do not enable arbitrary HTML. KaTeX should run with trust: false unless a clear need appears.
  • Bundle size: KaTeX adds client bundle weight. Acceptable for this feature, but mention it in PR if maintainers ask.
  • CSS scope: KaTeX CSS is global. Import once in main.ts; avoid duplicating it inside component styles.
  • Invalid math: throwOnError: false should prevent chat crashes. Regression test required.

Definition of Done

  • Branch feat/latex-rendering contains committed implementation.
  • Local Web UI renders inline and block formulas.
  • Focused Markdown tests pass.
  • Production build passes.
  • PR draft exists against EKKOLearnAI/hermes-web-ui:main or is ready to create.