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,420 @@
# Diff Integration Guide
This guide focuses on integrating the `stream-monaco` diff editor, which options to pass, and which TypeScript types are available out of the box.
## 1. Minimal setup
```ts
import { useMonaco } from 'stream-monaco'
const container = document.getElementById('diff')!
const { createDiffEditor, updateDiff, cleanupEditor } = useMonaco({
themes: ['github-dark', 'github-light'],
languages: ['typescript', 'javascript', 'json'],
readOnly: true,
MAX_HEIGHT: 560,
})
await createDiffEditor(
container,
'export const a = 1\n',
'export const a = 2\n',
'typescript',
)
updateDiff('export const a = 1\n', 'export const a = 3\n', 'typescript')
cleanupEditor()
```
The mount/update/unmount pattern is the same in Vue, React, Svelte, Solid, and vanilla JS:
- call `createDiffEditor(...)` on mount
- call `updateDiff(...)` / `updateOriginal(...)` / `updateModified(...)` when content changes
- call `cleanupEditor()` on unmount
## 2. Which options to pass
All options are passed through `useMonaco(options)`.
### Core options
```ts
const monaco = useMonaco({
themes: ['github-dark', 'github-light'],
languages: ['typescript', 'javascript'],
theme: 'github-light',
readOnly: true,
MAX_HEIGHT: 560,
})
```
Most common:
- `themes`: available themes, ideally one dark and one light
- `languages`: languages to register
- `theme`: initial theme
- `readOnly`: usually `true` for review/preview diffs
- `MAX_HEIGHT`: max editor height
### stream-monaco diff UX options
```ts
const monaco = useMonaco({
themes: ['github-dark', 'github-light'],
languages: ['typescript'],
readOnly: true,
diffHideUnchangedRegions: {
enabled: true,
contextLineCount: 3,
minimumLineCount: 3,
revealLineCount: 5,
},
diffLineStyle: 'background',
diffAppearance: 'auto',
diffUnchangedRegionStyle: 'line-info',
diffHunkActionsOnHover: true,
diffHunkHoverHideDelayMs: 160,
diffUpdateThrottleMs: 50,
revealDebounceMs: 75,
})
```
What they do:
- `diffHideUnchangedRegions`
Controls unchanged-region folding. Accepts `true`, `false`, or Monaco's native `hideUnchangedRegions` object.
- `diffLineStyle`
Visual emphasis for changed lines.
- `background`: richer filled blocks
- `bar`: subtler review-style leading bars
- `diffAppearance`
Controls diff chrome appearance.
- `auto`: infer from active Monaco theme
- `light`: force light shell
- `dark`: force dark shell
- `diffUnchangedRegionStyle`
Controls collapsed unchanged-region rendering.
- `line-info`: `71 unmodified lines` with line-number-width reveal buttons in a compact 32px collapsed row
- `line-info-basic`: `71 unmodified lines` with the wider legacy reveal rail in a compact 32px row
- `metadata`: `@@ -59,9 +59,11 @@` in a compact 32px row; lines below the collapsed region reflow to close the extra whitespace
- `simple`: gray placeholder bar in a tighter 28px row
- `diffHunkActionsOnHover`
Enables `Revert / Stage` on hovered hunks
- `diffHunkHoverHideDelayMs`
Hide delay for hunk hover actions
- `diffUpdateThrottleMs`
Diff streaming throttle. Default is `50`.
When `diffHideUnchangedRegions` is enabled, unchanged-region folding is deferred until streaming goes idle so the diff does not collapse while content is still streaming in.
- `revealDebounceMs`
Debounce for reveal/auto-scroll behavior
### Native Monaco diff options
`MonacoOptions` now includes both:
- `monaco.editor.IStandaloneEditorConstructionOptions`
- `monaco.editor.IDiffEditorConstructionOptions`
So you can also pass native Monaco diff options directly:
```ts
useMonaco({
renderSideBySide: true,
enableSplitViewResizing: true,
ignoreTrimWhitespace: false,
originalEditable: false,
})
```
To switch between side-by-side and inline at runtime:
```ts
const diff = monaco.getDiffEditorView()
diff?.updateOptions({ renderSideBySide: false })
```
## 3. Runtime methods
```ts
const {
createDiffEditor,
updateDiff,
updateOriginal,
updateModified,
appendOriginal,
appendModified,
setDiffModels,
setTheme,
refreshDiffPresentation,
getDiffEditorView,
getDiffModels,
getCode,
} = useMonaco()
```
Most useful diff methods:
- `createDiffEditor(container, original, modified, language)`
- `updateDiff(original, modified, language?)`
- `updateOriginal(code, language?)`
- `updateModified(code, language?)`
- `appendOriginal(text, language?)`
- `appendModified(text, language?)`
- `await setDiffModels({ original, modified }, options?)`
- `setTheme(theme, force?)`
- `refreshDiffPresentation()`
- `getDiffEditorView()`
- `getDiffModels()`
- `getCode()`
In diff mode, `getCode()` returns:
```ts
{
original: string
modified: string
}
```
### In-place theme / presentation switches
If you are only changing theme, appearance, line style, or unchanged-region style, prefer the in-place runtime methods instead of remounting the diff editor:
```ts
await monaco.setTheme('github-dark')
monaco.refreshDiffPresentation()
```
This keeps the existing diff shell, scroll position, and unchanged-region overlay alive while recomputing the presentation layer.
Theme and appearance switches should stay visually stable. When you switch between unchanged-region styles with different collapsed heights, such as `metadata` to `simple`, the shell stays mounted but Monaco will reflow the lines below those collapsed regions to match the new height.
### Whole-model swaps without remounting
If your app needs to replace the entire `original/modified` model pair, prefer `await setDiffModels(...)` over calling `diffEditor.setModel(...)` yourself:
```ts
const monacoApi = useMonaco()
const originalModel = monacoApi
.getMonacoInstance()
.editor.createModel(leftText, 'typescript')
const modifiedModel = monacoApi
.getMonacoInstance()
.editor.createModel(rightText, 'typescript')
await monacoApi.setDiffModels(
{
original: originalModel,
modified: modifiedModel,
},
{
codeLanguage: 'typescript',
},
)
```
Why this is better than raw `diffEditor.setModel(...)`:
- the library can precompute Monaco's diff view model before the visible swap, so same-content transitions avoid the first-frame scroll jump
- if the new models contain the same text, `stream-monaco` automatically takes the low-jitter path and preserves view state by default
- unchanged-region state and diff chrome are refreshed together
- internally-owned models are disposed safely, while externally-supplied models are left under your control
Options:
- `codeLanguage`: normalize both models to the target language before swapping
- `preserveViewState`: force preserving the diff view state even when content changed
## 4. Hunk action callback
If you want to intercept `Revert / Stage`, use `onDiffHunkAction`:
```ts
useMonaco({
diffHunkActionsOnHover: true,
onDiffHunkAction: async (ctx) => {
await saveHunk(ctx)
return false
},
})
```
Return semantics:
- `false`: skip built-in edit behavior, fully controlled by your app
- `true` or `undefined`: continue built-in behavior
The callback context type is `DiffHunkActionContext`.
## 5. Binding to real Git (`revert`, `stage`, `stash`)
`stream-monaco` does not talk to Git by default. The built-in hover actions only edit the in-memory Monaco models.
Important distinctions:
- Hover `Revert` is hunk-level undo. It is not `git revert <commit>`, because `git revert` works at commit granularity.
- Hover `Stage` is closest to `git add -p` / `git apply --cached`.
- If you want a stash workflow, treat `onDiffHunkAction` as a patch-intent callback and hand the selected half-hunk to a backend stash API or patch queue.
The practical pattern is:
1. Intercept `onDiffHunkAction`.
2. Send `action`, `side`, `lineChange`, `original`, and `modified` to your backend.
3. On the backend, compute the target file contents for that half-hunk.
4. Turn the current content -> target content delta into a unified patch for that file.
5. Apply it to Git:
- working tree revert: `git apply --recount -`
- index stage: `git apply --cached --recount -`
- custom stash: store the same patch in your own stash service, or wrap a server-side Git stash flow
6. Return the refreshed `original` / `modified` pair to the browser and call `updateDiff(...)` or `setDiffModels(...)`.
Frontend example:
```ts
const monaco = useMonaco({
diffHunkActionsOnHover: true,
onDiffHunkAction: async (ctx) => {
const response = await fetch('/api/git/hunks/apply', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
repoPath,
filePath,
action: ctx.action,
side: ctx.side,
range: ctx.lineChange,
original: ctx.originalModel.getValue(),
modified: ctx.modifiedModel.getValue(),
}),
})
const next = await response.json()
monaco.updateDiff(next.original, next.modified, language)
// Skip the built-in local-only model edit because Git already applied it.
return false
},
})
```
Common variant: backend returns the refreshed file pair, and the client updates the diff in-place with `updateDiff(...)`.
This is usually the simplest integration shape when your server already knows the post-apply file contents for both sides:
```ts
const monaco = useMonaco({
diffHunkActionsOnHover: true,
onDiffHunkAction: async (ctx) => {
const response = await fetch('/api/git/hunks/apply', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
action: ctx.action,
side: ctx.side,
range: ctx.lineChange,
original: ctx.originalModel.getValue(),
modified: ctx.modifiedModel.getValue(),
}),
})
const next = await response.json()
// The server returns the refreshed left/right file contents after Git apply.
monaco.updateDiff(next.original, next.modified, language)
return false
},
})
```
When to prefer this:
- your backend already computes the full refreshed `original` / `modified`
- you want to keep the current diff editor instance and just refresh its content
- you do not need to swap to externally-owned Monaco models
If the backend gives you brand-new Monaco models instead of plain strings, use `setDiffModels(...)` instead.
Backend mapping to keep in mind:
- `revert + lower`: remove the modified-side lines from the working tree
- `revert + upper`: restore the original-side lines into the working tree
- `stage + lower`: copy the modified-side lines into the index
- `stage + upper`: remove the original-side lines from the index
For replace hunks that have both upper and lower halves, do not think in terms of a single Git command name. Instead, compute the next desired file text for the selected half-hunk, build a patch for that exact delta, then apply it to the worktree or index.
If your product wants a real "stash this hunk" action, the safest route is usually a backend "saved patch" queue. `git stash push --patch` exists, but it is interactive and repo-scoped, so browser-driven review tools typically wrap it server-side or implement their own stash layer around patch files.
Reference validations in this repo:
- `pnpm run validate:diff-hunk-actions`: verifies the built-in local Revert/Stage semantics
- `pnpm run validate:diff-hunk-custom-flow`: verifies async custom intercept flow where user code mutates the models directly
- `pnpm run validate:diff-hunk-update-diff-flow`: verifies async custom intercept flow where the server returns refreshed file contents and the UI calls `updateDiff(...)`
## 6. TypeScript completeness
The public TS surface for diff integration is now fairly complete. These types are exported:
- `MonacoOptions`
- `UseMonacoReturn`
- `MonacoTheme`
- `MonacoLanguage`
- `MonacoEditorInstance`
- `MonacoDiffEditorInstance`
- `DiffHideUnchangedRegions`
- `DiffLineStyle`
- `DiffAppearance`
- `DiffUnchangedRegionStyle`
- `DiffModels`
- `DiffModelPair`
- `DiffModelTransitionOptions`
- `DiffCodeValue`
- `MonacoCodeValue`
- `DiffHunkActionContext`
### What is covered
- `useMonaco(options)` now has an explicit return type: `UseMonacoReturn`
- custom diff UX options have dedicated union types
- `MonacoOptions` covers both single-editor and native diff-editor construction options
- return values such as `getCode()` and `getDiffModels()` have named exported types
### Practical answer
If your question is "do I still need to write custom declarations for diff integration?", the answer is now basically no.
More directly:
- custom diff options: typed
- `useMonaco()` return value: typed
- native Monaco diff option pass-through: typed
## 6. Recommended typed setup
```ts
import type {
DiffAppearance,
DiffUnchangedRegionStyle,
MonacoOptions,
UseMonacoReturn,
} from 'stream-monaco'
const options: MonacoOptions = {
themes: ['github-dark', 'github-light'],
languages: ['typescript'],
readOnly: true,
diffAppearance: 'auto',
diffLineStyle: 'background',
diffUnchangedRegionStyle: 'line-info',
diffHideUnchangedRegions: true,
diffHunkActionsOnHover: true,
}
const monacoApi: UseMonacoReturn = useMonaco(options)
```
This gives the cleanest IDE autocomplete and keeps later UX upgrades easy to maintain.