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,464 @@
import type { PreparedText } from '@chenglou/pretext'
import type { ParsedNode } from 'stream-markdown-parser'
import { layout, prepare } from '@chenglou/pretext'
import { shallowRef } from 'vue'
type WhiteSpaceMode = 'normal' | 'pre-wrap'
type EstimateKind = 'simple-text' | 'code-block'
type CodeRendererKind = 'monaco' | 'markdown' | 'pre'
export interface HeightEstimationExperimentConfig {
enabled?: boolean
textEstimation?: boolean
codeBlockEstimation?: boolean
restore?: boolean
diagnostics?: boolean
}
export interface RestoreAnchor {
nodeIndex: number
offsetWithinNodePx: number
}
export interface RendererExperimentNodeRecord {
index: number
type: string
estimateKind: EstimateKind | null
rendererKind: CodeRendererKind | null
estimatedHeight: number | null
estimatedContentHeight: number | null
measuredHeight: number | null
}
export interface RendererExperimentReport {
totalNodes: number
measuredCount: number
estimatedCount: number
averageNodeHeight: number
topSpacerHeight: number
bottomSpacerHeight: number
estimatedTotalHeight: number
width: number
probe: {
paragraphReady: boolean
listItemReady: boolean
listWrapperOverhead: number
headingReadyLevels: number[]
}
nodes: RendererExperimentNodeRecord[]
}
export interface HeightEstimationRendererController {
captureRestoreAnchor: () => RestoreAnchor | null
restoreAnchor: (anchor: RestoreAnchor) => void
getAnchorDrift: (anchor: RestoreAnchor) => number | null
getReport: () => RendererExperimentReport
}
export interface BlockTextProfile {
font: string
lineHeight: number
wrapperOverhead: number
widthAdjustment: number
whiteSpace?: WhiteSpaceMode
}
export interface SimpleTextProbeProfile {
paragraph: BlockTextProfile | null
listItem: BlockTextProfile | null
listWrapperOverhead: number
headings: Record<number, BlockTextProfile | null>
}
export interface EstimatedNodeHeight {
kind: EstimateKind
height: number
contentHeight?: number
rendererKind?: CodeRendererKind
}
export interface CodeBlockEstimateOptions {
rendererKind: CodeRendererKind
monacoOptions?: Record<string, any> | null | undefined
showHeader?: boolean
}
const GLOBAL_STORE_KEY = '__MARKSTREAM_VUE_HEIGHT_ESTIMATION_EXPERIMENT__'
const PREPARED_CACHE_LIMIT = 240
const BLOCK_ESTIMATE_CACHE_LIMIT = 4000
const CODE_BLOCK_HEADER_HEIGHT = 40
const CODE_BLOCK_DEFAULT_CAP = 500
const MARKDOWN_CODE_LINE_HEIGHT = 21
const MARKDOWN_CODE_VERTICAL_PADDING = 32
const MONACO_LINE_EXTRA = 1.5
interface PreparedCacheEntry {
prepared: PreparedText
}
interface BlockEstimateCacheEntry {
height: number
contentHeight: number
}
interface HeightEstimationExperimentStore {
configs: Record<string, HeightEstimationExperimentConfig>
controllers: Record<string, HeightEstimationRendererController | undefined>
revision: ReturnType<typeof shallowRef<number>>
preparedCache: Map<string, PreparedCacheEntry>
blockEstimateCache: Map<string, BlockEstimateCacheEntry>
}
const store: HeightEstimationExperimentStore = (() => {
const g = globalThis as any
if (g[GLOBAL_STORE_KEY])
return g[GLOBAL_STORE_KEY] as HeightEstimationExperimentStore
const next: HeightEstimationExperimentStore = {
configs: {},
controllers: {},
revision: shallowRef(0),
preparedCache: new Map(),
blockEstimateCache: new Map(),
}
g[GLOBAL_STORE_KEY] = next
return next
})()
let canPrepareTextSupport: boolean | null = null
export const heightEstimationExperimentRevision = store.revision
export function setHeightEstimationExperiment(customId: string, config: HeightEstimationExperimentConfig) {
if (!customId)
return
store.configs[customId] = { ...config }
store.revision.value++
}
export function clearHeightEstimationExperiment(customId: string) {
if (!customId)
return
delete store.configs[customId]
delete store.controllers[customId]
store.revision.value++
}
export function resetHeightEstimationExperimentCaches() {
store.preparedCache.clear()
store.blockEstimateCache.clear()
canPrepareTextSupport = null
}
export function getHeightEstimationExperiment(customId?: string | null) {
if (!customId)
return null
return store.configs[customId] ?? null
}
export function registerHeightEstimationRendererController(customId: string, controller: HeightEstimationRendererController) {
if (!customId)
return () => {}
store.controllers[customId] = controller
return () => {
if (store.controllers[customId] === controller)
delete store.controllers[customId]
}
}
export function getHeightEstimationRendererController(customId?: string | null) {
if (!customId)
return null
return store.controllers[customId] ?? null
}
function canPrepareText() {
if (canPrepareTextSupport != null)
return canPrepareTextSupport
if (typeof document === 'undefined')
return false
try {
const canvas = document.createElement('canvas')
canPrepareTextSupport = !!canvas.getContext?.('2d')
return canPrepareTextSupport
}
catch {
canPrepareTextSupport = false
return false
}
}
function parsePositiveNumber(raw: string | null | undefined, fallback: number) {
const value = Number.parseFloat(String(raw ?? ''))
return Number.isFinite(value) && value > 0 ? value : fallback
}
function getPreparedText(text: string, font: string, whiteSpace: WhiteSpaceMode) {
const key = `${whiteSpace}\u0000${font}\u0000${text}`
const cached = store.preparedCache.get(key)
if (cached) {
store.preparedCache.delete(key)
store.preparedCache.set(key, cached)
return cached.prepared
}
const prepared = prepare(text, font, { whiteSpace })
store.preparedCache.set(key, { prepared })
while (store.preparedCache.size > PREPARED_CACHE_LIMIT) {
const oldestKey = store.preparedCache.keys().next().value
if (!oldestKey)
break
store.preparedCache.delete(oldestKey)
}
return prepared
}
function isTextLikeInlineNode(node: any) {
return node?.type === 'text' || node?.type === 'emoji' || node?.type === 'hardbreak'
}
function flattenSimpleInlineChildren(children: any[] | undefined): string | null {
if (!Array.isArray(children) || children.length === 0)
return null
let text = ''
for (const child of children) {
if (!isTextLikeInlineNode(child))
return null
if (child.type === 'text')
text += String(child.content ?? '')
else if (child.type === 'emoji')
text += String(child.name ?? child.raw ?? '')
else if (child.type === 'hardbreak')
text += '\n'
}
return text.length > 0 ? text : null
}
function estimateBlockTextHeight(text: string, width: number, profile: BlockTextProfile) {
if (!text || !Number.isFinite(width) || width <= 0 || !canPrepareText())
return null
try {
const roundedWidth = Math.round(width * 100) / 100
const cacheKey = [
profile.whiteSpace ?? 'pre-wrap',
profile.font,
profile.lineHeight,
profile.wrapperOverhead,
profile.widthAdjustment,
roundedWidth,
text,
].join('\u0000')
const cached = store.blockEstimateCache.get(cacheKey)
if (cached) {
store.blockEstimateCache.delete(cacheKey)
store.blockEstimateCache.set(cacheKey, cached)
return {
kind: 'simple-text' as const,
height: cached.height,
contentHeight: cached.contentHeight,
}
}
const whiteSpace = profile.whiteSpace ?? 'pre-wrap'
const prepared = getPreparedText(text, profile.font, whiteSpace)
const layoutWidth = Math.max(24, roundedWidth - profile.widthAdjustment)
const result = layout(prepared, layoutWidth, profile.lineHeight)
const contentHeight = Math.max(profile.lineHeight, result.height)
const height = Math.max(profile.lineHeight, Math.round(contentHeight + profile.wrapperOverhead))
store.blockEstimateCache.set(cacheKey, {
height,
contentHeight: Math.round(contentHeight),
})
while (store.blockEstimateCache.size > BLOCK_ESTIMATE_CACHE_LIMIT) {
const oldestKey = store.blockEstimateCache.keys().next().value
if (!oldestKey)
break
store.blockEstimateCache.delete(oldestKey)
}
return {
kind: 'simple-text' as const,
height,
contentHeight: Math.round(contentHeight),
}
}
catch {
return null
}
}
export function estimateSimpleTextBlockHeight(
node: ParsedNode,
width: number,
profile: SimpleTextProbeProfile | null | undefined,
): EstimatedNodeHeight | null {
if (!profile || !node || !Number.isFinite(width) || width <= 0)
return null
if (node.type === 'paragraph') {
const text = flattenSimpleInlineChildren((node as any).children)
if (!text || !profile.paragraph)
return null
return estimateBlockTextHeight(text, width, profile.paragraph)
}
if (node.type === 'heading') {
const level = Number((node as any).level || 0)
const text = flattenSimpleInlineChildren((node as any).children)
const headingProfile = profile.headings[level]
if (!text || !headingProfile)
return null
return estimateBlockTextHeight(text, width, headingProfile)
}
if (node.type === 'list_item') {
const children = Array.isArray((node as any).children) ? (node as any).children : []
if (children.length !== 1 || children[0]?.type !== 'paragraph' || !profile.listItem)
return null
const text = flattenSimpleInlineChildren(children[0]?.children)
if (!text)
return null
return estimateBlockTextHeight(text, width, profile.listItem)
}
if (node.type === 'list') {
const items = Array.isArray((node as any).items) ? (node as any).items : []
if (!items.length)
return null
let total = Math.max(0, profile.listWrapperOverhead)
for (const item of items) {
const estimatedItem = estimateSimpleTextBlockHeight(item as ParsedNode, width, profile)
if (!estimatedItem)
return null
total += estimatedItem.height
}
return {
kind: 'simple-text',
height: Math.max(1, Math.round(total)),
contentHeight: Math.max(1, Math.round(total)),
}
}
return null
}
function countCodeLines(source: string) {
if (!source)
return 1
const lines = String(source).split(/\r?\n/)
return Math.max(1, lines.length)
}
function getCodeBlockVisibleLineCount(node: ParsedNode) {
if ((node as any).diff) {
return Math.max(
countCodeLines(String((node as any).originalCode ?? '')),
countCodeLines(String((node as any).updatedCode ?? '')),
countCodeLines(String((node as any).code ?? '')),
)
}
return countCodeLines(String((node as any).code ?? ''))
}
function resolveMonacoLineHeight(monacoOptions: Record<string, any> | null | undefined, isDiff: boolean) {
const fontSize = typeof monacoOptions?.fontSize === 'number' && monacoOptions.fontSize > 0
? monacoOptions.fontSize
: (isDiff ? 13 : 12)
if (typeof monacoOptions?.lineHeight === 'number' && monacoOptions.lineHeight > 0)
return monacoOptions.lineHeight
return isDiff ? 30 : Math.round(fontSize * 1.5)
}
function resolveMonacoVerticalPadding(monacoOptions: Record<string, any> | null | undefined, isDiff: boolean) {
if (!isDiff)
return 0
const top = typeof monacoOptions?.padding?.top === 'number' ? monacoOptions.padding.top : 10
const bottom = typeof monacoOptions?.padding?.bottom === 'number' ? monacoOptions.padding.bottom : 22
return Math.max(0, top) + Math.max(0, bottom)
}
export function estimateCodeBlockHeight(
node: ParsedNode,
options: CodeBlockEstimateOptions,
): EstimatedNodeHeight | null {
if (!node || node.type !== 'code_block')
return null
const rendererKind = options.rendererKind
const showHeader = options.showHeader !== false
const lineCount = getCodeBlockVisibleLineCount(node)
const isDiff = Boolean((node as any).diff)
let contentHeight = 0
let cap = CODE_BLOCK_DEFAULT_CAP
if (rendererKind === 'monaco') {
const monacoOptions = options.monacoOptions ?? {}
const lineHeight = resolveMonacoLineHeight(monacoOptions, isDiff)
const verticalPadding = resolveMonacoVerticalPadding(monacoOptions, isDiff)
cap = typeof monacoOptions.MAX_HEIGHT === 'number' && monacoOptions.MAX_HEIGHT > 0
? monacoOptions.MAX_HEIGHT
: CODE_BLOCK_DEFAULT_CAP
contentHeight = isDiff
? Math.round(lineCount * lineHeight + verticalPadding)
: Math.round(lineCount * (lineHeight + MONACO_LINE_EXTRA))
}
else if (rendererKind === 'markdown') {
contentHeight = Math.round(lineCount * MARKDOWN_CODE_LINE_HEIGHT + MARKDOWN_CODE_VERTICAL_PADDING)
}
else {
contentHeight = Math.round(lineCount * MARKDOWN_CODE_LINE_HEIGHT + MARKDOWN_CODE_VERTICAL_PADDING)
cap = Number.POSITIVE_INFINITY
}
const visibleContentHeight = Math.max(1, Math.min(contentHeight, cap))
return {
kind: 'code-block',
height: Math.round(visibleContentHeight + (showHeader ? CODE_BLOCK_HEADER_HEIGHT : 0)),
contentHeight: visibleContentHeight,
rendererKind,
}
}
export function buildPretextFontFromComputedStyle(style: CSSStyleDeclaration | null | undefined) {
if (!style)
return ''
const fontStyle = style.fontStyle || 'normal'
const fontWeight = style.fontWeight || '400'
const fontSize = style.fontSize || '16px'
const fontFamily = style.fontFamily || 'sans-serif'
return `${fontStyle} ${fontWeight} ${fontSize} ${fontFamily}`
}
export function buildBlockTextProfile(
wrapper: HTMLElement | null | undefined,
textEl: HTMLElement | null | undefined,
whiteSpace: WhiteSpaceMode = 'pre-wrap',
): BlockTextProfile | null {
if (!wrapper || !textEl || typeof window === 'undefined')
return null
const style = window.getComputedStyle(textEl)
const wrapperHeight = wrapper.offsetHeight
const lineHeight = parsePositiveNumber(style.lineHeight, parsePositiveNumber(style.fontSize, 16) * 1.5)
const wrapperWidth = wrapper.getBoundingClientRect().width
const textWidth = textEl.getBoundingClientRect().width
return {
font: buildPretextFontFromComputedStyle(style),
lineHeight,
wrapperOverhead: Math.max(0, wrapperHeight - lineHeight),
widthAdjustment: Math.max(0, wrapperWidth - textWidth),
whiteSpace,
}
}
export function createEmptySimpleTextProbeProfile(): SimpleTextProbeProfile {
return {
paragraph: null,
listItem: null,
listWrapperOverhead: 0,
headings: {
1: null,
2: null,
3: null,
4: null,
5: null,
6: null,
},
}
}