Skip to main content
M

dsh-visualizer

moses14159/dsh-visualizer

DSH 外部插件:模型在对话流中流式生成 SVG/HTML Widget(```svg/```html 围栏逐 token 渲染于沙箱 iframe)与结构化图表(visualize 工具的 ChartSpec -> echarts)。复用已有 assistant/chunk + tool/result 事件,不改 DSH 源码。

Install

dsh plugin --profile web add github:moses14159/dsh-visualizer

README

dsh-visualizer

English · 简体中文

An external plugin that does not modify DSH source: lets the model render visuals on the fly in the conversation — streaming SVG/HTML widgets and structured charts (ChartSpec → echarts).

License: MIT Node.js: 20+ Tests: 97 passing CI


What is this

Inside a DSH (DeepSeek Harness) conversation, let the model produce visual content directly and render it safely into the chat:

  • Structured charts — the spec of the visualize tool, rendered with echarts (line / bar / area / pie / scatter).
  • SVG / HTML widgets — the model writes ```svg / ```html fences that render token-by-token, streaming into a sandboxed iframe; it can also deliver a complete widget via the widget parameter of visualize.

It reuses DSH's existing assistant/chunk and tool/call + tool/result events, and does not change DSH source.

Features

  • Two kinds of output, three delivery paths: visualize(spec) for charts; ```svg / ```html fences in the message body for streaming widgets; visualize(widget) for a complete widget.
  • Streaming render: reuses the existing assistant/chunk events to get token-by-token output, so the widget updates frame by frame as the text streams.
  • Two-sided validation: host execute and the client fold share the same pure-function parsers (chartspec / widget), so model drift cannot silently pass through.
  • Security isolation: widget code is inserted verbatim into a sandbox="" iframe + CSP default-src 'none'; there is no sanitizer to bypass.
  • Rendering experience: charts sample the --dsw-alias-* tokens; SVG widgets size to their intrinsic aspect ratio; cards carry a "fit / 1.5× / 2×" zoom and a status badge (generating / truncated / done).
  • Graceful degradation: any validation or render failure leaves no blank row and throws no error — it falls back to a plain code block or a JSON card.
  • Pure, testable: the core logic is all DSH-free pure modules — 97 unit tests run standalone in Node.

Preview

Real conversation screenshots (rendered inside the DSH Web client):

Structured chart: visualize(spec) → echarts
Structured chart · visualize(spec) → echarts (follows the DSH theme)

HTML widget: visualize(widget) → sandboxed iframe
Widget · visualize(widget) → sandboxed iframe

More render samples (rendered by the plugin's own code):

Bar chart SVG widget
HTML widget Streaming render GIF

Left: bar chart · Right: SVG widget · Bottom-left: HTML widget · Bottom-right: streaming render (```svg fence updated token by token)

Installation

Install directly from GitHub (recommended — the built artifacts are committed):

dsh plugin --profile web add github:Moses14159/dsh-visualizer

Alternatively, clone it and install from the local path:

git clone https://github.com/Moses14159/dsh-visualizer.git
dsh plugin --profile web add /path/to/dsh-visualizer
  • Plugins installed from Git build via their prepare script on install. For safety, pnpm blocks build scripts; if it prompts you, add the relevant allowBuilds key to the profile's pnpm-workspace.yaml and re-run.
  • Once dsh-visualizer is published to npm, you can also install it by name: dsh plugin --profile web add dsh-visualizer.
  • Requires DSH (deepseek-harness) installed locally and a working dsh web. It depends on DSH's @deepseek-ai/dsh-client-runtime, @deepseek-ai/dsh-llm, @deepseek-ai/dsh-tools (see peerDependencies).

Usage

After installing, just tell the model in the conversation:

  • Say "draw a chart with visualize" → the model calls visualize with a spec;
  • Say "write an SVG badge / HTML widget" → the model streams ```svg / ```html fences directly in the reply, and you see it render frame by frame as it generates;
  • The model can also pass a widget parameter to visualize to deliver a complete widget (validated, persisted, and replayable on the host).

Tool payloads

visualize takes exactly one of:

// Structured chart
{ "spec": {
    "kind": "bar",                    // bar | line | area | pie | scatter
    "title": "Shenzhen · 7-day temperature",
    "xAxis": ["Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri"],
    "yName": "°C",
    "series": [{ "name": "Max", "data": [32, 32, 30, 31, 29, 31, 32] }]
} }
// SVG / HTML widget
{ "widget": { "kind": "svg", "code": "<svg ...>…</svg>", "title": "Card title" } }

Streaming fence

```svg
<svg width="360" height="200" viewBox="0 0 360 200" xmlns="…">
  …rendered token by token…
</svg>
```

Examples

Say these directly in the conversation to see the effect (the images are from real conversations).

Generate a weather card

Help me generate a weather card for Shenzhen right now

The model calls visualize with a widget (HTML) and renders it as a sandboxed widget card:

Weather card
visualize(widget) · an HTML card rendered in a sandboxed iframe

Draw a line chart

Use visualize to draw a chart of tomorrow's 24-hour temperature change in Beijing

The model passes a spec (line) and renders it with echarts:

Line chart
visualize(spec) · echarts render (follows the DSH theme)

Render as it streams

Write an SVG card for Shenzhen's current weather

The model writes a ```svg fence in the reply and renders it as it streams:

Streaming render GIF
a ```svg fence in the reply · token-by-token streaming render

Ask for several at once

Draw a bar chart, a pie chart, and a weather card at the same time

The model calls visualize multiple times and lays out the charts and widgets in the conversation flow.

💡 Tip: these examples require the model to have the visualize tool loaded (registered once the plugin is installed). If the model doesn't reach for it, just describe what you want — it will prefer calling visualize.

Architecture

Architecture

Two delivery paths

OutputTriggerSession eventsFolded intoRender
Structured chartvisualize(spec)tool/call + tool/resultvisualizer-chartecharts
Widget (delivered)visualize(widget)tool/call + tool/resultvisualizer-widgetsandboxed iframe
Widget (streamed)```svg / ```html fencesassistant/chunkvisualizer-widget (frame updates)sandboxed iframe

Why this shape works (source-level facts)

  • assistant/chunk is an existing session event family: the agent-loop logs each StreamChunk as { turn, step, chunk }, and the web client's streaming text is exactly a fold of these events — the plugin reuses the same stream to obtain token-by-token output, without adding a new host-side event family.
  • conversationEvents is a cordis Service an external plugin can inject; visualizer-widget folds the same batch of assistant/chunk events in parallel with the built-in assistant-step, without interference.
  • conversation.chat.node is a keyed slot (replaceRisk: 'none'); registering a string key — { key: 'visualizer-chart' | 'visualizer-widget' } — is an additive contribution.
  • ChatNodeViewProps / ConversationNodeDefinition / ChatNodeDataMap are all pure types, erased at build time, so they don't trip the client bundle's purity gate.
  • Determinism: match reads only the current event; every event of one Context carries or independently derives the same stable id (step:<turn>:<step> / widget:<callId>); update folds one Match per log seq, so it is replayable.

Security boundary

LayerHandling
Model → spec / widgethost execute validates with parseChartSpec / parseWidgetSpec; invalid payloads are rejected (the tool errors)
Text stream → widgetthe client WidgetScanner only recognizes line-leading ```svg / ```html fences; widget code is not markup-validated (any byte can be a legal prefix while streaming), so the security boundary lives at the render side
session log → clientthe Definition's update/fallback re-parses the result text; the full assistant/message is the cold-replay recovery source
Render (widget)two layers of isolation: iframe sandbox="" (no scripts / same-origin / forms / popups / navigation, opaque origin) + an injected CSP default-src 'none' in the srcdoc; code is inserted verbatim, so there is no sanitizer to bypass
Render (chart)ChartSpec is pure data → echarts setOption; no HTML/SVG injection surface

Any validation or render failure degrades (the node isn't rendered / the fence stays a plain code block / a JSON card) — never a blank row or a thrown error in the chat.

Payload limits

  • Charts: ≤ 8 series, ≤ 500 points, ≤ 120 characters per label;
  • Widgets: ≤ 128 KB per item (UTF-8), ≤ 12 items / ≤ 512 KB total per node; overflow is truncated or omitted and shows a "truncated" badge.

Development

pnpm install
pnpm test        # pure-function unit tests (97 cases)
pnpm typecheck   # tsc --noEmit
pnpm build       # tsdown: host + replay + both channel client bundles
pnpm render-demo # regenerate docs/ demo images (needs a local Chrome)
  • The pure-function modules (chartspec / widget / to-echarts / to-iframe / svg-geometry / the two folds) don't import DSH or touch the DOM, so they can be tested standalone in Node.
  • The client entry is src/client/index.tsx; the host tool entry is src/index.ts.

Known limitations

  • Charts still appear "whole": ChartSpec is delivered via a tool call, so there is no token-by-token streaming chart (tool arguments aren't streamable); the fence widgets are the streaming path.
  • The fence source and the widget card coexist: the code block the model writes still renders as a normal markdown code block (as the "source" view), while the widget card renders in the stream — they are not mutually exclusive.
  • Widgets are static: the sandbox disables scripts, so interactive components (button logic, animated scripts) won't run; use a chart or a purely presentational widget when you need interaction.
  • No mermaid: v1 supports only svg/html fences; render mermaid diagrams as plain code blocks or in a later version.
  • Fences must be at line start (≤ 3 spaces of indent); an inline ```svg is not a fence, and a lone ``` line inside the content closes the fence (CommonMark semantics).
  • echarts is lazily import('echarts') and inlined into the client bundle (the registry route serves a single file, so it can't be code-split yet).

Compatibility

  • Node: >= 20; DSH: loaded through the external-plugin mechanism (profile bundle patch).
  • peer dependencies: @deepseek-ai/cordis, @deepseek-ai/dsh-client-runtime, @deepseek-ai/dsh-client-ui-conversation, @deepseek-ai/dsh-llm, @deepseek-ai/dsh-tools, react, react-dom.

License

MIT · Copyright (c) 2026 Moses14159

Related plugins