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,17 @@
// shallow array equality (order-sensitive)
export function arraysEqual<T>(
a: readonly T[] | null | undefined,
b: readonly T[] | null | undefined,
) {
if (a === b)
return true
if (!a || !b)
return false
if (a.length !== b.length)
return false
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i])
return false
}
return true
}
@@ -0,0 +1,105 @@
import { log } from './logger'
export function createHeightManager(container: HTMLElement, computeNext: () => number | null) {
// Debug logging is enabled during development but can be controlled via
let raf: number | null = null
let debounceTimer: number | null = null
let lastApplied = -1
let suppressed = false
// larger hysteresis to reduce perceived jitter during incremental growth
const HYSTERESIS_PX = 12
// debounce delay to coalesce frequent height changes (ms)
const DEBOUNCE_MS = 0
function apply() {
const next = computeNext()
if (next == null) {
return
}
log('heightManager', 'computeNext ->', { next, lastApplied })
// Guard against invalid or non-positive heights which can collapse the
// container. Some downstream callers may compute values incorrectly
// during transient states; ignore those to keep the editor visible.
if (!Number.isFinite(next) || next <= 0) {
log('heightManager', 'invalid next height, ignoring', next)
return
}
const currentHeight
= Number.parseFloat(container.style.height || '')
|| container.getBoundingClientRect?.().height
|| 0
if (
currentHeight > 0
&& Math.abs(next - currentHeight) <= HYSTERESIS_PX
) {
lastApplied = next
return
}
if (lastApplied !== -1 && Math.abs(next - lastApplied) <= HYSTERESIS_PX) {
return
}
if (next === lastApplied) {
return
}
suppressed = true
container.style.height = `${next}px`
lastApplied = next
log('heightManager', 'applied height ->', next)
queueMicrotask(() => {
suppressed = false
})
}
function scheduleApply() {
// Cancel any pending debounce timer and schedule a new one that will
// requestAnimationFrame and run `apply`. This batches updates that occur
// within DEBOUNCE_MS to avoid rapid reflows.
if (debounceTimer != null) {
clearTimeout(debounceTimer)
debounceTimer = null
}
if (DEBOUNCE_MS === 0) {
if (raf != null) {
return
}
raf = requestAnimationFrame(() => {
raf = null
apply()
})
return
}
debounceTimer = (setTimeout(() => {
debounceTimer = null
if (raf != null) {
return
}
raf = requestAnimationFrame(() => {
raf = null
apply()
})
}, DEBOUNCE_MS) as unknown) as number
}
function update() {
scheduleApply()
}
function dispose() {
if (raf != null) {
cancelAnimationFrame(raf)
raf = null
}
if (debounceTimer != null) {
clearTimeout(debounceTimer)
debounceTimer = null
}
}
function isSuppressed() {
return suppressed
}
function getLastApplied() {
return lastApplied
}
return { update, dispose, isSuppressed, getLastApplied }
}
@@ -0,0 +1,29 @@
let seq = 0
// Centralized debug determination. Priority:
// 1. browser global `window.__STREAM_MONACO_DEBUG__` if defined
// Default: enable debug during local development
export const DEBUG: boolean = (() => {
if (typeof window !== 'undefined' && (window as any).__STREAM_MONACO_DEBUG__ !== undefined)
return Boolean((window as any).__STREAM_MONACO_DEBUG__)
return false
})()
export function log(tag: string, ...args: any[]) {
if (!DEBUG)
return
seq += 1
const id = `#${seq}`
const ts = (typeof performance !== 'undefined' && performance.now)
? (performance.now()).toFixed(1)
: Date.now()
// Use console.warn to comply with ESLint console restrictions.
console.warn(`${id} [${tag}] @${ts}ms`, ...args)
}
export function error(tag: string, ...args: any[]) {
if (!DEBUG)
return
console.error(`[${tag}]`, ...args)
}
@@ -0,0 +1,38 @@
export interface TimeSource {
requestAnimationFrame: (cb: FrameRequestCallback) => number
cancelAnimationFrame: (id: number) => void
}
/**
* Create a per-instance RAF scheduler that coalesces tasks by "kind" within
* the same instance. Instances are isolated so different editors won't
* cancel each other's scheduled work.
*/
export function createRafScheduler(timeSource?: TimeSource) {
const ids: Record<string, number | null> = {}
const ts: TimeSource = timeSource ?? {
requestAnimationFrame: (cb: FrameRequestCallback) => requestAnimationFrame(cb),
cancelAnimationFrame: (id: number) => cancelAnimationFrame(id),
}
function schedule(kind: string, cb: FrameRequestCallback) {
const existing = ids[kind]
if (existing != null) {
ts.cancelAnimationFrame(existing)
}
ids[kind] = ts.requestAnimationFrame((t) => {
ids[kind] = null
cb(t)
})
}
function cancel(kind: string) {
const id = ids[kind]
if (id != null) {
ts.cancelAnimationFrame(id)
ids[kind] = null
}
}
return { schedule, cancel }
}
@@ -0,0 +1,398 @@
import type { SpecialTheme, ThemeInput } from 'shiki'
import { shikiToMonaco } from '@shikijs/monaco'
import { createHighlighter } from 'shiki'
import * as monaco from '../monaco-shim'
import { arraysEqual } from './arraysEqual'
const LEGACY_ONIG_INIT_KEY = '__streamMonacoLegacyOnigurumaInit__'
const LEGACY_ENGINE_KEY = '__streamMonacoLegacyShikiEngine__'
const LEGACY_MONACO_LANGS_INIT_KEY = '__streamMonacoLegacyMonacoLangsInit__'
async function awaitLegacyOnigurumaInitIfPresent() {
try {
const p = (globalThis as any)?.[LEGACY_ONIG_INIT_KEY]
if (p && typeof p.then === 'function')
await p
}
catch {}
}
async function awaitLegacyMonacoLanguageContributionsIfPresent() {
try {
const p = (globalThis as any)?.[LEGACY_MONACO_LANGS_INIT_KEY]
if (p && typeof p.then === 'function')
await p
}
catch {}
}
async function getLegacyShikiEngineIfPresent() {
try {
const p = (globalThis as any)?.[LEGACY_ENGINE_KEY]
if (p && typeof p.then === 'function')
return await p
}
catch {}
return null
}
async function createHighlighterWithLegacyEngineIfNeeded(options: any) {
await awaitLegacyOnigurumaInitIfPresent()
await awaitLegacyMonacoLanguageContributionsIfPresent()
const engine = await getLegacyShikiEngineIfPresent()
if (engine)
return createHighlighter({ ...options, engine })
return createHighlighter(options)
}
let languagesRegistered = false
let currentLanguages: string[] = []
// promise that resolves to a shiki highlighter or null when registration completes
let themeRegisterPromise: Promise<import('../type').ShikiHighlighter | null> | null = null
// Serialize registrations to avoid races where multiple calls patch Monaco's
// global `editor.setTheme` with different highlighters in an interleaved order.
let registrationQueue: Promise<unknown> = Promise.resolve()
function enqueueRegistration<T>(task: () => Promise<T>): Promise<T> {
const next = registrationQueue.then(task, task)
// keep queue alive even if a task rejects
registrationQueue = next.then(() => undefined, () => undefined)
return next
}
export function getThemeRegisterPromise() {
return themeRegisterPromise
}
export function setThemeRegisterPromise(p: Promise<import('../type').ShikiHighlighter | null> | null) {
return themeRegisterPromise = p
}
interface HighlighterEntry {
// promise that resolves to a shiki highlighter
promise: Promise<any>
// set of languages this highlighter was created with
languages: Set<string>
}
const highlighterCache = new Map<string, HighlighterEntry>()
// Monaco integration is global (monaco.editor.setTheme is patched by shikiToMonaco).
// When multiple editors/code blocks initialize concurrently with different theme
// sets, repeatedly calling shikiToMonaco with different highlighters causes the
// last one to win, and themes not present in that last highlighter can throw.
//
// To avoid that, keep a single shared highlighter for Monaco and incrementally
// load themes/languages into it.
let monacoHighlighterPromise: Promise<import('../type').ShikiHighlighter> | null = null
let lastPatchedHighlighter: import('../type').ShikiHighlighter | null = null
let lastPatchedLanguages = new Set<string>()
let lastPatchedThemes = new Set<string>()
const monacoThemeByKey = new Map<string, ThemeInput | string | SpecialTheme>()
const monacoLanguageSet = new Set<string>()
function themeKey(t: ThemeInput | string | SpecialTheme) {
return typeof t === 'string' ? t : (t as any).name ?? JSON.stringify(t)
}
async function ensureMonacoHighlighter(
themes: (ThemeInput | string | SpecialTheme)[],
languages: string[],
) {
// track union first
for (const t of themes)
monacoThemeByKey.set(themeKey(t), t)
for (const l of languages)
monacoLanguageSet.add(l)
// create shared highlighter once
if (!monacoHighlighterPromise) {
const initialThemes = Array.from(monacoThemeByKey.values())
const initialLangs = Array.from(monacoLanguageSet.values())
monacoHighlighterPromise = createHighlighterWithLegacyEngineIfNeeded({ themes: initialThemes, langs: initialLangs })
.then(h => h)
}
const h = await monacoHighlighterPromise
// Incrementally load missing themes/langs when supported by shiki.
// If incremental loading isn't available, fall back to re-creating the shared
// highlighter with the full union (still safe because the union only grows).
const wantsThemes = Array.from(monacoThemeByKey.values())
const wantsLangs = Array.from(monacoLanguageSet.values())
const canLoadTheme = typeof (h as any).loadTheme === 'function'
const canLoadLanguage = typeof (h as any).loadLanguage === 'function'
if (canLoadTheme || canLoadLanguage) {
if (canLoadTheme) {
// loadTheme is idempotent in shiki; call only for newly requested theme keys
for (const t of themes) {
const k = themeKey(t)
// If we've already loaded it, skip.
// We can't reliably introspect loaded themes from shiki, so track keys.
if (!(h as any).__streamMonacoLoadedThemes)
(h as any).__streamMonacoLoadedThemes = new Set<string>()
const loaded: Set<string> = (h as any).__streamMonacoLoadedThemes
if (loaded.has(k))
continue
await (h as any).loadTheme(t)
loaded.add(k)
}
}
if (canLoadLanguage) {
for (const l of languages) {
if (!(h as any).__streamMonacoLoadedLangs)
(h as any).__streamMonacoLoadedLangs = new Set<string>()
const loaded: Set<string> = (h as any).__streamMonacoLoadedLangs
if (loaded.has(l))
continue
await (h as any).loadLanguage(l)
loaded.add(l)
}
}
return h
}
// fallback: recreate shared highlighter with union
const p = createHighlighterWithLegacyEngineIfNeeded({ themes: wantsThemes, langs: wantsLangs })
.then(hh => hh)
monacoHighlighterPromise = p
return p
}
/**
* Clear all cached shiki highlighters.
*
* Useful for long-running apps that dynamically create many theme combinations,
* or in tests to ensure a clean state. Call this when you know the highlighters
* are no longer needed (for example on app shutdown) to free memory.
*/
export function clearHighlighterCache() {
highlighterCache.clear()
monacoHighlighterPromise = null
lastPatchedHighlighter = null
lastPatchedLanguages = new Set<string>()
monacoThemeByKey.clear()
monacoLanguageSet.clear()
themeRegisterPromise = null
languagesRegistered = false
currentLanguages = []
}
/**
* Return number of entries currently in the highlighter cache.
* Helpful for tests and debugging.
*/
export function getHighlighterCacheSize() {
return highlighterCache.size
}
function serializeThemes(themes: (ThemeInput | string | SpecialTheme)[]) {
return JSON.stringify(
themes.map(t => typeof t === 'string' ? t : (t as any).name ?? JSON.stringify(t)).sort(),
)
}
async function getOrCreateHighlighter(
themes: (ThemeInput | string | SpecialTheme)[],
languages: string[],
): Promise<import('../type').ShikiHighlighter> {
const key = serializeThemes(themes)
const requestedSet = new Set(languages)
let existing = highlighterCache.get(key)
if (existing) {
// if existing entry already covers requested languages, reuse
let allIncluded = true
for (const l of requestedSet) {
if (!existing.languages.has(l)) {
allIncluded = false
break
}
}
if (allIncluded) {
return existing.promise
}
// double-check cache in case a concurrent request already replaced/expanded the entry
const prev = existing
const current = highlighterCache.get(key)
if (current && current !== prev) {
// if the current cached entry already covers requested languages, reuse it
let allIncludedCurrent = true
for (const l of requestedSet) {
if (!current.languages.has(l)) {
allIncludedCurrent = false
break
}
}
if (allIncludedCurrent) {
return current.promise
}
// otherwise prefer the most recent cached entry for the union creation
existing = current
}
// otherwise create a new highlighter with the union of languages
const union = new Set<string>([...existing.languages, ...requestedSet])
const langsArray = Array.from(union)
const p = createHighlighterWithLegacyEngineIfNeeded({ themes, langs: langsArray })
const newEntry: HighlighterEntry = { promise: p, languages: union }
highlighterCache.set(key, newEntry)
// if creation fails, try to restore previous entry (prev)
p.catch(() => {
if (highlighterCache.get(key) === newEntry && prev) {
highlighterCache.set(key, prev)
}
})
return p
}
// no cached entry, create and cache
const p = createHighlighterWithLegacyEngineIfNeeded({ themes, langs: Array.from(requestedSet) })
const entry: HighlighterEntry = { promise: p, languages: requestedSet }
highlighterCache.set(key, entry)
p.catch(() => {
if (highlighterCache.get(key) === entry) {
highlighterCache.delete(key)
}
})
return p
}
// Exported for callers that need direct access to the shiki highlighter
export { getOrCreateHighlighter }
/**
* Update the theme used by the shiki highlighter for a given themes+languages
* combination. Useful when Monaco themes are already registered (so switching
* Monaco only requires `monaco.editor.setTheme`) but you also want shiki's
* standalone renderer to use the new theme without recreating everything.
*/
// NOTE: setHighlighterTheme removed — switching Monaco theme via
// `monaco.editor.setTheme(themeName)` is sufficient for editor theme changes.
// If consumers need to directly control a shiki highlighter they can use
// `getOrCreateHighlighter(...)` and call methods on the returned object.
export async function registerMonacoThemes(
themes: (ThemeInput | string | SpecialTheme)[],
languages: string[],
): Promise<import('../type').ShikiHighlighter | null> {
return enqueueRegistration(async () => {
registerMonacoLanguages(languages)
const p = (async () => {
const highlighter = await ensureMonacoHighlighter(themes, languages)
// Patch Monaco when:
// - the shared highlighter instance changes, OR
// - new languages were added (incremental load) and need tokens providers.
//
// `shikiToMonaco()` installs tokens providers per language at call-time.
// When we incrementally load languages into the shared highlighter, we
// must re-run `shikiToMonaco()` so Monaco receives providers for the newly
// loaded languages.
const wantsLangs = Array.from(monacoLanguageSet.values())
const loadedThemeIds = typeof highlighter.getLoadedThemes === 'function'
? highlighter.getLoadedThemes()
: []
const needsThemePatch = loadedThemeIds.some((t: string) => !lastPatchedThemes.has(t))
const needsLanguagePatch = lastPatchedHighlighter !== highlighter
|| wantsLangs.some(l => !lastPatchedLanguages.has(l))
|| needsThemePatch
if (needsLanguagePatch) {
if (lastPatchedHighlighter !== highlighter)
lastPatchedLanguages = new Set<string>()
// In some bundlers (notably Webpack 4), Shiki/TextMate tokenization can
// still throw at runtime (e.g. `null.compileAG`) due to regex engine
// initialization quirks. Monaco reports these as "unexpected errors"
// and they crash the app.
//
// We cannot monkey-patch `import * as monaco` namespace exports (ESM
// namespace objects are immutable), so pass a proxy object into
// `shikiToMonaco()` that wraps `setTokensProvider` instead.
const realLanguages: any = (monaco as any).languages
const realEditor: any = (monaco as any).editor
const setTokensProvider = typeof realLanguages?.setTokensProvider === 'function'
? realLanguages.setTokensProvider.bind(realLanguages)
: null
const getLanguages = typeof realLanguages?.getLanguages === 'function'
? realLanguages.getLanguages.bind(realLanguages)
: null
const monacoProxy: any = {
// Forward everything else (Range, Uri, etc.) via prototype lookup.
__proto__: monaco as any,
editor: realEditor,
languages: {
__proto__: realLanguages,
getLanguages,
setTokensProvider(lang: string, provider: any) {
if (provider && typeof provider.tokenize === 'function') {
const originalTokenize = provider.tokenize.bind(provider)
provider = {
...provider,
tokenize(line: string, state: any) {
try {
return originalTokenize(line, state)
}
catch {
return {
endState: state,
tokens: [{ startIndex: 0, scopes: '' }],
}
}
},
}
}
return setTokensProvider?.(lang, provider)
},
},
}
shikiToMonaco(highlighter, monacoProxy)
lastPatchedHighlighter = highlighter
lastPatchedLanguages = new Set(wantsLangs)
lastPatchedThemes = new Set(loadedThemeIds)
}
// Track last language set for Monaco language registration short-circuit.
currentLanguages = languages.slice()
return highlighter
})()
setThemeRegisterPromise(p)
try {
const res = await p
return res
}
catch (e) {
setThemeRegisterPromise(null)
throw e
}
})
}
function registerMonacoLanguages(languages: string[]) {
if (languagesRegistered && arraysEqual(languages, currentLanguages)) {
return
}
const existing = new Set(monaco.languages.getLanguages().map(l => l.id))
for (const lang of languages) {
if (!existing.has(lang)) {
try {
monaco.languages.register({ id: lang })
}
catch {
// ignore unsupported ids
}
}
}
languagesRegistered = true
currentLanguages = languages
}
@@ -0,0 +1,145 @@
import { log } from './logger'
export function createScrollWatcherForEditor(
ed: any,
opts: {
onPause: () => void
onMaybeResume: () => void
getLast: () => number
setLast: (v: number) => void
},
) {
const initial = ed.getScrollTop?.() ?? 0
opts.setLast(initial)
log('scrollWatcher', 'initial scrollTop=', initial)
let suppressedExternally = false
const THRESHOLD_PX = 6
let domNode: HTMLElement | null = null
let interactionListener: ((e: Event) => void) | null = null
const listener = (e: any) => {
if (suppressedExternally) {
log('scrollWatcher', 'suppressedExternally, ignoring event')
return
}
const currentTop = e && typeof e.scrollTop === 'number' ? e.scrollTop : ed.getScrollTop?.() ?? 0
const delta = currentTop - opts.getLast()
// update last seen position immediately
opts.setLast(currentTop)
// ignore very small scroll deltas (usually from layout/height adjustments)
if (Math.abs(delta) < THRESHOLD_PX) {
log('scrollWatcher', 'small delta ignored', delta)
// If the delta is small, check whether the viewport is effectively
// at the bottom of the scrollable area. In that case we should treat
// this as a potential resume so streaming auto-scroll can re-enable.
try {
const scrollHeight = typeof ed.getScrollHeight === 'function' ? ed.getScrollHeight() : undefined
const li = typeof (ed as any).getLayoutInfo === 'function' ? (ed as any).getLayoutInfo() : undefined
const viewportH = li?.height ?? undefined
if (typeof scrollHeight === 'number' && typeof viewportH === 'number') {
const distance = scrollHeight - (currentTop + viewportH)
if (distance <= Math.max(THRESHOLD_PX, 0)) {
log('scrollWatcher', 'small delta but at bottom, maybe resume', { distance })
opts.onMaybeResume()
}
}
}
catch { }
return
}
log('scrollWatcher', 'delta=', delta, 'currentTop=', currentTop)
if (delta < 0) {
log('scrollWatcher', 'pause detected delta=', delta)
opts.onPause()
return
}
log('scrollWatcher', 'maybe resume delta=', delta)
opts.onMaybeResume()
}
const disp = ed.onDidScrollChange?.(listener) ?? null
// Return an object that provides dispose and suppression control. We keep
// the dispose method for compatibility with monaco.IDisposable.
const api: any = {
dispose() {
try {
if (disp && typeof disp.dispose === 'function')
disp.dispose()
else if (typeof disp === 'function')
disp()
}
catch { }
log('scrollWatcher', 'dispose')
},
setSuppressed(v: boolean) {
const newVal = !!v
if (newVal === suppressedExternally)
return
suppressedExternally = newVal
log('scrollWatcher', 'setSuppressed =>', suppressedExternally)
// When suppression is enabled, attach lightweight user-interaction
// listeners so real user input (wheel/pointer/touch) can immediately
// cancel suppression and mark the watcher as paused. This prevents
// programmatic suppression from blocking the user's attempt to scroll up.
try {
// Try to get the editor DOM node if available
if (!domNode && typeof (ed as any).getDomNode === 'function') {
domNode = (ed as any).getDomNode() as HTMLElement | null
}
if (suppressedExternally && domNode) {
if (!interactionListener) {
interactionListener = () => {
try {
log('scrollWatcher', 'user interaction detected while suppressed, cancelling suppression')
// Treat this as a pause (user took control)
opts.onPause()
// clear suppression so subsequent scroll events are observed
suppressedExternally = false
// update last known position to current scrollTop
const cur = ed.getScrollTop?.() ?? 0
opts.setLast(cur)
// If the user interaction already scrolled to bottom, treat as resume
try {
const scrollHeight = typeof (ed as any).getScrollHeight === 'function' ? (ed as any).getScrollHeight() : undefined
const li = typeof (ed as any).getLayoutInfo === 'function' ? (ed as any).getLayoutInfo() : undefined
const viewportH = li?.height ?? undefined
if (typeof scrollHeight === 'number' && typeof viewportH === 'number') {
const distance = scrollHeight - (cur + viewportH)
if (distance <= Math.max(THRESHOLD_PX, 0)) {
log('scrollWatcher', 'interaction moved to bottom, maybe resume', { distance })
opts.onMaybeResume()
}
}
}
catch { }
// remove listeners
if (domNode && interactionListener) {
domNode.removeEventListener('wheel', interactionListener, { passive: true } as any)
domNode.removeEventListener('pointerdown', interactionListener as any)
domNode.removeEventListener('touchstart', interactionListener as any)
}
interactionListener = null
}
catch { }
}
domNode.addEventListener('wheel', interactionListener, { passive: true } as any)
domNode.addEventListener('pointerdown', interactionListener as any)
domNode.addEventListener('touchstart', interactionListener as any)
}
}
else {
if (domNode && interactionListener) {
domNode.removeEventListener('wheel', interactionListener, { passive: true } as any)
domNode.removeEventListener('pointerdown', interactionListener as any)
domNode.removeEventListener('touchstart', interactionListener as any)
interactionListener = null
}
}
}
catch { }
},
}
return api
}