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-steprenderer in theconversation.chat.nodekeyed slot with a markstream-based implementation, registered withpriority: -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-diffspackage), Mermaid (optionalmermaid), 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.
- Text blocks render through
- The renderer's
tlocale seat is bound to theconversationnamespace 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 theConfigschema and used as the settings namespacebase. - User layer: a floating gear button at the frame's bottom-right (
shell.overlayentrydsh-markstream-config) opens a non-modal floating panel (DeepSeek-web style: 70% of the conversation window's height, vertically centered on it, 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). Every field row is two lines — the config item title on its own line, the control (value) below — so the control never covers the title, and every interactive control (toggle / select / number / button) is the unified component set incontrols.tsx(no raw HTML components in the settings surface).
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 + 70%-height centered floating panel
├── ConfigSurface.module.css
├── ConfigFields.tsx # shared 2-line field rows (title line + control line)
├── ConfigFields.module.css
├── controls.tsx # unified settings controls (toggle / select / number / buttons)
├── controls.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/*/katexare 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-primitivesstay as module-table externals;markstream-react/index.cssis compiled and injected as a taggeddata-plugin-cssstyle.lib/index.js/lib/invariant.js— the Node halves (plusconfig.js/schema.jsstaged by stage-node; Node resolves the plain.jsrelative imports;@deepseek-ai/dsh-settings/@deepseek-ai/schemasterystay 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 afterpnpm buildrebuildslib/client.jsthe 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 installRestart
dsh weband refresh the page. The plugin row mounts through theinsertincordis.patch.yml; you can also append it manually to the profile'scordis.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
MarkdownRenderindependently (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'sfileMentionsinline-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-stepis 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.
No comments yet. Be the first to write one.