Skip to content

Commit bdb6ad5

Browse files
committed
Merge origin/main into refactor/topview-retirement
- Settings window removal (#16489): drop SettingsApp's host mount; the settings page now renders inside the main window, already host-covered. - Composer focus-restore (#16733): reimplement over the popup track — add ConfirmPopupProps.focusOnClose (Radix onCloseAutoFocus + preventDefault) and route WebSearchButton through it, replacing the afterClose + requestAnimationFrame focus race. - Re-apply the window.toast/window.modal -> toast/popup codemod onto upstream's moved/renamed files (Appearance/Dependencies/System settings, MCP, paintings, knowledge) and SystemSettings' new call sites.
2 parents bec1001 + 1c1278a commit bdb6ad5

278 files changed

Lines changed: 10917 additions & 5006 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DESIGN.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -604,15 +604,14 @@ Source: `Switch` and `DescriptionSwitch` from `@cherrystudio/ui` (`packages/ui/s
604604
- **Top chrome height**: `var(--app-top-chrome-height)` = 44px. Use this for the main window tab bar and any standalone macOS window top drag area that should visually align with the main app chrome.
605605
- **Navbar content height**: `var(--navbar-height)` defaults to `var(--app-top-chrome-height)` for the fixed top-menu layout. Only override it for inner content calculations that intentionally do not include a top navbar.
606606
- Settings-style floating windows with a transparent macOS shell must keep the outer top inset tied to `var(--app-top-chrome-height)` instead of hard-coded pixel classes such as `h-11` or `h-[50px]`.
607-
- **Settings window sizing** (standalone settings window only): sized to 80% of the main window with a hard floor of 760×560 and a 1280px max width, centered on the main window. The 760×560 floor keeps the ~200px sidebar plus the detail column usable when the user shrinks the main window; the 1280px ceiling prevents 2K/4K displays from stretching settings into empty space. Canonical implementation: `SettingsWindowService` in `src/main/services/SettingsWindowService.ts`.
608607

609608
### Settings Panel Layout
610609

611-
Settings pages (both the in-app `/settings` route and the standalone settings window) share the same two-column shape:
610+
Settings pages use the same two-column shape:
612611

613612
| Column | Width | Composition |
614613
|---|---|---|
615-
| Left submenu | `var(--settings-width)` (200px in the standalone window, 250px default in `responsive.css`) | `PageHeader` (title) → `Scrollbar``MenuList` of grouped `MenuItem` rows |
614+
| Left submenu | `var(--settings-width)` (250px default in `responsive.css`) | `PageHeader` (title) → `Scrollbar``MenuList` of grouped `MenuItem` rows |
616615
| Right detail | `flex-1` | Page-owned content |
617616

618617
Submenu composition rules:

docs/references/command/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,7 @@ keeps only `ShortcutPreferenceKey` + `ResolvedShortcut`.
102102
| `menus.tsx` | `CommandContextMenu` — renders Cherry UI or a native popup based on `menu.presentation_mode` |
103103

104104
Mount `<ContextKeyProvider><CommandProvider>` once per renderer window — every
105-
window root mounts it: `windows/main/MainApp.tsx`,
106-
`windows/settings/SettingsApp.tsx`, and `windows/subWindow/SubWindowApp.tsx`.
105+
window root mounts it: `windows/main/MainApp.tsx` and `windows/subWindow/SubWindowApp.tsx`.
107106

108107
### Preferences
109108

docs/references/knowledge/experiment/knowledge-technical-design.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,7 @@ A vector base's `knowledge_base.embeddingModelId` / `dimensions` must be valid t
157157

158158
### 6.1 search() wiring and retrieval tuning
159159

160-
`searchMode` / `hybridAlpha` / `documentCount` / `threshold` are all **base-level configuration** (`knowledge_base` columns) for now; `search()` reads them from the base row (result cap `documentCount ?? 10`).
161-
162-
> **Decision note (2026-06-10)**: `hybridAlpha` describes whether a base's corpus leans lexical or semantic — a stable property of the base, not something the model should guess per call — so it stays a base column with the RagConfig slider (configurable only in hybrid mode; cleared when `searchMode` moves away). `threshold` only applies to relevance-scored hits (vector mode, or after rerank) and is a no-op for BM25/RRF ranking scores (`applyRelevanceThreshold` in `utils/search.ts`). Researched and decided, but **deferred to a later PR**: `topK` / `threshold` become per-call knobs (`KnowledgeSearchOptions`, exposed through `kb__search` arguments and REST `top_k`), and the `documentCount` column is removed with them. That refactor was implemented during PR A's development and then deliberately carved out to keep PR A reviewable; it will be re-done on top of the merged PR A in the per-call-tuning PR — the paragraph above records the agreed design so nothing depends on any developer-local state.
160+
`documentCount` and `threshold` are the base-level retrieval tuning columns left; `search()` reads `documentCount` from the base row as the result cap (`documentCount ?? 10`) and applies `threshold` as a relevance cutoff on scored hits (`applyRelevanceThreshold` in `utils/search.ts` — it filters vector-mode or post-rerank relevance scores and is a no-op for BM25/RRF ranking scores). Retrieval mode is derived at runtime: bases with a completed vector config use hybrid retrieval, and bases without an embedding model fall back to BM25.
163161

164162
### 6.2 Legacy result shape mapping
165163

@@ -185,5 +183,5 @@ A vector base's `knowledge_base.embeddingModelId` / `dimensions` must be valid t
185183
- Startup-recovery cross-cancellation: a crash-recovered delete-subtree job and the `recoverDeletingItems` re-enqueue get different idempotency keys and cancel each other via roots-intersection (`jobTouchesSubtree`); cancel only jobs whose roots are fully covered by the current job's roots.
186184
- Hybrid search runs its two lanes as independent read snapshots; a rebuild committing between them can transiently return both copies of a chunk — close with a shared read transaction or a second dedupe by material id + unit index.
187185
- The per-base index driver's `close()` no longer needs to take a write mutex: better-sqlite3 uses one synchronous, persistent connection and per-base writes are serialized by `KnowledgeLockManager.withBaseMutationLock(baseId)`, so there is no driver-level async write mutex to race — shutdown safety rests on JobManager draining before the store service stops.
188-
- Retrieval-surface follow-ups (PR C): the `searchMode` `default``vector` rename is externally visible through the gateway's pass-through base entity, and a permanent open failure (legacy layout) currently maps to a retryable 503.
186+
- Retrieval-surface follow-ups (PR C): permanent open failure for a legacy layout currently maps to a retryable 503.
189187
- PR A's full test matrix and risk notes live in this repo's test suites (`src/main/features/knowledge/**/__tests__`) and the PR #15973 description.

docs/references/knowledge/knowledge-service.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ Current persisted `knowledge_base` columns include:
165165

166166
- `groupId`: nullable group assignment; `null` means ungrouped.
167167
- `embeddingModelId`: the embedding model; `null` for BM25-only bases.
168-
- `dimensions`: positive embedding vector width for vector-capable bases; `null` for BM25-only completed bases (no embedding model) and for failed migrated bases with unknown dimensions. On a completed base it is paired with `embeddingModelId` — both set, or both `null` with `searchMode` forced to `bm25` (enforced by the DB CHECK and the entity schema).
168+
- `dimensions`: positive embedding vector width for vector-capable bases; `null` for BM25-only completed bases (no embedding model) and for failed migrated bases with unknown dimensions. On a completed base it is paired with `embeddingModelId` — both set, or both `null` for BM25-only retrieval (enforced by the DB CHECK and the entity schema).
169169
- `status`: `completed` for runnable bases, `failed` for recoverable base-level migration failures.
170170
- `error`: nullable `KnowledgeBaseErrorCode`; currently `missing_embedding_model` for recoverable failed bases.
171171

@@ -260,11 +260,12 @@ Search is executed by `KnowledgeService.search(baseId, query)`:
260260

261261
1. Reject failed bases.
262262
2. Reject queries without searchable tokens.
263-
3. Resolve the base's `searchMode` (`vector` / `bm25` / `hybrid`) and embed the query — skipped for `bm25`, which is lexical only.
264-
4. Call `KnowledgeIndexStore.search` on the base's per-base index store with an over-fetched candidate limit (`topK × overfetch`, capped). The store runs the BM25 lane (`search_text_fts`, with a LIKE fallback for short CJK tokens), the brute-force vector lane, or fuses both with RRF (`hybridAlpha`).
263+
3. Derive the retrieval mode from the base config and embed the query only for embedding-backed bases. Bases without an embedding model search BM25 only; embedding-backed bases use hybrid retrieval.
264+
4. Call `KnowledgeIndexStore.search` on the base's per-base index store with an over-fetched candidate limit (`topK × overfetch`, capped). The store runs the BM25 lane (`search_text_fts`, with a LIKE fallback for short CJK tokens) or fuses BM25 and brute-force vector results with RRF.
265265
5. Filter results whose source items are missing, outside the base, or `deleting`, then trim to `documentCount ?? 10`.
266266
6. Rerank when `base.rerankModelId` is configured.
267-
7. Apply relevance threshold (a no-op for `ranking`-kind scores) and assign ranks.
267+
7. Apply `threshold` only to results whose `scoreKind` is `relevance`; BM25/hybrid `ranking` scores pass through.
268+
8. Assign ranks.
268269

269270
Current `KnowledgeSearchResult` includes:
270271

docs/references/renderer-architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Target layout (in-flight directories pending migration are listed in §8):
6060

6161
```text
6262
src/renderer/
63-
├── windows/ # App — per-window entry roots (MainApp/SettingsApp/SubWindowApp) + shell
63+
├── windows/ # App — per-window entry roots (MainApp/SubWindowApp) + shell
6464
├── routes/ # App — route definitions
6565
├── pages/ # App — cross-domain shell pages only (domain pages live in features)
6666
├── features/ # Domain — one business domain per dir

docs/references/window-manager/window-manager-usage.md

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ In `types.ts`:
1212
export enum WindowType {
1313
Main = 'main',
1414
// ... existing types
15-
Settings = 'settings', // <-- add your new type
15+
MyWindow = 'myWindow', // <-- add your new type
1616
}
1717
```
1818

@@ -21,11 +21,11 @@ export enum WindowType {
2121
In `windowRegistry.ts`:
2222

2323
```typescript
24-
WINDOW_TYPE_REGISTRY[WindowType.Settings] = {
25-
type: WindowType.Settings,
24+
WINDOW_TYPE_REGISTRY[WindowType.MyWindow] = {
25+
type: WindowType.MyWindow,
2626
lifecycle: 'singleton',
27-
htmlPath: 'settings.html',
28-
// preload omitted → defaults to 'preload.js'
27+
htmlPath: 'windows/myWindow/index.html',
28+
// preload omitted → defaults to 'index.js'
2929
// showMode omitted → defaults to 'auto'
3030
windowOptions: {
3131
...DEFAULT_WINDOW_CONFIG,
@@ -46,31 +46,31 @@ import { WindowType } from '@main/core/window/types'
4646
const wm = application.get('WindowManager')
4747

4848
// open() is lifecycle-aware — handles singleton reuse, pool recycle, etc.
49-
const windowId = wm.open(WindowType.Settings)
49+
const windowId = wm.open(WindowType.MyWindow)
5050
```
5151

5252
### 4. Inject domain behavior via `onWindowCreatedByType`
5353

5454
```typescript
5555
// In your domain service's onInit():
5656
const wm = application.get('WindowManager')
57-
wm.onWindowCreatedByType(WindowType.Settings, ({ window, id }) => {
57+
wm.onWindowCreatedByType(WindowType.MyWindow, ({ window, id }) => {
5858
// Store the windowId for later use
59-
this.settingsWindowId = id
59+
this.myWindowId = id
6060

6161
// Attach event listeners BEFORE content loads
6262
window.on('closed', () => {
63-
this.settingsWindowId = undefined
63+
this.myWindowId = undefined
6464
})
6565
})
6666
```
6767

6868
The example above uses **destructuring**. An equivalent using the `mw` shorthand (useful when the callback body is long or accesses many fields):
6969

7070
```typescript
71-
wm.onWindowCreatedByType(WindowType.Settings, (mw) => {
72-
this.settingsWindowId = mw.id
73-
mw.window.on('closed', () => { this.settingsWindowId = undefined })
71+
wm.onWindowCreatedByType(WindowType.MyWindow, (mw) => {
72+
this.myWindowId = mw.id
73+
mw.window.on('closed', () => { this.myWindowId = undefined })
7474
})
7575
```
7676

@@ -83,30 +83,30 @@ The `onWindowCreated` event is the canonical hook for domain services to inject
8383
### The Pattern
8484

8585
```typescript
86-
@Injectable('SettingsService')
86+
@Injectable('MyWindowService')
8787
@ServicePhase(Phase.WhenReady)
88-
export class SettingsService extends BaseService {
89-
private settingsWindowId: string | undefined
88+
export class MyWindowService extends BaseService {
89+
private myWindowId: string | undefined
9090

9191
protected override onInit(): void {
9292
const wm = application.get('WindowManager')
9393

94-
wm.onWindowCreatedByType(WindowType.Settings, ({ window, id }) => {
94+
wm.onWindowCreatedByType(WindowType.MyWindow, ({ window, id }) => {
9595
// 1. Store the windowId
96-
this.settingsWindowId = id
96+
this.myWindowId = id
9797

9898
// 2. Attach listeners BEFORE content loads
9999
window.once('ready-to-show', () => {
100-
this.sendInitialConfig(window)
100+
this.sendInitialData(window)
101101
})
102102

103103
window.on('closed', () => {
104-
this.settingsWindowId = undefined
104+
this.myWindowId = undefined
105105
})
106106
})
107107

108-
wm.onWindowDestroyedByType(WindowType.Settings, () => {
109-
this.settingsWindowId = undefined
108+
wm.onWindowDestroyedByType(WindowType.MyWindow, () => {
109+
this.myWindowId = undefined
110110
})
111111
}
112112
}
@@ -130,7 +130,7 @@ For subscriptions that only care about a single window type (the typical consume
130130
It's tempting to attach listeners inline after `wm.open()` returns, since the ID is right there:
131131

132132
```typescript
133-
const id = wm.open(WindowType.Settings)
133+
const id = wm.open(WindowType.MyWindow)
134134
const window = wm.getWindow(id)!
135135
window.on('blur', this.hideIfUnpinned)
136136
window.once('closed', () => { this.windowId = null })
@@ -151,9 +151,9 @@ The `onWindowCreatedByType` / `onWindowDestroyedByType` listeners receive a `Man
151151
**Destructuring (recommended default, short callback):**
152152

153153
```typescript
154-
wm.onWindowCreatedByType(WindowType.Settings, ({ window, id }) => {
155-
this.settingsWindowId = id
156-
window.on('closed', () => { this.settingsWindowId = undefined })
154+
wm.onWindowCreatedByType(WindowType.MyWindow, ({ window, id }) => {
155+
this.myWindowId = id
156+
window.on('closed', () => { this.myWindowId = undefined })
157157
})
158158
```
159159

@@ -199,6 +199,19 @@ WindowManager exposes four lifecycle methods, arranged in two layers:
199199

200200
**Why `destroy()` is not a consumer API.** On non-pooled windows (default and singleton) `close()` falls through to the same `destroyWindow()` call — there is no behavioral difference. On pooled windows, `destroy()` bypasses the pool, which is almost never what a consumer actually wants; the correct API for "stop the whole pool" is `suspendPool(type)`, which destroys idle windows and prevents further recycling without touching in-use windows.
201201

202+
### Consumer-loaded windows (`htmlPath: ''`)
203+
204+
A registry entry with `htmlPath: ''` is **consumer-loaded**: WM wires the window (preload, behavior, bounds, lifecycle) but loads no content — the domain service loads it after `open()`. For hidden, one-shot surfaces rendering *generated* content (print / PDF, offscreen render).
205+
206+
```typescript
207+
const id = wm.open(WindowType.MyPrintSurface) // WM wires; loads nothing
208+
const win = wm.getWindow(id) // the sanctioned handle to load into
209+
await win?.webContents.loadURL(generatedHtmlDataUrl) // consumer owns content + show + close()
210+
// ... await 'did-finish-load', e.g. webContents.printToPDF(), then wm.close(id)
211+
```
212+
213+
`getWindow(id)` is the one exception to "consumers only call `open()` / `close()`" — use it only for `webContents` loading (payload encoding is the consumer's call). Main-initiated `loadURL` / `loadFile` is not blocked by WM's navigation guards (those only intercept renderer-initiated navigation).
214+
202215
### Domain-Key-to-WindowId Mapping
203216

204217
For window types that are keyed by domain data (e.g., a topic-specific window), the domain service maintains its own mapping:

electron.vite.config.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ const isProd = process.env.NODE_ENV === 'production'
2525
// pruned from production packages, the packaged app would fail at runtime with
2626
// MODULE_NOT_FOUND (no test catches this). See docs/references/api-gateway/README.md.
2727
const mainExternalDependencies = Object.keys(pkg.dependencies)
28+
const mainExternalModules = ['bufferutil', 'utf-8-validate', 'electron', ...mainExternalDependencies]
29+
30+
export const isMainExternalModule = (id: string) => {
31+
return mainExternalModules.some((moduleId) => id === moduleId || id.startsWith(`${moduleId}/`))
32+
}
2833

2934
export default defineConfig({
3035
main: {
@@ -51,7 +56,7 @@ export default defineConfig({
5156
build: {
5257
lib: { entry: resolve(__dirname, 'src/main/main.ts') },
5358
rollupOptions: {
54-
external: ['bufferutil', 'utf-8-validate', 'electron', ...mainExternalDependencies],
59+
external: isMainExternalModule,
5560
output: {
5661
manualChunks: undefined, // 彻底禁用代码分割 - 返回 null 强制单文件打包
5762
inlineDynamicImports: true // 内联所有动态导入,这是关键配置
@@ -145,7 +150,6 @@ export default defineConfig({
145150
rollupOptions: {
146151
input: {
147152
index: resolve(__dirname, 'src/renderer/windows/main/index.html'),
148-
settings: resolve(__dirname, 'src/renderer/windows/settings/index.html'),
149153
quickAssistant: resolve(__dirname, 'src/renderer/windows/quickAssistant/index.html'),
150154
selectionToolbar: resolve(__dirname, 'src/renderer/windows/selection/toolbar/index.html'),
151155
selectionAction: resolve(__dirname, 'src/renderer/windows/selection/action/index.html'),
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
PRAGMA foreign_keys=OFF;--> statement-breakpoint
2+
CREATE TABLE `__new_knowledge_base` (
3+
`id` text PRIMARY KEY NOT NULL,
4+
`name` text NOT NULL,
5+
`group_id` text,
6+
`dimensions` integer,
7+
`embedding_model_id` text,
8+
`status` text NOT NULL,
9+
`error` text,
10+
`rerank_model_id` text,
11+
`file_processor_id` text,
12+
`chunk_size` integer NOT NULL,
13+
`chunk_overlap` integer NOT NULL,
14+
`chunk_strategy` text DEFAULT 'structured' NOT NULL,
15+
`chunk_separator` text DEFAULT '\n\n' NOT NULL,
16+
`threshold` real,
17+
`document_count` integer,
18+
`created_at` integer NOT NULL,
19+
`updated_at` integer NOT NULL,
20+
FOREIGN KEY (`group_id`) REFERENCES `group`(`id`) ON UPDATE no action ON DELETE set null,
21+
FOREIGN KEY (`embedding_model_id`) REFERENCES `user_model`(`id`) ON UPDATE no action ON DELETE no action,
22+
FOREIGN KEY (`rerank_model_id`) REFERENCES `user_model`(`id`) ON UPDATE no action ON DELETE set null,
23+
CONSTRAINT "knowledge_base_chunk_strategy_check" CHECK("__new_knowledge_base"."chunk_strategy" IN ('structured', 'delimiter')),
24+
CONSTRAINT "knowledge_base_status_check" CHECK("__new_knowledge_base"."status" IN ('completed', 'failed')),
25+
CONSTRAINT "knowledge_base_status_error_check" CHECK(
26+
(
27+
"__new_knowledge_base"."status" = 'completed'
28+
AND "__new_knowledge_base"."error" IS NULL
29+
AND (
30+
(
31+
"__new_knowledge_base"."embedding_model_id" IS NOT NULL
32+
AND "__new_knowledge_base"."dimensions" IS NOT NULL
33+
AND "__new_knowledge_base"."dimensions" > 0
34+
)
35+
OR (
36+
"__new_knowledge_base"."embedding_model_id" IS NULL
37+
AND "__new_knowledge_base"."dimensions" IS NULL
38+
)
39+
)
40+
)
41+
OR (
42+
"__new_knowledge_base"."status" = 'failed'
43+
AND "__new_knowledge_base"."error" IS NOT NULL
44+
AND length(trim("__new_knowledge_base"."error")) > 0
45+
)
46+
)
47+
);
48+
--> statement-breakpoint
49+
INSERT INTO `__new_knowledge_base`("id", "name", "group_id", "dimensions", "embedding_model_id", "status", "error", "rerank_model_id", "file_processor_id", "chunk_size", "chunk_overlap", "chunk_strategy", "chunk_separator", "threshold", "document_count", "created_at", "updated_at") SELECT "id", "name", "group_id", "dimensions", "embedding_model_id", "status", "error", "rerank_model_id", "file_processor_id", "chunk_size", "chunk_overlap", "chunk_strategy", "chunk_separator", "threshold", "document_count", "created_at", "updated_at" FROM `knowledge_base`;--> statement-breakpoint
50+
DROP TABLE `knowledge_base`;--> statement-breakpoint
51+
ALTER TABLE `__new_knowledge_base` RENAME TO `knowledge_base`;--> statement-breakpoint
52+
PRAGMA foreign_keys=ON;

0 commit comments

Comments
 (0)