# AGENTS.md Guidance for AI coding assistants (Claude Code, Codex, Gemini CLI, …) working in this repository. Claude Code reads this file automatically; a local `CLAUDE.md` (gitignored per `.gitignore`) may override or extend it per-developer. ## Project Overview **CC Switch** is a cross-platform Tauri 2 desktop application that manages configurations for multiple AI coding CLIs: **Claude Code, Codex, Gemini CLI, OpenCode, and OpenClaw**. It provides provider switching, unified MCP/Prompts/Skills management, a local proxy with failover, usage tracking, session browsing, and cloud sync — all backed by a SQLite SSOT. - **Frontend**: React 18 + TypeScript + Vite + TailwindCSS 3.4 + shadcn/ui - **Backend**: Rust (Tauri 2.8) with SQLite (`rusqlite`) persistence - **State/cache**: TanStack Query v5 on the frontend; `Mutex` on the backend - **IPC**: Tauri commands (camelCase names) wrapped by a typed frontend API layer - **i18n**: `react-i18next` with `zh` / `en` / `ja` locales (Chinese is the primary UI language) ## Repository Layout ``` ├── src/ # Frontend (React + TypeScript) │ ├── App.tsx # Root shell — view routing, headers, dialogs │ ├── main.tsx # Bootstrap, providers, config-error handling │ ├── components/ │ │ ├── providers/ # Provider CRUD (cards, forms, dialogs) │ │ ├── mcp/ # Unified MCP panel + wizard │ │ ├── prompts/ # Prompts panel (Markdown editor) │ │ ├── skills/ # Skills install/management + repo manager │ │ ├── sessions/ # Session manager (history browser) │ │ ├── proxy/ # Proxy + failover panels │ │ ├── openclaw/ # OpenClaw-specific config panels │ │ ├── settings/ # Settings pages (theme, dir, webdav, proxy, about…) │ │ ├── deeplink/ # ccswitch:// import confirmation dialogs │ │ ├── env/ # Env conflict warning banner │ │ ├── universal/ # Cross-app (universal) provider UI │ │ ├── usage/ # Usage dashboard, charts, pricing │ │ ├── workspace/ # OpenClaw workspace/agent file editor │ │ └── ui/ # shadcn/ui primitives (button, dialog, ...) │ ├── hooks/ # Custom React hooks (business logic glue) │ ├── lib/ │ │ ├── api/ # Typed Tauri IPC wrappers (one module per domain) │ │ ├── query/ # TanStack Query config + query keys │ │ ├── schemas/ # Zod schemas (provider/mcp/settings/common) │ │ ├── errors/ # Error parsing helpers │ │ ├── utils/ # Small helpers (base64, ...) │ │ ├── authBinding.ts # Auth binding helpers │ │ ├── clipboard.ts # Clipboard utils │ │ ├── platform.ts # OS detection (isMac/isWin/isLinux) │ │ └── updater.ts # Updater helpers │ ├── contexts/UpdateContext.tsx │ ├── i18n/ # i18next init + locales (en/zh/ja) │ ├── config/ # Static presets (providers, mcp) │ ├── icons/ # Provider icon index │ ├── types.ts, types/ # Shared TypeScript types │ └── utils/ # DOM/error helpers │ ├── src-tauri/ # Backend (Rust + Tauri 2) │ ├── Cargo.toml # rust-version = 1.85 │ ├── tauri.conf.json # Deep link, updater, bundling config │ ├── capabilities/ # Tauri permission manifests │ └── src/ │ ├── lib.rs # App entry, tray, deep-link, setup │ ├── main.rs # Binary entry delegating to lib │ ├── commands/ # Tauri #[command] layer (by domain, mod.rs re-exports *) │ │ # auth, provider, mcp, prompt, skill, proxy, │ │ # session_manager, settings, usage, webdav_sync, … │ ├── services/ # Business-logic layer │ │ ├── provider/ # ProviderService (CRUD, switch, live sync, auth, usage) │ │ ├── mcp.rs # McpService │ │ ├── prompt.rs # PromptService │ │ ├── skill.rs # SkillService │ │ ├── proxy.rs # ProxyService (hot-switching local proxy) │ │ ├── config.rs # ConfigService (import/export, backups) │ │ ├── speedtest.rs # Endpoint latency │ │ ├── webdav*.rs # WebDAV sync engine + auto-sync │ │ └── usage_stats.rs # Usage aggregation │ ├── database/ │ │ ├── mod.rs # Database struct, Mutex, hooks │ │ ├── schema.rs # Schema + migration (SCHEMA_VERSION = 6) │ │ ├── migration.rs # JSON → SQLite migration │ │ ├── backup.rs # Snapshot + SQL export │ │ └── dao/ # providers, mcp, prompts, skills, settings, proxy, │ │ # failover, stream_check, usage_rollup, universal_providers │ ├── proxy/ # Local HTTP proxy (forwarder, circuit breaker, SSE, │ │ # failover, model mapping, thinking rectifier, …) │ ├── mcp/ # MCP live-file sync per app │ ├── session_manager/ # Conversation history browser │ ├── deeplink/ # ccswitch:// URL parser + importer │ ├── store.rs # AppState (Arc, caches) │ ├── config.rs # Paths helper (get_app_config_dir, …) │ ├── app_config.rs # AppType, MultiAppConfig, domain models │ ├── provider.rs # Provider model │ ├── {claude,codex,gemini,opencode,openclaw}_config.rs # Per-app live-file IO │ ├── {claude_mcp,claude_plugin,gemini_mcp}.rs # App-specific helpers │ ├── settings.rs # AppSettings │ ├── tray.rs # System tray + quick switch │ ├── error.rs # AppError (thiserror) │ ├── panic_hook.rs │ └── ... │ └── tests/ # Rust integration tests (provider, mcp, deeplink, skill, …) │ ├── tests/ # Frontend test suite (vitest) │ ├── setupGlobals.ts, setupTests.ts │ ├── msw/ # MSW handlers + tauri IPC mocks + state │ ├── components/ # Component tests │ ├── hooks/ # Hook tests │ ├── integration/ # App-level flows │ ├── config/ # Preset sanity tests │ └── utils/ # testQueryClient + helpers │ ├── docs/ # User manual, release notes, proxy guide ├── scripts/ # Icon extraction & index generation ├── assets/ # Screenshots, partner logos ├── flatpak/ # Flatpak build instructions ├── package.json # pnpm scripts (dev/build/typecheck/test/format) ├── vite.config.ts # root = src, alias @ → src ├── vitest.config.ts # jsdom + setup files ├── tsconfig.json # strict; noUnusedLocals/Parameters ├── tailwind.config.cjs, postcss.config.cjs, components.json (shadcn) └── README.md / README_ZH.md / README_JA.md / CHANGELOG.md / CONTRIBUTING.md ``` ## Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Frontend (React + TS) │ │ Components → Hooks (business logic) → TanStack Query │ │ │ │ │ src/lib/api/* (typed invoke wrappers) │ └────────────────────────────┬────────────────────────────────┘ │ Tauri IPC (camelCase commands) ┌────────────────────────────▼────────────────────────────────┐ │ Backend (Rust + Tauri 2.8) │ │ commands/* (#[tauri::command]) │ │ │ │ │ ▼ │ │ services/* (ProviderService, McpService, PromptService, │ │ SkillService, ProxyService, ConfigService…) │ │ │ │ │ ▼ │ │ database/dao/* → Mutex │ │ │ │ + per-app live-file writers (claude/codex/gemini/…) │ │ + proxy/ (hyper + rustls local HTTP proxy) │ │ + session_manager/, deeplink/, mcp/, tray, updater │ └─────────────────────────────────────────────────────────────┘ ``` ### Core Design Principles - **Single Source of Truth (SSOT)** — SQLite at `~/.cc-switch/cc-switch.db` holds providers, MCP, prompts, skills, settings. Syncable state lives in the DB; device-level UI preferences live in `~/.cc-switch/settings.json`. - **Dual-way live sync** — On switch, services write the active provider into the CLI's real config files (e.g. `~/.claude/settings.json`, `~/.codex/config.toml`). When editing the currently active provider, changes are backfilled from the live file first to avoid losing edits the user made outside the app. - **Atomic writes** — Write to a temp file and rename. Never overwrite a live config in-place. - **Concurrency safety** — `Database` wraps `rusqlite::Connection` in a `Mutex`, exposed through `AppState` as `Arc`. Use the `lock_conn!` macro (see `src-tauri/src/database/mod.rs`) instead of raw `.lock().unwrap()`. - **Layered backend** — `commands → services → dao → database`. Commands must stay thin; put business logic in services. DAOs are the only layer that touches SQL. - **Auto backups** — `~/.cc-switch/backups/` keeps the 10 most recent snapshots; `~/.cc-switch/skill-backups/` keeps up to 20 before skill uninstall. ### Key Services | Service | Responsibility | | ------------------ | ----------------------------------------------------------------------- | | `ProviderService` | Provider CRUD, switching, live-file sync, backfill, sort, auth, usage | | `McpService` | MCP server CRUD + bidirectional sync across Claude/Codex/Gemini/OpenCode| | `PromptService` | Prompt presets, active sync to `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` | | `SkillService` | Skill install from GitHub/ZIP, symlink or copy mode, repo management | | `ProxyService` | Local HTTP proxy (hyper+rustls) with hot-switch, failover, rectifiers | | `ConfigService` | Import/export, backup rotation | | `SpeedtestService` | API endpoint latency probing | ### Data Locations - `~/.cc-switch/cc-switch.db` — SQLite SSOT (schema version 6) - `~/.cc-switch/settings.json` — device-level UI preferences - `~/.cc-switch/backups/` — auto-rotated DB snapshots (keeps 10) - `~/.cc-switch/skills/` — skills (symlinked into each app by default) - `~/.cc-switch/skill-backups/` — pre-uninstall skill backups (keeps 20) ## Development Workflow ### Prerequisites - **Node.js 22.12** (see `.node-version`) — 18+ works but CI pins 20 - **pnpm 10.12.3** (pinned in CI; pnpm-workspace) - **Rust 1.85+** (pinned in `Cargo.toml`) - **Tauri 2.0 system deps** — see https://v2.tauri.app/start/prerequisites/ ### Common Commands ```bash pnpm install # Install frontend deps pnpm dev # Run full app (tauri dev with hot reload) pnpm dev:renderer # Vite-only (no Tauri shell) — useful for UI-only work pnpm build # Production build (tauri build) pnpm typecheck # tsc --noEmit (strict) pnpm format # Prettier write on src/** pnpm format:check # Prettier check (CI) pnpm test:unit # vitest run pnpm test:unit:watch # vitest in watch mode ``` Rust backend (from `src-tauri/`): ```bash cargo fmt # Format cargo fmt --check # CI format check cargo clippy -- -D warnings cargo test # Backend + integration tests cargo test --features test-hooks ``` ### Pre-submission Checklist CI will run these; run locally before opening a PR: ```bash pnpm typecheck && pnpm format:check && pnpm test:unit cd src-tauri && cargo fmt --check && cargo clippy -- -D warnings && cargo test ``` ### Testing - **Frontend**: `vitest` + `jsdom` + `@testing-library/react`. Tauri `invoke` is mocked via `tests/msw/tauriMocks.ts`; network requests are mocked with MSW. Shared state (providers etc.) is reset between tests in `tests/setupTests.ts`. - **Test query client**: use `tests/utils/testQueryClient.ts` instead of the app client — it disables retries/cache for deterministic tests. - **Backend**: integration tests live in `src-tauri/tests/`; unit tests are co-located in modules. Many tests use `serial_test::serial` because they mutate `HOME`/env — do not run them with parallelism hacks, and don't remove the `#[serial]` attribute. - **Rust test-only hooks**: the `test-hooks` cargo feature gates extra test instrumentation. ### CI (`.github/workflows/ci.yml`) Two jobs on PRs and pushes to `main`: 1. **Frontend Checks** (ubuntu-latest): `pnpm typecheck`, `pnpm format:check`, `pnpm test:unit` 2. **Backend Checks** (ubuntu-22.04): installs GTK/WebKit deps, then `cargo fmt --check`, `cargo clippy -- -D warnings`, `cargo test` ## Conventions ### Tauri 2.0 IPC - **Command names are camelCase** on the JS side (e.g. `getProviders`, `switchProvider`). On the Rust side, the `#[tauri::command]` functions use snake_case with the `#![allow(non_snake_case)]` at the crate boundary in `commands/mod.rs`. - **Never call `invoke` directly in components** — add the call to `src/lib/api/*.ts` with a typed signature, then import from `@/lib/api`. See `src/lib/api/providers.ts` for the pattern. - **Payloads use camelCase**: Rust types carry `#[serde(rename_all = "camelCase")]` where they cross the IPC boundary. ### Frontend - **Import alias**: `@/` resolves to `src/` (configured in `vite.config.ts`, `tsconfig.json`, `vitest.config.ts`). Use `@/components/...`, `@/lib/...`, `@/hooks/...`. - **Data access**: Prefer TanStack Query hooks from `src/lib/query/` (e.g. `useProvidersQuery`, `useSettingsQuery`) rather than calling the API layer ad-hoc — they own cache keys and invalidation. - **Forms**: `react-hook-form` + `zod` resolvers; schemas live in `src/lib/schemas/`. - **UI kit**: shadcn/ui primitives under `src/components/ui/`. Configure new primitives via `components.json` (`npx shadcn add ...`). Icons: `lucide-react`. - **Styling**: Tailwind utility classes; use the `cn()` helper from `@/lib/utils`. Dark/light/system theme is controlled by `ThemeProvider`. - **State strictness**: `noUnusedLocals` and `noUnusedParameters` are on — prefix intentionally-unused args with `_`. ### Backend (Rust) - **Errors**: return `Result` (from `src-tauri/src/error.rs`, built on `thiserror`). Do not `unwrap()` outside tests; use `?` and map into `AppError`. - **Concurrency**: never hold a DB lock across an `.await`. Use the `lock_conn!` macro from `database/mod.rs` for short critical sections. - **JSON serialization**: use `database::to_json_string` for DB payloads to avoid panics. - **Live-file IO**: always go through the per-app writer modules (`claude_config.rs`, `codex_config.rs`, etc.) — they implement atomic temp+rename. - **Adding a new Tauri command**: 1. Implement logic in the appropriate `services/*` module. 2. Add a thin `#[tauri::command]` wrapper in `src-tauri/src/commands/.rs`. 3. Register it in the `tauri::generate_handler!` list in `src-tauri/src/lib.rs`. 4. Add the typed wrapper to `src/lib/api/.ts` and re-export from `src/lib/api/index.ts`. 5. If it touches DB schema, bump `SCHEMA_VERSION` in `database/mod.rs` and add a migration step in `database/schema.rs` or `database/migration.rs`. ### Internationalization CC Switch ships **three locales** and requires all of them to stay in sync: - `src/i18n/locales/en.json` - `src/i18n/locales/zh.json` (primary) - `src/i18n/locales/ja.json` Rules: 1. Never hardcode user-visible strings. Always use `t('namespace.key')` from `react-i18next`. 2. When adding/renaming a key, update **all three** files. 3. When removing a key, delete it from all three files. 4. Chinese is the authoritative source for meaning — follow the tone of existing zh strings when writing new ones. ### Commit Style [Conventional Commits](https://www.conventionalcommits.org/): ``` feat(provider): add AWS Bedrock preset fix(tray): resolve menu not refreshing after switch docs(readme): update install instructions ci: add format check workflow chore(deps): bump tauri to 2.8.2 ``` Scope should usually match the subsystem (`provider`, `mcp`, `prompt`, `skill`, `proxy`, `session`, `tray`, `deeplink`, `usage`, `settings`, `i18n`, `backend`, `frontend`, …). ### Pull Requests - **Open an issue first** for new features — drive-by feature PRs can be closed. - **Keep PRs small and focused.** One issue, one PR. - `main` is the base branch; use `feat/…` or `fix/…` branches. - The repo enforces "explain every line" for AI-assisted PRs — see `CONTRIBUTING.md`. ## Things to Avoid - **Don't bypass the service/DAO layers.** Commands must not call `rusqlite` directly, and components must not call `invoke` directly. - **Don't mutate live CLI config files outside the dedicated writer modules.** They guarantee atomicity and backfill semantics. - **Don't add fields to the Tauri IPC boundary without `#[serde(rename_all = "camelCase")]`.** - **Don't remove `#[serial]` from backend tests that touch HOME / env** — they'll race. - **Don't add a new i18n key to only one language file** — CI doesn't catch it, but users will. - **Don't add emojis to source files / commits / UI copy** unless the user explicitly asks. - **Don't create new top-level docs** (README variants, wiki pages) unless asked — prefer editing `docs/user-manual/` or the existing README. - **Don't touch `CHANGELOG.md` by hand** for routine changes — it's maintained per release. ## Quick References - **Main app shell**: `src/App.tsx` (view routing + header) - **Bootstrap / providers**: `src/main.tsx` - **Tauri entry**: `src-tauri/src/lib.rs` - **Command registration**: search for `tauri::generate_handler!` in `src-tauri/src/lib.rs` - **DB schema + migrations**: `src-tauri/src/database/schema.rs`, `src-tauri/src/database/migration.rs` - **Per-app live config IO**: `src-tauri/src/{claude,codex,gemini,opencode,openclaw}_config.rs` - **Local proxy**: `src-tauri/src/proxy/` (entry `mod.rs` → `server.rs`) - **Frontend API layer**: `src/lib/api/*` re-exported from `src/lib/api/index.ts` - **Query keys & hooks**: `src/lib/query/` - **Test IPC mocks**: `tests/msw/tauriMocks.ts` + `tests/msw/state.ts`