vecnode/vncode--packages-dsh-diagrams ↗★ 3

dsh-diagrams

Mermaid与TikZ图表解析、渲染与导出 适合需要在DSH中生成、预览、编辑和导出专业图表与学术公式的用户。

套件
dsh-diagrams
相容性
待驗證
版本
0.1.0-alpha.7
授權
MIT
最近更新
2026年9月30日

安裝

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:vecnode/vncode#b3a6a0d53b96ef29d452af3962e8d726a122db3a&path:packages/dsh-diagrams

dsh-diagrams (alpha.6)

Mermaid and TikZ diagrams as a first-class surface of the harness: the model writes them as tools, the host validates every write with a real parser or a real TeX engine, they render inline in the conversation, and each one lives in its own right-bar tab with a zoom ladder, a source drawer and an export menu that saves to the Desktop of the machine running the harness.

A diagram can live in two places. By default it belongs to the conversation that drew it; publish it and it joins the LIBRARY, one shared store every conversation reads, whose address names no conversation (dsh-resource://diagram/library/) - so jepa-model means the same diagram in every chat, and survives the chat it was drawn in.

  • Row diagrams, bundle dsh-diagrams, tab kinds diagram (one tab per diagram, in either scope) and diagrams (the index, reachable from the tab strip's + / Start page, showing the library and this conversation).
  • Tools: diagram_write, diagram_patch, diagram_read, diagram_verify, diagram_publish, diagram_delete - each taking an optional scope (conversation | library) and resolving a bare id in the LIBRARY first, except diagram_publish, which takes an id from THIS conversation and copies it in.
  • Skills: mermaid-diagrams, tikz-diagrams (authored in skills/, copied into $DSH_HOME/skills by the installer), each with a reference/complex-diagrams.md for pictures too big for the syntax summary.
  • No core patch, no forked bundle, no npm dependency, no network.

Alpha. 0.1.0-alpha.6.


1. What the user sees

In the conversation. Every diagram tool call renders its picture inline - Mermaid drawn by the browser from its own source, TikZ drawn from the SVG the host compiled - under a header naming the diagram (kind, title, id, status pill) with two links: Open tab (opens or reveals that diagram's tab) and Show/Hide (the picture). The card is built from the tool call itself, so it is already correct on replay, before any request returns.

The card reads the shell's own block model: a call still running is a call block with no kind (ui-tool reads done = "kind" in block), and a settled call is a tool-result block carrying call.argsRaw plus the meta view the host declared. That second channel is what names the diagram for a diagram_write - the write never carries an id, because the host derives one from the title - so the settled card can name the diagram, show its status, refresh the conversation store and offer a live Open tab link.

In the right bar.

SurfaceWhat it is
dsh-resource://diagram/session// (a library diagram is dsh-resource://diagram/library/)one tab per diagram: the rendered picture laid out at 80% of the pane with a - / + / Fit zoom ladder (25%-400%), drag-to-pan when it overflows, a Recompile button for TikZ, Copy, and Export ▾
sidebar://diagramsthe index: every diagram of the conversation and of the shared library, with kind, status and size; New Mermaid / New TikZ; picking a row opens its tab

80% is the 100% rung: a diagram is read whole first. The zoom moves the layout BOX rather than a CSS transform, so a zoomed box stays scrollable to its edge; every child of a zoomed box is stretched to it, because a column flex container sizes its children to their content on the cross axis; panning is the canvas' own scrollLeft/scrollTop; and a zoom keeps the point the reader was looking at.

The index type carries the package's one guide entry (order: 40, after Files 10, Editor 20 and History 30), which is what the + control and the Start page list.

Both pane bodies re-read the conversation from the host when they mount and whenever the tab becomes visible again, so a tab that stayed mounted while the model wrote diagrams shows them the moment the user returns to it.

The source drawer. Source opens a monospace drawer beside the picture: edit, then Apply (validated and stored exactly like a model write, and recompiled when it is TikZ) or Revert. It is deliberately a plain textarea: the package depends on no editor, and the document that this drawer edits is a diagram, not a program. For a real editor, export the .mmd/.tex and open it in dsh-editor.

Export. Export ▾ saves every format to the Desktop of the machine running the harness (POST /api/dsh-diagrams/export, create-exclusive: the first free name wins and an existing file is never replaced), and reports the absolute path it wrote. The client names a format, never a path, so there is no traversal surface and no way to overwrite a file the user already had. The second half of the menu downloads through the browser instead - the fallback for a profile whose host row is not mounted.

KindFormatsWhere the bytes come from
Mermaidmmd, md, svg, pngsource for mmd/md; the browser's own render for svg, rasterized at 2x for png
TikZtex, pdf, svg, pngsource for tex; the host's compiled artifacts for the rest

2. What the model gets

Six tools with raw JSON-Schema parameters (the registry validates both the arguments and the returned canonical value):

ToolPurpose
diagram_writecreate or replace one diagram (kind, source, optional id, title, scope, note) and validate it
diagram_patchliteral oldString/newString replacement inside one diagram - the cheap iteration path for a long TikZ picture; ambiguous matches are refused (AMBIGUOUS), a miss is refused (NO_MATCH)
diagram_readone diagram's full source plus its last diagnostics, warnings and browser verdict, or both indexes (library and conversation)
diagram_verifyre-validate the stored source without writing: it never bumps the revision and never discards the browser's render report
diagram_publishcopy a conversation diagram into the shared LIBRARY, so every chat can read it and cite its id
diagram_deleteremove one diagram from whichever scope holds it

Five rules make this more than a text box:

  1. Every write is validated before it is stored, so a broken diagram returns the parser's or the compiler's own line-accurate error rather than a broken picture.
    • Mermaid: the source goes to a child process that loads the vendored engine behind a DOM stub and calls mermaid.parse(); the parse error comes back with the offending line, the caret and the parser's "Expecting" list.
    • TikZ: the source is compiled by the machine's own TeX engine and the compiler's lines come back line-accurate (diagram.tex:12: Package pgf Error: No shape named ...).
  2. The status is the contract. A tool result carries status: ok | error | unavailable. unavailable means "stored but NOT verified" (no TeX engine on this host, or the validator itself failed) and says so in the result text, because telling the model its diagram is wrong when the checker is what broke would be a lie.
  3. Warnings are advisory, never a refusal. After a successful validation the host lints what a parser cannot refuse but a reader pays for: a picture with nodes and no edges, more nodes than a person takes in at once, an unclosed-looking label, a Mermaid source whose first line names a different diagram type than the engine parsed, a TikZ document that compiled to several pages or to a canvas too wide to read. They ride the same result, marked as advisory, and never change status.
  4. Two degenerate sources are refused in plain words. An empty (or comments-only) source, and a TikZ document with no drawing command - both of which the engines handle with a success and a blank page.
  5. "It parses" and "it draws" are separate verdicts. See §3.

The returned address is the diagram's tab, so the model can point the user at it in prose as well.

diagram_verify re-runs the whole validation against the stored source without writing: it never bumps the revision, never claims an id, and never discards the browser's render report. It is the right call when a person edited the diagram in its panel, when the model's context was compacted, or before describing a diagram's contents - and it is why re-checking does not have to cost a rewrite.

Skills carry the craft the tools cannot: which Mermaid diagram type fits which question, the syntax traps that actually break diagrams, layout and readability budgets; the TikZ preamble the host supplies, node/edge/plot recipes, sizing, the host's hard limits (no shell escape, no file access, no package installation) and how to read each compile error. They also document the verdicts above - what status, warnings and the Browser: line each mean, and which one to act on. They are registered at runtime from skills/ and copied into $DSH_HOME/skills by the installer, so the catalog finds them however the bundle was installed.

3. How it is put together

lib/index.js          host row: 6 tools, 2 skills, the /api/dsh-diagrams/* routes
lib/store.js          per-conversation state (one JSON file, atomic writes)
lib/cache.js          content-addressed artifact cache (svg/png/pdf/tex + meta)
lib/latex.js          engine probe, source normalization, compile, convert
lib/mermaid-check.mjs CHILD process: DOM stub + vm-loaded engine + parse + lint
lib/client.js         browser half: 2 tab types, their bodies/titles, tool cards
lib/vendor/mermaid.min.js   GENERATED single-file mermaid build (~3.4 MB)
skills//SKILL.md      the two skills (copied to $DSH_HOME/skills on install)
vendor/build.mjs      generates lib/vendor/mermaid.min.js + VERSION.json

Routes (connection.fetch, exact paths, GET/HEAD/POST only)

The harness's Connection registry registers exact routes and its method vocabulary is GET | HEAD | POST. Two consequences are visible in the design: the vendored engine is one self-contained file served from one route (the chunked ESM build would have needed 104 routes), and every write - including delete - is a POST.

RouteBehavior
GET /healththe vendored Mermaid version, the TeX capability (engine, svg, png), cache entry count. ?refresh=1 re-probes the engines
GET /state?session=the conversation's diagrams and the shared library (each with source, revision, scope, warnings and last render report) + the capability block
GET /diagram?session=&id=&scope=one diagram, source included. Without scope the id resolves library-first
POST /diagramcreate/replace ({session, id?, kind?, title?, source, scope?, create?, recompile?}), or delete ({session, id, scope?, delete: true}). Used by the panel; the model goes through the tools
GET /artifact?session=&id=&scope=&format=svg/png/pdf/tex from the cache (Mermaid: mmd/source only - its picture exists in the browser)
POST /exportsave one format to the Desktop of the machine running the harness, create-exclusively, and answer with the absolute path, the folder and the name it wrote
POST /render-reportwhat the BROWSER did with one revision ({session, id, scope?, revision, ok, phase?, error?, theme?, ms?}). Deliberately forgiving: a report about a deleted diagram or an older revision answers 200, because a verification channel must not fail loudly
GET /vendor/mermaid.jsthe vendored engine (ETag, immutable)

State: one file per conversation, and one for the library

$DSH_HOME/dsh-diagrams/sessions/.json - {order, diagrams{id → {kind, title, source, status, diagnostics, warnings, render, artifact, revision, history}}}, one atomic write per change, capped (64 diagrams, 256 KiB per source, 4 MiB of source per conversation, 16 MiB per file). A conversation source budget is enforced on write AND on patch (replacing a diagram is charged once) and refuses with a typed BUDGET error the model can act on. The browser reads the file through the routes; the model reads it through diagram_read, which is what makes a diagram survive compaction, a reload or the browser closing.

$DSH_HOME/dsh-diagrams/library.json is the same shape in one file for the whole harness, written by the same store class with a fixed name instead of a name per conversation. It has its own 4 MiB source budget, its own render reports and its own revisions, so the library copy of a diagram is an independent entry that happens to share its source - and therefore its artifact, because the cache is content-addressed. Nothing about a conversation leaks into it: the file knows no session.

Four rules are load-bearing:

  • A panel edit writes back where the diagram already is - otherwise editing a library diagram in its tab would quietly fork a conversation-only copy.
  • Deleting a diagram does not drop its cached artifact blindly, because the cache is content-addressed and shared: an identical diagram elsewhere is the same file.
  • State is never a session event. @deepseek-ai/dsh-session-persistence refuses to load a log containing an event type outside KNOWN_SESSION_EVENT_TYPES unless the envelope carries ignorable: true, and Session.append() has no way to set that marker - a plugin-owned event type would make the conversation unreadable.
  • State is never a projection. A sessionProjections unit requires zod schemas, and this pack ships no npm dependencies (the profile installs bundles as live links, so a package dependency would not be installed). It would also fold the same events the rule above refuses.

render is the browser's own report about the revision it drew ({revision, ok, phase, error, theme, at}), stored by recordRender and read back as one of four states by verificationOf: drawn (a renderer reported success for THIS revision), failed (it reported failure, with its own error), stale (the newest report names a DIFFERENT revision, so this one has never been drawn) or pending (no report at all). The verdict is ONE function (lib/store.js: verificationOf) used by the tool result, the state route and the tab pill, so the three cannot disagree; it carries both revision numbers, so stale is read rather than guessed - a report about revision 4 is never evidence about revision 5 - and it omits its error/at keys rather than nulling them, because the tool registry refuses a value that does not survive a JSON round trip. A write sets render back to null, because the old picture was of different text.

Rendering

Mermaid, in the browser. The engine is fetched once from the plugin's own route and evaluated as a classic script (its last line is globalThis["mermaid"] = ...), exactly how the editor loads CodeMirror. Renders are cached per (source, theme) (LRU, 24 entries) and re-drawn when the app's light/dark scheme flips.

Every render goes through renderMermaidSafe, and the order inside it is load-bearing:

  1. mermaid.parse(source) first. It is the engine's own syntax check and it throws with the offending line, so render() is only ever reached by a source the parser accepted. A broken source therefore cannot produce a picture at all - clean or broken.
  2. suppressErrorRendering: true. If render() fails anyway (a renderer bug on a source the parser accepted), the engine throws instead of drawing its 2412x512 "Syntax error in text" diagram.
  3. A container the plugin owns. render(id, source) with no container builds #d on document.body and removes it only on success, so a failure would leave one behind - and one of those divs holds the error picture. Passing mermaidHost() - attached, laid out at zero size, offscreen
    • keeps every fixture out of the interface. A `