huiyeo/dsh-plugin-mermaid-preview1

dsh-plugin-mermaid-preview

Mermaid diagrams in DeepSeek Harness: .mmd file previews in the document sidebar, and ```mermaid fences rendered inside chat messages

包名
dsh-plugin-mermaid-preview
版本
0.2.0
许可证
MIT
最近更新
2026年9月12日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:huiyeo/dsh-plugin-mermaid-preview

dsh-plugin-mermaid-preview

Mermaid diagrams in DeepSeek Harness, in the two places they belong:

  • .mmd / .mermaid files open in the right-sidebar document viewer as diagrams instead of plain text.
  • ```mermaid fences in chat messages render as diagrams instead of syntax-highlighted code blocks.

What it does

  • Registers a document-preview implementation for the mmd and mermaid file suffixes through the harness's public documentPreviews extension point.
  • Claims the mermaid Markdown fence language through ui-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 web profile (@deepseek-ai/dsh-web-app), which mounts @deepseek-ai/dsh-client-ui-sidebar-documentpreview. The plugin stays parked until that viewer is mounted, because documentPreviews is the service it extends.
  • Chat fences additionally need a harness that has the Markdown fence registry — a ui-primitives exporting registeredFences. On an older harness the .mmd preview works normally and ```mermaid blocks 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 a require() 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, so codeSplitting: false folds 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