125 Commits
Author SHA1 Message Date
Jason 626529451b feat(kimi): edit official account models and remove them from Kimi
Official Kimi accounts added in the Authorization Center were written
straight into config.toml with the model list discovered at sign-in.
Kimi Code only refreshes its own managed:kimi-code node, so separate
accounts kept that snapshot forever, and their cards could neither be
edited nor removed.

Official cards can now be edited. The editor hides the connection and
fetches the account's own catalog through a new kimi_oauth_models
command (token refreshed under Kimi's lock, aliases keyed by the
credential slot), so picked models carry the official context window,
capabilities and efforts. apply_provider pins an official node's
connection to the signed-in one, and ProviderService::update rejects
edits that would turn another card into an official account.

Official cards can also be removed from config.toml like other Kimi
providers: the card and credential stay, services pointing at a
separate account move to Kimi's own login, and "Add" restores it.
Deleting one still requires removing it first or using the
Authorization Center.
2026-10-24 15:44:09 +08:00
Jason b34753b832 feat(omp): manage Oh My Pi global prompts
Oh My Pi reads its user-level instructions from AGENTS.md in the
profile-scoped agent directory, so prompts now project there like Kimi
Code's. Drop the prompt-sync skip and the stale deep-link notes that
assumed there was no prompt panel.
2026-10-24 15:30:32 +08:00
Jasonandallenxu09 3d7cf6aa32 refactor(omp): port #8308's text-level models.yml editor and share Pi plumbing
Replace the passthrough/splice YAML layer with a single omp_config.rs:
#6747's verified agent-dir resolution (profiles, PI_CONFIG_DIR, XDG data
dir) plus #8308's block-level editor that rewrites only the one provider
entry, refuses anchors/tags/flow style instead of re-emitting them, and
rolls the file back byte for byte when the database commit fails.

- Validate only the node being written; model-level `api` also accepts
  omp's runner APIs.
- Drop ProviderMeta.ompPassthroughYaml, validation.rs and omp_state; the
  current-state command lives in commands/omp.rs and reuses PiCurrentState.
- Usage scripts go through the Pi command with an `app` argument.
- MCP reuses Pi's transport helpers; mcp.json writes are revision checked.
- Skills follow Pi: no DB column, state derived from <agent>/skills, and
  uninstall keeps an unverifiable same-name directory in either app.
- Move enabled_omp to a new v22 -> v23 migration (v22 shipped in 4.0.19)
  and add it to mcp_servers only; revert the .gitignore additions.

Co-authored-by: allenxu09 <s23372.xu@stu.scie.com.cn>
2026-10-24 15:30:32 +08:00
Jason 2afe8cdbef refactor(omp): rename the internal app id from ohmypi to omp
Match the binary name, the Apps page tool id and the i18n keys that
already used omp. The app is unreleased, so nothing persisted needs a
migration; ohmypi stays accepted as an alias when parsing app ids.
Show the app by default like Pi and Kimi Code.
2026-10-24 15:30:32 +08:00
JackieJKandallenxu09 c8945d2d91 feat(omp): add Oh My Pi support
Squashed from #6747 (head 50c3cec1d) onto current main: providers in
models.yml, MCP in mcp.json, Skills, sessions, app visibility and
directory override for Oh My Pi. Conflicts with the Kimi Code refresh
refactor in App.tsx and the locale files were resolved by keeping both
sides and routing Oh My Pi through refreshAdditiveLiveState.

Co-authored-by: allenxu09 <s23372.xu@stu.scie.com.cn>
2026-10-24 15:30:32 +08:00
Jason cdd2190df2 feat(kimi): identify, remove and show quota for official accounts
Follow-up to #8726, which lets the Authorization Center hold several Kimi
official subscriptions, each with its own provider and credential slot.

Identity: after sign-in (and for logins found on disk) read /me to show
the account's nickname on its row and card. Signing in to an account that
is already listed writes back to its slot instead of adding a duplicate,
and the slot is chosen when the login completes, so a native Kimi login
made meanwhile is never overwritten. New separate slots are named from a
hash of region and user id, stable across devices. Opening the Kimi page
reads identities too, so card names carry the nickname without a visit to
the Authorization Center first.

Remove instead of sign out: a separate account is deleted together with
its provider node, models, credential and card (refused while in use);
web search/fetch pointing at it moves to Kimi's own default login. Kimi's
own login is only hidden from the list - CC Switch never logs Kimi out -
and stays hidden across Kimi's token rotations until the user signs in to
it again here. An empty list caused by that says so instead of "signed
out".

Quota: each account row and official card shows its quota. Tokens close
to expiry are refreshed under the same proper-lockfile-compatible lock
Kimi uses (mkdir <slot>.lock, 5 s stale takeover), with Kimi's revoked
token recovery. Windows, where Kimi takes no lock, only reads.

Also: re-login keeps custom fields such as custom_headers on the node;
deleting a card whose node was removed elsewhere deletes the leftover
CC Switch-owned credential; the card's switch button works for official
accounts after checking they are signed in on this machine; account rows
use the same compact layout as the other Authorization Center groups.
2026-10-23 11:15:35 +08:00
funkpopo 07265bc970 feat(kimi): support multiple official accounts in Auth Center (#8726)
* feat(kimi): manage multiple official accounts in auth center

Isolate account credentials, providers and model aliases; preserve native logins and protect official OAuth nodes. Fix session image access, drop unpublished usage repair paths and the kimi-cli alias, and synchronize all four READMEs.

* fix(kimi): address account compatibility review feedback

Restore the native default credential slot for the first account, stop rewriting tool services on account switches, allow deleting profiles removed from native config, and fix official-account test fixtures and documentation.

* fix(kimi): pin OAuth endpoints to the login region

Use fixed official hosts for mainland and global sign-ins, keep existing account regions and credential slots during reauthentication, and preserve sanitized transport error causes. Add regional routing and network-error regression coverage.
2026-10-23 08:30:23 +08:00
funkpopo 7da255387f feat(kimi): add official OAuth login and provider switching (#8710) 2026-10-22 23:35:49 +08:00
funkpopo e4873bc8aa feat(kimi): add Kimi Code support (#8687)
Add Kimi Code as the 12th managed app (additive mode): native multi-provider config in ~/.kimi-code/config.toml (honors KIMI_CODE_HOME), presets derived from the Pi catalog, MCP (stdio/HTTP/SSE), Skills with native-directory protection, global prompts, session browsing/resume, and session-log usage import. Local routing and aggregation do not apply; official OAuth stays managed by Kimi Code. Schema v21 -> v22 adds enabled_kimi columns.
2026-10-22 16:50:37 +08:00
Jason dfe100813f feat(agy): share prompts with key profiles, resume key sessions, price usage
- Store the agy prompt in ~/.gemini/config/AGENTS.md, which agy reads as a
  standalone customization root and every private key profile links to.
- List sessions recorded under each key profile and resume them with the
  matching --gemini_dir and the card's key in a launched terminal.
- Fetch the model list for API-key cards from agy's own offline catalog in a
  throwaway profile, so the form offers display names agy actually accepts.
- Record agy session usage under priced model IDs (display name kept as the
  request model) and estimate cost from Gemini API prices.
- Treat an imported settings file with a model as the official login card.
2026-10-21 00:12:44 +08:00
OwlbayandpeterLwk888 c7dceb8036 feat(agy): integrate Antigravity CLI management (#8268)
Add Antigravity CLI (agy) as a managed app: provider switching for the native login and API-key modes, MCP and Skills sync through ~/.gemini/config, session browsing and session-log usage import.

Co-authored-by: peterLwk888 <lwk1124@outlook.com>
2026-10-20 23:35:51 +08:00
Jason cf312f6af4 fix(mcp): take the app's switch lock before toggling a server
toggle_mcp_app and set_mcp_enabled called McpService::toggle_app without
the per-app switch lock. Each single-server write reads the app's live
config, edits it and writes it back, so two toggles in flight at once (or
a toggle racing a provider switch, a proxy enter or an MCP resync, which
all hold that lock) could overwrite each other and silently drop servers
from ~/.claude.json, config.toml and friends while the database still
showed them enabled.

Add McpService::toggle_app_locked, which takes the same switch lock
resync_mcp_to_apps already uses and then runs toggle_app, and route both
commands through it. toggle_app itself stays lock-free because the tokio
lock is not reentrant and internal callers may already hold it; MiniMax
Code and Pi keep their own file lock inside sync_and_commit, which never
takes the switch lock. A test fires 16 concurrent toggles at Claude and
checks every server lands in the live file (it fails without the lock).

Refs #8580
2026-10-20 21:50:17 +08:00
Jason 2c2d6c43c1 fix(codex): show the on-disk login in the official card's auth.json editor
Editing a Codex official card showed the row's stored `auth`, which is the
OAuth snapshot older versions backfilled. Switching no longer backfills, so
once Codex rotates its tokens the editor displayed an access token that no
longer matched ~/.codex/auth.json. Follow-login cards (empty row auth)
showed an empty object instead of the current login.

For official cards not bound to a managed account, the auth.json box now
shows what auth.json will contain after switching to the card, predicted
read-only from the same login plan the switch uses. Saving without touching
the box keeps the row's original auth, so opening and saving neither binds a
follow-login card to the current account nor stores soon-to-expire tokens.
Switching never wrote the stale snapshot over the same account's login, so
the live file was not affected.
2026-10-19 18:01:47 +08:00
海洋饼干 daf442e5d7 fix(opencode): keep Unicode-named deep-link imports editable (#8381) 2026-10-16 17:36:13 +08:00
Jason 16e7ff1c9d feat(deeplink): let links declare the API format, import Pi providers, and leave activation to the user
- Add a single `apiFormat` parameter (anthropic / openai_chat /
  openai_responses / gemini_native) that maps to each app's own field:
  meta.apiFormat for Claude Code, Codex and Grok Build (non-native formats
  need local routing), `npm` for OpenCode, `api` for OpenClaw and Pi,
  `api_mode` for Hermes. Unsupported combinations are rejected while
  parsing; links without it keep the previous defaults byte for byte.
- Support Pi provider links. They write a models.json node and must carry
  a model.
- The confirmation dialog now has a switch for "switch after import"
  (providers), "add to the app config" (additive apps, on by default) and
  "enable after import" (prompts). The link's `enabled` only sets its
  initial state.
- Turning the switch off really only saves the row: a new
  `ProviderService::add_without_switching` skips the first-provider path
  in `add` that would otherwise write the client config. The deep-link
  golden rows now have is_current false accordingly.
- Reject link values that additive apps would expand: Pi keys starting
  with `!` and `$NAME` / `${NAME}`, OpenCode `{env:}` / `{file:}`, and
  `${...}` for OpenClaw and Hermes. These would run a command or send
  local secrets and files to the link's endpoint. Checked when parsing,
  when merging `config`, and when building the provider.
- Update the deep-link manual in zh/en/ja.
2026-10-12 23:26:43 +08:00
Jason db93ac2083 fix(grokbuild): point session_summary at the active provider and keep a link's model profile
Session titles: when `models.session_summary` is unset, Grok Build
(verified on 1.0.50) generates titles with the built-in `grok-4.6`
model but sends the request to the default model table's endpoint, so
third-party upstreams reject it and title generation fails. Switching
never wrote this key, so fixing the row templates alone would not
reach live. The Grok switch patch now makes session_summary follow the
provider when it is unset or names a table CC Switch wrote (the
target, the write record, or a table retired in this switch): it points
at the target table, or is removed when switching to the official
account. A user's own choice (a built-in model or their own table) is
left alone.

Deep links: a Grok Build link carrying a config.toml no longer gets
rebuilt into a fixed template that dropped the profile's display name,
description, API backend and context window. The table named by
`[models] default` is kept through an allowlist of Grok 1.0.50 profile
fields; the endpoint, key and model come from the link parameters.
Links whose table uses auth_provider, env_http_headers or
mtls_cert_dir are rejected, and env_key is still dropped. Only one
model table is imported, since a provider owns one table in live.
Links without a config keep the old template byte for byte.

Fixes #7003
Refs #6457
2026-10-12 23:26:43 +08:00
Jason 11e1326bf5 fix(deeplink): keep a Codex link's provider config instead of rebuilding a fixed template
A Codex provider link could carry a full config.toml, but import only
pulled the key, endpoint and model out of it and rebuilt a fixed
template, silently dropping the provider's auth mode, headers, websocket
support and model catalog URL.

When a link carries a config, keep the parts that belong to the
provider, using the same field lists the live projection writes on
switch:

- from the selected [model_providers.*] table: connection and auth
  fields (wire_api, requires_openai_auth, http_headers, query_params,
  supports_websockets, retries/timeouts, capabilities, ...); the route
  is always renamed to `custom`
- model_catalog_url only on the same origin as the endpoint, since
  Codex sends the provider's credentials when fetching it
- top-level key and provider-exclusive fields (model, reasoning effort,
  web_search, context window, subagent model)

Global settings ([features], [tools], MCP, approval_policy, notify,
local file paths) are not imported. Links whose provider table makes
Codex run a command or read local credentials (auth, aws,
gateway_oauth, env_http_headers) are rejected; env_key is dropped in
favor of the link's own key, and an env_key-only link is rejected.
URL params still take priority, and a table's bearer token is reset to
the final key.

Links without a config keep the old template byte for byte; a new
golden snapshot locks the shape of config-carrying imports. The confirm
dialog notes that only provider settings are imported, and the manual
documents the rules (and that configUrl is rejected).

Refs #7438, #7942, #5510
2026-10-12 23:26:43 +08:00
Jason cad592e04f fix(codex): treat model_catalog_json as a plain key field
A model_catalog_json in config.toml that no provider owned was never
touched, and CC Switch skipped writing its own pointer while it was
there. A pointer left over from an older version could keep the
generated catalog (model mapping or aggregated models) from taking
effect, with no warning at all in direct mode. Users then tried to write
the pointer by hand, which every later write removed again.

- The pointer now follows the target provider like model does: the
  provider's own pointer if its config has one, otherwise CC Switch's
  catalog when one is generated, otherwise none. Whatever config.toml
  held before is replaced.
- Drop the configOwnsCatalog warning and the live-pointer handling behind
  "Use CC Switch's model catalog". The button stays for a routed provider
  whose own config points at another catalog.
- The provider editor no longer shows a pointer it would only carry over
  from config.toml, so the special case that kept it out of the card is
  gone too.
2026-10-05 20:30:56 +08:00
OwlbayandLystranG d455dd8572 feat(mcp): sync MCP servers to Pi 1.0's built-in MCP (#7862)
Pi 1.0 ships built-in MCP with a user-level <Pi agent dir>/mcp.json in the
same mcpServers shape as Claude and MiniMax Code. Manage Pi as an MCP app
instead of hard-coding it as unsupported.

- Writes replace only the connection fields (command, args, env, cwd, url,
  headers, type); Pi's own fields (enabled, timeout, description, exposure,
  toolExposure, oauth, auth) are kept as Pi wrote them.
- Unticking Pi marks the entry enabled: false instead of deleting it, so
  ticking it again restores Pi's settings; a missing entry is rebuilt from
  the Pi fields saved at import. Deleting the server in CC Switch removes
  the entry.
- Refuse what Pi refuses: the SSE transport, names outside [A-Za-z0-9_-],
  and names that collide once - and _ are folded. The file and database
  are left untouched on refusal.
- Like MiniMax Code, write the file first and commit the database after,
  restoring the file if the commit fails; a full resync never deletes
  same-name entries CC Switch does not manage.
- Import maps streamable-http to http, leaves enabled: false entries
  unticked, and skips same-name entries whose connection differs.
- Schema v19 -> v20 adds mcp_servers.enabled_pi (default 0), so upgrading
  writes nothing to Pi until a server is ticked.

Co-authored-by: LystranG <49083828+LystranG@users.noreply.github.com>
2026-10-05 18:15:32 +08:00
Jason b544bc27c6 ci: run backend tests under cargo-nextest and cache deps with rust-cache
- Backend tests run with `cargo nextest run --profile ci`: one process
  per test, a 180s per-test timeout (src-tauri/.config/nextest.toml),
  and no fail-fast. The WSL2 nightly uses it too, still one test at a
  time and with the same selection as the old `--skip database::`, so a
  hung test is killed instead of eating the whole job.
- Integration tests shared one fixed test home, and each process wiped
  it on startup. Under nextest they ran concurrently and failed en
  masse, so tests/support.rs now picks a directory per
  NEXTEST_TEST_GLOBAL_SLOT. Plain `cargo test` keeps the old directory.
- Swatinem/rust-cache replaces the hand-rolled actions/cache step. It
  caches dependencies only, not the cc-switch crate that is rebuilt
  every run, and keys on OS, rustc version and Cargo.lock. The WSL2 CI
  job and the nightly share the `backend` key but only restore it, so
  their lib-only builds never save over the full Windows cache.
- Quote the $GITHUB_OUTPUT redirect that shellcheck flagged.
2026-10-05 12:17:50 +08:00
Jason e9170adb65 feat(ui): settle the open v7 follow-ups for presets, tray and panels
Presets: add optional family/versionKey fields so one vendor's regional
and plan variants show as a single row in step 1 (Claude 94 presets ->
71 rows) and as a version switch in step 2. Switching versions after the
user typed into the form asks first, since the form is refilled. The
category field and preset order are unchanged.

Tray: show a dot and a problem tooltip on the icon only while the
routing service failed to start, fell back to direct, or a tray action
failed; spell out that quitting stops the routing service while it is in
use; name the default provider in the Stack submenu title; add the
"restart X to apply" line after tray switches; refresh the tray when the
routing service starts or stops.

Panels: record when the background session scan last finished and show
the newer of that and a manual sync on the usage page; add
resync_mcp_to_apps and resync_skills_to_apps; report per-repo failures
from skill discovery and update checks; report ZIP skills skipped
because the directory is taken. MCP/Skills bulk actions keep applying
to the filtered rows only.
2026-10-03 23:28:58 +08:00
JasonandAnxForever 81c0ff252b fix(mcp): keep projecting remaining servers when one fails to sync
Projecting an app's MCP servers stopped at the first server that could
not be written or removed, so one bad entry left every server after it
out of sync: newly enabled ones were never written and disabled ones
stayed in the client's config.

Sync each server independently, log the failures, and report them
together at the end, grouping server ids that failed for the same
reason.

Co-authored-by: AnxForever <130662349+AnxForever@users.noreply.github.com>
2026-10-02 22:33:33 +08:00
tingfeng347 4e46e6b630 fix(mcp): stop writing type into Codex mcp_servers (#7735)
Codex has no `type` key under `[mcp_servers.*]`; it infers the transport
from `command` (stdio) or `url` (streamable HTTP). Since Codex 0.158 the
`type` line CC Switch wrote on every MCP sync is reported as an
unrecognized setting, and `--strict-config` refuses to start.

Stop emitting the key from the shared Codex serializer while still using
the stored transport type to pick which fields to write. On import, infer
the transport from `url` when `type` is absent so url-only entries are no
longer treated as stdio and skipped. Grok Build output is unchanged.

Fixes #7734
Fixes #7554
Fixes #5820
2026-10-02 21:33:19 +08:00
Jason d3acdc2077 feat(proxy): make attached models a mode of its own next to routing
Attached models were a per-card toggle inside routing mode. Turn them
into an attached mode that sits beside routing: settings show either
the routing switch or the attached-mode switch on the main page, for
Claude Code and Codex only (Gemini CLI and Grok Build keep routing).
Turning one on first sends Claude Code and Codex apps that are in the
other mode back to direct.

In attached mode the provider list works like the additive apps: Add
and Remove put a provider's models into the client's model picker, and
"Set as default" picks the provider for unprefixed requests. The
default is always on the list and cannot be removed; a new default set
from the card, the tray or a project joins it. Official accounts can
only be the default. There is no failover: default requests go to the
default only, enabling failover is refused under the switch lock, and a
late failover result from a request sent before entering attached mode
is dropped. The queue is kept for routing mode.

Internally attached mode is proxy mode plus PoolState.enabled, written
on every proxy entry and committed with the client files. With the bit
off nothing is published and the list is kept for next time.

Also fix issues found reviewing the first two commits:
- strip ChatGPT fingerprint headers only from Codex attached requests
- recompute the Codex catalog when an attached member's row is edited
- skip a member whose row cannot be projected instead of failing every
  write, and refuse to attach such rows
- keep the gateway discovery switch as an exclusive field so a user's
  global value survives direct switches
- tell the user when the route or config.toml owns the model catalog
- read the Codex keychain entry by account key with a three-state
  result, outside the app lock and off the async runtime
- retry the Codex rewrite on the next check when a refreshed official
  list could not be written
2026-10-01 23:44:38 +08:00
Jason 370d089be8 fix(prompt): only clear the prompt file when the last enabled prompt is disabled
Saving any disabled prompt emptied the app's prompt file whenever no prompt
was enabled, so adding or importing a prompt wiped a hand-written AGENTS.md
or Hermes SOUL.md without a backup. A deeplink import also lost the file
before enable_prompt could back it up.

Follow the MiniMax Code branch: read the previous row before saving and
clear the file only when this prompt was enabled, is now disabled, and no
other prompt is enabled.
2026-10-01 23:20:06 +08:00
Jason 8f4a87d0cd feat(mcode): let users choose the provider key instead of a random UUID
MiniMax Code shows custom_provider:<key>/<model> in its model picker, so the
UUID CC Switch used as the key leaked into the UI. New MCode providers now
take a user-entered key, prefilled from the preset, like OpenCode. Existing
providers keep their UUID and the key is locked when editing, since MCode's
default-model references embed it.

- Duplicating an MCode provider generates an unused <key>-copy key.
- Adding rejects a key already used by a database row or any live
  custom_provider node, including MCode's own account providers. The check
  and save run under the MCode switch lock, and the live write re-checks the
  key while holding config.yaml.lock so it never replaces a node MCode
  created in the meantime.
2026-09-28 13:00:14 +08:00
Jason b0875f4cfd fix(live): settle before acting, mark publishing before the first rename and keep live-owned fields out of new rows
- A multi-file operation records that it has started publishing in
  live-state.json before the first file is replaced; if that cannot be
  recorded nothing is published. Recovery rolls such an operation forward
  even when a published file was changed again afterwards, so an
  unpublished Codex login stash is no longer discarded.
- controller::lock_settled fails when the previous operation cannot be
  finished, and entry points refuse to save or switch instead of writing
  rows the leftover operation cannot restore. Startup, detach and exit
  log the failure per app and continue.
- Saving, adding and syncing the current provider take the proxy switch
  lock before reading mode and pointer, so entering proxy mode can no
  longer be overwritten by direct key fields. A new proxy address is
  written to every attached app even if one of them fails.
- Codex: the row-level model catalog pointer leaves with its provider; a
  profile selecting the built-in openai provider counts as the official
  route; exclusive fields the editor shows only because live has them are
  compared by value, so an untouched field changed by the client stays
  with the client. The add dialog sends the preset draft it projected, and
  the backend uses it as the field origin like an existing row.
- Claude editor keeps a legacy Bedrock apiKey at the top level where older
  versions read it; the Gemini editor recognizes a legacy Google card the
  same way switching does.
- The request path passes the provider it already resolved to the router
  instead of reading live-state.json twice per request.
2026-09-27 22:38:42 +08:00
Jason 08a80b90f2 fix(editor): keep live-owned exclusive fields and route tables out of provider rows
- Exclusive fields shown in the editor only because live already had them
  are no longer adopted into the row when left untouched; deleting one now
  removes it from live instead of being a no-op. Modified values still
  belong to the row. The Codex add dialog compares against a projection of
  live with an empty card, since its base already carries the preset.
- Codex editor diffs skip the route tables selected by both the base and the
  edited document, so a table renamed away from `custom` no longer leaks
  into config.toml as a global setting.
- The Claude editor takes the proxy switch lock, saves the row before
  writing live and restores the row when the write fails without leaving a
  pending operation, matching the other editors. Row rollback is shared
  through ProviderService::restore_row.
2026-09-27 22:38:42 +08:00
Jason fb56453702 feat(gemini,grok): switch Gemini CLI and Grok Build by replacing only key fields
Switching, adding, editing, sync and entering or leaving local routing now
write Gemini CLI and Grok Build through the write engine. Whole-file
rewrites, backfill before switching and snippet merging are gone.

Gemini CLI:
- Only the key-field lines in .env change; comments, order and the user's
  own variables stay. In settings.json only security.auth.selectedType and
  model.name change. Both files commit as one operation.
- selectedType follows the provider type (Google login for official cards,
  API key otherwise) instead of the row, so a /auth choice an older version
  stored in a row can no longer send a third-party card through Google
  login.
- Local routing writes the route's model name along with the local address
  and placeholder key, so switching to a route with another model rewrites
  the client, as Claude Code does.
- Vertex rows pass validation with GOOGLE_API_KEY or
  GOOGLE_GENAI_USE_VERTEXAI instead of GEMINI_API_KEY.
- The common-config snippet is frozen like Claude's and Codex's; its UI and
  the explainer dialog are removed. New rows keep commonConfigEnabled: true
  for older versions that sync them.

Grok Build:
- Only models.default and the [model."<name>"] table CC Switch wrote
  change; MCP servers and the user's other tables stay.
- The written table name is recorded in live-state.json in the same
  operation, and switching away removes the recorded table rather than
  whatever models.default points to now, so a default changed in Grok's
  /settings no longer leaves the old provider's table behind. Without a
  record (just upgraded), the table is inferred from the current row.
  Tables holding the proxy placeholder key are always removed.
- Local routing writes the route's table pointing at the local
  /grokbuild/v1 endpoint with the placeholder key through the engine. An
  official card cannot be a route.

Editors:
- Both editors show what the client files will look like after switching.
  On save, key fields go to the row and other edits go to live as global
  settings with a three-way check. Gemini row fields that never reach live
  can be added to the global settings; Grok's can be copied, as in Codex.
  The TOML diffing shared with Codex moves to editor_toml.rs.

Also:
- First-run import no longer moves key fields (GEMINI_MODEL, Codex
  model_reasoning_effort and the like) from the default row into the
  snippet, where a frozen snippet would lose them.
- The .env patch can remove keys unconditionally and parse entries.
2026-09-27 22:38:42 +08:00
Jason 0e6430abdb feat(codex): switch Codex by replacing only key fields
Switching, adding, editing, sync, universal providers and entering or
leaving local routing now write Codex through the write engine. Only
routing, credential, model, reasoning, context-window and compatibility
fields are replaced; MCP servers, [projects], comments and the user's own
provider tables stay byte-for-byte. auth.json, config.toml, the model
catalog, the managed-account marker and a device-local login stash commit
as one operation, so a crash at any point rolls forward on startup.

- Third-party routes are always written as [model_providers.custom] with
  the key in experimental_bearer_token. Rows that use another id, a
  top-level openai_base_url or a legacy reserved table are normalized on
  the way out. Switching to an official card leaves a dormant custom table
  (local proxy address plus placeholder credential) so older third-party
  sessions can still resume.
- requires_openai_auth follows the route's auth and whether a login is on
  disk; keyring and ephemeral credential stores are handled separately.
- A switch is refused before anything is written when the active profile
  overrides the route, the selected table is missing or built in, a
  keyless row would fall back to the official login, or the row has no
  auth.
- Provider tables an older version wrote are removed only when they can
  be proven to be CC Switch's. Tables a profile references are kept, and
  reserved ids that cannot be proven are renamed.
- Provider-exclusive fields, including web_search and a row's own
  model_catalog_json, are removed on the way out only when live still
  holds the previous provider's value.
- No more backfill. A native ChatGPT login that CC Switch deletes or
  overwrites is kept in ~/.cc-switch/codex-login-stash.json (0600, not
  synced) and restored when switching back to an official card; logging
  out on an official card discards it. Login snapshots already in
  official rows seed the stash once. A leftover API key in auth.json is
  deleted only when it matches a third-party row's key.
- Managed accounts fetch their token before taking the lock. If the CLI
  refreshes the login during a switch, the switch stops before writing
  anything. Once files are published, a later failure no longer rolls the
  row back; startup finishes the switch.
- Local routing: third-party routes get a fixed local-proxy table and the
  official route uses cc-switch-official plus the client's own login. The
  contract covers every field written, so a route switch with the same
  contract touches no files.
- The Codex editor shows what config.toml will look like after switching.
  On save, key fields go to the row and other edits go to live as global
  settings with a three-way check. Row fields that never reach live are
  listed read-only and can be copied.
- The Codex common-config snippet is frozen like Claude's; its UI and the
  TOML snippet command are removed. New Codex rows keep
  commonConfigEnabled: true for older versions that sync them.
- The write engine can delete a file as part of an operation.
- Integration-test mutexes tolerate poisoning so one failure no longer
  cascades into unrelated tests.
2026-09-27 22:38:42 +08:00
Jason 15c0b3ce23 feat(proxy): keep a per-app direct/proxy mode instead of backing up live files
Local routing no longer takes over a client by backing up its live config
and restoring it later. Each routable app (Claude Code, Codex, Gemini CLI,
Grok Build) now has a device-local mode in live-state.json: direct or
proxy, whether it is attached, the proxy route, and the contract written
into the client. proxy_config.enabled is kept only as a mirror for older
versions.

- Entering proxy mode writes the route's contract (local proxy address,
  placeholder credential, model aliases, provider-exclusive fields) into
  the client; leaving re-projects the direct provider. No backups.
- The proxy route is independent of the direct provider. Switching while
  routing changes only the route; if the contract is unchanged the client
  file is neither read nor written. Failover records the new route and
  never touches client files.
- Current-provider reads go through mode::current::provider_for with an
  explicit purpose (direct pointer vs. the provider in use); a unit test
  rejects direct calls to the old getters.
- Quitting detaches (writes the direct config back, keeps the mode);
  startup re-attaches. Legacy takeover state is migrated on startup:
  backup rows are saved to ~/.cc-switch/backups/proxy-live-backup/ and
  deleted so an older build cannot replay them.
- "Stop routing" returns every app to direct and then stops the server.
  Applying a project no longer disables routing; it moves the direct
  provider or the route depending on the mode. A provider used as the
  route cannot be deleted.
- Codex and Grok Build still use their whole-file writers for proxy
  writes, and all three non-Claude apps for leaving; the direct row is
  backfilled from live before entering (never for the official Codex
  card), and leaving to an unbound official Codex card writes config.toml
  only.
- The provider list marks the direct provider while routing.
- Removes the backup/restore/hot-switch/crash-recovery paths from the
  proxy service, the unused tray Auto item, and the downgrade golden test.
2026-09-27 22:38:25 +08:00
Jason 63ed5002e5 feat(claude): switch Claude Code by replacing only key fields
Switching, adding the first provider, editing the current provider,
sync, universal providers and restoring after the local router all
write settings.json through the write engine now. Only endpoint,
credential, model and protocol fields plus the provider's own switches
are replaced; hooks, plugins, permissions and everything else stay
byte-for-byte. The file write and the current-provider pointer commit
as one operation, so a crash in between rolls forward on startup.

- No more backfill, no common-config snippet and no context-window
  injection for Claude. Window values left behind by the old injection
  are cleaned up from a frozen list.
- The provider editor shows what settings.json will look like after
  switching. On save, provider fields go back to the row and other
  edits go to live as global settings, with a three-way check against
  external changes and a dialog to resolve conflicts. Row fields that
  never reach live are listed with a one-click add.
- New Claude rows keep commonConfigEnabled: true so older versions that
  sync the row still merge their snippet.
- Presets drop timeout and telemetry keys; a test now requires Claude
  presets to contain only key or provider-owned fields.
2026-09-27 22:38:25 +08:00
Jason 46fa2e0e49 refactor(usage): drop per-provider and global cost multipliers
Relays already price in "official price x their own multiplier", and the
real spend is visible from the relay's own balance or usage script. A
second copy of that multiplier in CC Switch only drifts out of sync, and
it only ever applied to requests going through local routing (session
log imports always recorded 1.0), so users setting it in direct mode saw
no effect at all.

- Remove the pricing section from the provider editor (it was also shown
  for Pi and MiniMax Code, which never go through local routing).
- Remove the default multiplier column from the usage pricing defaults;
  keep the global pricing model source (request vs response model) and
  say it only applies to routed requests.
- Remove the get/set_default_cost_multiplier commands and DAO methods.
- Routed requests are now always recorded at multiplier 1. Existing log
  rows keep the multiplier they were recorded with.
- Keep costMultiplier / pricingModelSource in ProviderMeta, unread, so
  older devices syncing the same rows do not lose their values; the
  proxy_config column stays as well, so no schema migration is needed.
2026-09-27 22:21:45 +08:00
Jason bcb83d7ccd test: add golden tests that lock live-config behavior before the switching refactor
The upcoming refactor replaces whole-file live writes with key-field
replacement and splits direct and proxy modes. These tests pin what must
survive it, and pass on the current code.

Backend (`cargo test --test golden`, snapshots under
src-tauri/tests/golden/snapshots, refresh with CC_SWITCH_UPDATE_GOLDEN=1):
- mcp_bytes: MCP projection bytes in ~/.claude.json, Codex config.toml
  and Gemini settings.json, and that switching providers leaves the MCP
  section untouched.
- import_rows: raw DB rows written by deep-link import and first-run
  import (including seeded official rows and the generated snippet).
- file_modes (Unix): which client files a switch writes and their
  permission bits; existing 0600 files are never loosened.
- claude_key_fields: after a switch, live key fields are exactly the
  target provider's, across third-party, Bedrock, Vertex, apiKeyHelper
  and official providers, with or without common config.
- codex_red_lines: 25 Codex invariants (index at the top of the file),
  asserted as outcomes: the config loads on Codex 0.149, the official
  login never reaches a third-party URL, refused switches leave files,
  current provider and rows untouched.
- downgrade_contract: the previous release detects takeover by the
  PROXY_MANAGED literal, rebuilds live from current rows without backup
  rows, and replays leftover backup rows.

Frontend: ProviderForm submit payloads for ten representative Claude and
Codex presets.

Three spots are sorted before comparing because their output order is
not deterministic today (MCP maps written from a HashMap, Gemini .env
parsed into a HashMap). One ignored test records a known gap: exiting
the proxy restores a login the user logged out of while on a
third-party Codex route.
2026-09-26 23:05:45 +08:00
Jason 2185498e88 fix: stop shared config and live files from leaking provider settings
Common config snippet (Claude Code):
- Treat custom request headers (`*_CUSTOM_HEADERS`, `HEADERS`) and any
  key containing COOKIE or AUTHORIZATION as credentials. Header names in
  general stay shareable (CLAUDE_CODE_ATTRIBUTION_HEADER,
  OTEL_EXPORTER_OTLP_HEADERS).
- Never share key fields: all ANTHROPIC_* settings, the protocol
  selectors (CLAUDE_CODE_USE_BEDROCK/VERTEX/...), AWS_* and Vertex
  regions, and top-level model/apiKeyHelper/fallbackModel. Switching
  from Bedrock to the official provider no longer keeps Claude Code on
  Bedrock, and a /model choice no longer follows every switch.
- Entries an existing snippet already shared are stripped from the
  current provider's row on its next switch-away, so they cannot get
  stuck in the wrong provider. A provider carrying AWS or Google
  credentials keeps that family's selectors and regions.

Live files holding keys are now written owner-only (0600): Codex
auth.json and config.toml, Claude Code settings.json and Grok Build
config.toml. New files used to follow the umask (0644).

Presets:
- E-FlowCode and PIPELLM no longer ship personal preferences; Codex
  E-FlowCode drops `personality`.
- AWS Bedrock (API Key) stores the key in env.AWS_BEARER_TOKEN_BEDROCK,
  which Claude Code reads. The key field and the proxy read it too;
  existing rows with a top-level apiKey keep working and are edited in
  place.
- A preset scan keeps preference keys out of Claude and Codex presets.

Remove dead code only reached from tests: ProxyService::start_with_takeover
and its helpers, and the ConfigService sync_current_providers_to_live
family.

Behavior changes: ANTHROPIC_* settings such as ANTHROPIC_LOG are no
longer shared; a Bedrock or Vertex provider whose selectors only lived in
the shared snippet may need them re-entered once.
2026-09-26 23:03:42 +08:00
Jason f33f9c41cb fix(grokbuild): keep provider model table selected when backfilling live config
Grok Build's /settings "Default model" rewrites models.default in
~/.grok/config.toml. Selecting a built-in model while a third-party
provider was active left the provider's own [model.*] table
unreferenced; the switch backfill stored that config verbatim, and
switching back then failed validation with "missing [model.\"...\"]".

When models.default is missing or dangling during backfill and the
provider's original default table is still present, restore it.
2026-09-26 21:51:48 +08:00
Allen Xu a659440b9f fix(prompts): refresh active prompts after external file edits (#7194)
Reload the enabled prompt from its live file when the Prompts list loads, and refresh the open panel when the window regains focus, so external edits to CLAUDE.md / AGENTS.md show up without a restart.

- Keep saved templates available when the live file cannot be read.
- Skip the backfill for empty or whitespace-only files, matching enable_prompt.
- Skip the backfill while a sync/restore holds the sync lock, so a list load cannot write a stale live file into a freshly restored database.
2026-09-20 17:13:55 +08:00
SaladDay 06082e189d feat: add MiniMax Code harness support (#7383)
Add MiniMax Code (mcode) as a managed application: provider and model management through the native custom_provider configuration, MCP servers, Skills, global instructions (AGENTS.md), read-only local session history with resume, and usage import from the native runtime database. Native files shared by the TUI and desktop are written under the native lock with rollback when the database commit fails, unrelated native configuration is preserved, and the data directory honors MINIMAX_DATA_DIR and MAVIS_DATA_DIR. Deleting sessions stays in the native application because they are coupled to runtime state.
2026-09-15 12:33:49 +08:00
Doudou 3c1d3c94ac fix(tests): isolate LOCALAPPDATA so tests stop writing the real Claude Desktop config
On Windows, Claude Desktop config paths resolve from LOCALAPPDATA, which the test home override in tests/support.rs did not redirect, so profile_roundtrip wrote the https://desktop.test fixture into the developer's real Claude Desktop config. Point LOCALAPPDATA at the isolated test home alongside HOME and USERPROFILE.

Fixes #6077
2026-09-07 15:54:13 +08:00
Jason 97a7425f7f fix(codex): preflight switches before committing current, share normalization with proxy paths
Four hardening fixes on top of the config-only unification:

- Split write_codex_live_for_provider into a pure plan builder
  (normalization, safety gates, token injection, TOML parsing) and an
  executor, and preflight the plan in switch_normal BEFORE current is
  committed. Previously a write-layer refusal landed after local/DB
  current had moved, so the next switch would backfill the old live
  config into the refused provider's DB row.
- Sink the legacy openai_base_url normalization into
  prepare_codex_provider_live_config, making it the single
  normalize→inject entry point. Takeover backup rebuilds and restore call
  prepare directly; without the migration a restored legacy backup left
  the key in a top-level field Codex ignores while auth.json credentials
  stayed live.
- Stop update_codex_toml_field from creating [model_providers.openai]:
  Codex 0.149 rejects the whole config when a reserved built-in id is
  overridden (validate_reserved_model_provider_ids). The built-in openai
  provider is rerouted via the top-level openai_base_url knob instead,
  and wire_api writes become a no-op for it.
- Never overwrite a user-authored [model_providers.cc-switch] table
  during normalization — nothing proves it is ours, and their headers or
  query params would be lost and backfilled into the DB. The shape falls
  through to the safety gates instead.

The keyless-fallback rejection test now also locks that a refused switch
leaves current on the previous provider.
2026-08-27 08:37:56 +08:00
Jason cbb7912776 refactor(codex): unify provider switching on config-only auth writes
Since Codex 0.149 (openai/codex#39214) custom providers no longer inherit
ambient auth from auth.json, so the API key must travel as a
provider-scoped experimental_bearer_token in config.toml (honored since
Codex 0.48; verified against rust-v0.148 sources). Instead of dual-writing
auth.json + config.toml depending on the preservation setting, make every
third-party switch config-only:

- auth.json is reserved for the official ChatGPT login: preserved when
  the preservation setting is on, deleted otherwise. It never carries
  third-party keys, so official OAuth cannot leak to a third-party
  endpoint through a requires_openai_auth fallback or a lingering login.
- The legacy reroute shape (built-in openai provider + top-level
  openai_base_url) is normalized into a cc-switch-owned custom provider
  table pinning wire_api = "responses", giving the key a provider-scoped
  slot instead of a top-level field Codex ignores.
- Two safety gates guard every third-party write (previously only the
  preservation path): a carried key with no provider table to land in is
  refused, and a keyless config that would resolve auth from auth.json
  (requires_openai_auth without provider-own credentials, or an
  openai_base_url reroute) is refused.
- Token injection skips provider tables carrying their own credentials
  (env_key / auth / aws / Authorization headers); requires_openai_auth
  overrides the header guard so the injected token short-circuits the
  auth.json fallback. Inline provider tables are handled in
  set/extract/remove, and amazon-bedrock-runtime joins the reserved
  provider id lists (Rust + TS).

Official cards are unchanged. Takeover placeholder writes ride in
config.toml the same way, which is what Codex >= 0.149 actually sends to
the local proxy.

Known trade-offs: pre-0.48 Codex releases (which never read the token
field) lose third-party auth, and an empty third-party config with a key
is now refused on every path instead of riding in auth.json.

Refs #6746, #6744
2026-08-27 08:37:56 +08:00
SaladDayandJason 0455a92c1f fix(codex): allow multiple follow-login providers (#6535)
Co-authored-by: Jason <farion1231@gmail.com>
2026-08-18 10:51:38 +08:00
Jason e3f80a98f3 fix(codex): clear stale auth on official switch 2026-07-31 19:12:58 +08:00
Jason f733def452 feat(grokbuild): add Grok Official provider with official-state import
Add a "Grok Official" preset and seed (grokbuild-official) whose empty
config represents the official login state: no custom [model.*] tables
are written, so Grok CLI falls back to its built-in xAI OAuth login and
cc-switch never touches those credentials.

Backend:
- Seed entry in OFFICIAL_SEEDS plus ensure_grokbuild_official_provider
  command for on-demand repair (the one-shot master seeding flag is
  already set for existing databases).
- Split validation into syntax-only (empty allowed) for live reads,
  writes and official snapshots, keeping the full custom-model shape
  check for non-official provider writes and imports. Backup/restore
  can now round-trip an official-state live file.
- Manual import (command layer only) recognizes an official-state live
  config and imports it as the official entry set as current, matching
  the Codex official-login import outcome. Startup auto-import keeps
  rejecting official-state live so a deleted official entry is never
  resurrected on launch: startup import only captures real user data
  as "default" and never manufactures official entries.
- Manual import also ensures the official entry before importing
  (claude-desktop precedent) and after a successful custom import, so
  first-time users end up with default + official like other apps.
- Proxy takeover guards skip or reject official-state live configs in
  all three takeover paths, consistent with the official-provider
  takeover ban.

Frontend:
- Grok Official preset entry in the GrokBuild form: official category
  hides connection fields and passes the raw config through untouched.
- Filter managed-OAuth presets out of the GrokBuild preset list; they
  were never wired for this app and produced keyless broken configs.

Tests cover seed presence, official round-trip, ensure-after-deletion,
and the four import scenarios including startup non-resurrection.
2026-07-21 16:39:34 +08:00
Thefool 1c0ee0c58a feat(grokbuild): add first-class Grok Build support (#5453)
* feat(grokbuild): add backend integration

* feat(grokbuild): add provider and management UI

* test(grokbuild): cover configuration and integrations

* fix(grokbuild): address backend review feedback

* fix(grokbuild): complete UI review feedback

* feat(grokbuild): add CLI lifecycle management

* fix(grokbuild): align provider icon fallback

* test(grokbuild): cover provider icon persistence
2026-07-17 15:50:50 +08:00
Jason 94fc1cc064 fix(mcp): surface per-app failures when importing MCP servers from apps
import_mcp_from_apps swallowed every importer error with unwrap_or(0),
so a corrupt config.toml surfaced as "imported 0 servers" with no hint
that anything went wrong.

Move the aggregation into McpService::import_from_all_apps: each app
imports best-effort (one bad file doesn't block the rest), and failures
are collected into a single error naming the failing apps alongside the
count that did import. The frontend now refreshes the server list on
settle rather than success, since a partial failure still means new
servers were persisted.
2026-07-08 22:46:09 +08:00
Jason 11c173c730 fix(mcp): stop cross-app failures from blocking MCP re-projection
sync_all_enabled iterated AppType::all() with `?`, so one app's corrupt
live file (e.g. a broken ~/.claude.json, which passes the existence
gate but fails to parse) blocked every app behind it in the iteration
order -- and bubbled the error into whatever operation triggered the
sync:

- switch/save had just rewritten only the target app's live file, yet a
  broken unrelated file failed the whole operation after DB and live
  were already updated, reporting a false "switch failed" to the user.
  Both now project only the target app (sync_enabled_for_app) and
  degrade projection failure to a warning: the primary operation has
  already taken effect, and the projection self-heals on the next
  switch or MCP toggle.
- sync_current_provider_for_app_to_live syncs a single app; it now
  projects that app only, keeping failures (which can only concern the
  target app) as errors.
- sync_current_to_live (config import / cloud-sync restore) keeps the
  all-apps sweep, but sync_all_enabled is now best-effort: it projects
  every app, collects failures, and reports them aggregated. Its error
  is held until after skill sync so an MCP failure no longer skips it.
2026-07-08 22:46:09 +08:00
Jason 1f36f0cfed feat(provider): extend switch-time common-config autosync to Codex
Claude already re-extracts the live config into the shared common-config
snippet right before switching away, so shared tweaks (plugins,
preferences) made in live propagate to all opted-in providers. Codex was
gated out because its TOML pipeline leaked provider-specific and
injected content into the snippet.

With the extractor now stripping all injection artifacts and routing
fields, and backfill stripping the MCP projection, open the gate to
Claude + Codex. The autosync-before-strip ordering also self-heals
stale snippet values previously baked into provider snapshots: the
re-extracted snippet matches the live values, so the value-match strip
removes them on the same switch.

End-to-end tests cover: new shared keys captured, deletions synced,
secrets/injected artifacts never entering the snippet, and provider A's
key not leaking into provider B's live.
2026-07-08 22:46:09 +08:00
Jason 6d2ee2472f fix(provider): re-project Codex MCP after unified-session toggle rewrites live config
Toggling unify_codex_session_history rewrites the current official
provider's live config.toml in full (intended design), which drops the
[mcp_servers] projection -- and nothing put it back, so enabled MCP
servers silently vanished until the next provider switch (#C2).

Re-project after the rewrite, with two deliberate choices:

- Project Codex only (new McpService::sync_enabled_for_app) instead of
  sync_all_enabled: the all-apps sync short-circuits in AppType::all()
  order, so a corrupt ~/.claude.json would error before Codex is ever
  reached and the freshly wiped [mcp_servers] would stay missing. Only
  Codex's live file was rewritten here, so only Codex needs
  re-projection.
- Degrade projection failure to a warning: by this point the live file
  already carries the new bucket state, so the toggle has taken effect.
  Propagating the error would make save_settings roll back the setting,
  creating the exact "setting=old, live=new bucket" session split the
  rollback exists to prevent. The projection self-heals on the next
  switch or any MCP toggle.
2026-07-08 22:46:09 +08:00
Jason 8b1ce764f1 fix(mcp): fail closed when Codex config.toml is unparseable during MCP sync
sync_single_server_to_codex silently fell back to an empty DocumentMut
when the existing config.toml failed to parse, then wrote the whole file
back -- wiping every other section (model, model_providers, comments)
and leaving only the single synced [mcp_servers.<id>] entry. Introduced
by 3a548152, which fixed the removal path but left the destructive
fallback on the sync path.

Now the sync path returns an McpValidation error and leaves the file
untouched, matching the fail-closed behavior of the read/validate path.
2026-07-08 22:46:09 +08:00