Files
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

20 KiB
Raw Permalink Blame History

React Components & API

markstream-react provides the same powerful components as markstream-vue, but built for React. All components support React 18+ with full TypeScript support.

The root markstream-react, markstream-react/next, and markstream-react/server entrypoints all ship declaration files. Shared renderer and component types such as NodeRendererProps, NodeRendererCodeBlockProps, NodeComponentProps, RenderContext, RenderNodeFn, CustomComponentMap, CodeBlockMonacoOptions, MarkdownCodeBlockNodeProps, ListItemNodeProps, HtmlPreviewFrameProps, TooltipProps, TooltipPlacement, and LinkNodeStyleProps can be imported directly from the entrypoint you use.

Main Component: MarkdownRender

The primary component for rendering markdown content in React.

Props

MarkdownRender uses the NodeRendererProps interface from markstream-react.

Core props

Prop Type Default Description
content string - Markdown content to render
nodes BaseNode[] - Pre-parsed AST nodes (typically ParsedNode[] from the parser)
customId string - Identifier for scoping custom components and CSS
final boolean false Marks end-of-stream; stops emitting streaming loading nodes
parseOptions ParseOptions - Parser options and token hooks (only when content is provided)
customHtmlTags readonly string[] - HTML-like tags emitted as custom nodes (e.g. thinking)
customMarkdownIt (md: MarkdownIt) => MarkdownIt - Customize the internal MarkdownIt instance
debugPerformance boolean false Log parse/render timing and virtualization stats (dev only)
isDark boolean false Theme flag forwarded to heavy nodes; adds .dark to the root container
indexKey number | string - Key prefix when rendering multiple instances in lists
typewriter boolean true Enable the non-code-node enter transition
showTooltips boolean true Global tooltip switch for LinkNode and code block nodes

Streaming & heavy-node toggles

Prop Default Description
renderCodeBlocksAsPre false Render code blocks as <pre><code> (Mermaid/D2/Infographic blocks will also fall back)
codeBlockStream true Stream code block updates as content arrives
viewportPriority true Defer heavy work (Monaco/Mermaid/D2/KaTeX) until near viewport
deferNodesUntilVisible true Render heavy nodes as placeholders until visible (non-virtualized mode only)

Performance (virtualization & batching)

Prop Default Description
maxLiveNodes 320 Max fully rendered nodes kept in DOM (set 0 to disable virtualization)
liveNodeBuffer 60 Overscan buffer around the focus range
batchRendering true Incremental batch rendering when virtualization is disabled
initialRenderBatchSize 40 Nodes rendered immediately before batching starts
renderBatchSize 80 Nodes rendered per batch tick
renderBatchDelay 16 Extra delay (ms) before each batch after rAF
renderBatchBudgetMs 6 Time budget (ms) before adaptive batch sizes shrink
renderBatchIdleTimeoutMs 120 Timeout (ms) for requestIdleCallback slices

Global code block options

Prop Type Description
codeBlockDarkTheme CodeBlockMonacoTheme Monaco dark theme object forwarded to every CodeBlockNode
codeBlockLightTheme CodeBlockMonacoTheme Monaco light theme object forwarded to every CodeBlockNode
codeBlockMonacoOptions CodeBlockMonacoOptions Options forwarded to stream-monaco, including diff hover-action settings like diffHunkActionsOnHover, diffHunkHoverHideDelayMs, and onDiffHunkAction
codeBlockMinWidth string | number Min width forwarded to CodeBlockNode
codeBlockMaxWidth string | number Max width forwarded to CodeBlockNode
codeBlockProps NodeRendererCodeBlockProps Extra props forwarded to every code-block renderer (CodeBlockNode / MarkdownCodeBlockNode)
mermaidProps Partial<Omit<MermaidBlockNodeProps, 'node' | 'loading' | 'isDark'>> Extra props forwarded to Mermaid fences and custom mermaid renderers
d2Props Partial<Omit<D2BlockNodeProps, 'node' | 'loading' | 'isDark'>> Extra props forwarded to D2 fences and custom d2 renderers
infographicProps Partial<Omit<InfographicBlockNodeProps, 'node' | 'loading' | 'isDark'>> Extra props forwarded to infographic fences and custom infographic renderers
themes CodeBlockMonacoTheme[] Theme list forwarded to stream-monaco

Heavy renderer prop forwarding

NodeRenderer can tune heavy blocks directly:

<MarkdownRender
  content={markdown}
  viewportPriority
  mermaidProps={{
    showHeader: false,
    renderDebounceMs: 180,
    previewPollDelayMs: 500,
  }}
  d2Props={{ progressiveIntervalMs: 500 }}
  infographicProps={{ showHeader: false }}
/>

Streaming notes:

  • Keep viewportPriority enabled to prevent offscreen Mermaid / Monaco / D2 work from running while text is still streaming.
  • For high-frequency SSE, prefer passing nodes instead of reparsing the full content string every chunk.
  • Common Mermaid tuning keys: renderDebounceMs, contentStableDelayMs, previewPollDelayMs, previewPollMaxDelayMs, previewPollMaxAttempts.

NodeRendererCodeBlockProps follows the public CodeBlockNode prop surface except for node, so you get completion for flags like showHeader, showFontSizeButtons, and showTooltips without dropping to any.

import type { NodeRendererCodeBlockProps } from 'markstream-react'

const codeBlockProps: NodeRendererCodeBlockProps = {
  showHeader: false,
  showFontSizeButtons: false,
  showTooltips: false,
}

Events

Prop Type Description
onCopy (code: string) => void Fired when code blocks emit copy events
onHandleArtifactClick (payload: any) => void Fired on artifact/preview clicks
onClick (event: React.MouseEvent<HTMLDivElement>) => void Click handler for the renderer root
onMouseOver (event: React.MouseEvent<HTMLElement>) => void Mouseover handler for the renderer root
onMouseOut (event: React.MouseEvent<HTMLElement>) => void Mouseout handler for the renderer root

Usage

import MarkdownRender from 'markstream-react'

function App() {
  const markdown = `# Hello React!

This is markstream-react.`

  return (
    <MarkdownRender
      customId="docs"
      content={markdown}
      maxLiveNodes={150}
    />
  )
}

Code Block Components

MarkdownCodeBlockNode

Lightweight code highlighting using Shiki.

import { MarkdownCodeBlockNode } from 'markstream-react'

function CodeBlock() {
  const codeNode = {
    type: 'code_block',
    language: 'javascript',
    code: 'const hello = "world"',
    raw: 'const hello = "world"'
  }

  const handleCopy = () => {
    alert('Code copied!')
  }

  return (
    <div className="markstream-react">
      <MarkdownCodeBlockNode
        node={codeNode}
        showCopyButton={true}
        showCollapseButton={false}
        onCopy={handleCopy}
      />
    </div>
  )
}

CodeBlockNode

Feature-rich Monaco-powered code blocks.

import { CodeBlockNode } from 'markstream-react'

function MonacoCodeBlock() {
  const codeNode = {
    type: 'code_block',
    language: 'typescript',
    code: 'const greeting: string = "Hello"',
    raw: 'const greeting: string = "Hello"'
  }

  const handleCopy = (code: string) => {
    console.log('Code copied:', code)
  }

  const handlePreviewCode = (artifact: any) => {
    console.log('Preview code:', artifact)
  }

  return (
    <div className="markstream-react">
      <CodeBlockNode
        node={codeNode}
        monacoOptions={{ fontSize: 14, theme: 'vs-dark' }}
        stream={true}
        showCollapseButton={false}
        onCopy={handleCopy}
        onPreviewCode={handlePreviewCode}
      />
    </div>
  )
}

Math Components

MathBlockNode

Renders block-level math formulas with KaTeX.

import { MathBlockNode } from 'markstream-react'

function MathBlock() {
  const mathNode = {
    type: 'math_block',
    content: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}',
    raw: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}'
  }

  return (
    <div className="markstream-react">
      <MathBlockNode node={mathNode} />
    </div>
  )
}

MathInlineNode

Renders inline math formulas.

import { MathInlineNode } from 'markstream-react'

function MathInline() {
  const inlineMathNode = {
    type: 'math_inline',
    content: 'E = mc^2',
    raw: 'E = mc^2'
  }

  return (
    <div className="markstream-react">
      <p>
        The formula is:
        {' '}
        <MathInlineNode node={inlineMathNode} />
      </p>
    </div>
  )
}

Mermaid Diagrams

MermaidBlockNode

Progressive Mermaid diagram rendering.

Quick reference

  • Tooltip control: showTooltips defaults to true; set false to disable header action tooltips.
import { MermaidBlockNode } from 'markstream-react'

function MermaidDiagram() {
  const mermaidNode = {
    type: 'code_block',
    language: 'mermaid',
    code: `graph TD
    A[Start] --> B{Is it working?}
    B -->|Yes| C[Great!]`,
    raw: ''
  }

  const handleExport = (ev: any) => {
    console.log('Mermaid SVG:', ev.svgString)
  }

  return (
    <div className="markstream-react">
      <MermaidBlockNode
        node={mermaidNode}
        isStrict={true}
        onExport={handleExport}
        showTooltips={true}
      />
    </div>
  )
}

Event notes:

  • onCopy(code: string) receives the source text directly (no MermaidBlockEvent wrapper in React).
  • onExport, onOpenModal, onToggleMode receive MermaidBlockEvent and support ev.preventDefault() to stop the default behavior.
  • onToggleMode signature: (target: 'source' | 'preview', ev).
  • For renderer-wide usage, prefer passing these knobs through mermaidProps on MarkdownRender / NodeRenderer.

D2 Diagrams

D2BlockNode

Progressive D2 diagram rendering with source fallback.

import { D2BlockNode } from 'markstream-react'

function D2Diagram() {
  const d2Node = {
    type: 'code_block',
    language: 'd2',
    code: `direction: right
Client -> API: request
API -> DB: query`,
    raw: ''
  }

  return (
    <div className="markstream-react">
      <D2BlockNode
        node={d2Node}
        progressiveIntervalMs={600}
      />
    </div>
  )
}

Other Node Components

HeadingNode

import { HeadingNode } from 'markstream-react'

function CustomHeading() {
  const headingNode = {
    type: 'heading',
    level: 1
  }

  return <HeadingNode node={headingNode}>Hello World</HeadingNode>
}

ParagraphNode

import { ParagraphNode } from 'markstream-react'

function CustomParagraph() {
  const paragraphNode = {
    type: 'paragraph'
  }

  return (
    <ParagraphNode node={paragraphNode}>
      This is a
      {' '}
      <strong>bold</strong>
      {' '}
      word.
    </ParagraphNode>
  )
}

ListNode

import { ListNode, renderNode } from 'markstream-react'

function CustomList() {
  const listNode = {
    type: 'list',
    ordered: false,
    items: [
      {
        type: 'list_item',
        children: [
          { type: 'paragraph', children: [{ type: 'text', content: 'Item 1' }] }
        ]
      },
      {
        type: 'list_item',
        children: [
          { type: 'paragraph', children: [{ type: 'text', content: 'Item 2' }] }
        ]
      }
    ]
  }

  const ctx = { events: {} }

  return <ListNode node={listNode} ctx={ctx} renderNode={renderNode} />
}

LinkNode

import { LinkNode } from 'markstream-react'

function CustomLink() {
  const linkNode = {
    type: 'link',
    href: 'https://example.com',
    title: 'Example',
    text: 'Click me'
  }

  return (
    <LinkNode
      node={linkNode}
      color="#e11d48"
      underlineHeight={3}
      showTooltip={true}
    />
  )
}

ImageNode

import { ImageNode } from 'markstream-react'

function CustomImage() {
  const imageNode = {
    type: 'image',
    src: 'https://example.com/image.jpg',
    alt: 'Example image',
    title: 'Example',
    raw: '![Example image](https://example.com/image.jpg)'
  }

  const handleClick = () => {
    console.log('Image clicked!')
  }

  const handleLoad = () => {
    console.log('Image loaded!')
  }

  return (
    <ImageNode
      node={imageNode}
      onClick={handleClick}
      onLoad={handleLoad}
    />
  )
}

Utility Functions

getMarkdown

Get a configured markdown-it instance.

import { getMarkdown } from 'stream-markdown-parser'

const md = getMarkdown('my-msg-id', {
  html: true,
  linkify: true,
  typographer: true
})

const tokens = md.parse('# Hello World')

parseMarkdownToStructure

Parse markdown string to AST structure.

import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'

const md = getMarkdown()
const nodes = parseMarkdownToStructure('# Title\n\nContent here...', md)

// Use with MarkdownRender
// <MarkdownRender nodes={nodes} />

Optional workers (Mermaid / KaTeX)

Mermaid/KaTeX auto-load when installed. If you want off‑thread parsing/rendering, inject workers:

import { setKaTeXWorker, setMermaidWorker } from 'markstream-react'
import KatexWorker from 'markstream-react/workers/katexRenderer.worker?worker'
import MermaidWorker from 'markstream-react/workers/mermaidParser.worker?worker'

setMermaidWorker(new MermaidWorker())
setKaTeXWorker(new KatexWorker())

Custom Component API

Props Interface

All custom node components receive these props:

import type { CustomComponentMap, NodeComponentProps, RenderContext, RenderNodeFn } from 'markstream-react'

interface NodeComponentProps<TNode = unknown> {
  node: TNode // The parsed node data
  ctx?: RenderContext // Renderer context (themes, events, flags)
  renderNode?: RenderNodeFn // Helper to render child nodes
  indexKey?: React.Key // Unique key for the node
  customId?: string // Custom ID for scoping
  isDark?: boolean
  typewriter?: boolean
  children?: React.ReactNode
}

CustomComponentMap is the typed mapping shape accepted by setCustomComponents(...).

Example Custom Component

import React from 'react'

interface CustomParagraphProps {
  node: {
    type: string
    children: Array<{
      type: string
      content?: string
      children?: any[]
    }>
  }
  indexKey?: number | string
  customId?: string
}

function CustomParagraph({ node, indexKey, customId }: CustomParagraphProps) {
  return (
    <p
      className={`custom-paragraph custom-paragraph-${indexKey}`}
      data-custom-id={customId}
      data-node-type={node.type}
    >
      {node.children.map((child, i) => (
        <span key={i}>{child.content || ''}</span>
      ))}
    </p>
  )
}

// Usage
function App() {
  const paragraphNode = {
    type: 'paragraph',
    children: [
      { type: 'text', content: 'Custom paragraph content' }
    ]
  }

  return <CustomParagraph node={paragraphNode} indexKey={0} customId="docs" />
}

Context + Custom Components

You can use React Context inside custom node components, while still registering them via setCustomComponents:

import MarkdownRender, { setCustomComponents } from 'markstream-react'
import React, { createContext, useContext } from 'react'

const ThemeContext = createContext<'light' | 'dark'>('light')

function CustomHeading({ node, customId }: any) {
  const theme = useContext(ThemeContext)
  const level = node.level || 1
  const Tag = `h${level}` as keyof JSX.IntrinsicElements

  return (
    <Tag className={`custom-heading ${theme}`} data-custom-id={customId}>
      {node.children?.map((child: any, i: number) => (
        <span key={i}>{child.content || ''}</span>
      ))}
    </Tag>
  )
}

setCustomComponents('docs', { heading: CustomHeading })

function App() {
  const markdown = `# Custom Heading

This uses a custom heading component.
`

  return (
    <ThemeContext.Provider value="dark">
      <MarkdownRender customId="docs" content={markdown} />
    </ThemeContext.Provider>
  )
}

Streaming Support

markstream-react supports streaming markdown content:

import MarkdownRender from 'markstream-react'
import { useEffect, useState } from 'react'

function StreamingDemo() {
  const [content, setContent] = useState('')
  const fullContent = `# Streaming Demo

This content streams in **small chunks**.
`

  useEffect(() => {
    let i = 0
    const interval = setInterval(() => {
      if (i < fullContent.length) {
        setContent(prev => prev + fullContent[i])
        i++
      }
      else {
        clearInterval(interval)
      }
    }, 30)

    return () => clearInterval(interval)
  }, [])

  return <MarkdownRender content={content} />
}

TypeScript Support

markstream-react includes full TypeScript definitions:

import type { NodeComponentProps, NodeRendererProps } from 'markstream-react'
import type { ParsedNode } from 'stream-markdown-parser'
import MarkdownRender from 'markstream-react'

function App() {
  const markdown = '# Hello TypeScript!'
  const nodes: ParsedNode[] = []

  return <MarkdownRender content={markdown} nodes={nodes} />
}

Code block prop interfaces (CodeBlockNodeProps, MermaidBlockNodeProps, D2BlockNodeProps, InfographicBlockNodeProps, PreCodeNodeProps) all use node: CodeBlockNode from stream-markdown-parser (use language: 'mermaid' / language: 'd2' / language: 'd2lang' / language: 'infographic' when targeting specialized renderers).

Next.js Best Practices

Use the dedicated Next SSR entrypoints instead of mounted guards or ssr: false.

import MarkdownRender from 'markstream-react/next'

export default function MarkdownPage() {
  return <MarkdownRender content="# Hello Next.js!" final />
}

For a pure server render path:

import MarkdownRender from 'markstream-react/server'

export default function MarkdownPage() {
  return <MarkdownRender content="# Hello!" final />
}

See React Next SSR for the full entrypoint model.

Hooks Integration

You can easily integrate with React hooks:

import type { ChangeEvent } from 'react'
import MarkdownRender from 'markstream-react'
import { useCallback, useMemo, useState } from 'react'

function MarkdownEditor() {
  const [content, setContent] = useState('# Edit me!')
  const [theme, setTheme] = useState('light')

  const memoizedContent = useMemo(() => content, [content])

  const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
    setContent(e.target.value)
  }, [])

  return (
    <div>
      <textarea value={content} onChange={handleChange} />
      <MarkdownRender
        content={memoizedContent}
        customId={`editor-${theme}`}
      />
    </div>
  )
}

Error Handling

import MarkdownRender from 'markstream-react'
import { useState } from 'react'

function SafeMarkdown({ content }: { content: string }) {
  const [error, setError] = useState<Error | null>(null)

  if (error) {
    return (
      <div>
        Error rendering markdown:
        {error.message}
      </div>
    )
  }

  try {
    return <MarkdownRender content={content} />
  }
  catch (err) {
    setError(err as Error)
    return null
  }
}

Next Steps