Three core barrels, three diagnoses under naming-conventions §6.4:
- paths: remove the barrel — it only re-exported types while the real entry
is application.getPath(). constants.ts and pathRegistry.ts are independent
building blocks deep-imported by their specific consumers (rules 2/3/4).
- lifecycle: keep the barrel (a cohesive framework consumed as a set by 80+
services), route the last deep-import leaks through it, and prune 13
framework-internal machinery exports (resolver, decorator getters, capability
guards) that no consumer touches — tightening the door to its public surface.
- application: remove the barrel and repoint @application directly at
Application.ts. The barrel bundled the hot `application` locator (~218
consumers) with the bootstrap-only serviceRegistry, so every @application
import dragged in the whole service graph — the root of the JobManager-to-
serviceRegistry eval-time cycle. The locator now loads on its own.
Relocate SHUTDOWN_TIMEOUT_MS from an Application static into
lifecycle/constants.ts: a shared shutdown-grace policy honored by both
Application and JobManager, so JobManager no longer imports the Application
class at all.
Docs (paths/README, application/README, application-overview) and per-file
test mocks (vi.mock('@main/core/application') to '@application';
Application.getPath vi.unmock) are updated in lockstep.
The FileHandle type union moved to src/shared/data/types/file.ts and the
watcher/ directory was flattened to watcher.ts during the naming §6.4
barrel refactor, leaving two dead links caught by docs:check-links.
The adapters/ root barrel deep-imported leaves inside four sub-barrels
(stream/converters/formatters/factory), tripping both no-nesting and closed.
The sub-barrels were themselves dead — incomplete and imported by nobody: the
root barrel, the factories, and the tests all bypassed them for leaves.
- Delete the four internal sub-barrels; the subdirs become plain folders, so
the factory/formatter/root leaf imports are now legal (no barrel to bypass).
- Curate adapters/index.ts to the real public surface — the two factories plus
the interface/format types actually consumed by proxyStream and errors. The
concrete stream adapters, converters, and formatters have zero external
consumers and stay internal.
- Move openrouter.ts up to apiGateway/, next to its sole consumer
reasoningCache.ts — it is a reasoning-cache type contract, not adapter
machinery — clearing the last closed violation.
- Document openrouter.ts's new home in the api-gateway README tree.
apiGateway barrel warnings: ~38 -> 0.
Rewrite streamManager/index.ts to export the full consumed surface (all
listeners, startAgentSessionRun, PersistenceBackend contract, TranslationBackend,
stream types); route all 15 external deep imports through it. Remove the
internal-only context/ and lifecycle/ sub-barrels so internal siblings import
directly. Expose AiStreamRequest/CallOverrides on the ai/types barrel and route
ChannelAdapter through the channels barrel.
Two deep imports remain, blocked by nested sub-barrels in sibling dirs
(runtime/claudeCode, channels/security) — deferred to those dirs' cleanup.
Clear all `barrel/index-no-impl` violations in src/main (14 barrels) by
turning each index.ts into a pure re-export or dissolving it:
- Aggregation buckets → drop the barrel, move the assembled object/array/fn
to a named file, deep-import at the sole consumer: data/api/handlers →
apiHandlers.ts, ipc/handlers → ipcHandlers.ts, migrators → migratorRegistry.ts,
params/features → internalFeatures.ts, browser/tools → registry.ts,
builtin → registerBuiltinTools.ts, db/seeding → seederRegistry.ts.
- Single-impl dirs flattened: provider/extensions.ts, provider/cherryai.ts.
- Barrel kept, impl extracted to a named file: i18n/resolver.ts,
ai/types/providerConfig.ts.
- Route subdir flattened to match its flat siblings: routes/knowledge.ts +
routes/knowledgeSchemas.ts.
- runtime: replace the import-time `claudeCode/register` side effect with an
explicit registerRuntimeDrivers() invoked from AgentSessionRuntimeService.onInit,
so runtime/index.ts stays a pure re-export.
- loop: dissolve the non-enforced barrel into loop/types.ts + loop/hookRunner.ts.
Also route DataApiService's apiHandlers import through the data/api barrel,
and expose ClaudeCodeRuntimeDriver via the claudeCode barrel, clearing the two
deep-import warnings this batch touched. Docs and comments updated to the new paths.
schemas/index.ts and errors/index.ts carried real local declarations — the
`ipcRequestSchemas` runtime registry plus composed types, and the `IpcError`
class plus `IpcErrorCode` — yet sat under the index.ts name, tripping
barrel/index-no-impl (§6.4 rule 1: an index must be a pure re-export barrel).
Unlike the api barrel there is no outer barrel to remove — ipc/ is already an
open namespace — so this is a straight rename to named modules.
- schemas/index.ts -> schemas/ipcSchemas.ts; errors/index.ts -> errors/IpcError.ts.
- Repoint all consumers (18 deep imports + one intra-package import), including
directory-form relative imports (./schemas, ../errors) that resolved via Node's
index lookup.
- Sync the ipc docs: rename path references and reword the "no errors/index.ts
aggregation" prose, since errors/ now has no index at all.
The api/index.ts barrel re-exported only infra types yet required domain DTOs
to deep-import schemas/<domain>, colliding with naming §6.4 rule 2
(enforced-or-no-barrel) and accounting for the bulk of barrel/closed lint
violations. Because schema files carry zod runtime values, any aggregating
barrel drags all 24 domains in on a single import — exactly what the barrel
reform removes.
- Remove api/index.ts; api/ is now an open namespace.
- Flatten infra file names: apiTypes/apiErrors/apiPaths -> types/errors/paths;
schemas/index.ts -> schemas/apiSchemas.ts.
- Rewrite all consumers: 71 deep-import path renames + 106 barrel imports split
to their owning module (types/paths/errors), including two dynamic imports and
the tests/__mocks__ fixtures.
- Sync data docs and codify the schemas/ `_xxx.ts` = cross-resource construct
convention in naming-conventions §3.2.
src/main/index.ts → main.ts and src/preload/index.ts → preload.ts, so the process entries stop occupying the barrel-reserved index filename (§6.4) without needing an exemption list. The rename is wired through electron.vite (main build.lib.entry — rollupOptions.input would bypass electron-vite's output-format detection — and the preload input key), package.json main (out/main/main.js), WindowManager's default preload filename, MainWindowService's webview preload path, and DbService's slow-query stack filter, which matches the bundled artifact name. preload.d.ts becomes globals.d.ts: sharing preload.ts's basename makes TypeScript drop the .d.ts as that file's presumed output declaration, and tsgo only applies its global Window augmentation when the declaration sorts before its importer, so the file keeps a distinct, alphabetically-early name. Entry-path mentions in code comments and reference docs follow.
Rename the four directory-form index routes (settings/, settings/mcp/, app/mini-app/, app/paintings/) to TanStack's default-supported flat dot form (settings.index.tsx, …) so no file under routes/ is named index.* and the §6.4 iron rule — the index filename is reserved for barrels — holds across src with no exceptions. URLs and route ids are unchanged; routeTree.gen.ts regenerates with only the four import paths moved. Naming §6.4 drops the routes/ carve-out, §6.6 documents the dot-form token, and the routes READMEs replace a stale bare-index example.
Promote §6.4 from three loose rules to the full barrel contract (re-export-only, enforced-sole-entry-or-no-barrel, no nesting, one-cohesive-unit) and point shared §3.1 / main §2.1 / renderer §5 at it, fixing main's stale pointer to shared. Reserve the index filename for barrels — always index.ts, never .tsx outside routes/ — with implementations in named files; add the renderer component-directory form and align §4.2's component test. Note what the contract does not remove: dev builds load a barrel's full graph (rule 4 is the bound), and dynamic import() crosses a boundary through the barrel, with the React.lazy form in renderer §5. Docs only; lint enforcement and call-site fixes follow.
Implements the accepted upstream items from issue 16738:
- JobSettledEvent<TPayload> now carries typed input, parentId, and the
final metadata (post-patchMetadata); JobContext gains parentId. Both
are projections of the persisted snapshot - no extra DB reads.
- JobListFilter.type widened to string | string[] (inArray), parentId
filter added (rides job_parent_id_idx); ACTIVE_JOB_STATUSES exported
from shared schemas replacing JobService's private copy; list/count
WHERE composition extracted into a shared listConditions helper.
- JobManager.enqueueTx(tx, ...) rides the caller's withWriteTx
transaction so business writes and the job INSERT commit atomically;
post-commit side effects are deferred one microtask and re-read the
row (a rollback discards the resolver). JobService gains createTx /
findActiveByIdempotencyKeyTx; enqueue is refactored onto the shared
prepareEnqueue with unchanged external behavior.
Refs #16738
Replace the fuzzy business-orchestration routing criterion with an outward-side-effects test: stateless shared modules default to utils/ and promote to services/ only for a stated reason (side effects, or a forced services import).
Rekey the Service/Manager suffix from stateful classes to stateful singleton capabilities, mechanism-independent: module-scope mutable state must take the class + singleton form, multi-instance helper classes take no suffix, and what counts as state is defined.
Codify renderer services/<topic>/ topic directories (single barrel entry, private topic-specific satellites exempt from shape routing, no UI) mirroring the existing main-process rule, and add the middle growth step to Naming 4.10.
Align residual wording across the three docs: same-kind peering edges in the renderer dependency table, the section-6 corollary vs its own worked example, quick-reference suffix note, utils charter and examples.
Re-sync the "Target vs Current State" section with the code after the
dependency-cleanup landed. §8 now tracks only what is still unfinished:
- Drop the "Already aligned" list and the six resolved deviations
(config/, queue/, components/app/Navbar, the utils/ root barrel, and
the "Boundary enforcement: none" row) -- once resolved they no longer
violate the target, so they need not appear.
- Correct the components/app/Sidebar row: it is not dead code. It is
rendered by components/layout/AppShell.tsx (main-window shell), so it
folds into the App-shell row and moves to windows/main with that shell.
- Sync the enforcement status in §5 and §8: the import/no-restricted-paths
gate is live -- shared-layer edges at error, pages -> pages at warn -- and
the remaining pages -> pages coupling is ~13, not ~35.
- Keep only the still-open half of the barrel item: utils/message/ still
needs a curated index.ts.
Add a maintenance note so resolved deviations are dropped, not recorded
as done -- the "already aligned" list is what rots.
Record the placement rule settled during the renderer cleanup: utils/ is
stateless and domain-agnostic and MAY import downward infra
(data/ipc/workers), while services/ owns state/lifecycle or performs
business/domain orchestration. A helper touching infra is still a helper;
the decisive test is role, not purity.
- renderer-architecture.md: co-equal utils/data/ipc/workers foundation
(§2); positive service identity and the revised utils row (§3).
- naming-conventions.md §5.2: add the stateless-orchestration -> services
routing row; note utils may call downward infra.
- main-process-architecture.md: Pure -> Stateless; route by role.
Main previously imported all 12 renderer translation JSONs by relative path to
build its t() table — a main->renderer boundary violation that also inlined
~3.86 MB of translations the main process never uses. Main now owns a small,
independent catalog under src/main/i18n (~59 keys: app menu, tray, dialogs,
context menu, the OAuth callback page and a few shared strings), statically
imported for all 12 languages (~48 KB total).
- src/main/utils/language.ts -> src/main/i18n/index.ts (git mv): swap the renderer
JSON imports for the local catalog, add an en-US fallback to t(), and narrow the
now-internal `locales` export. AppMenuService reads getI18n() instead of the
locales map; the OAuth callback drops its duplicate translator for t().
- src/main/i18n/{locales,translate}/*.json: the main catalog, extracted from the
renderer catalog (zero translation loss), aligned and sorted across all 12 files.
- Renderer catalog: delete the now main-exclusive keys (appMenu.*, tray.*, dialog.*,
settings.mcp.oauth.callback.*, common.{inspect,paste,cut},
agent.session.workspace_status.{missing,not_directory}).
- Composer delete buttons used t('appMenu.delete') by mistake; switch to
t('common.delete') (byte-identical in all 12 languages) so appMenu is fully
main-owned.
- readErrorMessage: take a pre-translated `fallback` string instead of an i18next
key, and move it from @shared/ai into src/main/ai/provider/custom (main-only after
the change); its 6 provider call sites pass main's t(...).
- Tooling: check-i18n now validates the main catalog's 12 files plus a literal-t()
key-coverage check for main sources; sync-i18n and auto-translate-i18n cover both
catalogs. eslint bans relative **/renderer/** in main/preload; TopicNamingService
test reads the renderer catalog from disk instead of importing it.
- Docs: main-process-architecture.md records i18n as a governed top-level expansion
and closes the resolved deviation; CLAUDE.md lists the new directory.
The pre-migration backup was a redundant safety net: the migration engine
never deletes v1 data (it stays on disk, so a failed run is retryable), and
StartMigration never required or verified a backup. Removing it shortens the
wizard to introduction -> migration -> completion/error (progress rail 3 steps).
- shared: drop the backup_required/progress/confirmed stages, the
backupInfo/isCompressing fields, MigrationBackupInfo, and the 5 backup IPC
channels; delete the dead DataMigrate_* IpcChannel enum block (superseded by
MigrationIpcChannels, zero consumers)
- main: remove the backup handlers, LegacyBackupManager wiring, progress
bridge, and single-flight guards from MigrationIpcHandler; simplify
updateProgress; Retry now returns to the introduction stage
- main: flip to the protected `migration` stage before running the engine so
the destructive verifyAndClearNewTables() runs under the close-confirm /
write-deferral guard instead of the unprotected introduction stage
- renderer: the introduction Start button now runs the migration directly
(with a loading state; Skip is disabled while exporting); drop the backup
stages, choice UI, and completed-screen backup path; remove the now-dead
ProgressBar indeterminate branch and its keyframe
- i18n, docs, and tests updated accordingly
LegacyBackupManager and IpcChannel.BackupProgress are retained -- still used by
the in-app backup/restore feature.
useMutation's trigger was rebuilt on every render — the only non-memoized
function return in useDataApi.ts. Consumers place trigger in dependency
arrays, so the identity churn cascades downstream and can drive infinite
render loops (Maximum update depth exceeded).
Codify stable function identity as an official contract of the data-hook
layer, consistent with SWR's own mutate/trigger:
- Wrap useMutation's trigger in useCallback; options are still read via
optionsRef at call time, so option freshness is unchanged
- Wrap useInvalidateCache's invalidate in useCallback
- Wrap usePaginatedQuery's nextPage/prevPage/reset in useCallback, keyed
on their gating booleans so page clamping stays fresh
- Return a frozen shared EMPTY_ITEMS constant for usePaginatedQuery's
empty state and tighten the result type to readonly T[]
- Document the contract in data-api-in-renderer.md and pin it with
identity, option-freshness, and clamp regression tests
Refs #16696
The resources/database/drizzle SQL files and meta snapshots are v1-era CherryClaw agents-db migrations with no runtime consumer — the active v2 migrations live in migrations/sqlite-drizzle and are loaded from app.database.migrations. Delete the directory and clean up the now-dangling references: three source comments in AgentsDbMappings.ts that cited the deleted SQL files as the v1 column-type source (the epoch-ms notes are kept inline), and a stale doc row in cherryclaw/scheduler.md pointing to a migration file that no longer exists.
The setter updater contract previously said "must be pure" only in the sense of "don't mutate prev / return a new value" (the isEqual short-circuit footgun); it did not cover side effects inside the updater.
Document that updaters must also be side-effect-free: don't smuggle a derived value out (e.g. into an enclosing-scope variable) to drive post-write work, and don't rely on how often or when the updater runs. To react to what changed, derive it from the value transition in a useEffect that watches the value.
Deliberately does not promise single synchronous invocation, to keep the setter free to batch/retry/defer later. Updates the CacheSetStateAction type doc, the useCache @remarks (canonical reference for all three hooks), and cache-usage.md.
useCache, useSharedCache and usePersistCache setters now accept a React-style functional updater `(prev) => next` in addition to a concrete value. The updater resolves `prev` from the latest stored value at write time rather than the render-time snapshot, making read-modify-write correct across an `await` — the root cause of the keep-alive overwrite race behind #16460.
`prev` is typed shallow-readonly (`ReadonlyValue<T>`), so mutating it in place and returning the same reference — which the CacheService `isEqual` short-circuit would otherwise swallow silently — is a compile error. Concrete-value calls are unchanged, so existing consumers keep compiling.
The renderer useCache mock mirrors the functional branch with the same default fallback; docs and hook tests updated. Consumer call-site adoption lands separately.
Move the renderer-side AI-streaming runtime (IpcChatTransport,
TopicStreamSubscription, streamDispatchCoordinator) out of the top-level
src/renderer/transport/ directory into the shared services/aiTransport/
bucket. By shape these are stateful runtime singletons/classes, and the
runtime is cross-surface (consumed by chat, quick-assistant, selection),
so per the renderer architecture it routes into services/, not its own
top-level directory.
- Add a curated index.ts barrel exposing only the externally consumed
symbols (ipcChatTransport, TopicStreamSubscription, ExecutionTerminal);
the class, dispatch coordinator and helpers stay private.
- Update the 6 consumer sites (5 imports + 1 vi.mock) to the barrel.
- Sync architecture and AI docs to the new path; drop the now-resolved
transport/ deviation from the renderer-architecture pending table.
- Slim @shared/types/error.ts to the cross-process SerializedError shape
- Relocate serializeError (+ toSerializable) to src/main/ai/utils/serializeError.ts;
re-point the 4 main/ai consumers
- Delete the @shared-side dead AI-SDK subtype family + 23 isSerialized* guards
(utils/error.ts) and the unused ProviderSpecificError class
(renderer keeps its own live parallel copy)
- Move the serializeError test to main/ai/utils; assert discriminant fields directly
instead of via the deleted guards
- Doc: mark the section 6 error/serializable row resolved
Eliminate src/renderer/config entirely, completing the §8 dissolution
started in 1065cd4dfd. The two remaining files held a mix of genuinely
app-global constants, domain-owned constants, and v1 dead code.
- Add utils/platform.ts for the platform predicates (platform/isMac/
isWin/isLinux/isDev/isProd) and repoint ~60 cross-domain consumers.
- Relocate domain constants to their owners: LONG_TEXT_PASTE_THRESHOLD
-> composer/composerPaste; knowledge defaults -> knowledge/rag;
THEME_COLOR_PRESETS + defaultByPassRules -> CommonSettings;
MAX_COLLAPSED_CODE_HEIGHT -> CodeBlockView/constants;
API_SERVER_DEFAULTS -> ApiGatewaySettings; SiliconFlow/PPIO client ids
+ TOKENFLUX_HOST -> utils/oauth (kept below the consuming pages layer);
occupiedDirs -> BasicDataSettings (still @deprecated v1).
- Dissolve env.ts: inline the APP_NAME literal at AboutSettings and
import the avatar/logo PNGs directly at each consumer; drop the now
obsolete config/env test mocks (Vite resolves the assets natively).
- Delete v1 dead constants with no consumers (DEFAULT_TEMPERATURE,
DEFAULT_CONTEXTCOUNT, SYSTEM_PROMPT_THRESHOLD, message-count caps,
context caps, DEFAULT_STREAM_OPTIONS_INCLUDE_USAGE).
- Update renderer-architecture (§3/target layout/§8) and the preboot
doc/comment references that pointed at the old occupiedDirs location.
src/renderer/hooks/agents/ is a feature namespace grouping the agent
feature's hooks, not a collection of agents. Per naming conventions
§4.9 it should be singular, matching sibling namespaces hooks/chat/,
hooks/tab/, hooks/translate/.
- git mv src/renderer/hooks/agents -> src/renderer/hooks/agent
- update 62 import paths across 33 files
- clarify §4.9 so the plural `agents/` example is read by role
(collection bucket vs feature namespace), not by the word
Migrate window-bounds persistence off the electron-window-state library into
a WindowManager built-in `rememberBounds` capability, backed by the main-process
persist cache (`window.bounds` key — its first real consumer).
- New `windowBoundsTracker` free-function module: validates the stored record
(including displayBounds), restores onto the display the window was last on
(clamping into its work area, never resetting to primary), and snapshots at
teardown via getNormalBounds + isMaximized.
- Singleton-only gate (dev warning for non-singleton types). Runtime toggle
`wm.setRememberBounds` (orthogonal to the registry flag; OFF drops only that
type's slot) plus `wm.peekWindowBounds`.
- Persist at three teardown exits: native close (singletons), before
window.destroy() in destroyWindow (programmatic destroys), and a new onStop
so shutdown writes land before CacheService flushes its persist map.
- Wire Main + QuickAssistant. Main re-applies maximize consumer-side on its own
show schedule (tray-on-launch defers to first show); remove electron-window-state
and its orphaned keepers/constants/comments.
Fullscreen is not persisted and old *-state.json is not migrated (one-time
reset, loseable). Adds tracker/integration/persist tests, extends the main
CacheService mock with persist methods, and documents the capability plus a
breaking-change note.
State the previously-implicit rules that produced "features is an isolated
island" misreadings, then tighten the result for concision:
- Make explicit that the app layer (windows/routes/pages) is a feature's
only legal consumer (window/page -> feature are legal downward edges); a
feature is isolated only horizontally, from sibling features.
- Add the same-kind-peering vs same-slice-isolation distinction
(component -> component allowed; feature -> feature not) and note that a
feature-internal piece follows identical type-axis rules as its top-level
counterpart.
- Add a 4.1 Promotion Rule with an operational trigger and a
chat -> features/chat worked example; fix the dangling 4.4 references.
- Add the DAG rationale for the two banned edges (shared -> feature,
feature -> feature).
- Fix small inconsistencies: pages/ may depend on primitives; shared-layer
order includes workers; disambiguate bare (4.8) -> (Naming Conventions
4.8); reclassify transport/ as a cross-surface ai runtime (not
chat-exclusive) bound for the shared services/ bucket.
Also clean up two dead command mocks targeting the non-existent
@renderer/features/command path: fix the stale path in TopicBranchPanel's
test so the mock intercepts the real @renderer/components/command import,
and drop the unused mock in GlobalSearchPanel's test.
Make the main-authoritative and renderer persist tiers express presence as
deviation from the schema default rather than backing-store membership, extend
the API symmetrically, and unify the debounced-write cadence.
- hasPersist now reports whether the effective value differs from the schema
default (main + renderer). loadPersist/loadPersistCache seed every key, so the
old Map-membership check was always true and carried no information.
- Add deletePersist (reset-to-default) on both tiers; delegates to setPersist so
it inherits the same-value no-op, subscriber notify, and renderer cross-window
broadcast.
- Add subscribePersistChange on the main tier for in-main consumers, mirroring
subscribeChange (main-local, never relayed to renderers). setPersist now
notifies persist subscribers on actual change.
- Align persist debounce to 350ms (main + renderer) and BootConfig save debounce
to 350ms for more coalescing of paused/bursty writes; extract the renderer
magic number into a named constant.
- Normalize CacheService comments to JSDoc on core methods and document the
default-relative persist semantics; update cache-overview.md.
Add an independent, main-authoritative persist tier to the main-process
CacheService, stored as a JSON file at {userData}/cache.json. It is inline
(mirroring the renderer persist structure), fixed-keys-only, with no delete
or TTL; values are loseable and fall back to schema defaults on miss.
Writes are debounced (200ms) and flushed atomically (temp file + rename),
with a flush on service stop. Unknown/stale keys in the file are pruned on
load to keep the fixed-keys contract. The renderer persist IPC relay is
untouched: Main cannot read renderer persist and vice versa.
This phase is architecture-only: the schema ships a single scaffold key
(internal.persist_probe) and no business consumer; window-state and other
consumers will follow. Docs under docs/references/data and CLAUDE.md are
updated to distinguish the new Main persist store from the relay.
The context/ directory was a by-kind bucket: React providers are
components, not their own layer. Dissolve it per renderer-architecture
section 8 by splitting each provider into its component (-> components/)
and its context object + accessor hooks (-> hooks/), mirroring the
existing command decomposition.
- ThemeProvider -> components/ThemeProvider.tsx + hooks/useTheme.ts
- CodeStyleProvider -> components/CodeStyleProvider.tsx + hooks/useCodeStyle.ts
- Tab behavior layer -> hooks/tab/ (useTabsContext, useCurrentTab,
useTabSelfMetadata, useTabs, index); TabsProvider/TabIdProvider ->
components/layout/
- Extract pure emoji-icon logic to utils/tabIcons.ts
- Remove dead TabsContextValue ops (hibernateTab/wakeTab/setTabs) and a
stale ThemeProvider comment
- Repoint ~90 import sites and test mocks; migrate provider/hook tests
- Update routes README and renderer-architecture section 8
With ImportService migrated to DataApi (#16415), the v1 Redux store at
src/renderer/store/ has zero runtime consumers. Delete the directory (30
files) along with the already-stubbed main-process ReduxService bridge.
Clean up the now-stale residue:
- Remove dead vi.mock('@renderer/store') factories from 14 test files
whose code under test no longer imports the store.
- Drop Redux from the CLAUDE.md "Removing" enumerations and refresh the
renderer-architecture / knowledge-service / v2-todo docs.
- Fix dangling store/ references in the MiniAppMigrator and
builtinMcpServers comments.
The v2 migration's Redux Persist readers (ReduxStateReader etc.) are left
untouched: they read serialized on-disk data, not this store.
Dissolve the renderer types/index.ts re-export barrel (Phase 2):
- Repoint 252 consumers to @renderer/types/<topic> (intra-types files use
relative ./topic); covers @renderer/types, @types, and the equivalent
'./', './index', '@renderer/types/index' specifier forms.
- Delete src/renderer/types/index.ts.
- Remove the @types alias from tsconfig.web.json, electron.vite.config.ts,
and the eslint main/preload boundary guard.
- Retire the resolved types-barrel and @types deviations from the renderer
and main architecture docs.
Move src/shared/ipc/errors.ts to src/shared/ipc/errors/index.ts so each migrated domain can host its own error-code map as a sibling errors/<domain>.ts — value-importable by both processes and zod-free. The @shared/ipc/errors barrel path is unchanged, so every importer and test resolves identically (typecheck + IPC tests green).
Document the IpcErrorCode usage convention that previously lived only in code comments: the framework-code single source of truth, the open (string & {}) tail for domain codes, where domain codes belong (errors/, not schemas/, since the renderer may only import type from schemas), and why they are imported directly rather than aggregated through a barrel.
### What this PR does
**Before this PR**, Cherry Studio managed external CLI binaries through
five uncoordinated mechanisms — each requiring its own download script,
IPC channel, and on-disk layout:
| Mechanism | Where it lived | How it worked |
|---|---|---|
| rtk extraction | `src/main/utils/rtk.ts` + `AgentBootstrapService` |
Copies bundled binary to `~/.cherrystudio/bin/` |
| OpenClaw installer | `resources/scripts/install-openclaw.js` |
Downloads from GitHub/mirror with custom extraction |
| CodeCliService | `src/main/services/CodeCliService.ts` | `bun install
-g` to `~/.cherrystudio/install/global/` |
| ripgrep | `node_modules/@anthropic-ai/claude-agent-sdk/vendor/` |
Vendored, hardcoded path in `FileStorage` |
| (old) MiseService | `src/main/services/MiseService.ts` | Bundled mise
+ `mise use -g` |
**After this PR**, a single `BinaryManager` lifecycle service owns all
third-party CLI binary acquisition. It wraps
[mise](https://mise.jdx.dev) as the only acquisition backend (no custom
`BinaryBackend` interface — mise's polyglot grammar already covers
`npm:`, `pipx:`, `github:`, registry entries). Tools install into an
isolated environment under `~/.cherrystudio/mise/` so user-level mise
installs are never touched. `uv`, `bun`, `rg`, and mise itself ship
bundled at build time for instant first-run availability; everything
else flows through mise on demand.
<img width=\"1304\" height=\"714\" alt=\"BinaryManager settings UI\"
src=\"https://github.com/user-attachments/assets/7a4b78ab-5aa2-4e97-9ab7-134b20a4d78d\"
/>
<img width=\"1165\" height=\"748\" alt=\"Three-state managed vs bundled
vs not-installed\"
src=\"https://github.com/user-attachments/assets/a0dcfb7d-8bc3-4acd-b563-0fc04d99e252\"
/>
<img width=\"523\" height=\"328\" alt=\"Custom tool dialog\"
src=\"https://github.com/user-attachments/assets/90c3ee95-7f2a-4daf-a334-f20de6ff5ca2\"
/>
Fixes#15183. Addresses #15370.
### Why we need it and why it was done in this way
Adding a new managed CLI tool should be a one-line preset entry — not 4+
files of bespoke download/extract/IPC code. mise is a mature polyglot
tool manager that already speaks the backends Cherry needs.
**Tradeoffs made:**
- **mise bundled at build time** (~15 MB per platform) rather than
downloaded at first run — faster first-run UX, no chicken-and-egg on a
fresh install.
- **Fully isolated mise environment** (\`HOME\`/\`XDG_*\`/\`MISE_*\` all
relocated under \`feature.binaries.data\`) — Cherry never reads or
writes the user's own \`~/.config/mise/\` or \`~/.local/share/mise/\`.
- **No custom \`BinaryBackend\` interface.** mise's grammar (\`npm:\`,
\`pipx:\`, \`github:\`, registry) is already polyglot; wrapping it would
be a shallow seam that re-implements what mise owns. Removing this
abstraction makes consumers simpler (deletion test passes).
- **Auth-token policy: opt-in only.** Ambient \`GITHUB_TOKEN\` /
\`GH_TOKEN\` are not forwarded into mise's process env. Users who hit
GitHub's unauthenticated 60 req/hr API limit can set
\`CHERRY_GITHUB_TOKEN\` to raise it to 5000 req/hr without consenting to
share their general shell token.
- **China mirror auto-detection.** \`isUserInChina()\` toggles
\`NPM_CONFIG_REGISTRY=registry.npmmirror.com\` +
\`PIP_INDEX_URL=pypi.tuna.tsinghua.edu.cn\` for every npm/pipx backend
transparently.
**Alternatives considered:**
- *Keep per-tool install scripts.* Doesn't scale — each new tool is 4+
files of duplicated logic.
- *Use mise from user's \`PATH\`.* Would depend on user having mise
installed and could conflict with their config.
- *Custom \`BinaryBackend\` abstraction.* Shallow wrapper over mise's
grammar; no second backend in sight; deletion test passes.
**Scope (what's in / what's out):**
- **In:** uv, bun, ripgrep, claude-code, openclaw, gh, opencode,
gemini-cli, lark, kimi-cli, qwen-code, iflow-cli, github-copilot-cli —
anything mise can install as a single relocatable binary.
- **Out:** \`OvmsManager\` (multi-file server provisioning, hardware
detection, generated config); Tesseract OCR data (not a binary; lives
with \`OvOcrService\`).
### Breaking changes
None at the user-facing layer. v2 data is throwaway per CLAUDE.md, so
the preference-key rename (\`feature.mise.*\` → \`feature.binaries.*\`)
intentionally ships without a migrator.
### Special notes for your reviewer
**Architecture & docs**
- \`docs/references/binary-manager/README.md\` — scope criterion,
persisted/contract surface, bundled-vs-mise state contract, China mirror
behavior, \`CHERRY_GITHUB_TOKEN\` opt-in, adding a new managed binary.
- \`CLAUDE.md\` adds a \`**MUST READ**\` link next to Lifecycle / Window
Manager / Data / Paths.
- The mise-shim-wins-over-\`cherry.bin\` precedence rule is documented
in the README's "State contract" section.
**Code orientation**
- \`src/main/services/BinaryManager.ts\` — the lifecycle service. Wraps
mise via \`runMise()\`; isolated env in \`buildIsolatedEnv()\`; bundled
extraction with atomic tmp+rename; per-tool try/catch so a single
failure can't abort init; \`isManagedBinaryReady()\` verifies the
resolved file is executable (not just that mise thinks it's installed).
- \`packages/shared/data/presets/binary-tools.ts\` —
\`PREDEFINED_BINARY_TOOLS\` registry; \`tool\` field is a mise spec.
-
\`src/renderer/src/pages/settings/McpSettings/EnvironmentDependencies.tsx\`
— three-state UI (\`managed\` / \`bundled\` / \`not-installed\`) backed
by \`Binary_GetState\` + \`Binary_ProbeBundled\`.
- \`scripts/download-binaries.js\` — build-time downloader for mise / uv
/ bun / rg (HTTPS + sha256-verified, archive-aware extraction).
**Migration**
- \`OpenClawService.install()\` is now two lines delegating to
\`BinaryManager\` — \`install-openclaw.js\` is gone.
- \`CodeCliService\` no longer uses \`bun install -g\`; the
\`BUN_INSTALL\` / \`~/.cherrystudio/install/global/\` path is removed.
- \`FileStorage.getRipgrepBinaryPath()\` now resolves via
\`getBinaryPath('rg')\`; the vendored SDK rg is no longer used.
- \`extractRtkBinaries\` + the \`AgentBootstrapService\` call are
deleted. \`rtkRewrite()\` degrades gracefully when rtk is absent;
\`rtkAvailable\` caches with a 60s TTL so install-via-mise takes effect
without restart.
**Multi-round review**
Two adversarial review rounds against this branch (Bug Hunter / Security
/ Architecture / Correctness) ran during development; both rounds'
High-severity findings are addressed in commits \`70afde6af\` and
\`1d864439d\`. R3 known follow-ups (architecture duplications in
\`CodeCliService\`'s switch statements, etc.) are tracked as Medium and
intentionally deferred.
### Checklist
- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and Keep it simple
- [x] Refactor: Leaves binary acquisition meaningfully cleaner than
before (five mechanisms → one)
- [x] Upgrade: v2 data is throwaway; no migrator needed and the absence
is intentional
- [x] Documentation: \`docs/references/binary-manager/README.md\` +
\`CLAUDE.md\` Architecture section
- [x] Self-review: Two multi-perspective review rounds against the
branch; all Highs addressed
### Release note
\`\`\`release-note
NONE
\`\`\`
---------
Signed-off-by: Vaayne <liu.vaayne@gmail.com>
Signed-off-by: Vaayne Liu <vaayne@macos.shared>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Renderer windows set their logger source inline via initWindowSource() in
entryPoint.tsx, which had to execute before any import-time log or the source
fell back to 'UNKNOWN'. ESM hoists imports above statements, so this ordering
was fragile and applied inconsistently across windows.
LoggerService now derives the source at construction from a
<meta name="logger-window-source"> tag in each window's index.html. The meta
is parsed before any module script runs, so the source is set before any
import-time log -- no ordering rules in entryPoint.tsx. initWindowSource() is
kept as an explicit override for documentless contexts (workers, tests) and
takes precedence over the derived value.
- Add resolveWindowSourceFromMeta() and derivedWindow with
explicit > derived > UNKNOWN precedence; export the LoggerService class
- Declare the meta in all 7 window index.html files (source strings unchanged)
- Drop the 7 inline initWindowSource() calls and the subWindow initLogger.ts
module; keep the worker's explicit initWindowSource('Worker')
- Update windows/README.md and docs/guides/logging.md
- Add LoggerService unit tests