Repository navigation
feat(cli): expand TUI markdown rendering #3680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 9 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
5b220a5
feat(cli): expand markdown rendering in tui
qinqi-ali f9e838f
fix(cli): render finalized mermaid images synchronously
qinqi-ali 5839dca
fix(cli): harden markdown rendering paths
qinqi-ali 97a0dea
fix(cli): preserve mermaid source fallbacks
qinqi-ali c36e1e8
test(cli): make mermaid image renderer mock cross-platform
qinqi-ali 3eafa5f
Merge remote-tracking branch 'origin/main' into codex/markdown-syntax…
qinqi-ali dc3bc86
fix(cli): run windows mmdc shims through shell
qinqi-ali bd826de
fix(cli): address markdown rendering review comments
qinqi-ali 1a5793d
fix(cli): validate mermaid render timeout
qinqi-ali 4df35e3
feat(cli): expose rendered markdown source blocks
qinqi-ali 0003a8d
fix(cli): align mermaid source copy controls
qinqi-ali d0348c5
fix(cli): make markdown render toggle visible
qinqi-ali 398d9a5
fix(cli): keep markdown render toggle quiet
qinqi-ali 96cad6b
fix(cli): generalize markdown render mode
qinqi-ali f47fc26
fix(cli): broaden render mode and copy latex blocks
qinqi-ali e032f64
fix(cli): align rendered copy source indices
qinqi-ali 5ad907c
fix(cli): support copying inline latex expressions
qinqi-ali 0b371a2
feat(cli): document markdown render controls
qinqi-ali 06f80c9
test(cli): cover markdown render controls
qinqi-ali 7448a80
fix(cli): tighten markdown render fallbacks
qinqi-ali 2056b99
fix(cli): bound mermaid renderer cache
qinqi-ali e83d8b3
Merge remote-tracking branch 'origin/main' into codex/markdown-syntax…
qinqi-ali 5aceede
chore: merge main into markdown syntax extension
qinqi-ali 0e08496
fix(cli): address markdown render review feedback
qinqi-ali b28c07f
fix(cli): address markdown render review comments
qinqi-ali c32b3ab
test(cli): strengthen render mode shortcut coverage
qinqi-ali 9410d39
fix(cli): address mermaid image review comments
qinqi-ali 11c6a3c
fix(cli): stabilize renderer output truncation
qinqi-ali 5fe276c
test(cli): flush fake renderer stderr before exit
qinqi-ali 81a1e00
fix(cli): address markdown renderer review feedback
qinqi-ali 696655d
docs(cli): clarify mermaid image limits
qinqi-ali 715bb44
fix(cli): refresh mermaid images on height resize
qinqi-ali 854e395
fix(cli): address markdown render review
qinqi-ali dc4def7
chore: revert unrelated review formatting churn
qinqi-ali 259c610
fix(cli): avoid mermaid regex codeql alert
qinqi-ali 4bc06e0
fix(cli): silence mermaid operator codeql alert
qinqi-ali a450d6a
fix(cli): render inline math in markdown tables
qinqi-ali ee3b9dc
fix(cli): harden markdown visual renderers
qinqi-ali 5b0029c
fix(cli): strip c1 controls from mermaid previews
qinqi-ali File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| # Markdown Syntax Extension Design | ||
|
|
||
| ## Context | ||
|
|
||
| This document keeps the implementation references for the integrated Markdown | ||
| syntax extension PR. It is based on the TUI optimization research from | ||
| `origin/docs/tui-optimization-design`, especially: | ||
|
|
||
| - `docs/design/tui-optimization/00-overview.md` | ||
| - `docs/design/tui-optimization/03-rendering-extensibility.md` | ||
| - `docs/design/tui-optimization/04-gemini-cli-research.md` | ||
| - `docs/design/tui-optimization/05-claude-code-research.md` | ||
| - `docs/design/tui-optimization/06-implementation-rollout-checklist.md` | ||
| - `docs/design/tui-optimization/08-execution-plan-and-test-matrix.md` | ||
|
|
||
| The referenced research recommends a long-term Markdown architecture built | ||
| around an AST parser, block/token caching, stable-prefix streaming, bounded | ||
| detail panels, and terminal capability detection. This first implementation | ||
| keeps the runtime footprint small and makes the new behavior visible | ||
| immediately. | ||
|
|
||
| ## Integrated PR Scope | ||
|
|
||
| This PR treats Markdown syntax expansion as one coherent renderer improvement, | ||
| not separate feature PRs. | ||
|
|
||
| Included in the first implementation: | ||
|
|
||
| - Mermaid code blocks render visually in the TUI. | ||
| - Mermaid diagrams render through PNG terminal images when image rendering is | ||
| explicitly enabled, `mmdc` is available, and the terminal supports an image | ||
| path. | ||
| - `flowchart` / `graph` Mermaid diagrams fall back to box-and-arrow previews. | ||
| - `sequenceDiagram` Mermaid diagrams fall back to participant-arrow previews. | ||
| - Basic `classDiagram`, `stateDiagram`, `erDiagram`, `gantt`, `pie`, | ||
| `journey`, `mindmap`, `gitGraph`, and `requirementDiagram` blocks fall back | ||
| to bounded text previews. | ||
| - Mermaid types without a text preview fall back to their original fenced | ||
| source so the user can still read and copy the diagram definition. | ||
| - Task list items render checked/unchecked markers. | ||
| - Blockquotes render with a visible quote bar. | ||
| - Inline `$...$` math and block `$$...$$` math render with common Unicode | ||
| substitutions. | ||
| - Existing Markdown tables continue to use `TableRenderer`. | ||
| - Existing non-Mermaid fenced code blocks continue to use `CodeColorizer`. | ||
|
|
||
| ## Mermaid Rendering Strategy | ||
|
|
||
| ### First version: capability-gated image rendering plus text fallback | ||
|
|
||
| The implementation now treats Mermaid's own layout as the preferred path. When | ||
| the local environment supports it, the TUI renders Mermaid blocks through this | ||
| pipeline: | ||
|
|
||
| ```text | ||
| Mermaid source | ||
| -> mmdc / Mermaid CLI | ||
| -> PNG | ||
| -> Kitty or iTerm2 terminal image protocol | ||
| ``` | ||
|
|
||
| If the terminal does not support inline images but `chafa` is installed, the | ||
| same PNG is rendered as ANSI block graphics. If neither image protocol nor | ||
| `chafa` is available, the renderer falls back to the synchronous terminal text | ||
| preview described below. | ||
|
|
||
| The image render is not attempted while a response is still streaming. During | ||
| streaming, Mermaid blocks show a bounded pending preview. Once the response is | ||
| finalized, the image path is attempted only when explicitly enabled. This keeps | ||
| slow `mmdc` startup, especially the opt-in `npx` path, out of the default | ||
| interactive render path. | ||
|
|
||
| PNG generation is cached independently from terminal placement. Repeated | ||
| renders of the same Mermaid source, including terminal resize updates, reuse | ||
| the generated PNG and only recompute the Kitty/iTerm2 placement dimensions. | ||
|
|
||
| The image path is intentionally opt-in and capability-gated instead of always | ||
| bundling or invoking Puppeteer/Chromium from the hot CLI path. A user can enable | ||
| the image path with `QWEN_CODE_MERMAID_IMAGE_RENDERING=1`, then provide | ||
| `@mermaid-js/mermaid-cli` by installing `mmdc` on `PATH` or by setting | ||
| `QWEN_CODE_MERMAID_MMD_CLI` to the binary path. For ad-hoc local verification, | ||
| `QWEN_CODE_MERMAID_ALLOW_NPX=1` allows the renderer to invoke | ||
| `npx -y @mermaid-js/mermaid-cli@11.12.0`; this is intentionally opt-in because | ||
| the first run may install Puppeteer/Chromium and block rendering. Repo-local | ||
| `node_modules/.bin` renderers are not auto-discovered unless | ||
| `QWEN_CODE_MERMAID_ALLOW_LOCAL_RENDERERS=1` is set. Terminal protocol selection | ||
| can be forced with `QWEN_CODE_MERMAID_IMAGE_PROTOCOL=kitty|iterm2|off`. | ||
|
|
||
| For Kitty-compatible terminals such as Ghostty, the renderer uses Kitty | ||
| Unicode placeholders instead of writing the image payload as Ink text. The PNG | ||
| is transmitted through raw stdout in quiet mode (`q=2`) with a virtual | ||
| placement (`U=1`), and the React tree renders the normal placeholder character | ||
| grid (`U+10EEEE`) with explicit row and column diacritics for each cell. This | ||
| keeps Ink responsible for layout and resize while preventing APC payload bytes | ||
| from being wrapped into visible base64 text. | ||
|
|
||
| ### Fallback: resizable wireframe preview | ||
|
|
||
| The fallback avoids async work because Ink's `<Static>` path is append-only: a | ||
| finalized message cannot reliably wait for a background render job and then | ||
| update in place without forcing a full static refresh. The fallback must | ||
| therefore produce terminal output during the normal React render pass. | ||
|
|
||
| For `flowchart` / `graph` diagrams, the fallback builds a lightweight graph | ||
| model instead of printing one edge at a time: | ||
|
|
||
| - Nodes are normalized by Mermaid id, label, and basic shape. | ||
| - Node labels support Mermaid-style `\n` / `<br>` line breaks. | ||
| - Top-down diagrams are ranked into horizontal layers. | ||
| - Left-to-right diagrams are ranked into vertical columns when they fit. | ||
| - Multiple outgoing edges from the same node are drawn as one fork with | ||
| bracketed edge labels such as `[Yes]`, `[No]`, `[是]`, and `[否]`. | ||
| - Back edges and cycles are summarized in a `Cycles:` section with explicit | ||
| `↩ to <node>` markers. This avoids unstable long cross-diagram routes in | ||
| terminal fonts while keeping the loop semantics visible. | ||
| - The graph is recomputed from `contentWidth`, so resize changes node width, | ||
| spacing, and connector paths. | ||
| - Large previews are bounded before graph layout so very large Mermaid blocks | ||
| do not allocate an unbounded terminal canvas during render. | ||
|
|
||
| Example: | ||
|
|
||
| ```mermaid | ||
| flowchart LR | ||
| A[Client] --> B[API] | ||
| ``` | ||
|
|
||
| renders as a terminal visual preview rather than Mermaid source. | ||
|
|
||
| Other common Mermaid diagram families use bounded text summaries rather than a | ||
| full layout engine: class relationships/members, state transitions, ER | ||
| entities/relationships, Gantt tasks, pie slices, journey steps, mindmap trees, | ||
| git graph entries, and requirement trees. If a diagram type is unknown or not | ||
| previewable, the renderer shows the original fenced Mermaid source rather than | ||
| a placeholder so the content remains readable and selectable/copyable in the | ||
| terminal. A dedicated one-key "copy Mermaid source" interaction is still a | ||
| separate UI affordance for a future PR because the current message renderer | ||
| does not expose per-block actions. | ||
|
|
||
| The fallback is still not a complete Mermaid engine. It is a fast, | ||
| dependency-light preview layer for common LLM-generated diagrams when | ||
| high-fidelity rendering is not available. | ||
|
|
||
| ### Future providers | ||
|
|
||
| The provider boundary is intentionally open for additional native image | ||
| providers: | ||
|
|
||
| - `mmdc` / `@mermaid-js/mermaid-cli` for SVG/PNG output. | ||
| - `terminal-image` for Kitty/iTerm2 plus ANSI fallback. | ||
| - `chafa` when present for Sixel/Kitty/iTerm2/Unicode mosaics. | ||
|
|
||
| This path should remain optional, cached, and capability-gated, with cache keys | ||
| based on source hash, terminal width, renderer provider, and terminal protocol. | ||
| It should not block startup or add bundled Mermaid/Puppeteer work to the hot TUI | ||
| path by default. | ||
|
|
||
| ## AST Renderer Compatibility | ||
|
|
||
| The first version extends the existing parser to minimize blast radius. The | ||
| feature boundaries are still compatible with a future `marked` token pipeline: | ||
|
|
||
| - `code(lang=mermaid)` -> `MermaidDiagram` | ||
| - `code(lang=*)` -> existing `CodeColorizer` | ||
| - `table` -> existing `TableRenderer` | ||
| - `blockquote` -> quote block renderer | ||
| - `list(task=true)` -> task list renderer | ||
| - `paragraph/text` -> inline renderer with math/link/style support | ||
|
|
||
| The implementation does not cache React nodes. A future AST renderer should | ||
| cache tokens/blocks, then render from current width/theme/settings props. | ||
|
|
||
| ## Safety And Performance | ||
|
|
||
| - Mermaid source is treated as untrusted input. | ||
| - The first renderer does not execute Mermaid JavaScript. | ||
| - Native image rendering must be opt-in or capability-gated. | ||
| - Future browser-based rendering must use timeouts and size limits. | ||
| - Rendering should degrade to terminal text instead of throwing. | ||
| - Large blocks should respect available height and width. | ||
|
|
||
| ## Validation | ||
|
|
||
| Targeted unit verification: | ||
|
|
||
| ```bash | ||
| cd packages/cli | ||
| npx vitest run src/ui/utils/MarkdownDisplay.test.tsx | ||
| npx vitest run src/ui/utils/mermaidImageRenderer.test.ts | ||
| ``` | ||
|
|
||
| Broader verification before PR submission: | ||
|
|
||
| ```bash | ||
| npm run typecheck --workspace=packages/cli | ||
| npm run lint --workspace=packages/cli | ||
| git diff --check | ||
| ``` | ||
|
|
||
| Manual scenarios: | ||
|
|
||
| - Assistant response with a Mermaid `flowchart LR` block. | ||
| - Assistant response with a Mermaid `sequenceDiagram` block. | ||
| - Markdown table plus Mermaid in the same answer. | ||
| - Fenced JavaScript code block still showing code formatting. | ||
| - Narrow terminal width. | ||
| - Constrained tool/detail surface. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { describe, expect, it } from 'vitest'; | ||
| import { renderWithProviders } from '../../test-utils/render.js'; | ||
| import { RenderInline } from './InlineMarkdownRenderer.js'; | ||
|
|
||
| describe('<RenderInline />', () => { | ||
| it('leaves shell-style dollar variables untouched by default', () => { | ||
| const { lastFrame } = renderWithProviders( | ||
| <RenderInline text="echo $HOME && echo $PATH" />, | ||
| ); | ||
|
|
||
| expect(lastFrame()).toContain('echo $HOME && echo $PATH'); | ||
| }); | ||
|
|
||
| it('renders inline math only when explicitly enabled', () => { | ||
| const { lastFrame } = renderWithProviders( | ||
| <RenderInline text="value $\\alpha$" enableInlineMath />, | ||
| ); | ||
|
|
||
| expect(lastFrame()).toContain('α'); | ||
| expect(lastFrame()).not.toContain('$\\alpha$'); | ||
| }); | ||
|
|
||
| it('does not parse ordinary dollar amounts as inline math', () => { | ||
| const { lastFrame } = renderWithProviders( | ||
| <RenderInline text="cost is $5 and $10 later" enableInlineMath />, | ||
| ); | ||
|
|
||
| expect(lastFrame()).toContain('cost is $5 and $10 later'); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.