How to approach any coding task in this repo.
- 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.
- 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.
- 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.
- 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]
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.mdin 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.
- Research via subagent: Lean on
subagentfor external docs, APIs, news, and references. - Build with Tailwind CSS & Shadcn UI: Use components from
@cherrystudio/ui(located inpackages/ui, Shadcn UI + Tailwind CSS) for every new UI component; never addantd,HeroUI, orstyled-components. - Log centrally: Route all logging through
loggerServicewith the right context—noconsole.log. - Access paths centrally: Use
application.getPath('namespace.key', filename?)for all main-process filesystem paths—never callapp.getPath(),os.homedir(), or construct paths ad-hoc. Import the singleton viaimport { application } from '@application'. - Lint, test, and format before completion: Coding tasks are only complete after running
pnpm lint,pnpm test, andpnpm formatsuccessfully. - 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 likemain— whengit logconflicts with this rule, this rule wins. - Keep history linear: On shared branches, never use plain
git pull— it creates merge commits. Alwaysgit pull --rebase(orgit fetch && git rebase origin/<branch>). Beforegit push, rungit fetch; iforigin/<branch>has advanced, rebase your local commits onto it first. If you notice a merge commit in local history that hasn't been pushed yet, rebase it away — cleaning one up after it's public requires a risky force-push on a shared branch. - Sign commits: Use
git commit --signoffas required by contributor guidelines. - Target the right branch:
mainis the default branch for active development — submit features, refactors, optimizations, and fixes for the current codebase here. v1 maintenance fixes (hotfixes and subsequent v1 releases) must branch from and target thev1branch (nevermain); a v1 fix does not auto-carry tomain, so forward-port it with a separate PR if the bug also exists onmain. See v2 Refactoring.
Run pnpm install first (requires Node ≥22, pnpm 10.27.0). For every other script, read package.json — the ones you must know:
pnpm lint— oxlint + eslint fix + typecheck + i18n check + format checkpnpm test— run all Vitest testspnpm format— Biome format + lint (write mode)pnpm build:check— REQUIRED before commits (pnpm lint && pnpm test). If it fails on i18n sort, runpnpm i18n:syncfirst; on formatting, runpnpm formatfirst.
- Tests run with Vitest 3 (see
vitest.config.*for project setup). - Features without tests are not considered complete
- Test Mocking: Use the unified mock system — do NOT create ad-hoc mocks for
application, services, or data layers. See 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-writeCREATE TABLESQL, override@application, or stub Drizzle chains. See docs/references/testing/database-testing.md.
Before upgrading any dependency, check patches/ for custom patches.
Use the gh-create-pr skill. Fallback: read .agents/skills/gh-create-pr/SKILL.md directly.
Do NOT run pnpm lint / pnpm test / pnpm format locally — inspect CI via gh instead.
Use the gh-create-issue skill. Fallback: read .agents/skills/gh-create-issue/SKILL.md directly.
- Place shared type definitions in
src/renderer/types/orsrc/shared/.
MUST READ: docs/references/naming-conventions.md — files, directories, identifiers, and singular/plural rules.
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);- Never use
console.log— always useloggerService
MUST READ: src/main/core/paths/README.md — namespaces, naming, adding new keys, testing patterns. (Rule stated in Guiding Principle "Access paths centrally".)
- All user-visible strings must use
i18next— never hardcode UI strings - Run
pnpm i18n:checkto validate;pnpm i18n:syncto add missing keys - Locale files in
src/renderer/i18n/
For any UI component or page style work, read DESIGN.md first and follow its colors, fonts, spacing, and component specs strictly.
MUST READ: docs/references/data/README.md for system selection, architecture, and patterns.
| System | Use Case | APIs |
|---|---|---|
| BootConfig | Early boot settings (pre-lifecycle) | bootConfigService.get(), usePreference('BootConfig.*') |
| Cache | Temp data (can lose) | useCache, useSharedCache, usePersistCache |
| Preference | User settings | usePreference |
| DataApi | 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 is renderer-only (main relays IPC but doesn't store)
- Preference: cross-process (main + renderer); auto-syncs across windows
- DataApi: SQLite-backed; no auto-sync, fetch on demand from renderer
Database: SQLite + Drizzle ORM, schemas in src/main/data/db/schemas/, migrations via pnpm db:migrations:generate
Write serialization: concurrent write paths MUST go through application.get('DbService').withWriteTx(fn) instead of db.transaction(fn) to avoid SQLITE_BUSY from libsql client-ts upstream issue #288. See Database Patterns — Write Serialization.
DataApi boundary rule: DataApi is for SQLite-backed business data only. No database table → no DataApi endpoint; use IPC instead. See Scope & Boundaries.
MUST READ: 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). Framework shipped (Stage 0); domains migrate incrementally and coexist with legacy IPC. Decision: SQLite data → DataApi; user setting → Preference; losable/shared → Cache; everything else imperative → IpcApi.
MUST READ: 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()— nevercreate()/destroy()in business code. - Attach listeners in
onWindowCreated, not afteropen()— reused windows skip the latter. - Renderer reads init data via
useWindowInitData.
MUST READ: 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,@DependsOndecorators - Register in
serviceRegistry.ts(src/main/core/application/serviceRegistry.ts) — one line per service - Use
@DependsOnfor 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')(orgetOptional()for@Conditionalservices) - Use
this.ipcHandle()/this.ipcOn()for IPC — auto-cleaned on stop/destroy, returnsDisposable - Use
this.registerInterval()for recurring timers — auto-unref'd, exception-isolated, auto-cleaned on stop/destroy, returnsDisposable - Use
this.registerDisposable()for cleanup tracking — acceptsDisposableobjects or() => voidcleanup functions - Use
Emitter<T>/Event<T>for inter-service events,Signal<T>for one-shot completion - Implement
Activatablefor services with heavy on-demand resources (IPC stays registered, resources load/release viaonActivate()/onDeactivate()) - Do NOT use
newor manual singleton patterns — the container manages instantiation, ordering, and shutdown
For detailed code examples, see Usage Guide. For migrating legacy services, see Migration Guide.
Services without long-lived resources or persistent side effects: use named export singleton (export const x = new X()). No getInstance() patterns. See Decision Guide for criteria.
Current state — read before contributing. The former
v2branch has been merged intomain;mainis now the default branch for active development, with v1 and v2 code coexisting. Expect large, frequent, breaking changes — code you touch today may be deleted or reshaped tomorrow. Before touching subsystems being replaced, read docs/references/data to learn which are being deleted, and heed@deprecatedannotations in the code — they mark call sites slated for removal. v1 maintenance fixes (hotfixes and subsequent v1 releases) go to thev1branch, notmain; forward-port tomainwith a separate PR if the bug also exists there.
- Removing: Redux, Dexie, ElectronStore
- Adopting: Cache / Preference / DataApi architecture (see Data)
- Prohibited: antd, HeroUI, styled-components
- Adopting:
@cherrystudio/ui(located inpackages/ui, Tailwind CSS + Shadcn UI)
Two things on this branch are throwaway — do not defend them.
v1 is throwaway. "v1" here means the legacy data stacks listed in Data Layer above (Redux, Dexie, ElectronStore) and any call site that reads or writes through them. All such code will be deleted; v1 data reaches v2 only through the migrators in src/main/data/migration/v2/. So: no fallbacks, dual-writes, or guards for v1 save / read / loss; no fixing v1 bugs encountered during v2 work; leave mixed-branch v1 code alone unless it blocks v2.
Schemas and drizzle SQL are throwaway. src/main/data/db/schemas/ may change freely; migrations/sqlite-drizzle/*.sql are dev-only artifacts overwritten by drizzle-kit generate on every schema change. Mid-development DB drift is acceptable — do not author patch migrations to "fix" it. migrations/sqlite-drizzle/ will be wiped and regenerated from the final schemas as a single clean initial migration before release; only that regenerated migration must be correct.
Resolving migration merge conflicts: regenerate, never rename. When a merge/rebase brings in an upstream migration that conflicts with your local one, delete your local migration .sql + its meta/*_snapshot.json and re-run pnpm db:migrations:generate. Never just rename/renumber the .sql or hand-edit the snapshot to make room — renaming silently reuses the snapshot's random id, which forks the chain and makes pnpm db:migrations:generate abort for everyone (#15438), and leaves the schema source diverged from the migration SQL. Note drizzle-kit generate exits 0 even on a forked chain, so it will not warn you; only pnpm db:migrations:check (drizzle-kit check) does. CI enforces both — chain integrity via db:migrations:check and schema↔migration drift via a generate-and-diff step.
The v2-refactor-temp/tools/data-classify/ directory is the code generation pipeline for the v2 data layer. classification.json is the single source of truth.
The following four files are auto-generated — NEVER edit them by hand:
src/shared/data/preference/preferenceSchemas.tssrc/shared/data/bootConfig/bootConfigSchemas.tssrc/main/data/migration/v2/migrators/mappings/PreferencesMappings.tssrc/main/data/migration/v2/migrators/mappings/BootConfigMappings.ts
To change any of them, edit classification.json or target-key-definitions.json, then regenerate:
cd v2-refactor-temp/tools/data-classify && npm run generateWhen 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 for conventions.
- Never expose Node.js APIs directly to renderer; use
contextBridgein preload - Validate all IPC inputs in main process handlers
- URL sanitization via
strict-url-sanitise - IP validation via
ipaddr.js(API server) express-validatorfor API server request validation