English | 简体中文
Multi-role scheduler for Claude Code. Define any number of roles (default: 🧠 Planner + ⚙️ Worker), bind a different model to each — e.g. a reasoning model for planning and a fast model for execution — then launch Claude Code in the right role from a built-in multi-tab terminal (xterm.js + node-pty) or your favourite external terminal. Tab = one role, one pty session, one tool allow / deny list.
Built with Electron + Vue 3 + TypeScript. Disk-backed YAML configuration (no localStorage for the things that matter).
The idea comes from how Anthropic routes work internally: planning-shaped tasks go to certain models, execution-shaped tasks to others. Think of a company — the cognition you expect in leadership is different from what you need in the execution layer; the people who make the plan and the people who carry it out are never the same group, and their cost is never the same either.
This tool can't be that smart. It draws the simplest possible line — plan vs. everything else — and, because it sits on top of Claude Code, leaves the routing decision to a human convention: you decide which model thinks (Plan) and which model executes (Work). Expensive reasoning tokens are spent only where they matter; cheap models do the mechanical work.
- Add / edit / duplicate / delete model configs (display name, base URL, API key, model ID)
- Provider presets with URL autocomplete — GLM (Zhipu), MiniMax, DeepSeek, Kimi (Moonshot), Z.ai, Qwen (DashScope)
- Type a name or model ID (
glm-5.3,MiniMax-M3, …) and the base URL auto-fills - Quick-pick model ID chips once a provider is matched
- Connection test per model — green toast with latency (
connected in 143ms) or red toast with the network error
- Any number of roles, defined in
~/.cc-mode-switcher/roles.yaml— no hard-coded list of roles in the app - Each role picks: a model, an extended-thinking toggle, a system-prompt file path, allowed tools, denied tools, and denied plugins
- Default roles are 🧠 Planner (read-only) and ⚙️ Worker (writes files) — built-in prompts at
~/.cc-mode-switcher/prompts/{plan,worker}.mddefine the.cc-delivery/file contract between roles - Table view for clicks: filter, inline cell edits, right-click to duplicate / delete
- YAML view for power users: live syntax check, dirty-state indicator, save reloads the table
- Two-way same source: editing the table writes YAML; editing YAML and saving refreshes the table
- Reset restores the default plan + worker roles but keeps your models and any edits you made to the prompt files
- xterm.js + node-pty in the main process — each tab is one pty session, bound to one role + one model
- Tab title:
{project} | {roleLabel}({modelName})(e.g.acme-web | 🧠 Planner(glm-5.3)) - Right-click a tab to clone (reuse the same role + cwd) or detach to a separate window
- Mac shortcuts (active only when the terminal pane has focus):
- Cmd+T — clone the focused tab
- Cmd+N — open the role picker and start a new session
- Option+T — start the role currently selected on the left
- Detached window exposes a single "↩ merge into main" button that transfers the session back
- Per-session snapshot: changing role bindings afterwards does not affect already-open tabs
Claude Code's ~/.claude/settings.json env block would otherwise override terminal env vars. We sidestep it with two flags per session:
--setting-sources ""— Claude Code skips all default settings files (user / project / local)--settings "$CC_MODE_DIR/<ModelName>.json"— loads the per-session temp JSON (named after the bound model) at highest priority, overriding everything- The app never reads or writes
~/.claude/settings.jsonor any settings file — no backups, no surprises
macOS / Windows / Linux installers are published to GitHub Releases — download the latest build here, or see the download page for per-OS install notes.
Releases are driven by three manual workflows — no automatic triggers, no local CLI needed:
- List releases — see what already exists before doing anything
- Set version & tag — bump up (
patch/minor/major), downgrade, or set any version. Writes arelease vX.Y.Zcommit and pushes the tag - Release Electron App — builds the three OS targets in parallel and creates / updates the GitHub Release
Full operational guide: Release & Versioning Workflow.
- 🌙 Dark (default) / ☀️ Light theme — CSS-variable based, toggle in the toolbar
- English (default) / 简体中文 — quick toggle in the toolbar
- Icon-only action buttons with hover tooltips
- Centered toast notifications
- Models + role config persists in
~/.cc-mode-switcher/*.yaml; UI preferences (theme, language) inlocalStorage
The detail panel below the role table shows the full shell snippet for the selected role — same content the built-in terminal runs and same what the "open in external terminal" button sends. Example, plan role bound to GLM-5.3:
# cc-mode-switcher · plan
export ANTHROPIC_BASE_URL="https://open.bigmodel.cn/api/anthropic"
export ANTHROPIC_AUTH_TOKEN="sk-..."
export ANTHROPIC_DEFAULT_OPUS_MODEL="glm-5.3"
export ANTHROPIC_DEFAULT_SONNET_MODEL="glm-5.3"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="glm-5.3"
export ANTHROPIC_DEFAULT_FABLE_MODEL="glm-5.3"
export ANTHROPIC_DEFAULT_OPUS_MODEL_NAME="glm-5.3"
export ANTHROPIC_DEFAULT_SONNET_MODEL_NAME="glm-5.3"
export ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME="glm-5.3"
export ANTHROPIC_DEFAULT_FABLE_MODEL_NAME="glm-5.3"
export ANTHROPIC_MODEL="glm-5.3"
export CLAUDE_CODE_SUBAGENT_MODEL="glm-5.3"
export MAX_THINKING_TOKENS=16000
# Per-role temp settings file (priority > ~/.claude/settings.json)
CC_MODE_DIR=$(mktemp -d -t cc-mode-XXXXXX)
cat > "$CC_MODE_DIR/GLM-5.3.json" <<'CCMODE_EOF'
{
"env": { ... same 13 keys as above ... }
}
CCMODE_EOF
# Launch claude with role-scoped prompt + tool permissions
exec claude --setting-sources '' --settings "$CC_MODE_DIR/GLM-5.3.json" \
--system-prompt-file ~/.cc-mode-switcher/prompts/plan.md \
--disallowed-plugins 'superpowers' \
--allowedTools Read,LS,Glob,Grep \
--disallowedTools Edit,Write,NotebookEdit,Bashsuperpowers is always forced into --disallowed-plugins regardless of role config, and every session uses --setting-sources "" so ~/.claude/settings.json is never read.
Selecting the Work card instead regenerates the same block with Work's bindings — MAX_THINKING_TOKENS line is omitted, the temp file becomes e.g. $CC_MODE_DIR/MiniMax-M3.json, and the alias is cc-w='claude --setting-sources "" --settings "$CC_MODE_DIR/MiniMax-M3.json"'. Switch cards any time and click
--setting-sources "" skips every default settings file (user / project / local), so ~/.claude/settings.json never loads. --settings "$CC_MODE_DIR/<ModelName>.json" then loads the per-mode temp JSON at higher priority than any other source, making it the single source of truth.
| Dependency | Version | Notes |
|---|---|---|
| Node.js | ≥ 22 (24+ recommended) | Vite 7 requires Node 20.19+ / 22.12+; this project targets Node 24 |
| pnpm | ≥ 9 | Preferred — the lockfile is pnpm-lock.yaml (npm ≥ 10 also works) |
| macOS | 12+ | Apple Silicon or Intel. Packaging installers needs Xcode Command Line Tools (xcode-select --install) |
Windows/Linux: the UI builds and runs, but "Open in Terminal" is macOS-only for now (it uses AppleScript /
.commandfiles).
pnpm 10 blocks dependency postinstall scripts by default, so Electron's binary never downloads and npm run dev fails with Error: Electron uninstall. Fix:
pnpm approve-builds # select electron (and esbuild)or add to package.json before installing:
"pnpm": { "onlyBuiltDependencies": ["electron", "esbuild"] }Speed up dependency and Electron binary downloads:
# npm/pnpm registry
pnpm config set registry https://registry.npmmirror.com
# Electron binary + electron-builder helper binaries
export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
export ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/Or commit a project-level .npmrc:
registry=https://registry.npmmirror.com
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/pnpm install # or npm install
npm run dev # start dev server + ElectronOther scripts:
npm run build # build main/preload/renderer bundles
npm run dist # package installers (electron-builder)Add an entry to src/renderer/src/data/providers.ts — the form autocomplete, keyword auto-fill, and model ID chips pick it up automatically:
{
id: 'glm',
name: 'GLM (Zhipu)',
baseUrl: 'https://open.bigmodel.cn/api/anthropic',
keywords: ['glm', 'bigmodel', 'zhipu', '智谱'],
models: ['glm-5.3', 'glm-5.3-air', 'glm-4.6', 'glm-4.5']
}Any Anthropic-compatible endpoint works.
Full chaptered guides live in docs/ — English by default, 简体中文版 available:
- Quick start — first-time setup in 5 steps
- Models & providers — presets, env vars, override guard, model pairing
- Plan mode playbook — produce plan documents as the intermediate artifact
- Work mode playbook — execute the plan strictly, never re-plan mid-flight
- End-to-end example — one feature from requirement to delivery
- Release & versioning workflow — GitHub Actions workflows for cloud builds, version up / down, GitHub Releases (all manual, no local CLI)
- Local build — clean install: clear node_modules, pnpm store, electron / electron-builder caches, then reinstall (when
pnpm run dev/pnpm run distmisbehaves)
src/
├── main/ # Electron main process (IPC, terminal launch)
├── preload/ # contextBridge API
└── renderer/
└── src/
├── components/ # Vue SFCs (panels, cards, modal, toast, …)
├── composables/ # models, i18n, theme, terminal, toast
├── i18n/ # en / zh message catalogs
├── data/ # provider presets
└── assets/ # global styles
Publishing is a two-step manual process — nothing fires automatically. Both steps happen entirely on GitHub via the Actions tab; no local CLI required.
| Step | Workflow | What it does |
|---|---|---|
| 1 | Set version & tag | Writes a release vX.Y.Z commit, pushes the vX.Y.Z tag to origin. Old tags/releases are untouched. |
| 2 | Release Electron App | Builds mac / win / linux artifacts and creates/updates the GitHub Release. |
A third helper, List releases, prints what's already on the server so you can decide whether to bump, downgrade, or re-publish.
Repo → Settings → Actions → General → Workflow permissions → Read and write permissions → Save. Without this, the runner can't push back to the repo.
Actions → List releases → Run workflow → wait → open the run → expand Print releases + tags. You get two lists:
- Releases (via
gh release list) — every published GitHub Release with status (Published / Draft / Pre-release) - Tags (via
git ls-remote --tags) — every tag, including ones whose Release was deleted
If a tag shows up in the second list but not the first, its Release was deleted — re-publish it via step 2 with that tag.
Actions → Set version & tag → Run workflow:
| Input | Value |
|---|---|
mode |
auto |
bump |
patch (or minor / major) |
version |
(leave blank) |
What happens: bumps package.json + pnpm-lock.yaml + the version label in SettingsPanel.vue, commits release vX.Y.Z, pushes the new tag. Nothing is built yet — go to step 2.
Actions → Set version & tag → Run workflow:
| Input | Value |
|---|---|
mode |
set |
bump |
(ignored) |
version |
2.0.0 (or anything — lower than current is a downgrade, e.g. 0.9.6) |
What happens: same as above, but the target version is whatever you typed. Downgrades are non-destructive — the previous tag and its Release stay in place.
If the tag you typed already exists on origin, the workflow aborts and tells you to either pick a different version or re-publish via step 2.
Actions → Release Electron App → Run workflow:
| Input | Value |
|---|---|
tag |
(leave blank to build whatever is currently on main — i.e. the commit step 1 just pushed) |
The runner checks out that ref, runs electron-builder --publish always, and creates a GitHub Release named after the version in package.json. mac / win / linux all build in parallel.
Actions → Release Electron App → Run workflow:
| Input | Value |
|---|---|
tag |
v1.0.0 (fill the tag you want to re-publish) |
The runner checks out that tag, rebuilds, and overwrites the existing Release with fresh artifacts.
The workflows never delete. To clean up, do it from GitHub's web UI (repo → Releases → trash icon on the release) or from any terminal that has the gh CLI:
# Delete just the Release (keep the tag — re-publishable via step above)
gh release delete v1.0.0
# Delete Release + tag
gh release delete v1.0.0 --yes
git push origin --delete v1.0.0Or mark it Draft in the web UI to hide without losing it.
Why doesn't this app touch my ~/.claude/settings.json?
Because it doesn't need to. Every alias launches Claude Code with two flags: --setting-sources "" skips user / project / local settings files entirely, and --settings "$CC_MODE_DIR/<ModelName>.json" loads a per-mode temp JSON (named after the bound model, regenerated on every click) at higher priority than any source — including ~/.claude/settings.json. App settings live in $CC_MODE_DIR, never in your home directory.
The TSM AdjustCapsLock… / IMKCFRunLoopWakeUpReliable log lines in dev?
Harmless macOS input-method noise present in every Electron app. The window auto-reloads if the renderer ever crashes.
MIT © 2026 leochan007



