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.
This commit is contained in:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,116 @@
export type HeightEstimationWorkload = 'text' | 'code'
interface HeightEstimationTranscriptOptions {
total?: number
}
function buildCodeFence(language: string, lines: string[]) {
return [`\`\`\`${language}`, ...lines, '```'].join('\n')
}
function buildPlainParagraph(index: number) {
const variants = [
'This keeps the structure simple enough for conservative measurement.',
'It also mirrors restored chat history where adjacent messages can have noticeably different lengths.',
'Some paragraphs stay compact while others expand with additional context about resize, restore anchoring, and viewport math.',
'The goal is to avoid a perfectly uniform document so baseline average-height fallback has a more realistic distribution to approximate.',
]
const repeat = (index % 4) + 1
const suffix = Array.from({ length: repeat }, (_, variantIndex) =>
variants[(index + variantIndex) % variants.length]).join(' ')
return `Paragraph ${index} is intentionally plain text with enough words to wrap over multiple lines as the viewport moves between 375 and 1280 pixels. ${suffix}`
}
function buildCodeHeavyLines(index: number, count: number) {
return Array.from({ length: count }, (_, lineIndex) => {
const label = `section-${index}-${lineIndex}`
if (lineIndex % 6 === 0)
return `export const ${label.replace(/-/g, '_')} = ${index + lineIndex}`
if (lineIndex % 6 === 1)
return `export function render${index}_${lineIndex}(input: string) {`
if (lineIndex % 6 === 2)
return ` return input.split(/\\r?\\n/).map((line, idx) => \`\${idx}:\${line.trim()}\`)`
if (lineIndex % 6 === 3)
return '}'
if (lineIndex % 6 === 4)
return `const tuple${lineIndex} = ['${label}', ${index}, ${lineIndex}] as const`
return `console.log('${label}', tuple${Math.max(0, lineIndex - 1)})`
})
}
export function buildHeightEstimationTranscript(workload: HeightEstimationWorkload, options: HeightEstimationTranscriptOptions = {}) {
const blocks: string[] = [
`# Height Estimation ${workload === 'text' ? 'Text' : 'Code'} Workload`,
'This transcript is generated for cross-framework height-estimation experiments.',
]
const total = Math.max(32, Math.round(options.total ?? (workload === 'text' ? 960 : 720)))
for (let index = 1; index <= total; index += 1) {
if (index % 64 === 0) {
blocks.push(`## Section ${index / 64}`)
continue
}
if (workload === 'text') {
if (index % 33 === 0) {
blocks.push([
'- A simple list item that keeps the inline structure predictable for measurement.',
'- Another simple list item that should still go through the conservative text estimator.',
'- Final list item used to validate list wrapper overhead.',
].join('\n'))
continue
}
if (index % 29 === 0) {
blocks.push(`Paragraph ${index} mixes [links](https://example.com/${index}) with **strong text** and \`inline code\` so conservative text estimation should skip it.`)
continue
}
if (index % 87 === 0) {
blocks.push(buildCodeFence('ts', [
`export function helper${index}(source: string) {`,
' return source.split(/\\r?\\n/).map((line, lineIndex) => ({ lineIndex, line }))',
'}',
]))
continue
}
blocks.push(buildPlainParagraph(index))
continue
}
if (index % 3 === 0) {
const lineCount = 8 + (index % 5) * 6
blocks.push(buildCodeFence('ts', [
`export const record${index} = {`,
` id: ${index},`,
` label: 'code-heavy-${index}',`,
` values: Array.from({ length: 8 }, (_, i) => i + ${index}),`,
'}',
'',
...buildCodeHeavyLines(index, lineCount),
]))
continue
}
if (index % 11 === 0) {
const diffLines = 4 + (index % 4) * 3
blocks.push(buildCodeFence('diff-ts', [
'<<<<<<< original',
`export const diff${index} = () => 'before-${index}'`,
...Array.from({ length: diffLines }, (_, lineIndex) => `const before_${index}_${lineIndex} = ${lineIndex}`),
'=======',
`export const diff${index} = () => 'after-${index}'`,
...Array.from({ length: diffLines }, (_, lineIndex) => `const after_${index}_${lineIndex} = ${lineIndex + 1}`),
'>>>>>>> updated',
]))
continue
}
blocks.push(`Code workload paragraph ${index} still provides text around code blocks so the renderer has mixed content and realistic spacing between editors. ${index % 2 === 0 ? 'Some of these paragraphs intentionally stay short.' : 'Others stretch longer to keep the surrounding non-code heights uneven across the transcript.'}`)
}
return blocks.join('\n\n')
}
@@ -0,0 +1,66 @@
interface TextareaSelectionLike {
value: string
selectionStart: number | null
selectionEnd: number | null
}
export interface MarkdownPasteResult {
nextValue: string
selectionStart: number
selectionEnd: number
}
export function normalizePastedMarkdownNewlines(content: string) {
if (!content.includes('\\n') || /[\r\n]/.test(content))
return content
let normalized = ''
let changed = false
for (let index = 0; index < content.length; index++) {
if (content[index] !== '\\') {
normalized += content[index]
continue
}
let slashEnd = index
while (slashEnd < content.length && content[slashEnd] === '\\')
slashEnd++
const slashCount = slashEnd - index
const nextChar = content[slashEnd]
if (nextChar === 'n' && slashCount % 2 === 1) {
normalized += '\\'.repeat((slashCount - 1) / 2)
normalized += '\n'
changed = true
index = slashEnd
continue
}
normalized += '\\'.repeat(slashCount)
index = slashEnd - 1
}
return changed ? normalized : content
}
export function resolveMarkdownTextareaPaste(
textarea: TextareaSelectionLike,
pasted: string,
): MarkdownPasteResult | null {
if (!pasted || !pasted.includes('\\n') || /[\r\n]/.test(pasted))
return null
const normalized = normalizePastedMarkdownNewlines(pasted)
const selectionStart = textarea.selectionStart ?? textarea.value.length
const selectionEnd = textarea.selectionEnd ?? textarea.value.length
const nextValue = `${textarea.value.slice(0, selectionStart)}${normalized}${textarea.value.slice(selectionEnd)}`
const cursor = selectionStart + normalized.length
return {
nextValue,
selectionStart: cursor,
selectionEnd: cursor,
}
}
@@ -0,0 +1,225 @@
export type TestLabSampleId = 'baseline' | 'thinking' | 'diff' | 'stress'
export type TestLabFrameworkId = 'vue3' | 'vue2' | 'react' | 'angular'
export interface TestLabFrameworkCard {
id: TestLabFrameworkId
label: string
note: string
origin: string
localPort: number | null
}
export interface TestLabSampleCard {
id: TestLabSampleId
title: string
summary: string
content: string
}
export const TEST_LAB_FRAMEWORKS: ReadonlyArray<TestLabFrameworkCard> = [
{
id: 'vue3',
label: 'Vue 3',
note: '完整特性 / 主 playground',
origin: 'https://markstream-vue.simonhe.me',
localPort: null,
},
{
id: 'vue2',
label: 'Vue 2',
note: '兼容层回归 / 老项目',
origin: 'https://markstream-vue2.pages.dev',
localPort: 3334,
},
{
id: 'react',
label: 'React',
note: 'hooks / 渲染对照',
origin: 'https://markstream-react.pages.dev',
localPort: 4174,
},
{
id: 'angular',
label: 'Angular',
note: 'standalone / parity lab',
origin: 'https://markstream-angular.pages.dev',
localPort: 4175,
},
] as const
export const TEST_LAB_SAMPLES: ReadonlyArray<TestLabSampleCard> = [
{
id: 'baseline',
title: '基础回归',
summary: '标题、强调、数学、Mermaid、infographic 和 D2 一次看全。',
content: `# Markstream Test Lab
在这里可以快速验证 **Vue 3 / Vue 2 / React / Angular** 四套渲染器的表现是否一致。
## 基础格式
- **加粗**
- *斜体*
- \`inline code\`
- [链接](https://github.com/Simon-He95/markstream-vue)
## 数学
行内公式:$E = mc^2$
块级公式:
$$
\\int_0^1 x^2 dx = \\frac{1}{3}
$$
## Mermaid
\`\`\`mermaid
flowchart LR
Prompt --> Parser --> Renderer --> Preview
\`\`\`
## 代码块
\`\`\`ts
export function compareFramework(name: string) {
return \`\${name} test page is ready.\`
}
\`\`\`
## Infographic
\`\`\`infographic
infographic list-row-simple-horizontal-arrow
data
items
- label 输入
desc markdown
- label 渲染
desc 解析与增量更新
- label 对照
desc 跨框架检查
\`\`\`
## D2
\`\`\`d2
App: Angular
Parser: Markdown AST
Renderer: Enhanced HTML
Lab: Playground/Test
App -> Parser -> Renderer -> Lab
\`\`\`
`,
},
{
id: 'thinking',
title: 'Thinking 嵌套重节点',
summary: '检查 custom thinking 节点里是否还能稳定渲染 Mermaid、代码块和列表。',
content: `# Thinking / Nested Blocks
<thinking>
在这个 thinking 容器里,我们希望 Angular 和 Vue / React 一样,仍然按正常 markdown 节点树渲染。
## Nested Mermaid
\`\`\`mermaid
flowchart TD
Thinking --> Parse
Parse --> Mermaid
Parse --> CodeBlock
\`\`\`
## Nested Code Block
\`\`\`ts
export function ensureParity(name: string) {
return \`nested renderer ok: \${name}\`
}
\`\`\`
- nested list item
- another item
</thinking>
外层正文不应该被吞掉。
`,
},
{
id: 'diff',
title: 'Diff 与代码流',
summary: '观察 diff code block、长文本和流式更新的稳定性。',
content: `# Diff Regression
下面这个样例更适合观察 **code block stream** 和折叠逻辑:
\`\`\`diff typescript
diff --git a/src/render.ts b/src/render.ts
index 0000000..1111111 100644
--- a/src/render.ts
+++ b/src/render.ts
@@ -1,7 +1,12 @@
-export function render(input) {
- return input
+export function render(input: string) {
+ if (!input.trim())
+ return 'empty'
+
+ const normalized = input.replace(/\\r\\n/g, '\\n')
+ return normalized
}
\`\`\`
再加一段普通代码,方便对比 Monaco / MarkdownCodeBlock / PreCodeNode:
\`\`\`tsx
export function TestHarness() {
return (
<section>
<h2>Regression</h2>
<p>Streaming should remain smooth.</p>
</section>
)
}
\`\`\`
`,
},
{
id: 'stress',
title: '结构压力',
summary: '列表、表格、引用、HTML 和长段落一起压一遍。',
content: `# Structural Stress
> 这个样例用于检查复杂结构在 streaming 中是否抖动、错位或丢节点。
## 列表
1. 第一层
- 第二层
- 第三层
2. 继续
## 表格
| Framework | Route | Purpose |
| --- | --- | --- |
| Vue 3 | \`/test\` | 主调试台 |
| Vue 2 | \`/test\` | 兼容回归 |
| React | \`/test\` | 跨框架对照 |
| Angular | \`/test\` | baseline 对照 |
## HTML
<details>
<summary>展开看一段 HTML</summary>
<p>如果这里的结构错了,通常说明 HTML block / inline 的边界处理有问题。</p>
</details>
## 长段落
Markstream 现在不仅要处理单次完整渲染,还要处理 AI 场景下不断追加的 markdown 内容,所以这个页面更像一个回归驾驶舱。你可以一边编辑左侧输入,一边切换 Vue 2、React 或 Angular 的 test page,用同一段内容观察差异,判断问题是解析层、组件层,还是框架适配层。
`,
},
] as const
@@ -0,0 +1,268 @@
import { deflateSync, inflateSync } from 'fflate'
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from 'lz-string'
export interface FrameworkTarget {
id: string
origin: string
localPort: number | null
}
export interface LocationLike {
hostname: string
protocol: string
}
export type TestPageViewMode = 'lab' | 'preview'
const FALLBACK_ORIGIN = 'https://markstream.local'
const RAW_PREFIX = 'raw:'
const DEFLATE_PREFIX = 'z:'
const BROTLI_PREFIX = 'br:'
const BASE64_CHUNK_SIZE = 0x8000
interface BrotliCompressModule {
compress: (input: Uint8Array, options?: { quality?: number }) => Promise<Uint8Array>
decompress: (input: Uint8Array) => Promise<Uint8Array>
default?: BrotliCompressModule
}
let brotliCompressPromise: Promise<BrotliCompressModule> | null = null
async function loadBrotliCompress() {
brotliCompressPromise ||= import('brotli-compress').then((module) => {
const loaded = module as BrotliCompressModule
return loaded.compress ? loaded : loaded.default!
})
return brotliCompressPromise
}
function encodeBase64Url(bytes: Uint8Array) {
let binary = ''
for (let index = 0; index < bytes.length; index += BASE64_CHUNK_SIZE)
binary += String.fromCharCode(...bytes.subarray(index, index + BASE64_CHUNK_SIZE))
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '')
}
function decodeBase64Url(payload: string) {
const normalized = payload
.replace(/-/g, '+')
.replace(/_/g, '/')
.padEnd(payload.length + ((4 - payload.length % 4) % 4), '=')
const binary = atob(normalized)
const bytes = new Uint8Array(binary.length)
for (let index = 0; index < binary.length; index++)
bytes[index] = binary.charCodeAt(index)
return bytes
}
function encodeDeflatedPayload(markdown: string) {
try {
const compressed = deflateSync(new TextEncoder().encode(markdown), { level: 9 })
return `${DEFLATE_PREFIX}${encodeBase64Url(compressed)}`
}
catch {
return ''
}
}
async function encodeBrotliPayload(markdown: string) {
try {
const brotli = await loadBrotliCompress()
const compressed = await brotli.compress(new TextEncoder().encode(markdown), { quality: 11 })
return `${BROTLI_PREFIX}${encodeBase64Url(compressed)}`
}
catch {
return ''
}
}
export function isLocalHost(hostname: string) {
return hostname === 'localhost' || hostname === '127.0.0.1'
}
export function encodeMarkdownPayload(markdown: string) {
if (!markdown)
return ''
const encodedRaw = encodeURIComponent(markdown)
const rawPayload = `${RAW_PREFIX}${encodedRaw}`
const legacyCompressed = compressToEncodedURIComponent(markdown)
const deflated = encodeDeflatedPayload(markdown)
const candidates = [rawPayload, legacyCompressed, deflated].filter(Boolean)
if (!candidates.length)
return ''
return candidates.reduce((shortest, candidate) => candidate.length < shortest.length ? candidate : shortest)
}
export async function encodeMarkdownPayloadAsync(markdown: string) {
if (!markdown)
return ''
const currentPayload = encodeMarkdownPayload(markdown)
const brotliPayload = await encodeBrotliPayload(markdown)
const candidates = [currentPayload, brotliPayload].filter(Boolean)
if (!candidates.length)
return ''
return candidates.reduce((shortest, candidate) => candidate.length < shortest.length ? candidate : shortest)
}
export function createMarkdownHash(markdown: string) {
const payload = encodeMarkdownPayload(markdown)
return payload ? `data=${payload}` : ''
}
export async function createMarkdownHashAsync(markdown: string) {
const payload = await encodeMarkdownPayloadAsync(markdown)
return payload ? `data=${payload}` : ''
}
export function decodeMarkdownHash(hash: string) {
const matched = (hash || '').match(/^#?data=([^&]+)/)
if (!matched?.[1])
return null
const payload = matched[1]
if (payload.startsWith(RAW_PREFIX)) {
try {
return decodeURIComponent(payload.slice(RAW_PREFIX.length))
}
catch {
return null
}
}
if (payload.startsWith(DEFLATE_PREFIX)) {
try {
return new TextDecoder().decode(inflateSync(decodeBase64Url(payload.slice(DEFLATE_PREFIX.length))))
}
catch {
return null
}
}
if (payload.startsWith(BROTLI_PREFIX))
return null
try {
return decompressFromEncodedURIComponent(payload) || null
}
catch {
return null
}
}
export async function decodeMarkdownHashAsync(hash: string) {
const matched = (hash || '').match(/^#?data=([^&]+)/)
if (!matched?.[1])
return null
const payload = matched[1]
if (!payload.startsWith(BROTLI_PREFIX))
return decodeMarkdownHash(hash)
try {
const brotli = await loadBrotliCompress()
return new TextDecoder().decode(await brotli.decompress(decodeBase64Url(payload.slice(BROTLI_PREFIX.length))))
}
catch {
return null
}
}
export function withMarkdownHash(baseUrl: string, markdown: string) {
const nextHash = createMarkdownHash(markdown)
if (!nextHash)
return baseUrl
const isAbsolute = /^[a-z]+:\/\//i.test(baseUrl)
const url = new URL(baseUrl, FALLBACK_ORIGIN)
url.hash = nextHash
if (isAbsolute)
return url.toString()
return `${url.pathname}${url.search}${url.hash}`
}
export async function withMarkdownHashAsync(baseUrl: string, markdown: string) {
const nextHash = await createMarkdownHashAsync(markdown)
if (!nextHash)
return baseUrl
const isAbsolute = /^[a-z]+:\/\//i.test(baseUrl)
const url = new URL(baseUrl, FALLBACK_ORIGIN)
url.hash = nextHash
if (isAbsolute)
return url.toString()
return `${url.pathname}${url.search}${url.hash}`
}
export function resolveTestPageViewMode(search: string): TestPageViewMode {
const params = new URLSearchParams(search)
return params.get('view') === 'preview' ? 'preview' : 'lab'
}
export function withTestPageViewMode(baseUrl: string, viewMode: TestPageViewMode = 'lab') {
const isAbsolute = /^[a-z]+:\/\//i.test(baseUrl)
const url = new URL(baseUrl, FALLBACK_ORIGIN)
if (viewMode === 'preview')
url.searchParams.set('view', 'preview')
else
url.searchParams.delete('view')
if (isAbsolute)
return url.toString()
return `${url.pathname}${url.search}${url.hash}`
}
export function buildTestPageHref(
baseUrl: string,
markdown: string,
viewMode: TestPageViewMode = 'lab',
) {
return withMarkdownHash(withTestPageViewMode(baseUrl, viewMode), markdown)
}
export async function buildTestPageHrefAsync(
baseUrl: string,
markdown: string,
viewMode: TestPageViewMode = 'lab',
) {
return withMarkdownHashAsync(withTestPageViewMode(baseUrl, viewMode), markdown)
}
export function resolveFrameworkTestHref(
framework: FrameworkTarget,
currentFrameworkId: string,
markdown: string,
locationLike?: LocationLike,
viewMode: TestPageViewMode = 'lab',
) {
let baseUrl = framework.id === currentFrameworkId
? '/test'
: `${framework.origin}/test`
if (
framework.id !== currentFrameworkId
&& locationLike
&& framework.localPort
&& isLocalHost(locationLike.hostname)
) {
baseUrl = `${locationLike.protocol}//${locationLike.hostname}:${framework.localPort}/test`
}
return buildTestPageHref(baseUrl, markdown, viewMode)
}
@@ -0,0 +1,91 @@
import { withMarkdownHash } from './testPageState'
export type SandboxFrameworkId = 'vue3' | 'vue2' | 'react18' | 'react19' | 'angular'
export type SandboxRenderSource = 'workspace' | 'npm'
export interface SandboxFrameworkTarget {
id: SandboxFrameworkId
label: string
packageName: string
defaultVersion: string
runtimeVersion: string
supportsWorkspace: boolean
}
export interface SandboxSelectionInput {
frameworkId?: string | null
source?: string | null
version?: string | null
}
export interface SandboxSelection {
framework: SandboxFrameworkTarget
frameworkId: SandboxFrameworkId
source: SandboxRenderSource
version: string
}
export function normalizeSandboxVersion(version: string | null | undefined, fallback: string) {
const trimmed = typeof version === 'string' ? version.trim() : ''
return trimmed || fallback
}
export function resolveSandboxFramework(
frameworks: ReadonlyArray<SandboxFrameworkTarget>,
frameworkId?: string | null,
) {
return frameworks.find(item => item.id === frameworkId) ?? frameworks[0]
}
export function normalizeSandboxSource(
framework: SandboxFrameworkTarget,
source?: string | null,
): SandboxRenderSource {
if (framework.supportsWorkspace && source === 'workspace')
return 'workspace'
return 'npm'
}
export function resolveSandboxSelection(
frameworks: ReadonlyArray<SandboxFrameworkTarget>,
input: SandboxSelectionInput = {},
): SandboxSelection {
const framework = resolveSandboxFramework(frameworks, input.frameworkId)
const source = normalizeSandboxSource(framework, input.source)
const version = normalizeSandboxVersion(input.version, framework.defaultVersion)
return {
framework,
frameworkId: framework.id,
source,
version,
}
}
export function parseSandboxSelection(
search: string,
frameworks: ReadonlyArray<SandboxFrameworkTarget>,
) {
const params = new URLSearchParams(search)
return resolveSandboxSelection(frameworks, {
frameworkId: params.get('framework'),
source: params.get('source'),
version: params.get('version'),
})
}
export function buildSandboxSearch(selection: SandboxSelection) {
const params = new URLSearchParams()
params.set('framework', selection.frameworkId)
params.set('source', selection.source)
params.set('version', selection.version)
return `?${params.toString()}`
}
export function buildTestSandboxHref(
selection: SandboxSelection,
markdown: string,
basePath = '/test-sandbox',
) {
return withMarkdownHash(`${basePath}${buildSandboxSearch(selection)}`, markdown)
}