huiyeo/dsh-plugin-mermaid-preview ↗★ 1
dsh-plugin-mermaid-preview
Mermaid diagrams in DeepSeek Harness: .mmd file previews in the document sidebar, and ```mermaid fences rendered inside chat messages
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:huiyeo/dsh-plugin-mermaid-preview说明文档
阅读完整 README ↗dsh-plugin-mermaid-preview
Mermaid diagrams in DeepSeek Harness, in the two places they belong:
.mmd/.mermaidfiles open in the right-sidebar document viewer as diagrams instead of plain text.```mermaidfences in chat messages render as diagrams instead of syntax-highlighted code blocks.
What it does
- Registers a document-preview implementation for the
mmdandmermaidfile suffixes through the harness's publicdocumentPreviewsextension point. - Claims the
mermaidMarkdown fence language throughui-primitives's fence registry, so chat transcripts render the diagram with no cooperation from the chat package. - Renders with mermaid 11, bundled into the plugin's own client artifact. Both surfaces share one runtime (configuration, palette sampling, measurement, device-pixel sizing), so a diagram cannot look right in one place and wrong in the other.
- Follows the shell's light/dark palette and re-renders when the theme changes.
- Zooms per diagram view:
−/ percentage /+/重置in a small control under the diagram, plus Ctrl (or ⌘) + wheel over it. 100% means "fills the pane width", the remembered zoom is a view preference rather than document state, and each preview tab keeps its own zoom because the state lives in the renderer instance. - In chat, a fence that is still streaming keeps its code block until the body is complete: a half-drawn diagram is worse than the source the reader can already see.
- Falls back to showing the source, with the parse error above it, when a diagram does not parse — a file the agent is still writing is the common case, so a failed render never hides the content.
- Ships Simplified Chinese and English copy through the shell's locale service.
Supported diagram types are mermaid's own: flowchart, sequence, class, state, entity-relationship, gantt, pie, git graph, mindmap, timeline, quadrant, requirement, sankey, block, architecture, and the rest.
Requirements
- DSH with the
webprofile (@deepseek-ai/dsh-web-app), which mounts@deepseek-ai/dsh-client-ui-sidebar-documentpreview. The plugin stays parked until that viewer is mounted, becausedocumentPreviewsis the service it extends. - Chat fences additionally need a harness that has the Markdown fence
registry — a
ui-primitivesexportingregisteredFences. On an older harness the.mmdpreview works normally and```mermaidblocks keep rendering as code blocks; the plugin logs one line saying so. It does not fail, and it does not take the file preview down with it.
Install
From npm
dsh plugin --profile web add dsh-plugin-mermaid-preview
The published tarball ships the built artifacts, and prepublishOnly rebuilds
them before every release.
From a checkout
The harness serves the built lib/client.js and never reads sources, and build
output is not committed, so build first:
git clone https://github.com/huiyeo/dsh-plugin-mermaid-preview
cd dsh-plugin-mermaid-preview
pnpm install
pnpm run build
Then install that directory as a profile layer:
dsh plugin --profile web add /path/to/dsh-plugin-mermaid-preview
dsh plugin add installs the package and, because the manifest declares
dsh.bundle.patch, appends it to dsh.profile.bundles. The bundle layer is read
at process start, so restart the dsh web process, then reload the page.
To see it without waiting for a restart, a profile with patchReload: live also
accepts the row directly in $DSH_HOME/profiles/web/cordis.patch.yml, which
hot-mounts on save so only a page reload is needed:
- insert:
- id: mermaid-preview
name: dsh-plugin-mermaid-preview
Add that only while iterating, and remove it once the process restarts: the
bundle layer then mounts the same row, and documentPreviews.register() throws
on a duplicate implementation id.
Verify the layer landed:
node -e "console.log(require('./package.json').dsh.profile.bundles)" \
# run in $DSH_HOME/profiles/web → should list dsh-plugin-mermaid-preview
Remove it with dsh plugin --profile web remove dsh-plugin-mermaid-preview.
Development
pnpm install
pnpm run build # lib/index.js (host half) + lib/client.js (browser half)
pnpm test # load the built artifact the way the module system does
tests/browser-smoke.mjs additionally renders a diagram in headless Chrome and
asserts on the produced SVG. It needs a browser that can actually start —
a confined sandbox blocks Chrome's own IPC, so run it somewhere unrestricted:
CHROME_PATH=/path/to/chrome pnpm run test:browser
How the two halves fit together
lib/index.js is the host half and deliberately contributes nothing: a package
is only scanned for a browser half when it is mounted as a host row, so the row
exists to make the browser half discoverable.
lib/client.js is a single classic script that calls
window.__ModuleLoader__.load({ id, factory }) with a CommonJS-style factory.
That format is the harness's own client-bundle contract; the browser half of
@deepseek-ai/dsh-client-modules serves the file and drives it. Two
consequences shape the build:
- Only the harness browser platform's module-table words may stay external
(
react,react/jsx-runtime, the UI registries). Everything else — mermaid included — must be inlined, because arequire()the module table cannot answer throws when the bundle materializes. - The artifact must be one file. mermaid loads each diagram grammar through
a dynamic
import(); the plugin loader serves exactly one file per package and there is no dynamic-import hook, socodeSplitting: falsefolds every grammar into the factory. The result is ~7 MB uncompressed, fetched once and cached by revision.
The document body receives loading: 'bytes-complete' content, so it gets the
whole file in one shot and never has to page — the paged mode would hand it an
accumulated prefix with no way to request the rest.
Zoom must snap to the DEVICE pixel grid
The renderer re-sizes the SVG through its own width/height attributes rather
than through CSS, so the browser re-rasterizes the vector at the target size
instead of resampling a bitmap. The size it writes is snapped so that
width × devicePixelRatio is a whole number:
devicePixels / devicePixelRatio // not Math.round(cssPixels)
A whole number of CSS pixels is not a whole number of device pixels. On a
125%-scaled Windows display (devicePixelRatio === 1.25) a 326 px box covers
407.5 device pixels — half a pixel off the grid — and the browser resolves that
by resampling, which reads as blur. It shows up at only some zoom steps: 50%
was soft while 100% happened to land whole. Rounding CSS pixels does not fix it;
snapping device pixels does. tests/ cannot catch this, because jsdom has no
rasterizer.
If it ever needs re-checking, the numbers are visible from the page itself —
getBoundingClientRect() on the SVG times devicePixelRatio must be a whole
number at every zoom step. To reach a real browser from an agent session, the
DSH Browser Control extension
(Chrome MV3 + a local WebSocket bridge on 127.0.0.1:9777) works when a sandbox
forbids launching one: the extension dials out as a client, so the agent only has
to accept that socket and can then eval in the tab.
Layout
src/index.js host half (empty by design)
src/client/index.ts plugin body: file-preview registration + chat fence claim
src/client/MermaidBody.tsx the document-preview renderer (owns its zoom)
src/client/MermaidFence.tsx the chat fence renderer (falls back to the code block)
src/client/mermaid-runtime.ts shared by both: config, palette, measurement, sizing
src/client/theme.ts shell palette sampling + dark-mode subscription
src/client/locales.ts zh/en copy
src/client/styles.ts plugin-owned stylesheet, injected with a tagged tag
src/client/types.ts the minimal ambient contracts this package compiles against
cordis.patch.yml the profile patch that mounts the row
tests/load-artifact.mjs loads the artifact; asserts the shared-registry contract
tests/load-artifact-legacy.mjs asserts clean degradation without the fence registry
tests/browser-smoke.mjs headless render check (needs an unrestricted browser)
tests/zoom.html two-instance harness proving zoom is per-view
License
MIT