dsh-markstream
A DeepSeek Harness (DSH) web plugin that renders assistant messages in the conversation stream with markstream-react — the low-jitter streaming Markdown renderer for AI chat (streamed code blocks, Mermaid, KaTeX, safe HTML).
English | 中文
What it does
- Replaces the
assistant-step renderer in the conversation.chat.node keyed slot with a markstream-based implementation, registered with priority: -10 (slot priority is ascending — lowest renders) to shadow the shipped renderer's same-key entry at default priority 0. Registering the same key at the same priority throws:
- Text blocks render through
<MarkdownRender content final fade> with low-jitter incremental streaming, including intermediate states of incomplete Markdown;
- Streamed code blocks (via the optional
stream-diffs package), Mermaid (optional mermaid), KaTeX (inlined with the dependency tree in the current build), and safe HTML;
- Reasoning blocks keep the Think disclosure row, image groups keep the attachment gallery, unknown blocks keep the JSON fallback, and interrupted messages keep the "stopped" marker — matching the shipped renderer;
- The turn-tail node (
turn-tail, including IconActions / produced-file rows) and tool-row rendering remain the product's own.
- The renderer's
t locale seat is bound to the conversation namespace by the slot declaration, so it reuses ui-conversation's dictionary (row.running / message.stopped / message.unknownBlock / json.truncated) without registering a new namespace.
Floating config panel
The config surface is modeled on the demo site markstream-react.pages.dev (CODE THEME / DARK MODE etc.), the component docs, and the recommended code-block config, delivered as two layers:
- Composition layer: the plugin row's
config (full template in cordis.patch.yml) — deployment defaults, validated by the Config schema and used as the settings namespace base.
- User layer: a floating gear button at the frame's bottom-right (
shell.overlay entry dsh-markstream-config) opens a non-modal floating panel (demo-site style: side drawer, non-blocking, the rest of the page stays interactive); the header × closes it back to the gear. Fields override and persist to $DSH_HOME/settings.yaml; the "reset" affordance next to a field clears the user override and falls back to the composition value (a field's presence in the user layer marks it as overridden). Writes go through a revision-fenced settings scope and take effect immediately (the renderer re-installs itself on config changes).
Settings → Plugins → Config no longer shows a Markstream card; the gear panel is the single config entry point.
| Field | Default | Meaning |
|---|
enabled | true | Enable switch: false restores the shipped Markdown renderer (the gear panel stays reachable) |
theme | auto | auto follows the DSH theme (body[data-ds-dark-theme]) / light / dark (maps to MarkdownRender isDark) |
fade | true | Fade-in animation for streamed non-code nodes |
typewriter | false | Blinking typewriter cursor while content streams |
Code blocks (codeBlockProps header toggles + codeBlockOptions, following the stream-diffs surface recommendation; effective only with stream-diffs, falls back to a plain <pre> without it):
| Field | Default | Meaning |
|---|
codeBlockShowHeader | true | Show the code-block header |
codeBlockShowTooltips | true | Show tooltips |
codeBlockShowFontSizeButtons | true | Show font-size buttons |
codeBlockShowCollapseButton | false | Show collapse button |
codeBlockDiffStyle | unified | unified / split |
codeBlockOverflow | wrap | wrap / scroll |
codeBlockExpandUnchanged | false | Collapse unchanged diff regions |
codeBlockEnableLineSelection | true | Enable line selection |
codeBlockDisableLineNumbers | false | Hide line numbers |
codeBlockFontSize | 13 | Code font size in px (13 recommended) |
codeBlockTabSize | 2 | Tab width |
codeBlockPadding | 12 | Symmetric code padding in px |
codeBlockMaxHeight | 480 | Max height in px (0 = unlimited) |
codeBlockLightTheme | vitesse-light | Light code theme (Shiki registration name) |
codeBlockDarkTheme | vitesse-dark | Dark code theme |
Diagrams (each needs its optional dependency; without it the source is shown as fallback):
| Field | Default | Meaning |
|---|
mermaidEnabled | false | Forward mermaidProps (needs mermaid) |
mermaidIsStrict | true | mermaidProps.isStrict |
mermaidMaxHeight | 480 | mermaidProps.maxHeight in px (0 = unlimited) |
d2Enabled | false | Forward d2Props (needs @terrastruct/d2) |
d2MaxHeight | 480 | d2Props.maxHeight in px (0 = unlimited) |
Directory layout
dsh-markstream/
├── package.json # dsh.client manifest + dsh.bundle.patch declaration
├── cordis.patch.yml # plugin-row insert + composition config template (bundle channel)
├── tsconfig.json # typecheck (@deepseek-ai/* types from npm peers 0.1.0-rc.8)
├── tsconfig.build.json # tsc full emit to lib/types (JS + d.ts)
├── tsdown.config.ts # emits lib/client.js (browser half)
├── scripts/stage-node.mjs # stages the Node-half files from lib/types to lib/
└── src/
├── index.ts # Node half: registers the dsh-markstream settings namespace (base = row config)
├── schema.ts # flat schema (schemastery z; row Config = namespace schema)
├── config.ts # shared field definitions / defaults / option tables
├── invariant.ts # invariant companion
└── client/
├── index.ts # browser half: shell.overlay floating panel + assistant-step renderer
├── card-store.ts # snapshot store (settings scope binding + set/unset)
├── ConfigSurface.tsx # bottom-right gear + non-modal floating panel
├── ConfigSurface.module.css
├── ConfigFields.tsx # shared field rows (demo-site style: section titles + hairline dividers)
├── ConfigFields.module.css
├── AssistantMarkstream.tsx # config-driven assistant-step renderer (markstream-react)
├── AssistantMarkstream.module.css
├── locales.ts # dsh-markstream dictionaries (zh/en)
└── css-modules.d.ts
Build
pnpm install
pnpm build # tsc -p tsconfig.build.json && node scripts/stage-node.mjs && tsdown
Artifacts:
lib/client.js — the browser half in the DSH client-bundle format (window.__ModuleLoader__.load({ id, factory })): markstream-react / markstream-core / stream-markdown-parser / @floating-ui/* / katex are all inlined into one file (inlineDynamicImports; the module loader serves only /plugins/<id>/client.js); react / react-dom / react/jsx-runtime / @deepseek-ai/dsh-client-ui-primitives stay as module-table externals; markstream-react/index.css is compiled and injected as a tagged data-plugin-css style.
lib/index.js / lib/invariant.js — the Node halves (plus config.js / schema.js staged by stage-node; Node resolves the plain .js relative imports; @deepseek-ai/dsh-settings / @deepseek-ai/schemastery stay external).
lib/types/**/*.d.ts — declarations.
@deepseek-ai/* types come from the auto-installed npm peers (0.1.0-rc.8, the same release the deployed GUI runs); at runtime the DSH client module table provides them.
Install into a DSH profile (web)
This package declares dsh.bundle.patch, so it installs through the official CLI channel:
dsh plugin --profile web add dsh-markstream
For an unpublished local package, wire it manually (local ~/.dsh/profiles/web):
-
Add the package to the profile dependencies and install (heal node_modules). For local development use link:: file: makes pnpm pack and copy the package, so every plugin edit needs a reinstall; link: is a symlink, so after pnpm build rebuilds lib/client.js the server serves the new file directly (client-hmr picks up the change and hot-reloads without a restart):
# edit ~/.dsh/profiles/web/package.json
# "dependencies": { "dsh-markstream": "link:D:/path/to/dsh-markstream" }
# "dsh": { "profile": { "bundles": [ ..., "dsh-markstream" ] } }
cd ~/.dsh/profiles/web && pnpm install
-
Restart dsh web and refresh the page. The plugin row mounts through the insert in cordis.patch.yml; you can also append it manually to the profile's cordis.patch.yml:
- insert:
- id: markstream
name: 'dsh-markstream'
Optional features (markstream optional peers)
| Feature | Package | Notes |
|---|
| Enhanced code blocks | stream-diffs (bundled dependency) | File/FileDiff surface + syntax highlighting; registered theme names resolve exact colors, unregistered names fall back through the documented seam (--markstream-code-fallback-*) to the selected theme's bg/fg |
| Mermaid diagrams | mermaid | Mermaid blocks degrade when absent |
| KaTeX math | katex | Inlined with the @deepseek-ai/dsh-client-ui-primitives dependency tree in the current build |
| D2 diagrams | @terrastruct/d2 | Unresolved at build time, treated as external (only needed when rendering d2 blocks) |
| Infographics | @antv/infographic | Same, treated as external |
Known limitations
- Per-block rendering: each text block is handed to
MarkdownRender independently (matching the shipped per-block renderer); Markdown structures split across text blocks (e.g. a code fence spanning two blocks) are not merged.
- No file-mention enhancement: the shipped
MarkdownText's fileMentions inline-code highlighting is not replicated in v1 (markstream renders them as plain inline code).
- No tail-follow scrolling for Think rows: the collapsed summary follows the latest line, but without the shipped renderer's automatic horizontal follow scroll.
- Replaces the shipped renderer:
assistant-step is a keyed slot, so registering the same key replaces the shipped implementation wholesale; this plugin replicates text / reasoning / image / unknown-block / stopped-marker presentation, but later product changes are not synced automatically.
- Panel renders settings only: the bottom-right gear panel edits and persists rendering fields (
enabled / theme / …); writes persist to $DSH_HOME/settings.yaml.
License
MIT