Add mermaid PNG download and colour palettes to the V2 chat - #1388
Merged
Paul Lizer (paullizer) merged 2 commits intoSep 2, 2026
Merged
Conversation
The V2 chat could render mermaid diagrams and SimpleChart charts, but a diagram could not be saved as an image and neither could be recoloured. Diagrams now carry a PNG button, rasterized from the SVG already on screen so the file matches what is displayed. Both block kinds carry a Colors control offering the five palettes the classic chart editor already used, per-series and per-slice colours for charts, and a background colour. Colours resolve through three layers: the built-in default, a per-user default in Settings -> Preferences, and an override saved against one block of one message. Recolouring one chart never affects another, and a block nobody has touched renders exactly as it did before -- mermaid keeps its stock theme and the Chart.js configuration is byte-identical. A block is addressed by its number among blocks of the same kind, stamped onto the parsed tree by a rehype plugin rather than by scanning the markdown text: CommonMark admits fences nested in list items and behind a blockquote prefix, which a text scan misses, and an unnumbered block would take block zero's slot. A source fingerprint is stored alongside, so an override is ignored rather than applied to different content after an edit. Colours are reduced to #rrggbb on both sides before reaching a style attribute or mermaid's theme configuration, and the stored map is bounded so a message document cannot be grown by repeated requests. No new npm package and no asset fetched from the internet; the Content-Security-Policy already permits the data: image source rasterizing needs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The base branch gained inline image proposals, chat notices, conversation deep links and server-side mermaid rendering while this branch was open. Five files conflicted. AssistantMarkdown needed real integration rather than a pick. The base added a third rich fence, ```image_proposal, to the RICH_FENCE_LANGUAGES set and the `pre` renderer this branch had already replaced with tree-based numbering. The fence list now lives in rehypeRichBlockIndex.ts alongside the numbering, so one function answers both "does this fence lose its <pre> wrapper" and "which block is it"; image proposals are numbered too, since numbering is per kind and costs nothing. The proposal card now reads its payload from the hast node with `hast-util-to-text` rather than through the removed `fenceText`, which walked rendered React children and could be handed highlighted spans instead of the JSON. MessageList keeps the base's ImageProposalScope wrapper with this branch's messageId passed through to AssistantMarkdown. Two assertions in test_v2_inline_image_proposals.py pinned the old implementation by exact string -- the `fenceText(children)` call site and the location of the rich-fence set. Both were updated to the current mechanism, preserving their intent. Version resolved to 0.261.033 above the base's 0.261.032, and the release note section, feature doc and test headers renumbered to match. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What this adds
The V2 chat renders mermaid diagrams and SimpleChart charts, but a diagram could not be saved as an image and neither could be recoloured.
Save a diagram as a picture. Diagrams now carry a PNG button, matching the one charts already had. The image is rasterized from the SVG already on screen rather than re-rendered, so whatever colours and background are showing come with it, painted onto an opaque background so it stays readable wherever it is pasted. It reuses the path
static/js/chat/chat-visual-rasterizer.jsalready uses for exports.Choose the colours. Both block kinds get a Colors control offering the five palettes the classic chart editor already used — Default, Calm, Vivid, Warm, Contrast. Charts additionally get per-series and per-slice pickers, and both get a background colour that defaults to Match theme so it keeps following light and dark mode unless something specific is chosen.
Colours resolve through three layers, each overriding the one before it:
PALETTE_PRESETSinvisualPalettes.tsv2MermaidStyle/v2ChartStyle, set in Settings → Preferencesmetadata.visual_styleson the messageRecolouring one chart never affects another. Three charts in a reply keep three independent sets of colours, and a chart with no entry follows the user default.
The invariant this had to preserve
A block nobody has recoloured renders exactly as it did before. Mermaid keeps its stock
default/darktheme rather than switching tobase+ theme variables, andbuildChartConfigproduces byte-identical output. There is a test asserting the Chart.js configuration is unchanged for the default style, because existing conversations must not shift under this change.Three bugs found in review, fixed here
>prefix. Those blocks came out unnumbered, fell back to index0, and overwrote the real block zero's saved colours. Replaced withrehypeRichBlockIndex, which walks the parsed tree the renderer is about to render, so there is no gap between what the parser calls a code block and what gets numbered. A block that still cannot be numbered is treated as unaddressable rather than defaulted to zero.visualStyleSignaturedid not distinguish "untouched" from "explicitly set to the colour the theme happens to be showing". The two are drawn by different means, so an untouched diagram could be rendered with another block's theme variables depending on render order.activeConversationIdlazily, but switching conversations is one of the ways a block unmounts — by which point the active conversation is already the new one. The conversation is now captured when the change is scheduled.Security
Colours reach inline
styleattributes and mermaid's theme configuration, so the accepted form is deliberately narrow:#rrggbbor it is not stored.red,rgb(1,2,3)andurl(...)are all refused even though a browser would apply them.The endpoint authorizes the conversation via
_authorize_personal_conversation_accessrather than the message, because a diagram lives in an assistant message that carries no author of its own; this also admits a participant acting inside a shared conversation. Existing rendering guarantees are untouched: mermaid still runssecurityLevel: 'strict'withhtmlLabels: false, output still passes through DOMPurify as an independent second boundary, andbindFunctionsis still never called.No new npm package, no vendored library, and nothing fetched from the internet. The Content-Security-Policy is unchanged — it already permits the
data:image source rasterizing needs.Testing
test_v2_visual_style_controls.py(new)test_v2_visual_style_logic.ts(new, 58 checks)test_v2_rich_rendering.pytest_v2_settings_and_workspace_tags.pytest_v2_settings_tabs.pytest_docs_app_surface_coverage.pytest_docs_site_quality.pyroute_tests/(3 suites)npm run buildandtsc -bare clean.The TypeScript checks run the real remark/rehype pipeline rather than a stand-in, which is the point: the bug they replaced was a hand-written scanner disagreeing with the parser, and only the parser's answer matters. They cover the blockquote and deep-indent cases directly.
Two existing assertions in
test_v2_rich_rendering.pywere updated rather than removed — one pinnedisRichFenceby symbol name, and one pins the exact rehype plugin list so adding a plugin stays a reviewable change.Version and docs
0.261.027→0.261.028. Newdocs/explanation/features/V2_DIAGRAM_AND_CHART_STYLING.md, plus updates toREACT_V2_UI.md, the features index and the release notes.Note for the reviewer
docs/explanation/release-notes/*.mdare generated fromrelease_notes.mdand were already stale before this branch — there is nov0.261.mdpage despite 15 releases in that series. I verified this by regenerating against an unmodifiedrelease_notes.mdand getting the same drift, so I left them out rather than bundle an unrelated 7-file regeneration into this PR. Worth runningpython scripts/build_release_notes_pages.pyas its own commit.