Files
cc-switch/AGENTS.md
T
Claude 4c2f6485d8 docs: add AGENTS.md with codebase guide for AI assistants
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.
2026-04-08 11:43:37 +00:00

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-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<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.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 safetyDatabase wraps rusqlite::Connection in a Mutex, exposed through AppState as Arc<Database>. Use the lock_conn! macro (see src-tauri/src/database/mod.rs) instead of raw .lock().unwrap().
  • Layered backendcommands → 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. 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<T, AppError> (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/<domain>.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/<domain>.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:

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.rsserver.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