Commit Graph
100 Commits
Author SHA1 Message Date
Jason fda103e9b1 chore(release): v4.0.7 2026-10-10 16:53:17 +08:00
Jason e049660a50 fix(codex): explain that old third-party sessions cannot continue on the official route
When Codex is on the official provider, a request carrying the proxy
placeholder key comes from one of two places: a Codex process that has
not reloaded its config, or a session created under a third-party
provider. Codex resumes a session with the provider id it recorded, so
the latter keeps using the dormant `custom` table that points at the
local proxy with the placeholder key, and restarting Codex does not
help.

The error used to suggest restarting first, which sent users of the
second case into a loop. It now says that an old third-party session
keeps its original provider and needs a new session, and falls back to
the restart hint otherwise.
2026-10-10 16:36:44 +08:00
Jason b7d548b67c feat(quota): let quota windows show what is used instead of what is left
Since 4.0 every quota window reads "x% left". Some users had grown used
to the 3.x "x% used" and asked for a way back, while others had asked
for "left" to match Codex itself, so this becomes a setting instead of a
flip: Settings > General > Appearance gains "Quota display" with Left
(default) and Used.

The setting is stored as quotaDisplay in the device-local settings.json
so the tray, rendered in Rust, follows it too. Only per-window
percentages change: balances, Codex Credits and saved resets keep
showing what is left, and a used-up window still reads "used up".
Colors, the "almost out" hint and which windows the card keeps are
still decided by what is left, so 94% used is orange just like 6% left.

The expanded bars now take their length from the number shown, so a
window at 30% used draws a 30% bar instead of a 70% one; balances keep
drawing what is left. SubscriptionQuotaView stays a pure view and gets
the mode from its wrappers.

Closes #8024
2026-10-10 16:36:44 +08:00
Jason 0fc67c3b35 fix(codex): warn about stale Codex clients on Windows without a process table
The stale-client check reads the process table, which Windows does not
have, so neither the model list notice nor the cached-account notice
ever appeared there, and the restart button always reported that the
daemon was not running.

On Windows the managed daemon is now found through its own daemon.pid:
the process is opened by pid and counts as running only while it is
still active and its creation time matches the recorded one, so a
reused pid is not mistaken for it. Restart is only offered for a daemon
known to be running, since Codex's restart command starts one otherwise.

The desktop app and editor plugins cannot be seen there, so the notice
falls back to "changed since you last dismissed it": the model list and
login fingerprints are recorded in codex-client-ack.json when the user
closes the notice, through the new acknowledge_codex_stale_clients
command, and a later change shows it again with wording that says
CC Switch cannot tell whether those clients are open. Without a record
the current state is taken as acknowledged, so upgrading does not show
it, and the first login seen becomes the baseline, so signing in is not
reported as an account switch.

Closing the notice used to hide it until the app, mode or provider
changed, so changing the model list again on the same provider stayed
silent on every platform. The stack view now carries a revision derived
from the same fingerprints, and the notice reappears once it changes.
2026-10-10 16:36:44 +08:00
Jason e5b1e0966b fix(codex): tell ChatGPT workspaces of the same user apart when switching official cards
Since 4.0, an official Codex login was identified by the id_token subject
alone. The Plus and Team workspaces of one OpenAI user share that subject
and differ only in tokens.account_id, so switching between their cards
was treated as "already signed in as this person": auth.json was never
rewritten and the restart notice never fired. The device login stash was
keyed the same way, so all of that user's workspaces collapsed into one
entry. 3.20.4 wrote the card's login without comparing owners, which is
why downgrading helped.

A login is now identified by user plus workspace, matching how Codex
itself decides whether the account changed.

The stash file gains a version. An older stash is upgraded once on load:
its entries are rekeyed by the new identity, and workspaces that were
merged away are restored from the official cards, but only for users
still present in the stash or in the live auth.json. A user found in
neither has logged out and is not brought back. Switching still restores
a login only from the stash, never from a card snapshot, so a logout is
not revived later either.

Refs #8054
2026-10-10 16:36:44 +08:00
Jason e06ba573d2 fix(windows): keep non-ASCII paths out of generated batch files
cmd.exe decodes a .bat file with the console code page, but the batch
files are written as UTF-8. With a non-ASCII Windows username, paths
under the user profile were garbled.

Tool install/update scripts now start with `chcp 65001`. They run in a
hidden console of their own, so the code page change does not reach
the user's terminal. This fixes upgrades failing with "The system
cannot find the path specified" for npm installs under the profile.

The Claude provider launcher refers to the temporary settings file as
%~dp0<name>, since it sits next to the batch file, so the --settings
path no longer goes through the file bytes and Claude stops silently
starting with its default config. A non-ASCII project directory is
passed through CC_SWITCH_INTERNAL_CWD instead of being embedded. ASCII
directories are still written literally, so terminals that do not
inherit the caller's environment keep working as before. chcp is not
used here because it would persist in the user's interactive session.

Fixes #8034
2026-10-10 16:36:44 +08:00
Jason 69dbb307e5 fix(i18n): scope unified Codex session history copy to the CLI
The Codex desktop app now lists sessions from every provider, so the
unified history toggle only matters for the CLI resume list, which still
filters by the current provider id. Reword the setting description and
both enable/disable confirmations in all four locales to say so, and
note that desktop-only users don't need to turn it on.
2026-10-10 16:36:44 +08:00
Jason 84457b5105 fix(codex): keep a top-level base_url when saving a provider
A Codex provider whose config carries base_url (and wire_api) at the top
level with no model_provider selector was projected as a route-less card:
the editor showed an empty request address, and saving stripped both keys
as key fields with nothing to write them back, so the proxy then failed
with "Codex Provider 缺少 base_url 配置". Switching to such a card in direct
mode also sent Codex to the built-in OpenAI route.

The projection now treats a top-level base_url like the legacy
openai_base_url shape and normalizes it into [model_providers.custom], so
the editor shows the address and saving keeps it.

The form no longer produces this shape: with no selector, setCodexBaseUrl
selects a custom table and writes the address into it. The line-based
rewrite is checked against the parsed TOML and falls back to the old
top-level write (which the backend now normalizes) when it cannot be done
safely, e.g. inline tables, commented table headers or multi-line strings.

Fixes #8039
2026-10-10 16:36:44 +08:00
Jason 347c60c3f7 docs(aggregation): add Codex troubleshooting for sub-agent tasks and the official quota lock
- Explain why Codex sub-agents on another provider cannot read encrypted tasks
  and how the classic sub-agent tools setting avoids it
- Explain that Codex locks the picker to Luna Reserve once the official 5-hour
  quota runs out, and describe the workaround of temporarily making a
  third-party provider the default (#7945, #7962, #7974)
- Update the Chinese guide and the Chinese, English, and Japanese manuals
2026-10-10 10:49:23 +08:00
Jason 8641cd1d6b feat(codex): read compressed session rollouts and add a compression toggle
Codex can compress rollouts untouched for 7 days into a sibling
rollout-*.jsonl.zst and delete the original, either through
[features] local_thread_store_compression or through the app-server
rollout/compress RPC, which ignores that flag. It decompresses a rollout
back to .jsonl when the session is resumed. CC Switch only read .jsonl,
so compressed sessions vanished from the session list, their usage was
never imported, and history bucket migrations skipped them while still
recording themselves as done.

- Add codex_rollout_file, shared by the readers below: recognise both
  forms, read .jsonl.zst transparently, write a rollout back in its own
  form, and keep only the plain file when both forms briefly coexist.
- Session list: decompress only the head of a compressed rollout. Use
  its mtime, which Codex preserves, as the last active time and show no
  last-reply preview, so scanning tens of GB stays fast. Content refs are
  resolved against the decompressed stream. A stale source path falls
  back to the other form, and deleting a session removes both forms.
- Usage import: a cursor is inherited from any path with the same
  logical file name, taking the furthest line offset, in any directory
  and in both directions. Request-id dedup only covers the 30 days of
  detail rows kept before rollup, and compressed rollouts are older, so
  this is what prevents double counting across compress and resume.
  Compressed files persist their on-disk size so unchanged ones are
  skipped without decompressing.
- History bucket migrations rewrite compressed rollouts in place, keep
  them compressed, and keep their mtime so old sessions do not jump to
  the top of the list.
- Settings > Codex gets a "Compress old Codex sessions" switch backed
  directly by config.toml (write true to enable, remove the key to
  disable) through the live write engine, showing the current size of
  the sessions directories. It re-reads when the saved Codex directory
  changes.
2026-10-10 10:49:22 +08:00
Jason ef24a01912 test(codex): skip the thread version SQLite fixture on WSL temp dirs
The test added in dc93c4962 builds an on-disk SQLite fixture in a temp
dir. The WSL2 nightly points TEMP at a \\wsl.localhost 9P share, where
Windows cannot take SQLite locks, so creating the table failed with
"database is locked" on every retry. Guard it with
sqlite_unsupported_in_temp_dir() like the other SQLite fixture tests.
2026-10-10 08:06:30 +08:00
Jason c261126630 docs(release): add v4.0.6 release notes 2026-10-09 22:15:03 +08:00
Jason 314968b65d chore(release): v4.0.6 2026-10-09 22:15:02 +08:00
Jason ee50c12b81 test(apps): expect the action color on the apps page visibility switches
8919d05cb dropped the neutral switch tone but left the apps page test
asserting data-tone="neutral". Assert that the visibility switches no
longer carry a tone.
2026-10-09 22:15:02 +08:00
Jason c5efe0baaa feat(providers): add a search button to the provider page header
Provider search could only be opened with Cmd/Ctrl+F, so users with many
providers did not know it existed.

- Add a search button next to "Add provider" in the page header. It
  toggles the existing search panel; the panel closes when switching apps
  or leaving the provider page.
- Match the API address as well as name, notes, and website. The address
  is read from every app's settingsConfig shape by a new shared
  extractProviderBaseUrl, which ProviderCard now also uses.
- Only filter while the panel is open. Closing it with Esc used to leave
  the list silently filtered.
- Portal the search panel to document.body. As a child of the list's
  space-y container it counted as a sibling, so opening it pushed every
  provider card down by 1rem.
- Add a "Show provider search" setting (on by default, device-local).
  Hiding the button keeps the Cmd/Ctrl+F shortcut.
2026-10-09 21:50:56 +08:00
Jason dc93c4962d fix(codex): keep official models visible in the aggregation catalog
With the official account as the aggregation default, CC Switch fetches
the official model list with the version of the `codex` on PATH. The
server trims the list by client_version, and for 0.140 or older it only
returns hidden entries (gpt-5.5, codex-auto-review). Desktop-only users
often keep an old CLI on PATH, so the catalog was written with every
official row hidden and the picker showed only the aggregated third-party
models.

- Use the newest Codex in use: the PATH CLI or the cli_version recorded
  on the 20 most recently created threads in Codex's state DB, which also
  covers the desktop app and editor extensions on every platform.
- A source with no `visibility = "list"` row is not usable. When the
  fetched list has none, fall back to the bundled list (no catalog if that
  has none either) and show a new notice asking to update Codex.

Fixes #8014
2026-10-09 21:30:23 +08:00
Jason 4629ee2f27 fix(tray): show every quota tier in the app row title
The app row title kept only the two tiers with the least quota left. With
three windows (5-hour / weekly / monthly, e.g. Volcengine Agent/Coding
Plan, Zhipu, MiniMax) the 5-hour window resets most often and usually has
the most left, so it was almost always dropped. 3.x listed every tier.

Write all tiers in their original order. The "almost out" suffix still
follows the tier with the least left. Remove the now unused pick_lines.

Closes #8011
2026-10-09 21:11:44 +08:00
Jason 8919d05cb3 fix(ui): use the action color for the apps page visibility switches
The neutral switch tone drew the checked track in dark gray next to a
medium-gray unchecked track, so on and off were nearly indistinguishable
in light mode. Drop the neutral tone (its only caller was the apps page
"Show" column) and let those switches use the same action color as every
other switch.

Closes #8010
2026-10-09 21:09:53 +08:00
Jason 033017b0be fix(skills): stop the import dialog from locking while discover refetches
The import mutation's onSettled awaited every affected refresh, including
the discover list, which downloads each repository from GitHub (up to 60s
per repo). TanStack Query only settles a mutation after onSettled resolves,
so isPending stayed true and the import dialog could not be cancelled or
closed until discover finished. The discover segment stays mounted once
opened, so this hit anyone who had visited it on a slow GitHub connection.

Only await the installed and unmanaged refreshes the dialog depends on;
repos and discover are still invalidated in the background.

Fixes #7994
2026-10-09 21:07:30 +08:00
Jason c1cb531fc9 feat(skills): update all Skills from the check button and list them before confirming
Updating meant checking, then filtering the table to "updates", then
finding "Update all" in the filter row. The confirm dialog also only gave
a count, and it stayed open until every Skill had finished downloading.

- The "Check updates" button turns into "Update all (N)" once updates are
  known; re-checking moves to the "..." menu. A check that finds updates
  opens the confirm dialog directly instead of a toast.
- Drop the duplicate "Update all" button from the filter row.
- The confirm dialog lists each Skill with a clickable source repo that
  opens the same page as the row link. V7ConfirmDialog gains a `details`
  slot because the body renders inside a <p>.
- Confirming closes the dialog at once; the toolbar button shows
  "Updating 1/2..." and the row menu spins until the batch is done.
- Remove the now unused `skills.updatesFound` string; add
  `skillsPage.confirm.updateListAria` and `skillsPage.updatingProgress`
  in all four locales.
2026-10-09 20:41:37 +08:00
Jason ae934663b5 fix(codex): build third-party catalog entries from the bundled classic tool template
ProxyChat and Copilot catalogs cloned the gpt-5.5 entry from Codex's own
models_cache.json (falling back to `codex debug models --bundled`, then the
bundled copy). The template only supplies the tool contract — slug, name,
context window and visibility are all overwritten — but every change
OpenAI made to that cached entry leaked into third-party models: the
hidden flag behind #7924/#7954, and newer entries carry
`tool_mode: code_mode_only`, v2 sub-agents and experimental tools the
Chat converter was never built for. The cache also announces GPT-5.5's
retirement on 2026-10-14, after which every switch would fall through to
spawning the Codex CLI.

- Always use the bundled template for ProxyChat / Copilot; drop the cache
  and CLI lookups and the per-process template cache.
- Refresh the bundled template to match Codex 0.162's own gpt-5.5 entry:
  `shell_type: unified_exec`, and include the skills / plugin usage
  instructions (both default to false in codex-rs when absent).
- Rename the loader to `load_codex_classic_tool_template` and document
  why it stays on the classic gpt-5.5 shape.
- Tests lock the classic tool contract and that a hostile
  models_cache.json no longer affects generated entries.

Existing ProxyChat sessions lose the cached `comp_hash` and get compacted
once on resume.
2026-10-09 17:21:20 +08:00
Jason 1beb8fa881 feat(ui): make the CC Switch update dot green and open About from it
The sidebar used the same neutral dot for two different things: tool
updates on "Apps" and a new CC Switch release on "Settings". The dot was
barely darker than the icon, and clicking "Settings" landed on General,
so the update button was still two steps away.

- The CC Switch update dot on "Settings" and on the About row is now the
  success green. Tool updates on "Apps" keep the neutral dot.
- Clicking "Settings" in the sidebar while an update is available opens
  About directly. Cmd+, still opens General.
2026-10-09 16:56:59 +08:00
Jason 449c7e78af docs: add aggregation mode guide and update user manuals
- Add the Chinese aggregation guide with the final cover, client screenshots, and request-routing diagram
- Update Chinese, English, and Japanese manuals to distinguish model fetching, models.dev metadata completion, and manual entry
- Clarify official subscription support, default models, and client restart requirements; add README links
- Validate 90 local documentation links, five images, and Git diff formatting
2026-10-09 16:51:08 +08:00
Jason 4aeaccf125 fix(codex): unbind providers when a managed account is removed
Deleting a managed ChatGPT account (or logging out of all of them) left
every Codex provider bound to that account pointing at a dead local ID.
#7395 made such cards recoverable by hand (switch away, pick another
account); this removes the dangling bindings where they are created.

- Remove/logout detach the affected Codex providers in one SQLite
  transaction: drop the binding, store an empty auth like a manual
  unbind, backfill category=official for legacy cards, keep unknown
  meta keys. The card then follows the native Codex login.
- Only this device's account IDs are unbound. Provider rows sync across
  devices, so a row bound to another device's account stays bound.
- The command reports "account removed but unbinding failed" separately.
  Any row still left dangling (failed unbinding, older versions, synced
  rows) keeps going through the #7395 recovery path, whose tests now
  create such rows directly.
- Frontend invalidates the Codex provider list after remove/logout,
  including on partial failure.

Thanks @SaladDay for the report and the first fix in #7060.

Fixes #7055
Refs #6969
2026-10-09 16:51:08 +08:00
Jason c37a26259e fix(proxy): explain unreadable sub-agent tasks instead of emptying them
A v2 sub-agent's task arrives as the trailing agent_message whose
payload only the main agent's backend can decrypt. The opaque-state
rectifier used to replace that payload with a placeholder like any
other foreign ciphertext, so a retry sent the sub-agent an empty task.
The upstream also often rejects it inside a 200 stream, which bypassed
the rectifier entirely and left Codex retrying the same failure.

- Keep the current task untouched when rectifying; earlier agent
  messages and other foreign state are still cleared.
- For native Responses passthrough requests ending in an encrypted task,
  hold the stream until the first non-lifecycle event under one fixed
  deadline (first-byte timeout, at most 10s), so heartbeats cannot extend
  it. A self-identified rejection before output joins the normal
  rectifier path; anything else replays byte for byte.
- When nothing else is left to clear, or the retry is still rejected,
  return a neutral 400 unreadable_encrypted_agent_task that names the
  classic sub-agent toggle, instead of letting Codex retry.
2026-10-09 16:51:08 +08:00
Jason 6760cfbbe1 feat(codex): add an opt-in classic sub-agent tools toggle for aggregation
Codex multi-agent v2 has the main agent's backend encrypt the task it
hands to a sub-agent, so in aggregation mode a sub-agent on a different
provider cannot read its task. Codex picks the tool version from each
catalog row's multi_agent_version, which the aggregated catalog copies
from the official catalog (v2 for Sol and Terra).

Add a device-level setting, off by default, that stamps
multi_agent_version = "v1" on every row of the aggregated catalog
(official rows included: ciphertext minted by OpenAI is just as
unreadable elsewhere). Classic tools pass the task as plain text at the
cost of v2-only tools such as followup_task and interrupt_agent. With
the setting off the catalog is unchanged.

Changing the setting rewrites the catalog right away when Codex is in
aggregation mode and rolls the setting back if the write fails. When
config.toml forces [features] multi_agent_v2, which Codex applies before
catalog rows, the settings row says the toggle has no effect.
2026-10-09 16:51:08 +08:00
Jason 18bdea7c73 chore(icons): update DMXAPI logo 2026-10-09 16:51:08 +08:00
Jason 0be33a8442 fix(quota): bring back colored quota numbers and warn below 20%
The all-gray quota text made it hard to tell a healthy limit from one
that is running low. Restore the v3 green / orange / red, but color only
the number ("88%", "¥82.10", "$2500", the reset count) so tier names and
"left" stay gray. Lines with no number (used up, expired, query failed)
are still colored as a whole.

- Warn once a tier has less than 20% left (was 10%), in the card and in
  the tray's "almost out".
- The "used $X" line has no total, so it stays uncolored.
- The expanded bars and the auth center keep neutral numbers while
  normal, since the bar already carries the color.
2026-10-09 16:51:08 +08:00
Jason 2db86e94da docs(release): add v4.0.5 release notes 2026-10-08 23:00:52 +08:00
Jason cf02f670ff chore(release): v4.0.5 2026-10-08 23:00:52 +08:00
Jason 5d24fdf9a6 feat(usage): show all history in the all-time heatmap
The all-time heatmap only covered the last 53 weeks, so older usage was
cut off. It now keeps the last 53 weeks as the default view and adds a
"Show more" toggle that lists earlier history in contiguous 53-week
spans, back to the first recorded day. Each span is titled with its
exact date range and totals, and shading is bucketed across all spans
so they stay comparable.

A new get_usage_first_date command returns the earliest local day across
request logs and daily rollups, honoring the dashboard's app, provider
and model filters, so the heatmap knows how many spans to fetch in one
trends query.
2026-10-08 22:48:02 +08:00
Jason 5ae6ad3888 ci(wsl): retry flaky lib tests in the WSL2 nightly
The 9P share sometimes aborts a write mid-test with os error 995
(ERROR_OPERATION_ABORTED): a Claude Desktop config test passed in 1.3s
on one run and failed after 17s on the next with no code change.

Retry up to twice in the nightly only. The ci profile used by PRs keeps
failing on the first error, and nextest still lists retried passes as
FLAKY so repeat offenders stay visible.
2026-10-08 11:25:51 +08:00
Jason 7d8004c408 test(wsl): skip three lib tests the 9P share cannot host
The WSL2 nightly now reaches the lib suite: 3299 of 3302 pass. The rest
are limits of \\wsl.localhost, not product bugs:

- Windows cannot create symlinks on the share ("Incorrect function"),
  so the catalog escape and grokbuild symlink-cycle tests cannot build
  their fixtures. Skip when the link fails under a WSL temp dir and
  keep failing anywhere else.
- Extracting 13,248 files takes over the 180s per-test timeout on 9P.
  Production extracts to the system temp dir, so skip the entry-limit
  test when TEMP is a WSL share.
2026-10-08 11:11:24 +08:00
Jason faae0e4d7e ci(wsl): run the WSL2 nightly from a nextest archive
The nightly has failed since 10-05 with LNK1327: the contract step
re-invoked cargo with TEMP on \\wsl.localhost, cargo re-linked the lib
tests, and mt.exe cannot create its manifest temp file on a UNC path.

Build the lib tests once into a nextest archive with the native TEMP,
and run both the contract test and the lib suite from that archive, so
nothing is built while TEMP points at the WSL share. The archive is
extracted to the runner temp, and --no-tests fail replaces the manual
check that the contract test was discovered.

Supersedes #6856.
2026-10-08 10:43:48 +08:00
Jason efd236a474 docs(manual): update the user manual for v4.0
Bring the zh, en and ja user manuals up to the v4.0.4 interface: the
sidebar layout, the six Settings groups, the Direct / Routing /
Aggregation connection modes, Accounts, and the Apps page. Old
Settings paths (Advanced, Routing, Auth) and takeover wording are gone.

- Add 4.6 Aggregation Mode in all three languages and list it in the
  manual indexes.
- Rewrite 1.3 Interface Overview and 1.5 Settings around the new
  layout; refresh provider, extension, routing, usage and FAQ pages.
- Use the v4.0 release screenshots where they fit and leave
  TODO(v4 screenshot) markers elsewhere.
- Update the version section of each manual index to v4.0.4.
- Remove the 40 v3 screenshots that are no longer referenced.
2026-10-07 20:42:08 +08:00
Jason a40adef3f7 fix(usage): import Grok Build session usage after 60s instead of 10min
The settle window that delays Grok Build session imports reused the
±10-minute takeover guard window, so a finished turn in Direct mode took
10-11 minutes to show up in usage stats. The settle window only needs to
cover the few seconds between turn_completed being written and the proxy
rows for that turn landing in the database, so cut it to 60 seconds and
keep the guard window unchanged.

A compile-time assertion keeps the settle window below the guard window,
and two tests cover a 90-second-old turn importing and a proxy row that
lands a few seconds after the event still blocking the import.
2026-10-07 20:16:35 +08:00
Jason a7f66764e3 chore(github): list all ten apps in issue templates
Add Claude Desktop, Grok Build, Hermes, Pi and MiniMax Code to the app
dropdowns, bump the version examples to 4.0.4, and point to
Settings → About for the version number.
2026-10-07 20:02:36 +08:00
Jason 5dd824c730 docs(readme): update all four READMEs for v4.0
- Replace the v3.8 screenshots with six v4.0 screenshots (main, apps,
  aggregation, Codex aggregated models, session reader, usage)
- Rewrite Quick Start around the sidebar and the Direct / Routing /
  Aggregation tabs, add the Apps page and Accounts, and note that the
  project switcher is now hidden by default
- Add an Aggregation column and section; tray switching now covers
  Claude Desktop and Pi now supports MCP
- Replace v3 settings paths in the FAQ with the v4 locations and add an
  entry on restarting Codex after the aggregated model list changes
- Point the release notes link at v4.0.4
2026-10-07 20:02:36 +08:00
Jason 01ee685da8 docs(release): lead with usage stats in the v4.0.4 notes 2026-10-07 19:09:33 +08:00
Jason a29a4f3868 docs(release): add v4.0.4 release notes 2026-10-07 18:34:16 +08:00
Jason c2282a1073 chore(release): v4.0.4 2026-10-07 18:34:16 +08:00
Jason 5b10d1996c fix(usage): keep the speed column visible in a narrow window
At the minimum window width (900) with the sidebar expanded, the request
log table area is only 644px wide, but the table had min-w-[700px]. The
rightmost speed column was pushed into the table's horizontal scroll,
whose scrollbar sits below the last row and is usually off-screen, so
users saw the column as missing.

Lower the minimum to 620px, matching the provider, model and pricing
tables. The provider and model columns already truncate (max-w-0), so
they absorb the squeeze.
2026-10-07 12:37:37 +08:00
Jason e0b36500dc feat(profiles): hide the project switcher by default
The project switcher showed on the providers page header and in the tray
for every new install, even though most users never create a project.
Flip the default so it stays out of the way until turned on in Settings.

- Use plain #[serde(default)] for show_profile_switcher and drop the
  default_show_profile_switcher() helper; AppSettings::default() now
  sets it to false
- Change the frontend fallbacks in App.tsx and GeneralSection from
  ?? true to ?? false so the header does not flash the switcher before
  settings load

Existing users keep their current behavior: the field has been
persisted to settings.json since v3.17.0, so anyone who has saved
settings already has an explicit true.
2026-10-07 12:31:44 +08:00
Jason e09fdf046c feat(settings): give the About card's star prompt its own dismissible strip
The star prompt sat at the end of the link row with ms-auto, so it
wrapped onto a line of its own and read like a stray underlined caption.

- Move it into a tinted strip at the bottom of the card with a slowly
  spinning star (stops under prefers-reduced-motion) and a "Star on
  GitHub" button that opens the repo
- Drop the separate GitHub button while the strip is shown, since the
  star button goes to the same place
- Add a close button; dismissal is remembered per device in
  localStorage, and the plain GitHub button returns to the link row
- Add settings.starOnGithub to all four locales
2026-10-07 09:16:14 +08:00
Jason f61dc5f520 fix(macos): relaunch via LaunchServices so the window comes to the front after restart
tauri::process::restart spawns the binary directly. Since macOS 14 app
activation is cooperative, so the new process's activation request is
denied and its window stays behind other apps after an in-app update or
a config-dir restart.

restart_process now launches the bundle with `open -n` on macOS, letting
the still-frontmost old instance hand activation to the new one, and
falls back to tauri::process::restart if that fails. restart_app uses
restart_process instead of app.restart() so both restart paths behave
the same.
2026-10-06 23:52:59 +08:00
Jason d35726e286 test(hermes): batch the scan-limit fixture inserts in one transaction
first_user_messages_is_scoped_to_listed_sessions inserts 1002 rows, each
in its own autocommit. On the Windows runner every commit flushes to disk,
so the test took 150-180s and intermittently hit nextest's 180s timeout
(2.5s on Linux). Wrap the fixture inserts in a single transaction.
2026-10-06 22:14:45 +08:00
Jason 8596a233b2 docs(release): add v4.0.3 release notes 2026-10-06 21:20:09 +08:00
Jason 014a33a42b chore(release): v4.0.3 2026-10-06 21:20:09 +08:00
Jason f3e3a95560 docs(manual): add the auto mode server-check toggle to the Claude quick toggles table
The toggle shipped without a manual row. Describe what it changes, the
per-session notice it avoids on gateways, the new-provider default and
that official endpoints should leave it unchecked; the per-provider note
now covers three options instead of two.
2026-10-06 21:13:04 +08:00
Jason 65e79de845 feat(claude): explain each quick toggle with a help tip
The quick toggles above the Claude config editor had no explanation,
except a native title tooltip on the auto mode one. Put the standard "?"
help tip after each label: one sentence on what the toggle does, why
you would want it, and whether it is global or follows the provider.

Render the toggles from one list so every entry gets the same
checkbox + help layout, with copy under claudeConfig.<key> and
claudeConfig.<key>Help. The help button sits outside the <label>, so
opening the tip never flips the checkbox.
2026-10-06 21:13:04 +08:00
Jason 997a34fe3a refactor(claude): drop the max effort quick toggle
CLAUDE_CODE_EFFORT_LEVEL=max pins every session to the most expensive
effort and turns /effort into a no-op ("Not applied: ... overrides
effort this session"), while Claude Code deliberately keeps max
session-only (effortLevel in settings.json does not accept it). Users
who want max can still pick it per session with /effort max, or set the
variable by hand.

Remove the checkbox, its i18n keys, tests and the manual rows. Configs
that already carry the variable keep it untouched.
2026-10-06 21:13:04 +08:00
Jason cac218f3e2 refactor(claude): drop the Teammates quick toggle
Claude Code now gives every session a single implicit team: named
subagents, SendMessage and ListAgents are available without
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS (on 2.1.287 the tool list is
identical with and without it). The variable only gates tmux/iTerm2
split-pane teammates and the structured shutdown/plan-approval
messages, which no longer justifies a slot among the quick toggles.

Remove the checkbox, its i18n keys and the manual rows. Configs that
already carry the variable keep it untouched.
2026-10-06 21:13:04 +08:00
Jason 146f3196fa feat(claude): add auto mode server-check toggle and default it off for gateways
Add a "Disable auto mode server check" quick toggle to the Claude
provider editor that writes CLAUDE_CODE_AUTO_MODE_SERVER=0, with a hint
explaining that third-party gateways cannot serve the check and that
official endpoints should leave it unchecked.

New Claude providers now get the key by default unless the preset is
official or a cloud provider, or the preset already sets it. The default
form (no live base yet) also goes through the overlay so the custom
provider starts with the same value.
2026-10-06 21:13:04 +08:00
Jason 3144c820f1 feat(backup): show all backup locations and their sizes
Backup & Restore only listed database snapshots, while other backups kept
growing unseen under ~/.cc-switch: Codex session history migration copies
can reach several GB, plus skill uninstall backups, config first-write
backups, Hermes/OpenClaw config backups and files left by older versions.

A collapsible "Other Backups" section below the database list now shows
each location with its path, size and item count, and lets the user reveal
it in the file manager or delete a whole category after a confirmation that
explains what is lost (e.g. deleting the Codex unify migration backups
removes the "restore backup" option when turning that switch off).
Database snapshots are only summarized there and stay managed per file in
the list above.

The frontend only passes category ids; paths are computed by the backend
from each writer's own location, and deletes never follow symlinks. Codex
migration backups are deleted under the migration lock and skill backups
under the skills state lock, so a running migration or uninstall cannot
lose its directory mid-write.
2026-10-06 21:03:50 +08:00
Jason 58a9e8e2af fix(presets): drop the expired OpenCode Go referral code
OpenCode ended its referral program on 2026-09-28. Since then any /go page
opened with a ?ref= parameter shows a warning that referral links no longer
earn credit, so the "Get API Key" link on the five OpenCode Go presets
greeted users with that banner. Point them at the plain /go page instead.
2026-10-06 21:03:50 +08:00
Jason d68f008fe2 docs(contributing): accept large established aggregators as presets
Large, well-established aggregator platforms such as OpenRouter and
SiliconFlow can still get a regular built-in preset; the sponsors-only rule
applies to the remaining relay and aggregator services.
2026-10-06 21:03:50 +08:00
Jason 8569ddd75f feat(presets): add OpenCode Zen for Claude Code and Codex
OpenCode Zen is the pay-as-you-go side of the same workspace key that
OpenCode Go uses, so both now sit in one "OpenCode" family row (Go as the
coding subscription, Zen as pay-as-you-go).

Both presets connect directly without local routing: Claude Code talks to
Zen's native /v1/messages with ANTHROPIC_API_KEY (the endpoint only reads
x-api-key), and Codex talks to /v1/responses. Codex defaults to gpt-6-sol
because Zen currently returns 403 "Model access is disabled" for
gpt-5.6-sol. Free Zen models are rejected outside OpenCode, so no preset
defaults to them. OpenCode and Pi already ship Zen as a built-in provider
and get no preset.

Closes #6016
2026-10-06 21:03:50 +08:00
Jason 3c3b94939a docs(contributing): document which services get a built-in provider preset
Built-in presets are limited to model vendors and cloud platforms that serve
models under their own brand. Relay and aggregator services that resell other
vendors' models are included only as project sponsors; everything else keeps
working through Custom Provider.
2026-10-06 21:03:50 +08:00
Jason c615c3eeba feat(usage): show success rate and speed in model stats
The model stats tab lacked the success rate and speed columns the provider
tab has, and carried an extra average-cost column, so the two tabs read
inconsistently.

Compute success count and the exact / estimated speed totals per model in
get_model_stats, mirroring get_provider_stats (rollup rows count toward the
success rate but not speed, as they have no per-request timing). Extract the
success-rate and speed header/cells into shared statsColumns so both tables
render them identically, rename the cost header to "Cost", and move the
average cost per request into the cost cell's hover title.
2026-10-06 16:51:00 +08:00
Jason 6f4d365b73 fix(usage): use a short session-log label where the app is already shown
The request log table truncated "Claude Code · Session logs" in its narrow
provider column, and the request detail repeated the app name already shown
next to it. Show just "Session logs" in those two places; the provider
filter and provider stats table keep the full name since they have no app
column to tell the session-log rows of different apps apart. The hover
title still carries the full name and the explanation.
2026-10-06 16:51:00 +08:00
Jason 1400382219 fix(proxy): respect thinking-off and per-model effort floors when mapping Claude effort
When Claude Code requests are converted to OpenAI Chat / Responses, the
effort was forwarded verbatim even with thinking turned off. Claude Code
keeps sending `output_config.effort: "high"` when thinking is off, and
Workflow subagents send `thinking: disabled` together with an effort, so
GPT and Grok upstreams still reasoned at full strength.

- Clamp the effort to `low` when thinking is absent or disabled. `none`
  and `minimal` are not accepted by every whitelisted model.
- Clamp to the tiers the target model accepts: gpt-5-pro only takes
  `high`, and gpt-5.2/5.4/5.5-pro take medium/high/xhigh, so `low` is
  raised to `medium`. This also covers an explicit `/effort low` or a
  small thinking budget sent to a pro model, which used to return 400.
- The 5.x Claude models (Fable, Mythos, Opus, Sonnet) keep adaptive
  thinking on when `thinking` is omitted, and Claude Code omits it for
  Fable and Opus 5.5 when thinking is "off" because they cannot turn it
  off. That default is lost once the model is mapped to another vendor,
  so the forwarder now makes `thinking: adaptive` explicit from the
  client's model before the Chat / Responses transform. The Messages
  passthrough is unchanged.
- Add Opus 5 / 5.5 to `adaptive_thinking_is_default`, matching the
  per-model table in the Claude thinking docs.
2026-10-06 16:51:00 +08:00
Jason dd07b8f4b5 fix(usage): show localized names for session-log usage providers
Usage imported from client session logs (routing off) was labelled with
hardcoded English placeholders like "Claude (Session)", which showed up
untranslated in every locale and read ambiguously next to Claude Desktop.

Display them as "<full app name> · Session logs" in all four locales,
matching the "Session logs" wording already used elsewhere on the usage
page, with a hover hint explaining why the provider can't be told apart.
The backend placeholder names stay unchanged because the provider filter
sends them back for exact matching; only the rendered label is mapped.
2026-10-06 16:51:00 +08:00
Jason d7f615f06c fix(skills): deploy imported skills to every app ticked in the import dialog
Importing a skill discovered in one app while ticking other apps only
stored the enabled flags. The UI then showed the skill as enabled for
those apps, but their skill directories never received it until the next
provider switch happened to re-sync, so the client could not read it.

Project the skill into each ticked app that does not have it yet as part
of the import. Copies that already exist in an app directory are left as
they are. If deploying to an app fails, that app's flag is not recorded,
so the stored state always matches what is on disk, and the failure is
listed in the import error next to any skipped MiniMax Code entries.
2026-10-06 16:51:00 +08:00
Jason fc8d884b96 feat(quota): show the Codex Credits balance for ChatGPT subscriptions
wham/usage already returns a `credits` block next to the rate-limit
windows ({has_credits, unlimited, balance}, with balance as a string such
as "62500"). Parse it into `SubscriptionQuota.credits_balance`; a zero,
unlimited, missing or malformed balance is simply left out and never
fails the quota itself. No extra request is made.

Codex credits are metered at API prices: the official rate card's
credits per 1M tokens equal the API dollar price x 25 for every model,
so one credit is $0.04. The conversion lives in a single constant.

- Expanded view / auth center: a "Credits" row with the credit count in
  the middle column and "about $2500" as the value.
- Provider card: credits are only spent once a limit runs out, so the
  balance stays off the card until a tier is used up, then appears in
  dollars ahead of the saved resets.
- Card reset drop-down: the balance is listed below the expiries; a
  single saved reset can now drop down too when there is a balance.
2026-10-06 16:51:00 +08:00
Jason bc4d1a50d2 fix(usage): count cache tokens in provider and model stats
The provider and model tables summed only fresh input + output, while
the metric cards and trend chart show real total tokens (fresh input +
output + cache writes + cache reads). With a high cache hit rate the
table rows added up to a tiny fraction of the headline total.

Add real_total_tokens_sql and use it in both the detail and rollup
branches of get_provider_stats and get_model_stats, so the rows sum to
the summary's real_total_tokens.
2026-10-06 16:51:00 +08:00
Jason bf2fe0d0be docs(release): add v4.0.2 release notes 2026-10-06 00:07:54 +08:00
Jason 08f789a8c4 chore(release): v4.0.2 2026-10-06 00:07:54 +08:00
Jason 600fdb0c15 fix(ui): drop the restart hints for Claude Code aggregation changes
Aggregated models used to reach Claude Code through gateway discovery,
which only runs at startup, so the UI asked for a restart after every
change. Now that they are written into settings.json as modelPicker, a
running Claude Code picks up added and removed models, a new default
and window changes on its own. Codex still reads its model catalog only
at startup, so its hints stay as they are.

- useStackModelsChangedHint, which only existed to toast "restart
  Claude Code" when the published models changed, is removed together
  with its test and provider.stackModelsChanged.
- Adding or removing an aggregated provider says
  "Saved. Takes effect in Claude Code right away" for Claude Code
  (new provider.stackSavedLive); entering Aggregation says the models
  are now in /model (new mode.toast.enteredStackLive), and the entry
  dialog no longer lists a restart note for it
  (mode.dialog.stackNoteRestart removed).
- The Claude Code model list hint drops "Restart Claude Code after
  changes", and the Aggregation setting's description now says only
  Codex needs a restart.
- The unused proxy.stackMode.tooltip keys are removed.

All four locales are updated.
2026-10-05 23:22:05 +08:00
Jason 9ec89fe8ae feat(claude): list aggregated models through modelPicker instead of gateway discovery
In Aggregation mode all four model tiers point at the default
provider's first model, and Claude Code keeps its built-in Opus /
Sonnet / Haiku rows (plus Fable once its tier is set) above the
discovered models. /model therefore opened with four identical rows for
that model, and the model's own discovered row was deduplicated away.
Dropping the tier variables does not hide those rows: they fall back to
claude-opus-5-5 and friends, which would go to the upstream as is.

- Aggregation mode now writes a top-level modelPicker to settings.json
  with replaceBuiltInOptions: true and one row per aggregated model
  (Stack id, display name, "<upstream model> · <window>" description).
  The picker shows Default plus these rows only. The four tier variables
  are still written: Default, --model opus and subagent aliases resolve
  through them.
- modelPicker is a Claude top-level key field, so every switch replaces
  it: Aggregation writes the list, routing and direct leave none. A
  picker the user set by hand is not kept, the same rule as Codex's
  model_catalog_json pointer.
- CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY is no longer written. A
  value written by an older contract is removed on the next rewrite like
  any other exclusive field, and a user's own value is left alone.
- The Claude contract digest includes top-level fields only when there
  are any, so routing contracts stay byte-for-byte unchanged.

Checked against Claude Code 2.1.287 with an isolated config directory
and a fake gateway: the picker lists only the aggregated models, and a
running session picks up changes to modelPicker and to the tier and
window variables without a restart.
2026-10-05 23:21:52 +08:00
Jason fe831f0a74 feat(ui): pick the provider form layout from the tab it was opened in
The add and edit forms chose the Aggregation layout from the mode the
app was actually in, with a link to toggle between the simple and the
full form. Opening a provider from the Direct or Routing tab while
Aggregation was on still showed the simple form, and the toggle was one
more control to explain.

- SwitchModePanel reports the tab being viewed (Direct / Routing /
  Aggregation), and App passes it to the add and edit dialogs as
  modeView. Claude Code and Codex use the simple form only when the form
  is opened from the Aggregation tab; without a tab (the tray's "add"
  switches apps first) the app's actual mode decides as before.
- The "Show full form" / "Back to the Aggregation form" link and its
  providerForm.stackLayout.fullForm / simpleForm keys are removed.
- The dialog header shows "<app> (Aggregation mode)" while the simple
  form is in use (new provider.formSubtitleStack key in all four
  locales).
2026-10-05 23:21:39 +08:00
Jason 917653572e feat(ui): pick fetched models into the Codex model mapping table
Fetching the model list and picking a model is the easy way to fill a
row: the context window and reasoning levels are filled from presets and
models.dev. In the full Codex form, though, fetching only added a small
dropdown arrow beside each row's model field, and nothing visible at all
when the table was empty, so users had to add a row, find the arrow and
pick a model.

- The model mapping section now shows the same searchable picker as the
  Aggregation layout once models are fetched. Picked models become rows
  named after the model ID, with the context window and reasoning levels
  filled. The per-row dropdown stays for swapping one row's model.
- The add buttons next to "Fetch models" in the Codex model mapping and
  model list, the Claude Code Aggregation model list and the OpenCode
  model list are renamed to "Add manually", marking them as the fallback
  for models the fetched list does not have.
- The picker's "Add selected" button is neutral instead of solid, so
  Save stays the only solid button on the provider form.
2026-10-05 21:24:32 +08:00
Jason db8069ef7d fix(codex): keep the reasoning levels column from stretching the model catalog
Picking many reasoning levels for a Codex model catalog row pushed the
levels column wider, and the rows stopped lining up with each other and
with the header.

- Each catalog row is its own grid, and its `1fr` tracks could not shrink
  below their content, so a long label such as
  "none,low,medium,high,xhigh,max" widened its own row. The header and
  row templates now use `minmax(0,1fr)`, so every row splits the width the
  same way and the label's existing truncation takes effect.
- The trigger label collapses each run of three or more adjacent levels
  into "first → last": "low → max", or "none, low → max" when minimal is
  skipped. A range never spans a skipped level, so it cannot suggest a
  level that was not picked. Shorter runs, gapped picks such as
  "low, high, max" and lists that are not in canonical order are shown in
  full.
- Hovering or keyboard-focusing the trigger shows every picked level and,
  when one is set, the default level (new codexConfig.defaultReasoningLevelTip
  key in all four locales).
2026-10-05 21:03:10 +08:00
Jason 20222460a1 docs(whats-new): add the 4.0.2 summary
Drafted from the commits since v4.0.1, ordered by impact: the 400 error
that broke Claude Code on a ChatGPT account (#7876), Codex aggregated
models hidden by a leftover model catalog setting, MCP sync to Pi 1.0
(#7862), and the time MCP preset that exited on start (#7863).
2026-10-05 20:41:55 +08:00
Jason f5db60db87 feat(ui): fill the icon box with tile-style provider icons
Provider icons all sat as an 18-20px glyph inside the same 32px bordered
box, so icons that carry their own solid background (88API, Qiniu,
CherryIN, ...) showed up as a small square inside a square.

- A new ProviderIconBox replaces the box duplicated across the provider
  card, the name field's icon button and the two preset selector lists.
  Icons marked `shape: "tile"` in metadata fill the whole box, clipped
  to its radius, with an inset ring drawn above the image so white
  tiles keep an edge on white cards. Transparent logos keep the bordered
  box and grow from 18px (forms) / 20px (card) to a shared 22px, which
  matches the visual weight of the tiles without crowding the border.
- 28 icons are marked as tiles: solid squares, rounded tiles, circles
  and white-background images. UCloud and SSSAiCode gain metadata
  entries for this, which also makes them searchable in the icon picker.
- Icons with wide transparent margins are cropped to their content:
  a6api, byteplus, nekocode, etok, sudocode-us, claudeapi and
  teamorouter as lossless square crops (kept pixels identical), and
  crazyrouter, aigocode, yi and zenmux through a tighter viewBox (paths
  untouched). The Pi mark keeps its padding: it is the official README
  mark and its viewBox is pinned by a test.
- Xiaomi MiMo switches from a one-line wordmark, which shrank to a thin
  line at icon size and turned black-on-dark in dark mode, to the
  official favicon style: the two-line lobe-icons wordmark in white on a
  black tile.
- FennoAI's webp had a fake transparency checkerboard baked in. It is
  replaced by a 256px PNG with real transparency, cropped to the fox.
2026-10-05 20:30:56 +08:00
Jason 165622b840 feat: show a short summary of what changed after updating
The in-app updater only follows stable releases and its manifest notes
just say "Release vX", so users jumping between stable versions never
saw what the versions in between changed. The full release notes are
too long for a popup.

- Each version gets src/whats-new/<version>.json: up to 4 one-line
  items (new / fix / improve) in zh, zh-TW, en and ja, bundled into the
  app at build time. "items": [] marks a version that should not pop up.
- On the first launch after an update, a dialog lists every non-empty
  version between the last one seen and the current one, newest first.
  Older versions beyond the first three are collapsed. Each version
  links to its page in the website changelog.
- The last seen version is stored per device in settings.json
  (whatsNewSeenVersion) and only moves forward, so a downgrade and
  re-upgrade does not repeat anything. Without a record, only the
  current version is shown. When there is nothing to show, the version
  is recorded silently so the next update has a correct starting point.
- The dialog waits for the welcome and new-layout dialogs. Confirming
  either of them also records the current version, so a 3.x user does
  not get two dialogs in a row.
- Settings > About gets a "Recent updates" button to open the summaries
  again. The 4.0.0 summary is included so that list starts from 4.0; no
  update popup can reach it, since only 4.0.2 and later record a version.
- The release workflow first requires the tagged version's summary file
  and validates every bundled summary with the unit test, since a
  malformed item would crash the dialog and put the whole app on the
  error screen.
2026-10-05 20:30:56 +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
Jason 2fdcebf16b fix(codex): let users switch back to CC Switch's model catalog when aggregation is blocked
When the routed provider's row or config.toml points model_catalog_json at
another file, Codex reads only that file and aggregated models never show
up. The warning only told users to edit the config by hand.

- The routeOwnsCatalog / configOwnsCatalog warning now has a "Use CC
  Switch's model catalog" button. It removes the pointer from the routed
  provider's row, removes the user's pointer from config.toml (only while
  it still holds the same value), and rewrites Codex even when the
  contract is unchanged, so the merged catalog takes effect.
- The provider editor no longer stores a model_catalog_json it only
  carried over from config.toml. Saving a card as is used to turn the
  user's global pointer into that card's own pointer (seen on the seeded
  OpenAI Official card, whose config starts empty). A pointer the user
  changes in the editor is still kept on the card.
2026-10-05 20:30:56 +08:00
Jason 4804b723d4 docs(release): add v4.0.1 release notes 2026-10-05 12:51:16 +08:00
Jason d36bb7a938 chore(release): v4.0.1 2026-10-05 12:22:32 +08:00
Jason ee66be2222 ci(release): build macOS arches in parallel and add manual dry runs
- macOS used one runner that compiled aarch64 and x86_64 one after the
  other inside `tauri build --target universal-apple-darwin`, and every
  notarization retry recompiled both. Now `macos-binary` compiles each
  arch on its own runner with --no-bundle, and `macos-release` merges
  them with lipo and runs `tauri bundle --target universal-apple-darwin
  --bundles app`. Only that bundle/sign/notarize step is retried. The
  styled DMG from create-dmg, the zip and the updater tar.gz are made as
  before. `macos-binary` carries the release environment, so a release
  is still approved once at the start; `macos-release` must not, or it
  would wait for a second approval halfway.
- The Tauri signing key preparation moves into a composite action shared
  by the Linux/Windows matrix and `macos-release`. Its output matches
  the old inline script for every key format tried.
- Windows x64 builds only the MSI. Tauri also built an NSIS setup.exe
  that was never uploaded; the portable zip still comes from the exe.
- latest.json is generated inside publish-release from the downloaded
  signatures, before the release is created, so a missing platform
  fails without leaving a half-published release. The
  assemble-latest-json job is gone.
- workflow_dispatch runs the full build, signing and notarization as a
  dry run, with assets kept as workflow artifacts. Only a tag push
  publishes, even when a dispatch is started on a tag; the concurrency
  group includes the event name so a dry run cannot cancel a release in
  progress; and `/` in branch names becomes `-` in asset file names.
- Quote the $GITHUB_OUTPUT redirects that shellcheck flagged.
2026-10-05 12:18:17 +08:00
Jason e95c5e68d0 ci: path-filter pushes to main too, with a weekly full run
Pushes to main ran every job on the theory that they keep the caches
warm, but a cache whose key hits exactly is never saved again, so those
full runs refreshed nothing. Pushes now skip unaffected jobs like pull
requests do.

A scheduled run every Monday (03:41 Beijing) and workflow_dispatch run
everything, rebuilding caches after eviction: the filter step only runs
for pull_request and push, and the `|| 'true'` fallback on its outputs
turns a skipped filter into "both areas changed". The concurrency group
now includes the event name so the scheduled run and a push to main do
not cancel each other.
2026-10-05 12:17:58 +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 ef633b5789 test: fix the two clippy lints in test code
`cargo clippy --all-targets -- -D warnings` failed on two tests: an
`op_ref` comparison against `&data_url` in transform_codex_chat and a
`type_complexity` row tuple in usage_stats. Compare by value and name
the row type. CI's clippy does not lint test targets, so this only
unblocks the stricter local run.
2026-10-05 12:17:33 +08:00
Jason c62eab2ba1 build(release): compile with 16 codegen units
With codegen-units = 1, LLVM optimizes the large main crate on a single
thread, and that dominates release build time. 16 units make the main
crate compile about 64% faster for a binary about 17% larger, measured
locally. Applies to every release target; thin LTO and opt-level "s"
are unchanged.
2026-10-05 12:17:24 +08:00
Jason f344af0256 ci: install the Rust toolchain pinned in rust-toolchain.toml
dtolnay/rust-toolchain@stable installed the latest stable, then rustup
switched to the 1.95 channel from rust-toolchain.toml on the first cargo
call, so every job downloaded two toolchains. Read the channel from
rust-toolchain.toml and pass it as `toolchain` in the CI backend and
WSL2 jobs, the release build and the WSL2 nightly, keeping the file as
the single source of the compiler version.
2026-10-05 11:13:26 +08:00
Jason f9e2ebbb9f ci(release): fail fast on notarization rejections and missing updater signatures
- macOS: tee each build attempt to a log, and stop on HTTP 401/403 from
  Apple instead of retrying. An expired developer agreement cannot be
  fixed by a retry; for v4.0.0 the loop rebuilt both architectures twice
  more and ran into the job timeout.
- Treat a missing macOS .tar.gz.sig, Windows MSI or MSI signature
  (either arch), AppImage or AppImage signature as an error. They used to
  print a warning, and latest.json then silently left that platform out,
  so its users would get no update.
- assemble-latest-json downloads only the *.sig files it reads and fails
  if any of the six platforms lacks a signature.
2026-10-05 11:13:19 +08:00
Jason e4960bbaf0 test(wsl): run the WSL2 nightly as a lib suite that can pass on 9P
The nightly has not passed in weeks: 37 tests failed on the
\\wsl.localhost share before the run hung. They fall into a few causes
that come from the 9P filesystem, not from product bugs:

- Windows cannot lock SQLite files there, so every on-disk SQLite
  fixture fails with "database is locked". Skip `database::` by name and
  let fixture tests return early via config::sqlite_unsupported_in_temp_dir.
  The account recovery test now uses an in-memory DB that it reuses
  across the simulated restart.
- Integration tests all create the app database, so run --lib only.
  Users keep that database in the Windows home unless they override the
  app config dir.
- The share exposes no file identity, so parent timelines are not cached.
  The five cache tests now check reuse only where parent_cache_supported.
- Remote symlinks are not followed. The catalog escape test asserts None
  when the link resolves, and otherwise that the result is unreadable.

With one test binary left, the timeout drops from 90 to 30 minutes.
2026-10-05 11:13:08 +08:00
Jason d115fddb07 fix(codex): skip Codex state DBs on WSL paths Windows cannot lock
When the Codex config dir points into a WSL distro (\\wsl$ or
\\wsl.localhost), Windows reaches state_*.sqlite through the 9P
redirector, which has no byte-range locks. Every open waits out the 5s
busy timeout and fails with "database is locked", so the history
migration never wrote its completion marker and retried on each start,
and every session scan stalled loading thread titles.

Skip such DBs in the history migration, its restore path and the
session title loader, logging one warning. The migration counts them as
zero rows and completes; titles fall back to the session index.
is_wsl_path moves from claude_mcp into config so both can share it.
2026-10-05 11:12:52 +08:00
Jason 43077f5539 fix(usage): stop parented Codex sync from deadlocking on a poisoned cache
When the replay cache mutex was poisoned, sync_single_codex_file went
through `if let Ok(caches) = replay_caches().lock() { .. } else { .. }`.
Under Rust 2021 the scrutinee temporary lives until the end of the else
branch, so the PoisonError still held the guard while
resolve_parent_signatures locked the same mutex again, and the thread
hung. The WSL2 nightly hit this after an unrelated test panicked while
holding the lock.

Copy the cache lookup out before releasing the lock, and have
clear_codex_replay_caches recover from and clear the poison. A
regression test poisons the lock and times out after 30s on the old code.
2026-10-05 11:12:34 +08:00
Jason 259d807f0d feat(settings): add a help tip explaining the Claude Code onboarding skip
Rewrite the unused skipClaudeOnboardingDescription in all four locales to
say what the toggle actually does (writes or removes hasCompletedOnboarding
in ~/.claude.json) and wire it to the row's help popover.
2026-10-05 09:58:28 +08:00
Jason f6387b862b style(quota): put the reset-credits chevron in the countdown slot
When the reset-credits dropdown sits on its own card row, the chevron now
takes the countdown slot's icon position, lining up under the clock of the
row above; text right edges stay aligned. Merged rows are unchanged.
2026-10-05 09:58:28 +08:00
Jason df08bb15c3 fix(grokbuild): use preset apiKeyUrl for the get-API-key link
The Grok Build form passed the preset's websiteUrl to the API key link,
so sponsor presets opened the plain homepage instead of their referral
signup URL. Prefer the selected preset's apiKeyUrl, matching
useApiKeyLink, without changing the saved websiteUrl.
2026-10-05 09:58:28 +08:00
Jason 636e161658 feat(opencode): mark reasoning models so OpenCode offers thinking variants
OpenCode only generates thinking variants (ctrl+t) for models whose
capabilities.reasoning is true, and falls back to models.dev only when
the provider key matches a models.dev provider. CC Switch writes custom
provider keys, so preset and fetched models never got any variants.

- Fill `reasoning: true` when a fetched model is known to reason (preset
  row or models.dev); never write false, which would shadow models.dev.
- Mark reasoning-capable models in OpenCode presets, cross-checked with
  models.dev; skip non-reasoning, router (`auto`) and mixed entries, and
  LongCat 2.0 whose preset turns thinking off.
- Let OpenCode derive the variants itself instead of hardcoding effort
  tables that drift between OpenCode releases.
- Remove the unused getPresetModelDefaults helper.
2026-10-05 09:58:28 +08:00
Jason a189980f35 docs(release): shrink and center v4.0 release note screenshots
Render the screenshots at 720px (560px for the cropped Codex model
picker) and center them, matching the published release body.
2026-10-04 20:12:48 +08:00
Jason 0d0dd0a548 docs(readme): use English 88API banner in en/ja/de READMEs 2026-10-04 17:38:51 +08:00
Jason 412579f9ad chore(release): v4.0.0 2026-10-04 17:06:16 +08:00
Jason 8b977f2715 fix(quota): keep balances one quiet color until they run out
Balances under 10% of their total were drawn darker and bolder (and the
tray title added "running low"), so a few cards showed the same kind of
number in different shades with nothing saying why. Balances now stay
gray and only turn red at zero, on the card, in the expanded bar and in
the tray. The left percentage is still computed from the total, so bar
length and which lines get picked are unchanged. Per-tier percentages
keep their under-10% emphasis.
2026-10-04 16:54:30 +08:00
Jason 2153c1ccf0 feat(quota): show reset countdowns again instead of hiding them on hover
The v7 quota redesign moved reset times into the native hover title, which
takes over a second to appear and gives no hint it is there, so in practice
they were gone.

- Provider cards: each quota row now ends with a fixed-width "⏱ 2h30m" slot
  (empty when that row has no reset, so rows stay aligned). A merged row
  shows its earliest reset; the hover text still lists every tier. The
  column only widens (136 → 194px) when some tier has a reset time.
- Expanded quota bars write "Resets in 2h30m" after the value.
- Auth center account rows get the same countdown slot.
- Countdowns are computed at render time from resetsAt instead of being
  baked into the line when built, so they no longer go stale between
  queries.
- Copilot no longer overwrites the hover detail, which dropped its reset.
2026-10-04 16:48:27 +08:00
Jason 6d6bbe6747 docs(release): add v4.0 release notes in Chinese, English and Japanese
Add the v4.0 release notes and the screenshots they reference
(main page, Apps page, Aggregation, Codex model picker, session reader
and usage, in Chinese and English) under assets/release-notes/v4.0.0.
2026-10-04 16:25:20 +08:00
Jason bfd28b2f9f feat(presets): add Tu-zi (兔子API) presets across nine apps
Tu-zi (api.tu-zi.com) is a new-api based aggregator. It is not a
sponsor, so the presets sit in the non-sponsor block of each file.

- Presets for Claude Code, Claude Desktop, Codex, Gemini CLI, Grok
  Build, OpenCode, OpenClaw, Hermes and Pi. Endpoints follow the vendor
  wiki (wiki.tu-zi.com): Anthropic- and Gemini-style clients use the bare
  host; Codex, Grok Build and the OpenCode Anthropic SDK use /v1.
- Claude Code uses ANTHROPIC_API_KEY, as the vendor's own CC Switch
  guide tells users to switch from ANTHROPIC_AUTH_TOKEN.
- Claude Desktop uses the bare host although the vendor's Desktop guide
  shows /v1: Desktop appends /v1/messages itself, and /v1/v1/messages
  returns 404 on this gateway while /v1/messages reaches auth.
- Multi-model apps list Claude Opus 5, Sonnet 5 and Haiku 4.5. The
  public pricing catalog marks Opus/Sonnet 5.5 with a "claude code"
  endpoint type rather than "anthropic", so they are left out of the
  SDK-based clients. Codex defaults to gpt-5.6-sol, Gemini to
  gemini-3.8-flash, Grok Build to its default grok-4.5.
- Endpoint candidates add the vendor's Guangzhou (api.ourzhishi.top),
  Los Angeles (api.sydney-ai.com) and Cloudflare (apicdn.tu-zi.com)
  relays, which front the same backend, in the five apps whose presets
  support candidates.
- Display name is localized through providerForm.presets.tuzi:
  "兔子API" in zh / zh-TW, "Tu-zi" in en / ja. Search aliases add
  兔子 / 兔子api / tuzi.
- 128x128 raster icon from the API site's logo.png, plus metadata.
- Claude preset rows in the selector test go from 72 to 73.
2026-10-04 14:57:57 +08:00