Documents the Tauri 2 + React architecture, repo layout, service/command layering, data locations, development workflow, testing strategy, and key conventions (camelCase IPC, i18n, commit style) so AI coding assistants can contribute without re-discovering the codebase each session.
20 KiB
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<Connection>on the backend - IPC: Tauri commands (camelCase names) wrapped by a typed frontend API layer
- i18n:
react-i18nextwithzh/en/jalocales (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<Connection>, 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<Database>, 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<rusqlite::Connection> │
│ │
│ + 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.dbholds 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 —
Databasewrapsrusqlite::Connectionin aMutex, exposed throughAppStateasArc<Database>. Use thelock_conn!macro (seesrc-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
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/):
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:
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. Tauriinvokeis mocked viatests/msw/tauriMocks.ts; network requests are mocked with MSW. Shared state (providers etc.) is reset between tests intests/setupTests.ts. - Test query client: use
tests/utils/testQueryClient.tsinstead 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 useserial_test::serialbecause they mutateHOME/env — do not run them with parallelism hacks, and don't remove the#[serial]attribute. - Rust test-only hooks: the
test-hookscargo feature gates extra test instrumentation.
CI (.github/workflows/ci.yml)
Two jobs on PRs and pushes to main:
- Frontend Checks (ubuntu-latest):
pnpm typecheck,pnpm format:check,pnpm test:unit - 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 incommands/mod.rs. - Never call
invokedirectly in components — add the call tosrc/lib/api/*.tswith a typed signature, then import from@/lib/api. Seesrc/lib/api/providers.tsfor the pattern. - Payloads use camelCase: Rust types carry
#[serde(rename_all = "camelCase")]where they cross the IPC boundary.
Frontend
- Import alias:
@/resolves tosrc/(configured invite.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+zodresolvers; schemas live insrc/lib/schemas/. - UI kit: shadcn/ui primitives under
src/components/ui/. Configure new primitives viacomponents.json(npx shadcn add ...). Icons:lucide-react. - Styling: Tailwind utility classes; use the
cn()helper from@/lib/utils. Dark/light/system theme is controlled byThemeProvider. - State strictness:
noUnusedLocalsandnoUnusedParametersare on — prefix intentionally-unused args with_.
Backend (Rust)
- Errors: return
Result<T, AppError>(fromsrc-tauri/src/error.rs, built onthiserror). Do notunwrap()outside tests; use?and map intoAppError. - Concurrency: never hold a DB lock across an
.await. Use thelock_conn!macro fromdatabase/mod.rsfor short critical sections. - JSON serialization: use
database::to_json_stringfor 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:
- Implement logic in the appropriate
services/*module. - Add a thin
#[tauri::command]wrapper insrc-tauri/src/commands/<domain>.rs. - Register it in the
tauri::generate_handler!list insrc-tauri/src/lib.rs. - Add the typed wrapper to
src/lib/api/<domain>.tsand re-export fromsrc/lib/api/index.ts. - If it touches DB schema, bump
SCHEMA_VERSIONindatabase/mod.rsand add a migration step indatabase/schema.rsordatabase/migration.rs.
- Implement logic in the appropriate
Internationalization
CC Switch ships three locales and requires all of them to stay in sync:
src/i18n/locales/en.jsonsrc/i18n/locales/zh.json(primary)src/i18n/locales/ja.json
Rules:
- Never hardcode user-visible strings. Always use
t('namespace.key')fromreact-i18next. - When adding/renaming a key, update all three files.
- When removing a key, delete it from all three files.
- Chinese is the authoritative source for meaning — follow the tone of existing zh strings when writing new ones.
Commit Style
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.
mainis the base branch; usefeat/…orfix/…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
rusqlitedirectly, and components must not callinvokedirectly. - 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.mdby 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!insrc-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/(entrymod.rs→server.rs) - Frontend API layer:
src/lib/api/*re-exported fromsrc/lib/api/index.ts - Query keys & hooks:
src/lib/query/ - Test IPC mocks:
tests/msw/tauriMocks.ts+tests/msw/state.ts