Commit Graph
100 Commits
Author SHA1 Message Date
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
Jason d8983bb10b docs(release): add v4.0.19 release notes 2026-10-23 20:13:26 +08:00
Jason a043b1f863 chore(release): v4.0.19 2026-10-23 20:13:26 +08:00
Jason bada9a4bb9 fix(models): don't borrow a vendor-hosted entry that points at another vendor
When a relay's model is not recognized by address, the metadata lookup
falls back to the vendor's entry: first the entry that same-name entries
point at via canonical_model_id, and only if none resolves, an entry
listed under a provider that some canonical id points at.

That last step only checked whether the provider counts as a vendor, not
whether the entry claims to be its own. models.dev has no
deepseek/deepseek-v4.1-flash-1017, so the ~40 entries pointing there all
miss, and thinkingmachines (counted as a vendor for its own models) won
with its LoRA fine-tuning entry DeepSeek-V4.1-Flash-1017:peft:262144,
whose canonical id points back at deepseek. It is text-only with a 262144
window, so picking the model filled every app with text-only input and a
quarter of the real window; Codex additionally turned image input off.

Skip entries whose canonical id names another vendor in that fallback.
Against the current models.dev data, 27 ids stop borrowing from hosts of
other vendors' models (thinkingmachines, NVIDIA, vivgrid, alibaba) and
leave the fields empty instead, and laguna-xs-2.1 now resolves to
poolside's own entry rather than being ambiguous. Skipping ':' variants
instead was rejected: it fixes this id but newly resolves nine
thinkingmachines entries carrying 65K training windows (gpt-oss-120b and
others).

Refs #8765, #8682
2026-10-23 20:11:53 +08:00
Jason e53ca7e820 fix(usage): tighten request log column spacing
The request log has more columns than the other usage tables, so at the
default window size the last column (first token) was cut off and the
table had to scroll sideways. Its columns are already sized to their
content, so the model column had no slack to give back.

Narrow the gaps for this table only: 16px between columns (was 24px) and
32px between column groups (was 40px), keeping the 16px outer edges. The
table shrinks by 72px with the same rows; model names are not truncated.
The provider, model and pricing tables keep their spacing.
2026-10-23 17:37:41 +08:00
Jason 50e091b055 fix(usage): step 7/14/30-day presets back by calendar days across DST
getPresetLookbackStart subtracted (N-1) * 24h from the current instant
and then rounded to local midnight. Across a DST offset change that
subtraction can land on the adjacent date, so the 7d/14d/30d presets
included one extra local day after spring-forward and dropped one after
fall-back (e.g. America/New_York, Europe/Berlin). The shifted range fed
the usage summary, model/provider stats and request-log queries.

Build the start date from today's local year/month/day minus (N-1) days
instead, which yields the first instant of that calendar day, including
zones where midnight itself is skipped by DST. The 1d rolling window,
today, all-time and custom ranges are unchanged.

Refs #8719
2026-10-23 17:37:41 +08:00
Jason 9cb6b6d7fa fix(agy): run background agy commands without a console window on Windows
Refreshing the Antigravity official quota on Windows popped up a black
console window. Outside macOS the quota falls back to spawning
`agy --print /usage`, and the model list in the provider form spawns
`agy models`. The release build is a GUI-subsystem app, so a console
child spawned without CREATE_NO_WINDOW gets its own console window.

Both spawn sites now go through agy_config::background_command, which
sets CREATE_NO_WINDOW on Windows like the other background tool probes.
Behaviour on macOS and Linux is unchanged.

Refs #8700
2026-10-23 17:37:41 +08:00
Jason 10e0aa2380 fix(deeplink): don't let a finished import close a newer link's confirmation
While provider link A was still importing (awaiting the backend import or
the follow-up cache refresh), a second link B could arrive, replace the
displayed request and be acknowledged in the backend. When A's import
completed, handleImport unconditionally called setIsOpen(false), closing
B's confirmation even though B was never imported and could no longer be
fetched again.

Track the request currently shown in a ref (set together with setRequest
in showPending) and close the dialog after an import only if it still
shows the request that import started with. Add a regression test that
holds A's import, delivers B, then checks B stays open and still imports
and closes on its own.

Refs #8696
2026-10-23 17:37:41 +08:00
Jason 7ecce7c79e fix(proxy): carry encrypted-only reasoning as empty thinking for Claude clients
Claude Code routed to an OpenAI Responses upstream showed "Unsupported
content type: redacted_thinking" before tool calls in the VS Code
extension. Reasoning items that only carry encrypted_content (summary is
empty) were converted into redacted_thinking blocks, which the extension
cannot render.

Emit them as an empty thinking block whose signature holds the reasoning
envelope instead, the same shape Anthropic returns for omitted thinking.
Replay already decodes thinking.signature and redacted_thinking.data the
same way, so replayed upstream requests are byte-identical and histories
written by older versions keep working.

Also recover summary text that never arrived as deltas: when
output_item.done carries a summary that strictly extends what was
streamed (e.g. a later summary part had no deltas), emit only the
missing tail before the signature. Snapshots that do not extend the
streamed text are ignored, so nothing is re-emitted.

The upstream request is unchanged; reasoning.summary is not injected.

Refs #8691
2026-10-23 17:37:41 +08:00
Jason 2fbd9c70f4 fix(hermes): launch the dashboard inside WSL when Hermes lives there
On Windows, pointing the Hermes config directory at a WSL path
(\\wsl$\<distro>\... or \\wsl.localhost\<distro>\...) only redirected
config, sessions and the version probe. When the Web UI was offline, the
"launch dashboard" action still opened a Windows terminal running
`hermes dashboard` natively, so it started the Windows hermes (or failed
with command not found) instead of the instance inside the distribution.

Build the dashboard command line from the existing per-tool WSL distro
detection: for a WSL override run it as
`wsl.exe -d <distro> -- sh -c "\"${SHELL:-sh}\" -lic 'hermes dashboard'"`
so the user's login shell PATH (e.g. ~/.local/bin) applies. Only one
shell flag is used, since a Ctrl+C exit is non-zero and a fallback retry
would relaunch the dashboard. The Web UI probe on 127.0.0.1 is unchanged;
WSL2 forwards localhost to the distribution. Other platforms keep the
plain `hermes dashboard`.

Refs #8689
2026-10-23 17:37:41 +08:00
Jason b5aa7fbbcb fix(agy): show Gemini quota when querying Google directly
Official Antigravity cards on macOS often showed only the Claude and GPT
quota. Google's retrieveUserQuotaSummary decides what to return from the
User-Agent: "cc-switch/<version> (antigravity)" passes the license check
but gets only the Claude and GPT group, and the Gemini group is returned
only to "antigravity/cli/<version>" from 1.2.0 on. agy's own /usage sends
that, which is why the fallback path always showed all four tiers.

The User-Agent is now "cc-switch/<version> (antigravity/cli/<agy
version>)". The version comes from the local "agy --version", probed once
per process, and falls back to 1.4.6 when agy cannot be found or read.
2026-10-23 17:37:41 +08:00
Jason 3494d0d100 chore(presets): add V-API backup endpoints
List V-API's mirror lines (api.vveai.com, run.v3.cm, api1.v3.cm,
api2.v3.cm, us.gpt.ge) as endpoint candidates after the main
api.gpt.ge in the Claude Code, Claude Desktop, Gemini CLI, Codex and
Grok Build presets, with /v1 on the OpenAI-format apps.
2026-10-23 17:37:41 +08:00
Jason e85ade714a chore(presets): swap the FluxA TokenPlan and Shengsuanyun sponsor positions
Move FluxA TokenPlan up to Shengsuanyun's former slot after V-API and
Shengsuanyun down to FluxA's former slot after SubRouter, in the four
README sponsor tables and the seven preset files that carry both.
2026-10-23 17:05:55 +08:00
Jason 91d3ed101d feat(presets): add V-API sponsor presets across nine apps
Add V-API (gpt.ge) as a full sponsor placed after BuyToken: README rows
and banner in all four languages, the partner promotion copy, an inline
tile icon, and presets for Claude Code, Claude Desktop, Codex, Gemini
CLI, Grok Build, OpenCode, OpenClaw, Hermes and Pi. The gateway prices
and gates models per token group, so each preset notes which group it
needs. It has no undated claude-haiku-4-5, so the Desktop Haiku tier is
mapped to claude-haiku-5-5.
2026-10-23 16:57:28 +08:00
Jason 069c7fac19 feat(presets): add BuyToken sponsor presets across eight apps
Add BuyToken as a full sponsor placed after DIT.ai: README rows and
banner in all four languages, the partner promotion copy, an inline tile
icon, and presets for Claude Code, Claude Desktop, Codex, Grok Build,
OpenCode, OpenClaw, Hermes and Pi. Keys are bound to a model group, so
each preset notes which group's key it needs. The gateway only accepts
the dated claude-haiku-4-5-20251001 ID, so the Haiku tier is mapped to
it. There is no Gemini preset because /v1beta is not served.
2026-10-23 16:28:46 +08:00
Jason 717172bd6b chore(presets): point DIT.ai links at the ccswitch referral URL
Replace the placeholder /register link with
https://dit.ai?utm_source=ccswitch in the nine DIT.ai presets and the
four README sponsor rows.
2026-10-23 16:11:02 +08:00
Jason 03707c400d style(pi): use the shared required-field asterisk
The provider key, the model ID and display name column headings, and
the context window and max output labels now use REQUIRED_LABEL like
the rest of the provider forms, instead of a hand-written "*" span.
The asterisk is drawn by CSS, so it no longer becomes part of the
label text that screen readers and tests read.
2026-10-23 15:50:15 +08:00
Jason 1e2559ef9e feat(pi): always show the provider key and flag duplicates
The provider key names the provider node in models.json, so it is now
shown for presets too (prefilled, editable) instead of only for custom
and edited providers, matching Kimi Code. A key that is already used
by another Pi card is flagged under the field and blocks saving, so
adding the same preset twice no longer fails only at the backend.
2026-10-23 15:49:22 +08:00
Jason 0f76829001 feat(kimi): lay out the provider editor like Pi and OpenCode
Models are now one row each (expand arrow, model ID, display name,
delete) with capabilities and the context window in the expanded
details, instead of one large card per model. Fields follow the same
order as Pi and OpenCode (API format, API key, base URL), the API
format menu shows readable protocol names, request headers are shown
inline, and required fields use the shared asterisk style.

The new display name column writes the model's display_name, which
Kimi Code shows in /model; clearing it removes the field. The model
selection hint moves into a "?" beside the models heading, and the
Kimi-only labels that duplicated OpenCode's are removed from all
locales.
2026-10-23 15:48:09 +08:00
Jason d6db0c7570 fix(providers): stop the advanced options toggle from jumping when pressed
Small buttons shrink slightly while pressed. On the full-width,
left-aligned "Advanced options" toggle the shrink is centered, so the
label visibly shifts. Keep these toggles at full size in the Claude,
Codex and Kimi Code forms.
2026-10-23 15:15:19 +08:00
Jason b96ff61821 feat(kimi): move request headers into collapsible advanced options
Custom headers are rarely needed for Kimi Code providers, so keep them
under an "Advanced options" section like the Claude and Codex forms.
It opens by itself when headers are already set, from an existing card
or a preset that carries them.
2026-10-23 15:15:19 +08:00
Jason 8969264b96 feat(kimi): explain the less obvious model capabilities
"Always thinking" and "Load tools on demand" don't say what they do.
Add a "?" help tip next to each in all four locales: the first is for
models whose thinking can't be turned off, the second only works with
Kimi's own models and needs KIMI_CODE_EXPERIMENTAL_TOOL_SELECT. Move
"Always thinking" next to the last one so the common three come first.
2026-10-23 15:15:18 +08:00
Jason 535d12831f fix(models): match old OpenCode Zen/Go addresses to the moved models.dev entries
models.dev moved OpenCode Zen and Go to /inference/... endpoints, while
presets and existing providers still use /zen/... (both work and bill
the same). The endpoint lookup no longer found them, so models only
listed there, such as deepseek-v4.1-flash, filled nothing. Map the old
normalized addresses to the new ones; this applies to every app's fill.
2026-10-23 15:15:18 +08:00
Jason b61e2767c0 feat(kimi): fetch the model list and fill parameters from models.dev
The Kimi Code form only let users type model IDs and context windows by
hand, unlike the other apps' forms. Add the same "Fetch models" picker
and per-row dropdown: picking models adds them with tool use enabled,
then fills the context window, thinking and image input from the
matching preset or models.dev entry (the endpoint decides which). A
window the user typed is never overwritten; rows still on the
placeholder window take the looked-up value. Anthropic and Gemini
providers fetch with their own auth header.
2026-10-23 15:15:18 +08:00
Jason 93e33d110a fix(apps): drop the redundant not-installed error line under uninstalled tools
The version column already says the tool is not installed, so the extra
"not installed or not executable" caption repeated it in English. A plain
not-found probe now reports no error; WSL failures with real diagnostics
(invalid shell, exec failed) still surface.
2026-10-23 11:46:49 +08:00
Jason 1c52fa918d fix(providers): reread additive client state when a removal is refused
Kimi Code saves every /model pick as default_model, so the default shown
in CC Switch can go stale while the page stays open. The backend rereads
config.toml and refuses to remove the provider Kimi Code is using, but
the page kept showing the old default and an untranslated English reason.

Reread the additive app's live state after a failed removal as well as a
successful one (now including the OpenCode, OpenClaw and Hermes default
model queries), reread Kimi providers after failed edits and deletions,
and explain the two Kimi "model in use" refusals in all four locales.
2026-10-23 11:42:12 +08:00
Jason c6f85464e7 style(usage): restore the original trend chart and heatmap colors
Point --chart-usage back at --chart-1 and restore the heatmap's darkest
light-mode step (#0e7490), so the usage charts look as they did before
#8711. The separate token stays, so a later color change does not touch
the quota bars that also use --chart-1.
2026-10-23 11:21:04 +08:00
Jason 29f8bef378 fix(usage): clamp the request log page when the total shrinks
When the user sat on page 2+ and the total dropped (the "today" window
rolled over, Codex usage was rebuilt), the page index stayed out of
range. With a single page the pagination bar is hidden, leaving an
empty table with no way back. Pull the page back to the last valid one
once real (non-placeholder) data arrives.
2026-10-23 11:15:39 +08:00
Jason f4e9211a11 fix(panel): let Escape close only the topmost full-screen panel
Each FullScreenPanel adds its own keydown listener on window, and
stopPropagation does not stop other listeners on the same target, so
stacked panels (pricing -> pricing editor, edit provider -> endpoint
speed test) all closed on a single Escape. Track open panels in a
module-level stack and only let the top one react.
2026-10-23 11:15:39 +08:00
Jason 7cc3141cae style(providers): mark additive default providers in blue
The default provider in OpenCode, OpenClaw, Hermes and Kimi used a gray
card and dot. Use the same blue as an in-use switch-mode card instead,
and drop the now-unused neutral card tone.
2026-10-23 11:15:35 +08:00
Jason d6a2324196 feat(kimi): give provider cards real add/remove actions
Kimi is additive, but both card groups only offered "Switch", which
wrote the provider into config.toml and changed default_model at once,
so the Added / Available split did nothing.

- Available cards get "Add": a new add_provider_to_live_config command
  writes the provider and its models into config.toml without changing
  default_model, so they show up in Kimi's /model list. A node already in
  config.toml belongs to Kimi and is never overwritten from the archive.
- Added cards get a "Remove" button; "Set as default" (select the
  provider's model) moves into the more menu, as for OpenCode/OpenClaw.
- The provider owning default_model is marked with the gray "Default"
  status like OpenCode, and cannot be removed.
2026-10-23 11:15:35 +08:00
Jason 44e0fc9630 feat(kimi): name separate official accounts "Kimi 订阅 <nickname>"
Kimi Code shows a provider's key as its name in the model picker, so
separate accounts appeared as "kimi-code-ccs-<hash>". New separate
accounts are now keyed "Kimi 订阅 <nickname>" ("Kimi Subscription" when
CC Switch is not in Chinese), matching Kimi's own label for its default
login and limited to the characters Kimi accepts, numbered on clashes.

The credential slot and model aliases keep the stable
"kimi-code-ccs-<hash>" name, so default_model stays an ASCII key and
ownership checks are unchanged. A returning account is also recognised
by its credential slot, not only by the local identity cache. Card names
fall back to the readable key when no nickname is cached.
2026-10-23 11:15:35 +08:00
Jason c49c2be94b refactor(kimi): render Auth Center accounts with the shared account group
Kimi's account list drew its own layout (inline description, a permanent
region picker, highlighted current row, extra badges). It now uses the
same ManagedAccountsGroup as Copilot, ChatGPT, xAI and Antigravity:
help behind "?", region chosen when adding an account, "In use by Kimi"
badge, nickname and avatar, shared pending/error blocks, and a
"needs re-login" state for signed-out accounts.

The shared group gains optional per-row remove label and disabled
reason, an optional "remove all" action, and a looser device-code type.
Account action failures now toast instead of reusing the login error
block. Unused Kimi strings are removed from all locales.
2026-10-23 11:15:35 +08:00
Jason e12415b5ba refactor(providers): move OpenCode/OpenClaw set-default into the more menu
The icon-only star on added OpenCode/OpenClaw cards was hard to read and
had no selected state. "Set as default" now sits at the top of the card's
more menu, like the aggregation page; providers with several models open
a submenu to pick the default model. The card keeps only the remove
button, and the primary button no longer needs its own dropdown.
2026-10-23 11:15:35 +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
Jason e88ff5404d test(usage): scope Codex parent-cache count assertions to each test's dir
The parent_rollout_* tests asserted on the total size of the process-wide
CODEX_REPLAY_CACHES.parent_timelines map. #[serial_test::serial] only
excludes other serial tests, while non-serial tests such as
test_parse_codex_file_recognizes_branch_thread_paginated_continuation
reach load_parent_timeline (via resolve_continuation_baseline) and insert
their own entries concurrently. Under multi-threaded `cargo test` that
occasionally turned an expected count of 1 into 2; nextest's
process-per-test model hid it in CI.

Count only entries under the test's temp dir instead. Production code is
unchanged.
2026-10-23 11:15:35 +08:00
Jason 0640c6fcc7 fix(codex): never turn image input off when filling fetched model metadata
Picking a model from the fetched list fills its empty catalog fields from
known metadata (same-endpoint preset, models.dev). For a model the lookup
reported as text-only, the Codex fill wrote inputModalities ["text"].

For Codex an absent inputModalities is not "unknown = text": the backend
checks the exact text-only registry and otherwise fails open to
["text", "image"]. Writing ["text"] therefore switched image input off,
against the fill rule that modalities are only ever turned toward
"supported", and the Codex form has no control to switch it back, so
Codex kept refusing image attachments for that model.

Only fill inputModalities when the metadata includes image; otherwise
leave it absent and let the registry and fail-open default decide.

Refs #8682
2026-10-23 11:15:35 +08:00
Jason ce098caf22 fix(about): detach the shell version probe from the controlling terminal
try_get_version still ran `$SHELL -lic "<tool> --version"` via a bare
output(), so when CC Switch was launched from a terminal on Linux/WSL the
interactive shell kept the controlling tty, got stopped by SIGTTIN, and
opening Settings -> About froze the whole app.

Spawn it like resolve_path_default / login_shell_path already do: stdin
null, isolate_child_process_group (setsid) and the 10s probe deadline.
-lic is kept so PATH entries from .bashrc are still picked up; a timeout
falls back to the existing directory scan.

Fixes #3241
2026-10-23 11:15:35 +08:00
Jason 91fc2fcbf7 fix(codex): split the stack comp_hash by tool protocol
In the merged (stack) catalog every third-party row shared one comp_hash,
"cc-switch", so Codex never compacted when switching between them (#8485).
Third-party GPT rows mirror the official entry and carry
`tool_mode: "code_mode_only"`, where shell calls are recorded as the
custom tool `exec`; DeepSeek, Kimi and other rows are direct and only have
`exec_command`. After switching from a GPT row to a direct row without
compaction, the direct model copied the `exec` calls from history and
every turn failed with `unsupported call: exec`.

Group rows by tool protocol instead: a row with a tool_mode gets
"cc-switch-<tool_mode>", direct rows keep "cc-switch" so existing
sessions see no change. Switching within a group still skips compaction;
switching across groups compacts with the previous model first, which
folds the other protocol's calls into the summary. tool_mode and
supports_search_tool keep the template values, so third-party GPT rows
keep code mode. The cost is that a cross-group switch away from a
provider that is no longer usable fails to compact, as before #8485;
switching within the group or starting a new session avoids it.

Fixes #8676
2026-10-23 11:15:35 +08:00
Jason cb90a2440d docs(release): add v4.0.18 release notes 2026-10-21 23:57:09 +08:00
Jason 4aec1aecaa chore(release): v4.0.18 2026-10-21 23:57:09 +08:00
Jason 04d0ebf568 fix(app): raise the open-file soft limit at startup on Unix
macOS GUI apps started from Finder or a login item inherit launchd's
soft RLIMIT_NOFILE of 256. The local router holds a client socket and an
upstream connection per in-flight request on top of the log, database
and config files, so a burst of parallel requests exhausted the
descriptors: accept() failed with EMFILE, upstream connects failed,
reading live-state.json failed and SQLite could not open the database.

Right after the logger is initialized, lift the soft limit to the hard
limit capped at 10240 (macOS OPEN_MAX; setrlimit rejects larger values
there). The hard limit is never touched and a limit that is already high
enough is left alone; failures are only logged.

Refs #8668
2026-10-21 23:23:26 +08:00
Jason 47ffdff8e0 fix(apps): give runtime CLI commands the login-shell PATH on Unix
When CC Switch is launched from Finder, its PATH is launchd's narrow
/usr/bin:/bin:/usr/sbin:/sbin. run_detected_tool_command_with_timeout
located the tool through a login shell but then spawned it with only the
tool's own directory prepended to that narrow PATH. An npm-installed CLI
is a `#!/usr/bin/env node` script, so when node lives in a different
directory (codex in /opt/homebrew/bin, node in /usr/local/bin) the
"Restart Codex daemon" action failed with `env: node: No such file or
directory`. The same helper runs `opencode models`.

Merge the login-shell PATH in front of the inherited PATH, as tool
install/update already does. login_shell_path now honours the caller's
deadline and spawns like resolve_path_default (stdin null, own session);
when it fails or times out the previous PATH is used unchanged.

Refs #8653
2026-10-21 23:23:26 +08:00
Jason 6226f5c468 fix(terminal): stop waiting for the launched console to close on Windows
Opening a terminal on Windows ran `cmd /C start ...` through
Command::output(). The console that `start` opens inherits the wrapper's
stdout/stderr pipe, so the pipe only reached EOF once the user closed
that window, and the command (and the open-terminal dialog) kept
spinning until then.

Point stdio at NUL and wait only for the wrapper's exit status. Launch
failures still surface through the exit code. Same approach as the
community PR #6716 by @umutcankurt, applied to the current code path.

Refs #6712
2026-10-21 23:23:26 +08:00
Jason 132ab9cedb feat(presets): add hao.ai sponsor presets across eight apps
Add hao.ai as a full sponsor placed above Fenno: README rows and banner
in all four languages, the partner promotion copy, an inline tile icon,
and presets for Claude Code, Claude Desktop, Codex, Grok Build, OpenCode,
OpenClaw, Hermes and Pi. hao.ai serves Anthropic Messages under
/anthropic, OpenAI-compatible APIs under /v1 and xAI under /x-ai/v1, and
its catalog IDs carry a provider prefix. There is no Gemini preset since
the catalog has no Gemini models.
2026-10-21 23:14:08 +08:00
Jason 0a41250dc0 feat(presets): add DIT.ai sponsor presets across nine apps
DIT.ai (dit.ai), an AI token exchange routing each request across
competing providers, joins as a sponsor, placed directly below GrooRoute
Pro in the sponsor order (OpenModel in the Gemini file, which has no
GrooRoute preset).

- One key on api.dit.ai serves Chat Completions, Responses, Anthropic
  Messages and native Gemini (protocol docs at dit.ai/docs, catalog at
  dit.ai/models, checked 2026-10-21).
- The gateway has no Haiku 5.5 and only the dated
  claude-haiku-4-5-20251001, so Claude Code overrides the Haiku tier with
  it and Claude Desktop maps Sonnet 5.5 / Opus 5.5 / that Haiku.
- OpenCode, OpenClaw, Hermes and Pi use Anthropic Messages with Claude
  Opus 5.5 and Sonnet 5.5; Codex uses Responses with gpt-5.6-sol, Grok
  Build grok-4.7, Gemini CLI gemini-3.8-flash.
- Four-locale README sponsor rows with a banner, four-locale promo copy
  with the "ccswitch" coupon, and an inline icon from the official logo.
- Claude preset rows in the selector test go from 105 to 106.
2026-10-21 17:29:16 +08:00
Jason 8d4ec90fcb chore(presets): remove ZetaAPI sponsor and presets
ZetaAPI is no longer a sponsor; its presets go too.

- Drop the four-locale README sponsor rows and the banner.
- Remove the ZetaAPI presets from Claude Code, Claude Desktop, Codex,
  Grok Build, OpenCode, OpenClaw, Hermes and Pi.
- Remove partnerPromotion.zetaapi from every locale, Turkish included.
- Keep the zetaapi icon: existing providers saved from the preset still
  reference that icon key.
2026-10-21 17:28:05 +08:00
Jason e59252ee5b feat(presets): promote EveryAPI to a full sponsor
EveryAPI moves from the basic tier (in-app presets and website only) to
a full sponsor, placed directly below OpenModel in the sponsor order.

- Four-locale README sponsor rows with the new copy and a 1200px banner.
- The EveryAPI preset blocks in the seven preset files move below
  OpenModel so file order matches the README tables; the presets
  themselves are unchanged.
- The in-app promo copy adds "GPT from 15% of the official price" and
  folds the first top-up bonus and the "ccswitch" code into one perk,
  since they are the same offer.
2026-10-21 17:04:27 +08:00
Jason 6fabe63abb fix(agy): show Antigravity session-log rows as "Session log"
The frontend map of session placeholder names missed
"Antigravity (Session)", so the provider column showed the raw English
name and truncated it. Add the entry and a test that every placeholder
name in usage_stats.rs is translated.
2026-10-21 17:04:27 +08:00
Jason e91689f2da fix(agy): drop the Antigravity-only note on the usage page
No other app shows a pricing note above the usage tables, although
Codex, Gemini and the rest also estimate session-log amounts from API
prices. Remove the note, its locale key and the test that pinned it.
2026-10-21 17:04:27 +08:00
Jason 276eff8dce feat(agy): open all four quota windows from the card's quota column
The card keeps one line per group (Gemini, Claude/GPT) to fit, so the
5-hour and weekly windows could not both be seen anywhere on it. On
Antigravity cards the quota column is now one dropdown, like the resets
dropdown on Codex official cards, listing every window with its own
reset time. Refreshing moves to the ↻ button that appears on hover.
Other cards keep click-to-refresh.
2026-10-21 17:04:27 +08:00
Jason 997e78b565 feat(agy): show the bound account's quota on official cards
An official Antigravity card bound to an Auth Center account showed the
quota of whatever account agy was signed in with, which is a different
account until the card's terminal is opened. It now shows the bound
account's quota, shared with that account's row in the Auth Center, and
like Codex account cards it shows even when the card is not current.

Cards that follow agy's sign-in, and platforms that cannot switch
accounts, keep the signed-in quota.
2026-10-21 17:04:27 +08:00
Jason 2c63012b9e perf(agy): query the signed-in account's quota without starting agy
Official Antigravity cards asked agy's own /usage command for quota, which
starts agy and takes seven to nine seconds. On macOS the card now takes the
account agy is signed in with from the keychain, adds it to the Auth Center
if it is new, and asks Google directly with CC Switch's copy of the token,
the same way the Auth Center does (about one second).

agy's /usage is still used when the keychain holds no recognizable login,
when the user removed that account from CC Switch, and on platforms that
cannot switch accounts.
2026-10-21 17:04:27 +08:00
Jason c0490b0529 feat(agy): show each account's quota in the Auth Center
Every Antigravity row in the Auth Center now has a quota column, the
same one Copilot, ChatGPT and xAI rows use: Gemini and Claude/GPT, each
with a 5-hour and a weekly window. It is queried once when the page
opens and on the refresh button, through agy_account_quota, so accounts
agy is not signed in with show quota too. Accounts that need to sign in
again get no quota column.
2026-10-21 17:04:27 +08:00
Jason 8f9525b03d fix(agy): explain why an account is not eligible for Antigravity
Accounts that Google does not allow to use Antigravity, such as those
restricted by age, get the same 403 "You do not have a valid license,
contact your administrator" from the quota endpoint. That reads like a
CC Switch or licensing problem. On a 403 the quota query now asks
loadCodeAssist once, and when the account only has ineligible tiers it
shows Google's own reason instead.
2026-10-21 17:04:27 +08:00
Jason 5a08e230c0 feat(agy): query each Antigravity account's quota directly
The Auth Center can now show quota for every Antigravity account, not
only the one agy is signed in with. The new agy_account_quota command
calls Google's retrieveUserQuotaSummary with the account's own token, so
no switch is needed. The response carries the same buckets as agy's
/usage, and both now go through one tier builder.

The endpoint only answers clients that identify as Antigravity, so the
User-Agent is "cc-switch/<version> (antigravity)": it names CC Switch
and adds the word the server checks for. It only reads quota; it never
calls a model, warms up accounts or switches on low quota.

Tokens that expire within two minutes are refreshed first. The refreshed
token is saved only to CC Switch's copy, never to the keychain: agy keeps
refreshing its own login, and Google does not rotate the refresh token,
so both access tokens stay valid. For the account agy is using, the
keychain login is absorbed first so its newer token is used. A rejected
token triggers one forced refresh; invalid_grant marks the account as
needing sign-in again.
2026-10-21 17:04:27 +08:00
Jason 816346022b fix(pricing): price deepseek-v4.1-flash as V4.1 Flash
Third-party upstreams such as OpenCode Go, OpenRouter (deepseek/
namespace) and the Hermes presets call DeepSeek V4.1 Flash
deepseek-v4.1-flash. Pricing had no row for that id: none of the lookup
fallbacks turn it into deepseek-flash, and the prefix fallback only
matches longer ids. These requests showed "Unpriced" and were counted
at $0.

Add a deepseek-v4.1-flash row at the same official peak price as
deepseek-flash (0.3 / 1.2 / 0.006 per million). Default prices are added
again on every start, so existing installs get the row on next launch.
Requests already logged are not repriced.
2026-10-21 17:04:27 +08:00
Jason 0af8b77288 feat(agy): bind official cards to an Antigravity account
An official Antigravity card can now pick one of the Auth Center accounts
(macOS only). With an account picked, Open Terminal first switches agy's
global sign-in to it; when it is already signed in, nothing is written.
Without one, the card keeps following whatever agy is signed in with.

The binding is stored as authBinding with authProvider "agy_oauth". The
account id is the Google subject, so a card synced to another computer
works once the same account is added there; if it is missing, opening the
terminal stops with a message instead of launching under another account.
Platforms that cannot switch ignore the binding and keep it on the card.

The Open Terminal dialog says when opening will switch agy's sign-in, and
warns when agy is already running. The Auth Center counts bound cards as
users of an account, so the removal dialog lists them.
2026-10-21 17:04:27 +08:00
Jason 8627187954 feat(agy): add an Antigravity group to the Auth Center
On macOS the Auth Center now lists Antigravity accounts next to Copilot,
ChatGPT and xAI. The account agy is signed in with is tagged "In use by
agy", and "Switch agy to this account" in the row menu replaces agy's
sign-in. After switching, CC Switch checks whether agy is running and
warns that open sessions keep the old account and may write it back.

Adding an account opens Google's sign-in page in the browser. The shared
login hook treats a response without a user code as this browser flow: it
leaves the clipboard alone, polls the local result every 1.5 seconds and
cancels the backend flow so the callback port is released. The pending
block shows a "reopen sign-in page" button instead of a device code.

Removing an Antigravity account says that agy stays signed in.

The backend now records the sign-in time in seconds, which is what the
Auth Center formats.
2026-10-21 17:04:27 +08:00
Jason fdbac61c31 feat(agy): manage Antigravity official accounts in the Auth Center backend
agy keeps a single global login in the system keychain (service "gemini",
account "antigravity"). CC Switch now keeps its own copy of each account
and switches agy by writing the chosen one back into that entry.

- Sign in through the browser with agy's own OAuth client (PKCE, loopback
  callback on 127.0.0.1), requesting the same scopes agy does, so agy can
  keep refreshing the token on its own.
- Read and write the keychain through /usr/bin/security, the program the
  entry already trusts: the value goes over `security -i` stdin, `-U`
  updates in place and keeps the entry's access control, and the write is
  read back and checked.
- Before overwriting, take back the current login: refresh tokens agy
  renewed go to their account, an unknown account is added, and an
  unreadable value is stashed to agy-login-stash.json.
- Removing an account only drops CC Switch's copy; agy is never signed out.
- Exposed as the "agy_oauth" provider of the generic auth_* commands, where
  the default account is the one agy is signed in with. macOS only for now.
2026-10-21 17:04:27 +08:00
Jason 711a4127ff fix(usage): only list apps shown on the Apps page in the app filter
The usage dashboard listed every known app as a filter chip regardless of
the visibility switches on the Apps page. Filter the chips the same way the
session manager does: Claude Code stays while Claude Desktop is shown, and
the currently selected app is kept even if it has been hidden.
2026-10-21 17:04:27 +08:00
Jason 92ec18d774 docs(aggregation): correct the workaround for the exhausted-quota lock
Since route and aggregation mode keep the ChatGPT login (#8431), the
route table writes requires_openai_auth = true whenever a login is on
disk, so making a third-party card the aggregation default no longer
keeps Codex from locking the send button to Luna Reserve once the
official 5-hour quota runs out.

The guide and the 4.6 manual pages now describe the method that still
works: switch to Direct, keep "Keep official login for direct switches"
off, switch to a third-party card and fully restart Codex; switch back
to Aggregation once the quota resets. The old method is marked as no
longer working.

Refs #7962
2026-10-21 17:04:27 +08:00
Jason c776fd0ebf feat(agy): trim the form hint and require a key on custom cards
The long paragraph above the Antigravity form explained a case that should
not exist: a non-official card without a key is just the official login
card again. Drop the paragraph and warn on save when a custom card has no
API key, as Gemini CLI already does. A key without an endpoint stays valid
and uses Google's default API.

The endpoint and model rules move into "?" tips next to their fields. The
card's terminal button now reads "Open Terminal with This Provider"; the
shorter "Open Terminal" label stays for the Claude Code card menu.
2026-10-21 11:31:01 +08:00
Jason 9a10e4f7a9 feat(agy): replace the enable button with Open Terminal
CC Switch can only reach the agy it starts itself: a third-party key
travels in that process's environment, so "enabling" a key card never
changed what a plain `agy` used. Antigravity cards now only launch.

- Every card's primary action is Open Terminal; there is no "in use"
  state, the official quota shows on official cards at all times, and
  any card can be deleted.
- Official cards start `agy --model=<display name>`, which applies to
  that session only. Key cards keep their private --gemini_dir.
- The real ~/.gemini/antigravity-cli/settings.json is never written any
  more: switching is refused, adding the first card, full syncs and
  deep links with enabled=true leave it alone. The only remaining write
  removes the modelProvider left behind by the #8268 preview.
- The terminal dialog and form hints no longer talk about switching.
2026-10-21 10:47:46 +08:00
Jason fb91155a5a fix(agy): accept the empty mcp_config.json agy creates on first run
agy writes a zero-byte ~/.gemini/config/mcp_config.json the first time it
starts. Parsing it as JSON failed, so enabling any MCP server for
Antigravity showed "Invalid Antigravity MCP JSON" and importing from it
failed too. Treat a blank file as having no servers.
2026-10-21 10:03:45 +08:00
Jason 2e22ca209b feat(agy): give Antigravity its own icon and manage the CLI on the Apps page
- Use the Antigravity logo for the app instead of reusing Gemini's.
- List Antigravity CLI on the Apps page: install with the official script,
  update with `agy update` (also for the Homebrew cask, which is
  auto_updates), read the latest version from the release manifest, and
  uninstall official-script and cask installs.
- Find install.ps1's %LOCALAPPDATA%\agy\bin on Windows.
2026-10-21 09:23:59 +08:00
Jason 941267953e fix(agy): show only the typed prompt in sessions and describe the quota check
- agy stores every prompt wrapped in <USER_REQUEST>, followed by context it
  adds itself (<ADDITIONAL_METADATA>, <USER_SETTINGS_CHANGE>). Session titles
  and user messages now show only the text inside <USER_REQUEST>; an empty
  request keeps the original content.
- The official-quota hint said CC Switch reads the CLI's OAuth credentials.
  For agy it runs agy's own /usage command instead, so the usage dialog now
  says that (and that a check takes about 10 seconds).
2026-10-21 08:54:18 +08:00
Jason 1b4cd547c4 feat(agy): show the signed-in quota on the official card
- Ask agy's own /usage command (agy -p /usage --output-format json) instead of
  reading credentials; it makes no model call and opens no conversation.
- Keep the Gemini group and the Claude/GPT group, each with its 5-hour and
  weekly window, as four tiers; unknown buckets are skipped.
- On the card each group shows only its window with the least left, since
  four tiers do not fit; the expanded view lists all four.
- A missing agy is treated as no login (nothing shown); a timeout propagates
  so the last good value is kept; other failures do not echo agy's output.
2026-10-21 07:26:26 +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
Jasonandajia1206 b667b00a5c feat(agy): show the model on cards and attribute session usage to it
Ported from #8608:
- Model discovery works before agy has written a settings.json; the
  official login lives outside that file.
- Configurations imported from live are marked official: the key is never
  on disk, so an import is always agy's own login.
- Antigravity provider cards show the configured model.
- Session usage rows take their model from the `Model Selection` step agy
  injects at the start of a conversation, since responses carry no model.

Not ported: running `agy models` while importing on startup or when the form
opens. It takes several seconds on a signed-in machine and may prompt for
sign-in on one that never logged in; the Fetch Models button stays explicit.

Co-authored-by: ajia1206 <491387123@qq.com>
2026-10-20 23:58:50 +08:00
Jason 3854c6134f refactor(agy): launch API-key providers from the terminal instead of a shell wrapper
agy only reads GEMINI_API_KEY and GOOGLE_GEMINI_BASE_URL from the process
environment, and modelProvider is global. The previous approach wrote the key
to a file, installed a wrapper script and edited .zshenv/.zprofile/.zshrc so
every agy picked it up.

Now the native settings.json always stays in login mode: login cards set the
model (display name) and drop modelProvider; key cards only drop
modelProvider. A key card applies to agy started from the card's "Open
Terminal": `agy --gemini_dir=<private dir>` under ~/.cc-switch/agy/<id>, whose
settings carry modelProvider and the model and whose config links back to
~/.gemini/config so MCP, Skills and prompts stay shared. The key and endpoint
reach that one process through the environment only.

The terminal launcher now takes a per-app launch description (command, env,
temp files) so Claude Code and agy share the terminal layer. On macOS/Linux
env values are read from a 0600 file that is deleted right away and passed
via `env`, so values exported by the user's rc files cannot override them;
on Windows they live in a setlocal block. Claude's temporary settings file is
now written 0600 as well, and temp files are removed before handing over to
the interactive shell (exec skipped the EXIT trap).

On startup, wrappers, env files and PATH blocks left by earlier test builds
are removed (only CC Switch-marked content) and the current provider is
reapplied so modelProvider returns to login mode.
2026-10-20 23:56:05 +08:00
Jason 19798e2f92 docs(release): add v4.0.17 release notes 2026-10-20 22:08:53 +08:00
Jason e172f37d88 chore(release): v4.0.17 2026-10-20 22:08:53 +08:00
Jason f460410f5d feat(hermes): warn when a provider key is a Hermes built-in provider id
Hermes routes model.provider to its built-in provider whenever the name is a
canonical built-in id (runtime_provider_custom._shadowed_by_builtin), so a card
keyed "deepseek" or "openrouter" is ignored: Hermes asks for the built-in's own
API key env var and never uses the card's endpoint or key. #8538 only fixed the
two MiniMax presets by prefilling cc-switch-minimax.

The provider key field now shows a warning for any of the 52 canonical ids
(generated from hermes-agent rc.3-v0.22.0) and suggests cc-switch-<key>. A
locked key gets a variant that says to re-add the provider under another key.
Aliases such as kimi still reach the custom provider and are not flagged.
Saving is not blocked.
2026-10-20 21:59:20 +08:00
Jason 73adfaf01d fix(tray): release the tray panel WebView in lightweight mode
Lightweight mode frees memory by destroying the main window's WebView.
Since the tray panel became the default tray UI, the panel is a second
WebView window that is created on the first tray click and afterwards
only hidden, so a WebView stays alive and on Windows the WebView2
processes never exit after entering lightweight mode.

In lightweight mode the panel is now destroyed whenever it is hidden
(and right away when entering lightweight mode while it is already
hidden); the next tray click creates it again. The destroy is deferred
to a later main-thread turn and re-checks that the panel is still hidden
and not about to be revealed, so it never tears the WebView down inside
its own window-event callback or IPC command. toggle() decides "close"
before creating the window so a click that only closes the panel does
not recreate it, and a recreated window starts with a fresh theme state
and, on macOS, fresh glass views.

Refs #8612
2026-10-20 21:50:17 +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 52e8734476 fix(mcp): project url-only servers without a type to Hermes and OpenCode
A unified MCP spec can lack a `type` field when it was pasted in JSON
mode or created via a deeplink, e.g. `{"url": "https://..."}`. Hermes'
and OpenCode's projections defaulted a missing type to stdio, whose
branch only copies command/args/env. Hermes therefore got an entry with
nothing but `enabled: true`, and OpenCode got `type: local` with an
empty command, while the UI reported a successful sync.

Codex's projection already handled this in `spec_transport_type`
(missing or stdio type, no command, non-empty url => http). Move that
helper unchanged into `mcp/validation.rs` and use it from the Codex,
Hermes and OpenCode projections, so all three infer the transport the
same way. Add regression tests for url-only and command-only specs.

Refs #8579
2026-10-20 21:50:16 +08:00
Jason 95693d6c52 fix(codex): don't give an expired stashed login back to the proxy
Since #8431, entering or attaching the proxy for a third-party Codex
route gives back the most recently stashed ChatGPT login when auth.json
has none. The stash can hold a login set aside months ago by a direct
switch with "keep official login" off. Restoring it made the route
table require OpenAI auth, Codex desktop tried to refresh the dead
token and got stuck on the sign-in page, and every exit/enter put it
back again.

The restore now skips a login whose access_token JWT has already
expired. Codex refreshes the token every 8 days and it lives about 10,
so an expired one means the login has not been used for a long time.
Tokens that cannot be decoded are still restored, and the skipped login
stays in the stash so switching back to an official card returns it.

Refs #8573
2026-10-20 21:50:16 +08:00
Jason 1f1becfb42 fix(toml): only normalize smart quotes used as string delimiters
normalizeTomlText replaced every curly/fullwidth quote in the text before
TOML parsing, including quotes inside already-valid strings. MCP import and
validation (tomlUtils, mcpDraft) and the Codex config editor therefore
silently rewrote values such as args = ["don’t"] to "don't", and rejected
valid configs like args = ["say“hello”"] or args = ['don’t'] because the
inner quotes became delimiters.

Scan the text instead: smart quotes outside strings are delimiters and
become ASCII; strings opened by an ASCII quote keep smart quotes as
content; strings opened by a smart quote may close with either form;
multi-line strings and comments are handled. To keep the Chinese IME case
working (the editor normalizes each keystroke, so the left quote is already
ASCII when the right smart quote is typed), an ASCII-opened string left
unterminated at end of line is re-scanned with smart closers, and if the
strict result still fails to parse, a lenient scan is used only when it
parses. Escaped smart double quotes in basic strings become \". Text
without smart quotes is returned unchanged.

Refs #8559
2026-10-20 21:50:16 +08:00
Jason 5cbe670014 fix(webdav): upload the sync manifest as application/octet-stream
CSTCloud Data Capsule's WebDAV server answers PUT manifest.json with
500 "Uploaded object size does not match the upload session" whenever
the body is multi-line JSON sent as application/json. The same pretty
JSON is stored byte-for-byte when sent as application/octet-stream, and
db.sql / skills.zip already upload fine with their own types.

Send the manifest with application/octet-stream. The manifest bytes and
hash stay the same and readers never look at Content-Type, so old and
new versions keep reading each other's manifests. S3 sync is unchanged.

Refs #8554
2026-10-20 21:50:16 +08:00
Jason 2dfc848f4d fix(proxy): stop child processes from inheriting the listener socket on Windows
On Windows the proxy port could stay LISTENING after CC Switch exited,
owned by the PID of the dead process, so the next start failed to bind
with os error 10048 and fell back to direct mode until a reboot or a
port change.

The listener comes from tokio -> mio, and the locked mio 1.1.1 created
Windows sockets with plain socket(), which yields an inheritable handle.
CC Switch spawns long-lived children (terminals via `cmd /c start`, CLI
tools), and std's Command passes inheritable handles to them, so those
children kept the listening socket open after CC Switch was gone.

Bump mio to 1.2.1, which creates sockets with WSA_FLAG_NO_HANDLE_INHERIT
(tokio-rs/mio#1946), and add a Windows-only test asserting the listener
handle is not inheritable so the lockfile cannot silently regress.

Refs #8552
2026-10-20 21:50:16 +08:00
Jason c42e3067e9 feat(tray): show remaining balances in green in the tray panel
Color only the balance amount green, like the provider card's quota
line, while the label stays muted and a used-up balance keeps its
warning color.
2026-10-20 21:50:16 +08:00
Jason 7e97b44f4d feat(tray): mark the balance amount in panel quota lines
Carry an `emphasis` field on tray quota lines and the panel payload,
set to the amount only when a pay-as-you-go balance is left (matching
the card's `balanceLine`), so the panel can color just the number.
2026-10-20 21:50:16 +08:00
Jason 099c9761ec fix(tray): align quota bars and show time until reset
Lay out each quota block as one grid (label | bar | reset) so bars start
and end at the same x across tiers, and let the quota row extend under
the chevron / "In use" column so bars are no longer squeezed. Replace the
absolute reset date with a compact time-until-reset (at most two units,
e.g. "1d 23h"), with unit strings added to all panel text branches, and
keep quota labels from wrapping.
2026-10-20 21:50:16 +08:00
Jason 9be88e3e9b fix(provider-form): stop macOS from capitalizing typed API keys
The API key field is a plain input, so it missed the autocorrect,
auto-capitalization and spellcheck opt-outs the shared Input component
already sets. With the key shown in plain text, a typed "sk-..." key
could be saved as "Sk-..." and fail authentication. Pasted keys were
not affected.
2026-10-20 21:50:16 +08:00
Jason f92ff0b039 docs(release): add v4.0.16 release notes 2026-10-19 18:26:47 +08:00
Jason 855a2eb8b1 chore(release): v4.0.16 2026-10-19 18:26:47 +08:00
Jason 01bc780f89 feat(tray): make the tray panel the default again
Clicking the tray icon on macOS and Windows opens the panel again by
default. A settings.json without nativeTrayMenu now reads as the panel;
an explicitly saved true keeps the native menu, which stays available
under Settings > General > Tray style.
2026-10-19 18:26:40 +08:00
Jason e33a0f3581 test(tray): match the opaque macOS panel fill in the style test
39383aa7f laid a mostly opaque theme fill over the macOS glass cards
but left the style test asserting the old faint overlay.
2026-10-19 18:26:40 +08:00
Jason b89cccf971 feat(presets): add AllTokens SI sponsor presets
Add AllTokens SI (alltokens.si), the enterprise new-api deployment, as
a basic-tier sponsor separate from the alltokens.cc entry, with its own
icon and partner promotion copy in all four locales.

Models were checked against the public catalog (/api/pricing):

- Claude IDs use dots (claude-sonnet-5.5) and there is no Haiku, so the
  Claude Code preset spells out all four tiers with Haiku on Sonnet,
  and Claude Desktop maps its routes instead of passing them through.
- The GPT group only declares Chat Completions, so OpenCode, OpenClaw,
  Hermes and Pi use Chat; Codex keeps Responses, which new-api's OpenAI
  channels forward.
- Gemini and Grok are outside the default auto groups and need a key
  from their own group; Gemini uses gemini-3.8-flash-high and Grok
  Build grok-4.7, as the group has no grok-4.5.
2026-10-19 18:01:47 +08:00
Jason f3d4fe53f4 fix(presets): restore 1.05M GPT windows on API-billed platforms
OpenRouter, Vercel AI Gateway, Cloudflare AI Gateway and DigitalOcean
bill GPT through the OpenAI API, so the 272K subscription cap from
v4.0.13 understated their windows. Restore the values those presets had
before the cap: Codex DigitalOcean, Hermes OpenRouter / Vercel /
Cloudflare and OpenClaw Vercel / Cloudflare. Pi presets read windows from
the shared model catalog, which stays capped, so the Pi Vercel preset
overrides GPT-6 Astra explicitly.

The preset guard now allows GPT windows above 272K only for these four
hosts, and checks that each of them still uses the exception. Catalogs
without an address (Pi catalog, OpenCode variants) get no exception.
2026-10-19 18:01:47 +08:00
Jason 8d7a741540 fix(models): cap only borrowed GPT windows and warn on oversized entries
The 272K GPT cap (v4.0.13) applied to every window the metadata lookup
returned, so platforms that really serve the 1.05M OpenAI API window
(OpenRouter and other providers models.dev recognizes by address) were
capped too. The risk the cap guards against is narrower: a relay that
models.dev does not know falls back to the vendor's own entry, and its
1.05M lands on a model that is often served from a ChatGPT subscription
with a 272K window.

- Same-endpoint presets and the models.dev provider matched by address
  keep their own windows; only the window borrowed from the vendor entry
  (including when the matched provider lists none) is capped at 272K.
- A window the user types is kept as is. When a GPT model's window is
  above 272K, the Codex, Hermes, OpenClaw, OpenCode and Pi forms show a
  note under the field: relay GPT usually has the subscription window, and
  an oversized one delays compaction until the upstream rejects the turn.
  Text added in zh / en / ja / zh-TW.

Existing rows are not migrated. Codex GPT rows saved before v4.0.13 with
an auto-filled 1.05M now take effect since #8385; the note shows when the
form is opened.
2026-10-19 18:01:47 +08:00
Jason a9a93f06d2 test(skill): isolate HOME before touching the skill storage location
StorageLocationGuard writes settings.json both when it is set and when it
is dropped. Eight skill tests created it before TestHomeGuard, so the setup
write and the drop-time restore (locals drop in reverse order) hit the
runner's real ~/.cc-switch/settings.json, which intermittently failed on
Windows with os error 32 / 1176 when another process held the file.

Make the guard borrow an active TestHomeGuard so it can only be created
after HOME isolation and is always dropped before HOME is restored, and
assert that the settings directory lives under the test home.
2026-10-19 18:01:47 +08:00
Jason eadfc1516c feat(opencode): set the default model from an added provider card
OpenCode picks its startup model from the top-level `model`
("<provider>/<model>") in opencode.json, but CC Switch had no way to set
it, so users had to edit the file by hand after adding providers.

Reuse the OpenClaw "Set as default" card action for OpenCode:
- Backend: `opencode_config::{get,set}_default_model` read/write only the
  top-level `model` through the existing format-preserving edit path
  (comments kept, `small_model` untouched), rejecting values that are not
  "<provider>/<model>". New commands `get_opencode_default_model` and
  `set_opencode_default_model`.
- Frontend: the additive section builder's `openclawDefault/openclawModels`
  become generic `defaultModel/defaultModelOptions`; the default card is
  marked and other added cards get "Set as default" with a model menu.
  Model options come from the same fragment the backend writes (a stored
  full config is unwrapped) and skip natively disabled models. Built-in
  providers that declare no models get no button. Unlike OpenClaw, the
  default provider can still be removed, matching the old behavior.
  Existing generic notification texts are reused, so no new strings.

Refs #8509
2026-10-19 18:01:47 +08:00
Jason 56ac4f9ab1 fix(providers): let an idle card be deleted when only the stale DB current flag points at it
Deleting a switch-mode provider was refused if the device-local direct
pointer, the DB is_current flag or the proxy route pointed at it. The
UI only disables delete for the effective direct pointer and the route,
so a card that only DB is_current still pointed at looked deletable but
failed with "无法删除当前正在使用的供应商".

This happens after upgrading with an old Claude Desktop mapping card:
the startup migration moves the local Desktop pointer to the official
card and routes to the mapping card, but leaves is_current on it. Once
the user routes to another provider, that card is idle yet undeletable.

is_referenced now checks the effective direct pointer (provider_for
Direct: the local pointer when valid, else DB is_current) plus the
route, the same set the UI disables. The fallback still protects
is_current when there is no valid local pointer.

Refs #8506
2026-10-19 18:01:47 +08:00
Jason 3b1f11b6f1 fix(detect): find fnm-managed Node CLIs on Windows
On Windows fnm only exposes Node through `fnm env`, which prepends a
per-shell %LOCALAPPDATA%\fnm_multishells\<id> link to that shell's PATH.
The registry PATH never contains it, so the effective PATH CC Switch builds
for CLI detection and lifecycle commands had no node, npm or npm global
shims: fnm-installed Codex/Claude/OpenCode showed as not installed, and the
in-app install failed with "'npm' is not recognized". The only fnm search
path we had is the Unix multishell layout.

Append fnm's stable <fnm dir>\aliases\default (FNM_DIR, legacy ~\.fnm, then
%APPDATA%\fnm; only if it exists) as the last segment of the effective
PATH. On Windows that alias points at the default Node installation, which
holds node.exe, npm.cmd and the global CLI shims directly, so detection,
where.exe resolution and install/update commands all see it, while every
explicit PATH entry keeps precedence. Multishell links are not scanned
since they grow without bound (#8313).

Refs #8484
2026-10-19 18:01:47 +08:00
Jason f3d9e8c6b9 fix(settings): render the About card logo from the vector mark
The About card showed src/assets/icons/app-icon.png, a 32x32 bitmap,
in a 40px box, so it was upscaled and looked blurry, especially on
Retina displays (80 device pixels).

Use the existing logo.svg (already used by the sidebar header) instead.
The PNG carried about 4px of transparent padding while the SVG is
tight, so keep the 40px box and add p-1 to keep the mark at its
previous visual size. The PNG had no other references and is removed.

Refs #8470
2026-10-19 18:01:47 +08:00
Jason 5fa000796a fix(icons): soften the macOS app icon tile to light grey
The white rounded-square tile baked into icon.icns in v4.0.15 reads too
bright in the Dock, especially in dark mode. Recolor the tile to #e8e8e8
while keeping the tile geometry and the artwork pixels unchanged: the
artwork coverage of each pixel is solved from the white-tile icon itself,
and only the exposed tile area is moved from white to grey, so edges keep
their original anti-aliasing at every size in the iconset.

Windows (.ico) and Linux (PNG) icons stay transparent.

Refs #8495
2026-10-19 18:01:47 +08:00
Jason 8b09732a17 fix(usage): don't auto-enable Volcengine plan quota without AccessKey
Volcengine's plan quota is queried through its OpenAPI with an
account-level AccessKey ID + Secret, not the inference API key. Adding a
Volcengine Agent/Coding Plan provider (or importing one via deep link)
auto-enabled the token_plan query anyway, so every new card showed a red
"quota not found" right away and users read it as the provider not
working.

Preselect Volcengine in the usage settings but leave the query off until
the user fills in the AccessKey. Deep links that set usageEnabled are
still honored; the import dialog badge and the backend agree.

Refs #8494
2026-10-19 18:01:47 +08:00