- 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.
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.
- 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.
`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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
The bottom bar's border-t ran edge to edge and sat flush against the last
global item, so a selected Usage row touched the line. Draw it as an inset
divider with 6px above and below, like the divider above the global items,
and drop the extra gap between Apps and Settings on the collapsed rail.
A Responses-API upstream can reject a request before processing it and
still terminate the SSE stream with response.incomplete +
incomplete_details.reason=max_output_tokens while reporting zero input
and output usage (observed with the ChatGPT Codex backend when it
refuses a tool schema, e.g. a `pattern` regex with nested quantifiers).
The converter mapped that terminal to stop_reason "max_tokens", so
clients reported an output-token limit that was never hit.
Genuine max-output-token truncation always reports non-zero usage. When
an incomplete terminal reports zero input/output usage and produced no
content, emit an api_error event carrying the rejection details instead
of a fabricated max_tokens stop reason.
Covered emission paths:
- the empty-output fast path and the compatible-gateway single-JSON
path, both via responses_json_to_anthropic_sse
- the regular terminal handling in the streaming generator
Real truncation (non-zero usage) keeps stop_reason max_tokens, and the
non-stream codex_oauth aggregation propagates the error event as a
proxy error via anthropic_sse_to_message_value.
Lay out the sidebar and content area at their final widths once, then draw the moving edge with a cover panel that slides on a 200 ms transform animation, so the toggle runs at the display's refresh rate instead of WKWebView's ~60fps main-thread rate. Interrupting a toggle continues from the current edge; window-resize auto-collapse and prefers-reduced-motion skip the animation. Drop the width transition and the content-width pinning from #7845. <main> gets an opaque bg-app and isolate so its z-indexed descendants (sticky matrix headers, FullScreenPanel) stay under the cover while collapsing.
Newer Gemini CLI releases record chats as tmp/<hash>/chats/session-*.jsonl and migrate legacy .json files to .jsonl on resume, so the Sessions page showed no Gemini sessions and Gemini usage sync imported nothing from them.
- Replay JSONL like Gemini CLI's loadConversationRecord (in-place upsert by id, $set and $set.messages checkpoints, $rewindTo), with an id index so replay stays linear
- Use the shared parser for the session list, message loader, full-content refs and usage sync
- Hide a legacy .json that sits beside its migrated .jsonl and delete both together, removing the legacy file first
- Skip CLI-injected <session_context>/<hook_context> and slash commands when deriving titles, and mark injected context in the message view
Fixes#7861
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.
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.
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.
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.
88API (88api.ai), a multi-model API platform run by a Hong Kong company,
joins as a sponsor, placed directly below FluxA in the sponsor order
(preset files and the four README sponsor tables stay in sync; Gemini
and Grok Build have no FluxA preset, so it follows SubRouter there).
- Presets for Claude Code, Claude Desktop, Codex, Gemini CLI, Grok
Build, OpenCode, OpenClaw, Hermes and Pi on api.88api.ai, with
88api.ai as a second endpoint candidate (same new-api deployment, and
the host the vendor's own setup scripts default to). Anthropic- and
Gemini-style clients use the bare host; Codex, Grok Build and the
OpenCode Anthropic SDK use /v1.
- Model lineup is Claude Opus 5.5, Sonnet 5.5, Haiku 4.5 and Fable 5.1,
defaulting to Opus 5.5. Claude Desktop maps the Sonnet/Opus/Haiku role
routes to those models via mappedRoutes. Codex defaults to
gpt-5.6-sol (vendor config script), Gemini to gemini-3.8-flash.
- Adds Claude Sonnet 5.5 to the Pi model catalog (1M context, 128K
output, image input, reasoning; per models.dev) so the Pi preset can
reference it.
- No OpenClaw cost fields: prices vary by the vendor's channel groups.
- Referral link only in apiKeyUrl and README hrefs.
- 128x128 raster icon (square-padded from the vendor logo), metadata
entry, four-locale partnerPromotion copy and a 1200px banner.
- Claude preset rows in the selector test go from 71 to 72.
With more than one saved reset, the reset row opens a dropdown listing
every expiry date (same-day ones grouped, the ones expiring within three
days emphasized):
- Auth Center: the whole reset row is the trigger.
- Provider card: only the "Resets N" segment of the merged second row is
the trigger; the rest of the column still refreshes on click, and the
refresh icon only shows while hovering the refresh area.
Clicking the quota on a provider card already re-queried it, but the
only feedback was a brief dim, so it looked like nothing happened.
- Hovering or focusing the column reveals a small refresh icon left of
the first row (the column is right-aligned, so text never shifts).
- A click spins it until the query settles, for at least 600 ms, then
shows a check on success or a red cross on failure for one second.
Background polling and focus refetches still only dim, so several
cards never spin at once.
- Failure means the column turned into "quota unavailable" or the
refetch itself errored. The second case covers transient failures
inside the keep-last-good window, where the old quota stays on screen
and a check would be a lie; UsageFooter now returns the refetch
promise instead of discarding it. A retry that fails again on an
already failed column also gets the cross.
- After a click the icon stays hidden until the pointer leaves or focus
moves, so it does not pop back right after the check fades. The
button uses aria-disabled instead of disabled while busy, since
Chromium may not deliver mouseleave to a disabled button.
Codex now lets Go/Plus/Pro/Business subscribers bank rate-limit resets
and redeem them later. Query wham/rate-limit-reset-credits alongside
wham/usage (in parallel, best-effort: any failure just leaves the count
out and never fails the quota itself) and count the resets that are
still available and unexpired, the same way CodexBar does, instead of
trusting available_count.
The count shows wherever ChatGPT quota does, since both the Codex CLI
and the managed-account paths go through query_codex_quota:
- Provider card: joins the weekly tier on the second row
("Wk 64% · Resets 1"), reusing the existing merged-row rule.
- Expanded quota and Auth Center: its own row, with the earliest
expiry written where the bar would be.
- Hidden at zero; emphasized when the earliest reset expires within
three days. Resets that expire after the query are dropped at render.
SubscriptionQuota gains an optional resetCredits field (serde default),
so cached snapshots from older builds still deserialize.
Extract the request-log pager into a shared TablePagination and add useClientPagination for tables that load all rows at once. Pages hold 20 rows, reset when the range or filters change, and clamp when a refresh returns fewer rows.
Reapplies c932436dc, reverted in 6d077f2a1.
The Gemini preset list ended with a "自定义" entry from the original
Gemini integration. The v7 picker already renders its own built-in
custom row, and presetGroup files category "custom" under third-party,
so the add dialog showed two custom options, the extra one without an
icon.
The built-in row starts from GEMINI_DEFAULT_CONFIG, which holds the same
empty base URL and gemini-3.6-flash model, so nothing is lost. The entry
was last in the array, so the index-based `gemini-${index}` ids of the
other presets do not shift.
These presets either had no icon or, for Together AI in Hermes,
referenced a `together` key that did not exist, so the picker fell back
to the first letter without any error.
- TheRouter: the official logo-black.svg inlined with currentColor
strokes, so it follows the theme like other monochrome icons.
- Together AI: the LobeHub together-color SVG, inlined.
- DMXAPI, BaiLing: the 256px frame from each official favicon.ico,
added as URL imports since both are gradients.
- Astron (iFlytek MaaS): the official favicon; only 32px is published.
Each icon gets a metadata entry, and every preset of these providers
across all apps now sets `icon`. All 738 presets now resolve an icon.
The v7 pass gave the name and endpoint fields the required asterisk, but
ApiKeySection never forwarded `required` to ApiKeyInput, so the API Key
field was never marked even though saving warns when it is empty.
ApiKeySection now takes an opt-in `required` and drops it for official,
cloud_provider and disabled fields. Only the forms whose save path checks
the key pass it: Claude Code, Codex (also used by Grok Build), Gemini and
Claude Desktop. Additive apps don't validate the key and stay unmarked.
ApiKeyInput now sets only aria-required, like EndpointField: a native
`required` would let browser validation block the submit before the
"save anyway?" confirmation for an empty key could appear.
v7 sorts presets by display name with no pinning, and no longer reads
primePartner. The sponsor-block comments still claimed file order equals
in-app display order, which would mislead anyone adding a preset.
The v7 visual pass dropped the partner promotion box together with the
promoted ordering. Ordering stays by name, but the promotion text comes
back as the API Key field's hint (caption, secondary text, no box), shown
only when the "Get API Key" link is shown, as before.
ApiKeyInput gains a generic `hint` prop for text below the input.
The model picker's description column repeated the provider name that
the display name already carries, in two languages, on every row:
"经 CC Switch 路由到 X (Routed by CC Switch to X)".
Replace it with the upstream model id and its context window, e.g.
"kimi-k3 · 256K". The display name can be user-chosen and the published
id carries the reserved prefix, so this is the only place the real
upstream id shows up. Claude 1M models show 1M rather than the row's
global window; an unknown window leaves just the model id.
The reserved "ccs-" id prefix is unchanged.
Move the collapsed state and the Cmd/Ctrl+\ shortcut from App into Sidebar so a toggle only re-renders the sidebar. During the 200 ms width transition, pin the content area's children to the collapsed-sidebar width so ResizeObserver-driven content (usage chart, virtual lists, tab indicators) lays out once at the end instead of every frame; skipped under prefers-reduced-motion. useSlidingIndicator no longer sets state when the measured rect is unchanged.
* perf(sessions): defer the find highlight term like the hit search
The highlight term followed every keystroke, re-parsing the Markdown of
the whole page in long sessions. It now uses the same deferred value as
the hit computation.
* fix(sessions): include legacy OpenCode part files in the cache fingerprint
Legacy storage keeps message bodies under storage/part/{messageID}/ and
streaming only rewrites those files, so the fingerprint of the message
directory alone served stale transcripts.
* fix(sessions): show rejected Codex apply_patch calls as failed
apply_patch outputs like "apply_patch verification failed: ..." or
"patch rejected by user" carry no exit code and were shown as
successes. They are now errors and report no added/removed counts,
since nothing was written. Patch counting also stops at *** End Patch
and no longer counts '-' lines inside added files.
The test replaced a marker in already-serialized JSON with the raw sidecar
path; on Windows its backslashes became invalid escapes, the line failed to
parse and the transcript came back one message short.
With every cached image in use, a newly loaded entry (refs 0) was the only
evictable one and got revoked before its waiting hook acquired it, leaving
the image stuck on the loading placeholder; two loads finishing together
could also evict each other. Count pending waiters per image and skip
images that still have one when evicting.
When several function calls emit empty end events before their delayed
argument deltas, the first delta used to flush every held event, sending
the other calls on without arguments. Argument deltas from any call (held
or not) are now forwarded without releasing anything; held end events are
filled in and released together on the next non-delta event.
Switching Claude Desktop from a model-mapping card back to a direct card
left the local routing service running until some later check. The main
switch, tray click and project apply now record whether the outgoing card
was a mapping card and, if it was and nothing else uses the service, stop
it. Switches that never involved a mapping card leave the service alone,
so a service the user started by hand in settings keeps running.
The heatmap mixed --chart-1 (#0891b2) at 30/50/75/100%, so the darkest
cell was a mid-tone and the whole map read pale (3.68:1 against the card).
Mixing a darker cyan at every level fixed the top but washed out the low
levels.
Move the ramp into --heat-1..4 tokens defined per theme. Light keeps the
lower levels on chart-1 (32%, 62%, solid) and switches only the top level
to #0e7490, same hue one step darker (5.36:1). Dark keeps its previous
ramp, since brighter already means more there. Charts and quota meters
still use --chart-1.
The light --action-bg was #f97315, one unit off the #f97316 the old
add-provider button used via bg-orange-500. Use #f97316 so the accent
matches the pre-redesign orange exactly, recompute the light --ring HSL
from it, and update the design-system doc to the same value.
Open the bottom-bar tooltips upward and the expanded collapse-button tooltip to the left, so the Apps tooltip no longer covers the Settings button. Add an optional disableHoverableContent prop to HoverTip (closes on trigger leave and ignores pointer events) and enable it for all sidebar tooltips.
Keep the provider name from overlapping the failover switch and route settings button. The lead text still truncates first; the value only truncates when it alone exceeds the row, and a title shows the full text on hover.
The divider under the brand row sat about 11px below the page header's
bottom border. Two near-parallel lines that miss each other read as a
misalignment, and the divider only existed in the expanded macOS layout.
Separate the brand from the navigation with spacing instead.
The access token lasts about six hours, not seven days, and the Grok CLI
renews it from the stored refresh_token on its next run. The module doc
now matches the refresh-pending handling from add36d6df.