AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs
## Guiding Principles (MUST FOLLOW)
### Mindset
How to approach any coding task in this repo.
#### Think Before Coding
- State assumptions explicitly. If uncertain, ask before implementing.
- When multiple interpretations exist, surface them β do not pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what is confusing. Ask.
#### Simplicity First
- Write the minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that was not requested.
- No error handling for impossible scenarios.
- If you wrote 200 lines and it could be 50, rewrite it.
- Inline comments cap at 2 lines. Needing more means the code is a patch β fix the implementation instead of narrating it. Say *why*, never restate *what*; no changelogs, no rationale essays, no pasted chat/review replies. (Doc comments on an exported API β TSDoc `@param`/`@returns`/`@deprecated` β are documentation, not narration, and are exempt.)
#### Surgical Changes
- Touch only what the task requires. Do not "improve" adjacent code, comments, or formatting.
- Do not refactor things that are not broken.
- Match existing style even if you would do it differently.
- If you notice unrelated dead code, mention it β do not delete it.
- Remove imports / variables / functions that **your** changes orphaned. Leave pre-existing dead code alone unless asked.
- Every changed line must trace directly to the user's request.
#### Goal-Driven Execution
- Convert tasks into verifiable goals before coding:
- "Add validation" β "Write tests for invalid inputs, then make them pass."
- "Fix the bug" β "Write a test that reproduces it, then make it pass."
- "Refactor X" β "Ensure tests pass before and after."
- For multi-step tasks, state a brief plan with explicit verification per step:
```
1. [Step] β verify: [check]
2. [Step] β verify: [check]
```
### Operational Rules
Project-specific tools, paths, and conventions.
- **Keep it clear**: Write code that is easy to read, maintain, and explain.
- **Read local READMEs first**: Before editing code in a directory, check for a `README.md` in that directory (and its parents) and read it β these files capture local conventions, invariants, and entry points that aren't obvious from the code alone.
- **Fix upstream, don't hack downstream**: When a new feature hits an existing module's limitation, flag the upstream improvement for the user's decision before proposing a downstream workaround.
- **Library-first, custom-last**: Before writing custom code, check library/framework docs for built-in options or existing solutions. Write custom code only when no adequate alternative exists.
- **Build with Tailwind CSS & Shadcn UI**: Use components from `@cherrystudio/ui` (located in `packages/ui`, Shadcn UI + Tailwind CSS) for every new UI component.
- **Log centrally**: Route all logging through `loggerService` with the right contextβno `console.log`.
- **Access paths centrally**: Use `application.getPath('namespace.key', filename?)` for all main-process filesystem pathsβnever call `app.getPath()`, `os.homedir()`, or construct paths ad-hoc. Import the singleton via `import { application } from '@application'`.
- **Lint, test, and format before completion**: Coding tasks are only complete after running `pnpm lint`, `pnpm test`, and `pnpm format` successfully.
- **Write conventional commits**: Commit small, focused changes using Conventional Commit messages (e.g., `feat(data-api):`, `fix(lifecycle):`, `refactor(quick-assistant):`, `docs(testing):`, `chore(deps):`, `test(window-manager):`). Scope must be a specific kebab-case module, never generic like `main` β when `git log` conflicts with this rule, this rule wins.
- **Sign commits and sign off**: Every commit must be both cryptographically signed and DCO-signed off. Use `git commit -S --signoff` (not `--signoff` alone), verify the commit object contains a `gpgsig` header with `git cat-file commit HEAD`, and verify the pushed PR commits show `Verified` on GitHub.
- **Target the right branch**: `main` is the default branch for all active development β submit features, refactors, optimizations, and fixes here.
## Development
### Commands
Run `pnpm install` first (Node and pnpm versions are pinned in `package.json` β let it enforce them). For every other script, read `package.json` β the ones you must know:
- `pnpm lint` β oxlint + eslint fix + typecheck + i18n check + format (writes files)
- `pnpm test` β run all Vitest tests
- `pnpm format` β Biome format + lint (write mode)
- `pnpm build:check` β **REQUIRED before commits**. If it fails on i18n sort, run `pnpm i18n:sync` first; on formatting, run `pnpm format` first; on broken doc links, fix the link.
- `pnpm test:lint` β the CI-equivalent lint gate: it denies oxlint warnings that `pnpm lint` / `pnpm build:check` silently tolerate; run it when CI must pass.
### Testing
- Tests run with Vitest 3 (see `vitest.config.*` for project setup).
- **No behavior-pinning tests**: a test whose only assertion records what the code currently does β a snapshot of whatever came out, `toHaveBeenCalled` on a mock, an expected value re-derived the way the implementation derives it β has zero value. It cannot fail for a real reason, it breaks on every refactor, and it certifies existing bugs as "expected". Assert the contract instead: real input β the outcome the feature promises, plus the failure and edge cases. Before writing a test, state the bug it would catch; if you cannot, do not write it. **The existing suite is full of these** β delete the ones in a file you are already editing rather than keeping them green; a repo-wide purge is its own task, not a side effect of an unrelated PR.
- **Frontend Tests β MUST READ**: [Frontend Testing Guidelines](docs/references/testing/frontend-testing.md).
- **Test Mocking**: Use the unified mock system β do NOT create ad-hoc mocks for `application`, services, or data layers. See [tests/__mocks__/README.md](tests/__mocks__/README.md) for available mocks, usage patterns, and best practices.
- **Database Tests**: For any service/handler/seeder that reads or writes SQLite, use `setupTestDatabase()` from `@test-helpers/db` β it provides a real file-backed DB with production migrations. Do NOT hand-write `CREATE TABLE` SQL, override `@application`, or stub Drizzle chains. See [docs/references/testing/database-testing.md](docs/references/testing/database-testing.md).
### Patched Dependencies
Before upgrading any dependency, check `patches/` for custom patches.
## GitHub
### Pull Requests
Use the `gh-create-pr` skill. Fallback: read `.agents/skills/gh-create-pr/SKILL.md` directly.
### Code Review
When reviewing a GitHub PR, do NOT run `pnpm lint` / `pnpm test` / `pnpm format` locally β its CI already ran them; inspect via `gh` instead.
### Issues
Use the `gh-create-issue` skill. Fallback: read `.agents/skills/gh-create-issue/SKILL.md` directly.
## Conventions
### TypeScript
- Cross-process types belong in `src/shared/`; renderer-only shared types in `src/renderer/types/` (see [Shared Layer Architecture](docs/references/shared-layer-architecture.md)).
### Naming Conventions
**MUST READ**: [docs/references/naming-conventions.md](docs/references/naming-conventions.md) β files, directories, identifiers, and singular/plural rules.
### Logging
```typescript
import { loggerService } from "@logger";
const logger = loggerService.withContext("moduleName");
// Renderer only: loggerService.initWindowSource('windowName') first
logger.info("message", CONTEXT);
logger.warn("message");
logger.error("message", error);
```
### Paths
**MUST READ**: [src/main/core/paths/README.md](src/main/core/paths/README.md) β namespaces, naming, adding new keys, testing patterns. (Rule stated in Guiding Principle "Access paths centrally".)
### i18n
- All user-visible strings must use `i18next` β never hardcode UI strings
- Run `pnpm i18n:check` to validate; `pnpm i18n:sync` to add missing keys
- Locale files in `src/renderer/i18n/`
### UI Design
For any UI component or page style work, read [DESIGN.md](./DESIGN.md) first and follow its colors, fonts, spacing, and component specs strictly.
## Architecture
### Code Organization
Where each file and directory belongs β read the doc for the process you're touching before adding code or opening a directory. Each process root's top level is a **closed set**: route new code into an existing category, never a new top-level directory ([Naming Conventions Β§4.8](docs/references/naming-conventions.md)).
A directory's `index.ts` is a **barrel** β an enforced encapsulation boundary re-exporting one cohesive public API (internals private, outsiders import through it): re-export only (no logic / `export *`), no nesting, and it exists only if lint can seal off deep imports β else no barrel. `index.tsx` is always banned ([Naming Conventions Β§6.4](docs/references/naming-conventions.md)).
- [Main Process Architecture](docs/references/main-process-architecture.md) β `src/main/` directories (`core`/`ipc`/`data`/`ai`/`features`/`services`/`utils`/`i18n`) and dependency direction.
- [Renderer Architecture](docs/references/renderer-architecture.md) β `src/renderer/` two-axis (type Γ domain) layout and downward-only layering.
- [Shared Layer Architecture](docs/references/shared-layer-architecture.md) β what belongs in `@shared` (cross-process + no mutable runtime state) and its closed top-level set.
### Data
**MUST READ**: [docs/references/data/README.md](docs/references/data/README.md) for system selection, architecture, and patterns.
| System | Use Case | APIs |
| ---------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------- |
| [BootConfig](docs/references/data/boot-config-overview.md) | Early boot settings (pre-lifecycle) | `bootConfigService.get()`, `usePreference('BootConfig.*')` |
| [Cache](docs/references/data/cache-overview.md) | Temp data (can lose) | `useCache`, `useSharedCache`, `useSharedCacheValue`, `usePersistCache` |
| [Preference](docs/references/data/preference-overview.md) | User settings | `usePreference` |
| [DataApi](docs/references/data/data-api-overview.md) | Business data (**critical**) | `useQuery`, `useMutation` |
Scope:
- **BootConfig**: sync file-based; direct in main (pre-lifecycle), via `usePreference('BootConfig.*')` otherwise
- **Cache**: memory / shared (cross-window) / persist tiers; memory + shared on both main and renderer; persist on both too but as **independent** stores (renderer = localStorage, main = JSON file at `{userData}/cache.json`), never shared β main additionally relays renderer persist sync between windows
- **Preference**: cross-process (main + renderer); auto-syncs across windows
- **DataApi**: SQLite-backed; no auto-sync, fetch on demand from renderer
Database: SQLite via **better-sqlite3** + Drizzle ORM β the driver is **synchronous** (queries and transactions run inline with no `await`, unlike the app's otherwise-async data layers), so `getDb()` queries and `withWriteTx(fn)` callbacks must be written synchronously. Schemas in `src/main/data/db/schemas/`, migrations via `pnpm db:migrations:generate`
**Write atomicity**: use `application.get('DbService').withWriteTx(fn)` to commit multiple writes (or a read-then-write) all-or-nothing in one synchronous `BEGIN IMMEDIATE` transaction; `fn` must be synchronous. A single write doesn't need it β better-sqlite3 runs each statement atomically on its one connection. See [Database Patterns β Write Serialization](docs/references/data/database-patterns.md#write-serialization-dbservicewritewritetx).
**DataApi boundary rule**: DataApi is for SQLite-backed business data only. No database table β no DataApi endpoint; use IPC instead. See [Scope & Boundaries](docs/references/data/api-design-guidelines.md#dataapi-scope--boundaries).
### IPC (IpcApi)
**MUST READ**: [docs/references/ipc/README.md](docs/references/ipc/README.md) β paradigm boundary (RPC vs REST), schema/router/preload/facade layering, `IpcContext`, error model, security.
Non-data command IPC (window/system/shell/notification/external/file) goes through **IpcApi** β the fifth subsystem alongside BootConfig/Cache/Preference/DataApi, RPC-over-IPC with single-point schemas (`schema + handler` to add a route; `ipcApi.request('namespace.action', input)` to call; `IpcApiService.broadcast`/`send` + `useIpcOn` for events). Legacy command IPC still coexists, so you'll encounter both. Decision: SQLite data β DataApi; user setting β Preference; losable/shared β Cache; everything else imperative β IpcApi.
### Window Manager
**MUST READ**: [docs/references/window-manager/README.md](docs/references/window-manager/README.md) β lifecycle modes, pool mechanics, API reference.
All `BrowserWindow` goes through `WindowManager` with one of three modes (`default` / `singleton` / `pooled`), declared per type in `src/main/core/window/windowRegistry.ts`.
- **Consumer API**: use only `open()` / `close()` β never `create()` / `destroy()` in business code.
- **Attach listeners in `onWindowCreated`**, not after `open()` β reused windows skip the latter.
- **Renderer reads init data via `useWindowInitData`**.
### Main Process Services (Lifecycle)
**MUST READ**: [docs/references/lifecycle/README.md](docs/references/lifecycle/README.md) β architecture, decision guides, usage patterns, and migration steps.
All main-process services that own long-lived resources or register persistent side effects **must** use the lifecycle system:
- **Extend `BaseService`**, apply `@Injectable`, `@ServicePhase`, `@DependsOn` decorators
- **Register in `serviceRegistry.ts`** (`src/main/core/application/serviceRegistry.ts`) β one line per service
- **Use `@DependsOn` for same-phase dependencies only** β do NOT declare dependencies on BeforeReady services (`PreferenceService`, `DbService`, `CacheService`, `DataApiService`) from WhenReady services; phase ordering is auto-enforced by the container
- **Access via `application.get('Name')`** (or `getOptional()` for `@Conditional` services)
- **Use `this.ipcHandle()` / `this.ipcOn()`** for IPC β auto-cleaned on stop/destroy, returns `Disposable`
- **Use `this.registerInterval()`** for recurring timers β auto-unref'd, exception-isolated, auto-cleaned on stop/destroy, returns `Disposable`
- **Use `this.registerDisposable()`** for cleanup tracking β accepts `Disposable` objects or `() => void` cleanup functions
- **Use `Emitter<T>` / `Event<T>`** for inter-service events, **`Signal<T>`** for one-shot completion
- **Implement `Activatable`** for services with heavy on-demand resources (IPC stays registered, resources load/release via `onActivate()`/`onDeactivate()`)
- **Do NOT** use `new` or manual singleton patterns β the container manages instantiation, ordering, and shutdown
For detailed code examples, see [Usage Guide](docs/references/lifecycle/lifecycle-usage.md). For migrating legacy services, see [Migration Guide](docs/references/lifecycle/lifecycle-migration-guide.md).
### Non-Lifecycle Services (Direct-Import Singleton)
Services without long-lived resources or persistent side effects: use **named export singleton** (`export const x = new X()`). No `getInstance()` patterns. See [Decision Guide](docs/references/lifecycle/lifecycle-decision-guide.md) for criteria.
## Schema & Migration Rules
The v2 refactor has landed. v1 data reaches v2 only through the migrators in `src/main/data/migration/v2/` β never add fallbacks, dual-writes, or guards for v1 save / read / loss.
**The migration chain is no longer throwaway.** It was consolidated into a single clean initial migration and shipped with `v2.0.0-rc.1`, so `migrations/sqlite-drizzle/` now runs against databases holding real user rows. Never wipe or rewrite an already-shipped migration, and never tell a user to delete their database: schema changes go in as new appended migrations generated by `pnpm db:migrations:generate`. `src/main/data/db/schemas/` still changes freely β but every change must survive a migrate-forward on a populated database.
**Resolving migration merge conflicts: regenerate, never rename.** When an upstream migration conflicts with your local one, delete your local `.sql` + its `meta/*_snapshot.json` and re-run `pnpm db:migrations:generate`. Renaming/renumbering instead silently reuses the snapshot's random `id`, forking the chain for everyone β and `drizzle-kit generate` still exits `0`; only `pnpm db:migrations:check` catches it. CI enforces both the chain check and a schemaβmigration generate-and-diff step.
### Data Classification Toolchain
`v2-refactor-temp/tools/data-classify/` is the code generation pipeline for the v2 data layer; `classification.json` is the single source of truth (see its README). Four files are **auto-generated β NEVER edit them by hand**: `src/shared/data/preference/preferenceSchemas.ts`, `src/shared/data/bootConfig/bootConfigSchemas.ts`, and `PreferencesMappings.ts` + `BootConfigMappings.ts` in `src/main/data/migration/v2/migrators/mappings/`. To change them, edit `classification.json` or `target-key-definitions.json` (both in `data/`), then run `cd v2-refactor-temp/tools/data-classify && npm run generate`.
### Breaking Changes Log
When a v2 change is user-perceivable and affects how users use the app, add an entry under `v2-refactor-temp/docs/breaking-changes/`. See [v2-refactor-temp/docs/breaking-changes/README.md](v2-refactor-temp/docs/breaking-changes/README.md) for conventions.
## Local Instructions
If `CLAUDE.local.md` exists in the repository root (gitignored, may be absent), read it in full before acting on anything in this file β it holds the developer's private instructions and **OVERRIDES this file wherever they conflict**. Tools that auto-load it (e.g. Claude Code) need not re-read it.