Terminal protocol flow
This showcase covers the non-React path that is easy to miss if you only look at the browser demo. StreamMDX is not just a React renderer. It also exposes a worker, protocol surface, and snapshot-store model that works for terminal UIs, remote transports, and replay tooling.
Use this page when you already know you want a terminal or protocol consumer and need the shortest route to the right docs and example files.
What this showcases
- worker-thread parsing in Node
- snapshot materialization outside React
- protocol/event transport for process boundaries
- a concrete repo example you can run immediately
Recommended architecture
Use this shape when your consumer is a TUI, CLI, log viewer, or replay tool:
markdown stream
|
v
worker helper (`stream-mdx/worker/node`)
|
v
PATCH / event output
|
v
snapshot store (`@stream-mdx/tui`)
|
v
terminal renderer (Ink / blessed / custom ANSI)This keeps parsing semantics aligned with the browser path while giving the terminal consumer full control over layout.
Minimal runnable example
The repo now includes a minimal end-to-end example and a dedicated docs-page walkthrough:
From the repo root:
npm install
npm run build:packages
npm run example:tui-minimalThat example does four things only:
- starts the worker in Node
- appends streaming markdown in chunks
- applies patches to the TUI snapshot store
- renders
Block[]back to the terminal
Worker + snapshot-store loop
import { createWorkerThread } from "stream-mdx/worker/node";
import { createSnapshotStore } from "@stream-mdx/tui";
const worker = createWorkerThread({ stdout: true, stderr: true });
const store = createSnapshotStore();
worker.on("message", (msg) => {
if (msg.type === "PATCH") {
store.applyPatches(msg.patches);
renderToTerminal(store.getBlocks());
}
});
worker.postMessage({
type: "INIT",
initialContent: "",
docPlugins: { tables: true, html: true, mdx: false, math: false, footnotes: true },
mdx: { compileMode: "server" },
});When protocol transport matters
You do not need @stream-mdx/protocol when everything lives inside one local Node process.
You do want it when:
- the parser and renderer live in different processes
- you want NDJSON over a socket or subprocess pipe
- you want replayable structured event logs
- you need a typed boundary that is not tied directly to worker message objects
import { NdjsonDecoder, createSnapshotStore } from "@stream-mdx/tui";
import type { StreamMdxEventV1 } from "@stream-mdx/protocol";
const decoder = new NdjsonDecoder<StreamMdxEventV1>();
const store = createSnapshotStore();
for (const event of decoder.push(chunk)) {
store.applyEvent(event);
}Capability boundaries
This surface is already strong for:
- block snapshots
- replay tooling
- terminal rendering from
Block[] - structured event transport
It is intentionally still lightweight for:
- arbitrary MDX component rendering in a terminal
- built-in ANSI syntax-highlighting helpers
- a full reference terminal UI beyond the repo example
That boundary is deliberate. The library owns parsing and patch semantics; the terminal consumer owns final presentation.
Operational checklist
If you are shipping a TUI or protocol consumer, treat these as the default checklist:
- prefer
createWorkerThread()over a direct compile path whenworker_threadsare available - render from
Block[], not from raw HTML - keep MDX handling explicit and conservative in terminals
- use the protocol path only when you actually have a transport boundary
- keep regression and replay fixtures for any custom terminal formatting logic