Commit Graph

10 Commits

Author SHA1 Message Date
hello_world
3a508c987b refactor(miniapp): migrate to v2 data layer (#14049)
### What this PR does

Migrates the MiniApp feature from v1 (Redux + sidecar
`custom-minapps.json`) to the v2 data architecture (DataApi + Preference
+ Cache), and integrates it into the v2 AppShell tab system.

**Before this PR**
- App lists lived in three Redux arrays (`enabled` / `disabled` /
`pinned`); custom-app logos were stripped before persistence and
recovered at runtime from `{userData}/Data/Files/custom-minapps.json`.
- Settings (`region`, `max_keep_alive`, `open_link_external`,
`show_opened_in_sidebar`) lived in legacy redux/electron-store.
- Runtime keep-alive used a module-level `lru-cache` singleton, mirrored
into v2 cache via `onInsert` / `disposeAfter` (two sources of truth —
already a known race).
- Routes were `/app/minapp/*`; sidebar icon literal was `'minapp'`.
- Sidebar mode used the legacy popup container; top-navbar mode was
non-functional.

**After this PR**
- A single `mini_app` SQLite table owns every row (preset + custom).
Preset rows are seeded by `MiniAppSeeder` from `PRESETS_MINI_APPS` on
every boot; custom rows come in via `POST /mini-apps`. The seeder uses
`setWhere isNotNull(presetMiniappId)` so refreshing preset display
fields can never overwrite a custom row whose `appId` happens to collide
with a preset.
- `MiniAppMigrator` imports v1 Redux state and reads
`custom-minapps.json` (path resolved through
`MigrationPaths.customMiniAppsFile`) to recover stripped logos.
- Settings live under typed Preference keys
(`feature.mini_app.{region,max_keep_alive,open_link_external}`); sidebar
icon literal renamed `'minapp'` → `'mini_app'` with a complex preference
transform that rewrites existing user arrays in-place.
- API: `GET/POST/PATCH/DELETE /mini-apps` + `POST
/mini-apps/order:batch`, Zod-validated, fractional-indexing ordering
scoped by `status` (cross-status batches are rejected with
`VALIDATION_ERROR` per the data-ordering-guide contract). Status
transitions reassign `orderKey` to the tail of the target partition
inside a transaction.
- Renderer hook `useMiniApps` exposes **command-style** writes only:
`updateAppStatus(id, status)` and `setAppStatusBulk([{id, status}])`.
The legacy declarative `updateMiniApps(list)` /
`updateDisabledMiniApps(list)` / `updatePinnedMiniApps(list)` are gone —
they took region-filtered subsets and silently disabled rows the caller
never saw.
- Keep-alive list is stored solely in
`useCache('mini_app.opened_keep_alive')`. Cap eviction respects AppShell
pin status: `useMiniAppPopup` reads pinned mini-app routes from
`useTabs` and skips them in eviction. `MiniAppTabsPool` renders webviews
in a stable `appId`-sorted order so LRU reorders never move `<webview>`
DOM nodes (Electron `<webview>` loses its guest WebContents on
detach/reattach).
- **Unified launch path**: clicking any miniapp (from the launcher grid
or a top tab bar entry) calls `openTab('/app/mini-app/<id>', { title,
icon: app.logo })`. A globally-mounted `<MiniAppTabsPool>` in `AppShell`
keeps a `<webview>` alive per opened app, regardless of sidebar vs
top-navbar layout.
- Settings UI rewritten as a `PageSidePanel` drawer composed of
`MiniAppListPair` (visible / hidden columns with drag-drop) and
`MiniAppDisplaySettings` (region / cache slider). New custom-app form is
a separate `NewMiniAppPanel` drawer.
- Sidebar's running-mini-apps strip removed — opened apps live
exclusively in the top tab bar (per #3198804265). Companion preference
`feature.mini_app.show_opened_in_sidebar` deleted from the schema.

### Why we need it and why it was done in this way

Part of the broader v2 data-layer migration (Redux/Dexie/ElectronStore →
DataApi + Preference + Cache).

**Architecture**
- DataApi for entity rows (preserves user content); Preference for
atomic settings; Cache (Memory tier) for runtime ephemera.
- Layered preset pattern (`best-practice-layered-preset-pattern.md`):
preset and custom rows share the same table, discriminated by
`presetMiniappId`. Seeder refreshes preset display fields on re-run;
custom rows are immutable to the seeder.
- Region filtering is a **view-only** concern (read path); the write
path is command-style and never references region. This eliminated a
class of bugs where editing the visible (filtered) list caused
region-hidden rows to drift.
- AppShell tab pinning is the canonical "keep this loaded" signal. The
keep-alive cap respects it; pinned mini-app tabs never get evicted
regardless of cap. Render-order independence in `MiniAppTabsPool`
ensures LRU touches don't move `<webview>` nodes around.
- Per-app icon resolution: `app.logo` is a `CompoundIcon` id (e.g.
`"Moonshot"`) for presets and a URL for custom apps. UI consumers (tab
bar, sidebar entry, settings list) call `getMiniAppsLogo` to resolve the
id to a `CompoundIcon` before rendering, with `<img>` fallback for URL
strings.
- Per-entity tab icons are cleared on internal navigation, sidebar
reuse, and the top-bar settings button — three call sites that all flip
the active tab's URL now consistently reset `icon: undefined` so a
mini-app logo never sticks onto an unrelated route.

**Tradeoffs**
- `useMiniApps` still exposes `miniapps` (region-filtered
enabled+pinned) and `disabled` (region-filtered). These are display-only
views. Renamed/typed wrappers were considered but deferred — the
refactor to command-style writes already eliminated the bug class that
motivated the rename.
- The `applyReorderedList` integration test for
`reorderMiniAppsByStatus` was dropped — `MockUseDataApiUtils` doesn't
fill the SWR cache that `useReorder.readCurrent` reads. Splice logic is
straightforward and the server-side `applyScopedMoves` test covers the
contract.
- Sidebar primitives in `@cherrystudio/ui`-adjacent layout still accept
`miniAppTabs` / `onMiniAppTabClick` props (defensive defaults — render
nothing without a producer). Removing these from the primitive's API is
a separate refactor not in scope.

### Breaking changes

User-visible changes are auto-migrated by the v2 migration framework —
no manual user action required:
- Sidebar icon literal `'minapp'` → `'mini_app'` (rewritten by the
`sidebar_icons_rename` complex preference transform)
- Preference key rename `feature.minapp.*` → `feature.mini_app.*`
(auto-migrated via `classification.json`)
- Custom-app logos stripped from v1 Redux are recovered from
`custom-minapps.json` during migration

One product-shape change is documented under
`v2-refactor-temp/docs/breaking-changes/`:
- `2026-05-07-miniapp-sidebar-running-list-removed.md` — the sidebar no
longer surfaces opened mini-apps under the mini-app entry. Open apps are
accessed exclusively via the top tab bar; pin a tab to keep its state
across switches.

The legacy v1 preference `showOpenedMinappsInSidebar` is reclassified as
`status: deleted` in the migration pipeline; v1 values are dropped
during v1→v2 migration with no v2 destination.

### Special notes for your reviewer

**Verified end-to-end on a real dev profile**: v1 Redux state +
`custom-minapps.json` → v2 SQLite, including pinned-app cross-group
dedup (a v1 pinned app appears in both `pinned` and `enabled` Redux
arrays; the migrator counts duplicates as skipped so the engine's
`targetCount >= sourceCount - skippedCount` invariant holds — without
this, any user with pinned miniapps was blocked from migrating).

**Drizzle migrations** are throwaway in dev per `CLAUDE.md`.
`migrations/sqlite-drizzle/0020_even_hulk.sql` is the single regenerated
migration; it will be wiped to a clean initial migration before release.

**Review history**: 28 line-comments across multiple formal review
rounds. All resolved. The most consequential fixes:
- `applyScopedMoves` in `MiniAppService.reorder` — rejects cross-status
batches with `VALIDATION_ERROR` instead of silently splitting them.
- `update()` reassigns `orderKey` to a fresh tail in the target
partition on status change.
- Empty-string substitution in migrator mappings is now caught by the
post-transform validity check; bad rows are skipped + warned, never
inserted.
- Migrator validation switched from `limit(5)` sample to full `count(*)
WHERE empty-fields` — bad rows can no longer pass validation by virtue
of being beyond the sample window.
- Keep-alive cap exempts pinned tabs (#3198809321 + the kangfenmao
keepalive review); render order in `MiniAppTabsPool` is `appId`-stable
so LRU touches don't move `<webview>` nodes (this was the root cause of
"switching tabs reloads the webview").

**Out of scope**:
- The remaining `@renderer/store/tabs` import in
`PaintingsRoutePage.tsx` is pre-existing v1 residual (not introduced or
touched by this PR).

### Checklist

- [x] PR: description rewritten to reflect the final architecture +
integration with the AppShell tab system
- [x] Code: command-style writes (`updateAppStatus` /
`setAppStatusBulk`); see `useMiniApps`, `MiniAppService`,
`MiniAppMigrator`, `MiniAppTabsPool`, `useMiniAppPopup` for the main
entry points
- [x] Refactor: ~1500 lines of dead/legacy code removed
(`Tab/TabContainer`, `TabsService`, `MiniAppPopupContainer`,
`TopViewMiniAppContainer`, legacy LRU singleton, `PinnedMiniApps`, dead
`userOverrides` / `MiniAppRegistryService`, unused `Signal.ts`)
- [x] Upgrade: v1 → v2 migration verified end-to-end on a real dev
instance
- [x] Documentation: architecture covered by `docs/references/data/`;
one user-visible behavior change documented in
`v2-refactor-temp/docs/breaking-changes/`
- [x] Self-review: multi-agent review via `/gh-pr-review` (twice); all
28 review comments resolved

### Release note

```release-note
NONE - Internal v2 data refactor. User-facing renames (route, sidebar icon, preference keys) are auto-migrated. The sidebar no longer shows a running-mini-apps strip; opened apps live in the top tab bar.
```

---------

Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: chengcheng84 <hello_world0000@outlook.com>
Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
2026-05-07 20:45:20 +08:00
亢奋猫
60efa15e98 refactor(websearch-settings): remove local providers and use v2 preferences (#14443)
Co-authored-by: jdzhang <625013594@qq.com>
2026-05-06 13:53:40 +08:00
jd
2456dbdf96 feat(model-selector-v2): build v2 model selector on new data/UI stack (#14490)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: SuYao <sy20010504@gmail.com>
Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Fixes #14483
2026-04-27 16:33:29 +08:00
SuYao
a4309aad61 feat(migration): migrate llm model preferences to v2 with UniqueModelId format (#14229)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
2026-04-17 18:07:15 +08:00
亢奋猫
ea99c82115 refactor: remove memory module from v2 (#14251)
Co-authored-by: SuYao <sy20010504@gmail.com>
Fixes #14250
2026-04-16 13:22:30 +08:00
亢奋猫
4197117b51 refactor: migrate shortcut system to Preference architecture (#14086)
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
2026-04-16 11:38:21 +08:00
SuYao
13b333e9b3 feat(v2): decouple assistant from topic with dedicated table and migration (#13851)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
2026-04-13 12:55:06 +08:00
fullex
44b5edce54 feat(preboot): add core/preboot subsystem for userData location
Introduce a new `src/main/core/preboot/` subsystem that owns the
synchronous setup happening before `application.bootstrap()` runs. This
is the follow-up promised by c17dd033f to rewire v1's `initAppDataDir()`
to read from BootConfig instead of `~/.cherrystudio/config/config.json`.

## Startup phase vocabulary

The v2 main process now has three clearly-named startup phases, with the
definitions written into `src/main/core/README.md` as the authoritative
source:

  - preboot  — synchronous setup before the lifecycle container is
               built. BootConfig load, userData resolution, top-level
               Electron APIs. No DI, no services. Owned by core/preboot/.
  - bootstrap — the `application.bootstrap()` orchestration function.
                Freezes the path registry, runs Background/BeforeReady/
                WhenReady stages. NestJS/Spring-style term, the only
                meaning of "bootstrap" in v2 from this PR onward.
  - running  — steady state after bootstrap() returns.

v1's `src/main/bootstrap.ts` used "bootstrap" in the OS "early-boot
loader" sense, which clashes with the NestJS-style meaning v2's lifecycle
code has been using. "preboot" disambiguates without touching v2's
bootstrap terminology.

## userData location resolution

`resolveUserDataLocation()` is a single entry point that:

  1. Applies any pending relocation (see below).
  2. Reads `BootConfig.app.user_data_path[exe]` and calls setPath if
     valid.
  3. Falls back to portable-mode `<PORTABLE_EXECUTABLE_DIR>/data`.
  4. Otherwise leaves Electron's default userData.

Step 2 mirrors v1's `getAppDataPathFromConfig()` path-validity check
(existsSync + accessSync W_OK). Portable fallback mirrors v1's branch
character-for-character. Only the data source changes: BootConfig, not
v1 config.json. v1→v2 data migration is handled separately by
BootConfigMigrator (from c17dd033f).

## Pending relocation (new)

A new BootConfig field `temp.user_data_relocation` under a new top-level
`temp.*` namespace holds in-flight userData relocation state:

    { status: 'pending', from, to }
    { status: 'failed',  from, to, error, failedAt }
    null

The `temp.*` namespace is reserved for ephemeral runtime state that
should never be backed up or synced (restoring a stale temp.* entry
could re-execute a relocation that already happened).

When a pending request is present, `executePendingRelocation()`
synchronously copies the entire `from` tree to `to` with
`fs.cpSync({ recursive: true, force: true, verbatimSymlinks: true })`,
then atomically commits the new location to BootConfig
(`app.user_data_path[exe] = to` + clear `temp.user_data_relocation` +
flush via temp file + rename).

Pre-flight checks fail fast before any bytes are copied:
  - from !== to
  - to is not inside from (with path.sep guard against sibling-prefix
    false positives like /a vs /ab)
  - from exists
  - path.dirname(to) exists and is writable

On failure, the error is recorded as a `failed` state in BootConfig and
the function returns normally, letting Step 2 fall through to the
existing user_data_path. The `failed` record is the entry point for a
future renderer recovery UI (retry/abandon/investigate); this PR does
not implement the UI.

`failed` states are intentionally NOT auto-retried — that would turn a
one-off environmental failure into an infinite loop.

## v1 two-phase copy abandoned

v1's `copyOccupiedDirsInMainProcess()` worked around Windows file locks
on `occupiedDirs` (logs, Network, Partitions/webview/Network) by
splitting the copy into two phases (renderer copies the bulk, main
copies the locked dirs during the narrow "no renderer yet" window).

v2 abandons that distinction entirely: the previous process has fully
exited before preboot runs, so nothing is locked, and the entire
userData tree is copied as a single opaque unit. `occupiedDirs` has no
meaning in v2 and is deprecated in `packages/shared/config/constant.ts`
— it is kept on disk only because two v1-era call sites (bootstrap.ts
and BasicDataSettings.tsx) still reference it during the transition.

## Deprecations (code unchanged)

Two v1 files have their file-header @deprecated comments strengthened
but their code bodies untouched:

  - src/main/bootstrap.ts — no longer imported anywhere; the file stays
    on disk as reference for the follow-up cleanup PR.
  - src/main/utils/init.ts — initAppDataDir is dead code;
    updateAppDataConfig is still called by the v1 IPC handler at
    src/main/ipc.ts:300 (out of scope for this PR).

## Known limitation (pre-existing)

LoggerService opens its winston file transport at module-load time
pointing at `app.getPath('logs')`, which on Windows/Linux resolves to
`<userData>/logs`. This happens BEFORE preboot runs, so the winston
file handle is bound to the pre-setPath path. After preboot calls
setPath('userData', <new>), winston keeps writing to the old handle.
Post-relocation logs therefore appear at the old location until the
next restart. This is a pre-existing v2 issue (LoggerService doesn't
respond to setPath) and is documented in executePendingRelocation's
doc comment; a follow-up should either lazy-open winston transports
or teach LoggerService to rotate on userData change.

## Out of scope (follow-up cleanup PR)

  - App_SetAppDataPath IPC handler migration to write BootConfig
    pending_relocation instead of v1 argv protocol
  - BasicDataSettings.tsx renderer flow migration
  - src/main/bootstrap.ts and src/main/utils/init.ts deletion
  - occupiedDirs constant deletion
  - LoggerService setPath responsiveness

## Tests

25 unit tests in userDataLocation.test.ts covering:
  - getNormalizedExecutablePath (5 platform branches)
  - resolveUserDataLocation normal resolution (9 scenarios including
    AppImage/portable normalized key matching)
  - resolveUserDataLocation pending relocation (11 scenarios including
    pre-flight failure cases and the sibling-prefix regression guard)

Signed-off-by: fullex <0xfullex@gmail.com>
2026-04-08 01:03:45 -07:00
fullex
c17dd033fc feat(migration): migrate v1 ~/.cherrystudio/config/config.json into BootConfig
Add a 'configfile' source kind to BootConfigMigrator so the v1 home config
file's `appDataPath` is migrated into a new BootConfig key
`app.user_data_path: Record<string, string>`, keyed by executable path.

v1's `~/.cherrystudio/config/config.json` is conceptually "early-boot
configuration that must be readable before userData is decided" — the same
category as BootConfig. v1 `config/config.json` will eventually be replaced
entirely by BootConfig; this PR establishes the data migration path, and a
follow-up will rewire `initAppDataDir()` to read from BootConfig instead.

Key pieces:

- New `LegacyHomeConfigReader` — sync fs read, normalizes both the legacy
  string and the array-of-{executablePath,dataPath} v1 formats into a
  `Record<string, string>`. Returns `null` (not `{}`) for "no data" so the
  migrator's null-skip guard distinguishes "missing" from "empty record"
  and doesn't write a spurious empty entry.

- BootConfigMigrator: new 'configfile' branch in `prepare()`; inline
  `configFileMappings` local const with `targetKey: BootConfigKey` type
  annotation (regen safety net — schema loses `app.user_data_path` →
  compile error at the declaration site). `defaultValue: null` on
  config-file items so "no v1 file" skips the item rather than falling
  back to the schema default `{}`.

- Type tightening: `MigrationItem.targetKey` and `PreparedData.targetKey`
  narrowed from `string` to `BootConfigKey`; all 6 `as BootConfigKey`
  assertions deleted. This removes a silent "regen drops the key" failure
  mode where the assertions previously suppressed compile errors.

- Generator updates: `generate-boot-config.js` gains a
  `MANUAL_BOOT_CONFIG_ITEMS` list that flows through the same sort/emit
  pipeline as classification-derived items, keeping `bootConfigSchemas.ts`
  as a fully auto-generated single-interface file (no manual sections, no
  declaration merging, no preservation logic). `generate-migration.js`
  tightens the 3 BootConfig mapping arrays to `targetKey: BootConfigKey`.

- Tests: new `LegacyHomeConfigReader.test.ts` covers 9 scenarios (7 null
  returns + 2 populated records); new `BootConfigMigrator.test.ts` covers
  the configfile source end-to-end plus a redux-source regression.

- Docs: per-migrator `README-BootConfigMigrator.md` created per the
  convention in `v2-migration-guide.md`. `v2-migration-guide.md`,
  `boot-config-overview.md`, `boot-config-schema-guide.md`, and
  `src/main/data/migration/v2/README.md` updated to mention
  BootConfigMigrator, `LegacyHomeConfigReader`, and the 'configfile'
  source kind. The AppImage/Windows-portable `executablePath`
  normalization gap is documented as a known limitation that must be
  addressed in the follow-up read-side PR.

Explicitly out of scope:

- Rewiring `initAppDataDir()` / `updateAppDataConfig()` to read/write
  BootConfig — the v1 home config file keeps working unchanged.
- Adding a 'configfile' category to classification.json — the
  data-classify toolchain isn't modelled for file sources yet.
- Deleting the v1 home config file post-migration — left intact.

Verified end-to-end on a real v1 config.json: the migrator produced
`"app.user_data_path": { [executablePath]: dataPath }` in
`~/.cherrystudio/boot-config.json`, verbatim-preserving the v1 data.

Signed-off-by: fullex <0xfullex@gmail.com>
2026-04-07 09:20:23 -07:00
fullex
b618856b71 feat(shared): add BootConfig schema and types
Add shared type definitions for the file-based boot configuration system:
- BootConfigSchema interface with default values
- BootConfigKey type for type-safe key access
- BootConfigPreferenceKeys mapped type for unified preference integration

Signed-off-by: fullex <0xfullex@gmail.com>
2026-03-25 09:34:31 -07:00