Skip to content
dsh.fish
Bundle

dsh-markstream

DSH web plugin: renders assistant messages with markstream-react — the low-jitter streaming Markdown renderer for AI chat (streamed code blocks, Mermaid, KaTeX, safe HTML).

Source
boomboomxx
License
MIT
Updated
Updated 6 days ago

Readme

# dsh-markstream

A DeepSeek Harness (DSH) web plugin that renders assistant messages in the conversation stream with [markstream-react](https://www.npmjs.com/package/markstream-react) — the low-jitter streaming Markdown renderer for AI chat (streamed code blocks, Mermaid, KaTeX, safe HTML).

English | [中文](README.zh.md)

## 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](https://markstream-react.pages.dev/) (CODE THEME / DARK MODE etc.), the [component docs](https://markstream.simonhe.me/zh/guide/components.html), and the [recommended code-block config](https://markstream.simonhe.me/zh/guide/code-blocks.html#stream-diffs-surface-%E6%8E%A8%E8%8D%90), delivered as two layers:

1. **Composition layer**: the plugin row's `config` (full template in [cordis.patch.yml](cordis.patch.yml)) — deployment defaults, validated by the `Config` schema and used as the settings namespace `base`.
2. **User layer**: a **floating gear button at the frame's bottom-right** (`shell.overlay` entry `dsh-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** in `controls.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](https://markstream.simonhe.me/zh/guide/code-blocks.html#stream-diffs-surface-%E6%8E%A8%E8%8D%90); 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

```bash
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:

```bash
dsh plugin --profile web add dsh-markstream
```

For an unpublished local package, wire it manually (local `~/.dsh/profiles/web`):

1. 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):

   ```bash
   # 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
   ```

2. 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`:

   ```yaml
   - 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](LICENSE)

Install

dsh plugin --profile web add github:boomboomxx/dsh-markstream

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source