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

8.2 KiB

CodeBlockNode (Component)

CodeBlockNode 是库中用于渲染富交互代码块的组件。对于需要编辑/高亮/增量渲染的场景,推荐安装 stream-monaco。组件为头部提供灵活的自定义点(props + slots),并在常见场景下提供可拦截的事件。

Quick summary

  • Monaco mode (install stream-monaco) — editor-like rendering with workers
  • Fallback — plain <pre><code> when stream-monaco is not installed
  • If you want Shiki-based highlighting (no Monaco), use MarkdownCodeBlockNode (peer: stream-markdown)

Props

Refer to src/types/component-props.ts for full signature. Key props:

  • node — code_block node (required)
  • loading, stream, isShowPreview
  • monacoOptions — typed as CodeBlockMonacoOptions and forwarded to stream-monaco
    • diff options such as diffHideUnchangedRegions, diffLineStyle, diffAppearance, diffUnchangedRegionStyle, diffHunkActionsOnHover, diffHunkHoverHideDelayMs, and onDiffHunkAction belong here
  • Header controls: showHeader, showCollapseButton, showCopyButton, showExpandButton, showPreviewButton, showFontSizeButtons, showTooltips

Default diff UX in Monaco mode:

  • diffHideUnchangedRegions: { enabled: true, contextLineCount: 2, minimumLineCount: 4, revealLineCount: 5 }
  • diffLineStyle: 'background'
  • diffAppearance: 'auto'
  • diffUnchangedRegionStyle: 'line-info'
  • diffHunkActionsOnHover: true
  • diffHunkHoverHideDelayMs: 160

You can override any of them through monacoOptions. When the preset uses diffAppearance: 'auto', CodeBlockNode resolves it to the current light/dark surface before passing the options to stream-monaco.

Diff blocks also show - / + line counts in the built-in header.

Slots

  • header-left — replace left header
  • header-right — replace right header
  • loading — customize placeholder when streaming is disabled

Emits

  • copy(text: string) — when copy pressed
  • previewCode(payload) — only emitted when you attach a @preview-code listener; payload is { node, artifactType, artifactTitle, id }

Examples

Install and run (Monaco)

pnpm add stream-monaco

Basic example

<script setup lang="ts">
import type { CodeBlockNodeProps } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'

const node = {
  type: 'code_block',
  language: 'js',
  code: 'console.log(1)',
  raw: 'console.log(1)',
} satisfies CodeBlockNodeProps['node']
</script>

<template>
  <CodeBlockNode :node="node" />
</template>

Replace header and hide copy button

<script setup lang="ts">
import type { CodeBlockNodeProps } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'

const node = {
  type: 'code_block',
  language: 'js',
  code: 'console.log(1)',
  raw: 'console.log(1)',
} satisfies CodeBlockNodeProps['node']

function runSnippet() {}
</script>

<template>
  <CodeBlockNode :node="node" :show-copy-button="false">
    <template #header-left>
      <div class="flex items-center">
        Custom left
      </div>
    </template>
    <template #header-right>
      <button @click="runSnippet">
        Run
      </button>
    </template>
  </CodeBlockNode>
</template>

Custom loading placeholder

<script setup lang="ts">
import type { CodeBlockNodeProps } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'

const node = {
  type: 'code_block',
  language: 'ts',
  code: 'console.log("loading")',
  raw: 'console.log("loading")',
} satisfies CodeBlockNodeProps['node']
</script>

<template>
  <CodeBlockNode :node="node" :stream="false" :loading="true">
    <template #loading="{ loading, stream }">
      <div v-if="loading && !stream">
        Loading editor assets…
      </div>
    </template>
  </CodeBlockNode>
</template>

Theme Switching

CodeBlockNode supports automatic theme switching based on dark/light mode. Use @vueuse/core's useDark composable to track the theme state and pass theme names to MarkdownRender or CodeBlockNode.

Using @vueuse/core in standalone Vue apps

<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import MarkdownRender from 'markstream-vue'

const isDark = useDark() // Ref<boolean> reactive to system/theme preference
const toggleDark = useToggle(isDark)
const content = '# Example\n\n```js\nconsole.log("dark mode")\n```'

// Available themes (must include the themes you want to use)
const themes = [
  'vitesse-dark',
  'vitesse-light',
  'github-dark',
  'github-light',
  // ... more themes
]
</script>

<template>
  <div>
    <button @click="toggleDark()">
      Toggle Theme
    </button>
    <MarkdownRender
      :is-dark="isDark"
      code-block-dark-theme="vitesse-dark"
      code-block-light-theme="vitesse-light"
      :themes="themes"
      :content="content"
    />
  </div>
</template>

VitePress integration

For VitePress, use the built-in isDark from VitePress's useData():

// docs/.vitepress/theme/composables/useDark.ts
import { useData } from 'vitepress'

/**
 * VitePress theme composable for dark mode
 * Uses VitePress's built-in isDark from useData()
 */
export function useDark() {
  const { isDark } = useData()
  return isDark
}
<!-- In any .md file or component -->
<script setup lang="ts">
import MarkdownRender from 'markstream-vue'
import { useDark } from '../../.vitepress/theme'

const isDark = useDark()
const content = '# Example\n\n```js\nconsole.log("dark mode")\n```'

const themes = [
  'vitesse-dark',
  'vitesse-light',
  'github-dark',
  'github-light',
  // ... more themes
]
</script>

<template>
  <MarkdownRender
    :is-dark="isDark"
    code-block-dark-theme="vitesse-dark"
    code-block-light-theme="vitesse-light"
    :themes="themes"
    :content="content"
  />
</template>

How it works:

The theme prop accepts either a fixed theme or a light/dark pair:

<!-- Auto-switch between light and dark (recommended) -->
<CodeBlockNode :theme="{ light: 'vitesse-light', dark: 'vitesse-dark' }" />

<!-- Fixed theme (ignores isDark) -->
<CodeBlockNode theme="monokai" />

<!-- Theme object (fixed, ignores isDark) -->
<CodeBlockNode :theme="{ name: 'my-theme', colors: { ... } }" />

When using a { light, dark } pair, the component automatically switches based on the isDark prop.

The themes prop registers the available themes so Monaco can lazy-load them on demand.

Backward compatibility: darkTheme / lightTheme props still work but are deprecated. Prefer the unified theme prop.

Key differences for CodeBlockNode:

Prop Direct CodeBlockNode Via MarkdownRender
isDark Passed directly to <CodeBlockNode :is-dark="isDark" /> Passed via <MarkdownRender :is-dark="isDark" /> and automatically forwarded
Theme :theme="{ light: 'vitesse-light', dark: 'vitesse-dark' }" :code-block-dark-theme="'vitesse-dark'" :code-block-light-theme="'vitesse-light'" (legacy)
Themes list :themes="['vitesse-dark', 'vitesse-light', ...]" :themes="['vitesse-dark', 'vitesse-light', ...]"

Notes

  • The CodeBlock header API is documented in docs/guide/codeblock-header.md (examples for replacing header and custom loading placeholder).
  • CodeBlockNode and MermaidBlockNode intentionally use different copy event payloads: CodeBlockNode emits copy(text: string), while MermaidBlockNode emits copy(ev: MermaidBlockEvent<{ type: 'copy'; text: string }>) (supports preventDefault()).

Try this — simple snapshot example (inline usage):

<script setup lang="ts">
import type { CodeBlockNodeProps } from 'markstream-vue'
import { CodeBlockNode } from 'markstream-vue'

const node = { type: 'code_block', language: 'js', code: 'console.log("hello")', raw: 'console.log("hello")' } satisfies CodeBlockNodeProps['node']
</script>

<template>
  <CodeBlockNode :node="node" />
</template>

Tell me if you want me to also:

  • Add stricter TS types for defineEmits in the SFC
  • Update the docs sidebar to link this page (I can add the entry to docs/guide/index.md or the sidebar config)
  • Add a runnable example in playground/ demonstrating slot usage and event interception