Set the default Claude Code auto-compact window to 262144 for the
Kimi For Coding provider preset, matching the official Kimi docs:
https://www.kimi.com/code/docs/en/third-party-tools/other-coding-agents.html
Use templateValues so users can customize the value (e.g. for future
models or performance tuning) while keeping 262144 as the default.
Query coding-plan / agent-plan usage for Volcengine Ark via the
control-plane OpenAPI (open.volcengineapi.com), which requires
account-level AccessKey signing rather than the inference API key.
- Implement Volcengine Signature V4 (an AWS SigV4 variant: fixed
canonical header order, "HMAC-SHA256" algorithm without the AWS4
prefix, scope ending in "request", service "ark") in
services/coding_plan.rs
- Auto-detect the plan by probing GetAFPUsage (Agent Plan, AFP
five-hour/weekly/monthly quotas) first, falling back to
GetCodingPlanUsage (Coding Plan, session/weekly/monthly percentages);
a single credential covers whichever plan the account holds
- Parse the real response shape: the window label lives in the `Level`
field; guard ResetTimestamp <= 0 (session returns -1)
- Store account-level credentials on UsageScript (accessKeyId /
secretAccessKey), threaded through both the test command and the
TOKEN_PLAN auto-refresh path
- Add an independent AK/SK input block with a detailed hint pointing to
the Volcengine console -> API Access Keys
- Add the `monthly` tier label (frontend TIER_I18N_KEYS + tray
TIER_LABEL_GROUPS + subscription service)
- Sync zh / en / ja / zh-TW locales and the usage-query docs
- Reduce title->tabs and tabs->card vertical gaps from 24px to 12px
- Add an always-visible footer hint guiding users to fill the fields
below after choosing a preset (pinned footer, never scrolls off)
- FullScreenPanel: add optional contentClassName prop to scope content
padding overrides without affecting the other panels that share it
- i18n: add provider.addFooterHint in zh/en/ja/zh-TW
The OpenCode and OpenClaw presets were labeled "Kimi K2.7 Code", unlike
the plain "Kimi" used by the other apps. Rename the preset (and the
OpenCode provider display name) to "Kimi" for consistency; the model
label stays "Kimi K2.7 Code" since it describes the actual model.
In the default (Original) sort mode, float official-category presets to
the very top, followed by prime partners (Kimi), then the rest in their
original array order. Implemented as a three-way partition+concat so each
group keeps its internal order and no preset is listed twice.
Replace the inline Kimi placeholder mark with the vendor's refreshed
logo. The K glyph uses `currentColor` so it follows the theme text color
(dark in light mode, white in dark mode), while the brand accent dot is
pinned to the new #1783FF. Align the metadata fallback color accordingly.
The vendor renamed its domain, API endpoints, and trademark from CTok to
ETok. Update all user-facing branding accordingly:
- Presets (claude, claude-desktop, codex, gemini, hermes, openclaw,
opencode): ctok.ai -> etok.ai, api.ctok.ai -> api.etok.ai, internal
provider id ctok -> etok, display name CTok.ai -> ETok.ai
- Icons: replace inline placeholder SVG with the new ETok brand PNG,
moved from `icons` to `iconUrls`; key ctok -> etok; banner logo
ctok.png -> etok.png
- README (en/zh/ja/de) partner banner: new logo, links, and copy
- i18n (en/ja/zh/zh-TW): partnerPromotion key ctok -> etok
Keep "ctok" in the Codex history-migration whitelist as a legacy id and
add "etok", so existing users' local session history stays correctly
bucketed after the rename.
Introduce an optional primePartner flag on all provider preset interfaces.
Regular partners keep the gold star badge; prime partners render a solid
gold heart (no badge frame). Mark the first-party Moonshot Kimi presets
(Kimi / Kimi For Coding / Kimi K2.7 Code) across claude, codex, claude
desktop, opencode, openclaw and hermes as prime partners.
* feat(usage): support importing model pricing from models.dev
Add an "Import from models.dev" button to the Add Pricing panel that
fetches https://models.dev/api.json, lists priced models sorted by
release date (newest 50 by default, full-text search across ~4800),
and bulk-imports the selected entries through the same
update_model_pricing command as manual entry.
- Normalize imported model IDs to match the backend's
clean_model_id_for_pricing rules (strip vendor prefix, lowercase,
truncate ':' suffix, map '@' to '-', drop the [1m] marker) so the
stored rows actually match cost-attribution lookups
- Dedupe selections that collapse to the same model_id and report
skipped duplicates in the success toast
- Invalidate usage queries on settled (not just success) so partial
import failures still refresh the pricing list
- Keep ESC inside the picker's search input from closing the dialog
and discarding the selection
- Add i18n keys for zh/en/zh-TW/ja and unit tests for the
normalization, price formatting and flattening logic
Fixes#4017
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(usage): match scoped cost backfill against raw model aliases
The scoped backfill selected zero-cost rows via exact SQL string match,
but log columns store raw model strings (route prefixes, :free variants,
date suffixes), so alias rows were skipped until the next full backfill
on startup. Filter rows in Rust with the same model_pricing_candidates
normalization used by the pricing lookup; pricing decision logic is
untouched. Pre-existing gap from schema v11, surfaced by bulk import.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(usage): restrict models.dev pricing import to a single model
Each update_model_pricing call triggers a backfill pass that loads
every zero-cost usage row before filtering by model, so bulk-importing
N entries scaled as selectedModels x allZeroCostLogs full scans.
Re-importing pricing is rare, so drop the batch path instead of
optimizing it: the picker is now single-select, one import runs exactly
one update_model_pricing call and one backfill pass. This also removes
the normalized-ID dedup logic and the useImportModelPricing hook in
favor of the existing useUpdateModelPricing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(provider-preset-selector): after searching presets, it is impossible to select search results.
feat(provider-preset-selector): add keyboard shortcut for search input and improve focus handling.
* fix(provider-list): prevent keydown event from triggering when default is prevented
* fix(provider-preset): keep preset clickable after search and restore keyboard UX
- ProviderList: scope the typing guard to the Ctrl/Cmd+F branch so Escape
still closes the search panel (the top-level early return swallowed it);
reuse isTextEditableTarget instead of re-implementing the check.
- ProviderPresetSelector: drop the rAF select() that raced with typing and
ate the first character (gateway -> ateway), and restore the input
autoFocus for the open-by-click path; refocus via rAF when Ctrl/Cmd+F is
pressed while the box is already open so focus returns to the input.
- Add a regression test for the re-focus-on-shortcut behavior.
---------
Co-authored-by: Jason <farion1231@gmail.com>
* feat(session-manager): show source file name in session detail header
Display the session log file name (with full path on hover) alongside
the project directory, so users can locate and copy the underlying
JSONL file directly from the UI.
* fix(session-manager): truncate long source file name in detail header
Long, space-less JSONL basenames (e.g. Codex rollout files at ~70 chars)
overflowed the flex meta row and bled into the action-button area on
narrow windows. Mirror the sibling project-dir span by capping the
filename at max-w-[200px] with truncation; the full path stays available
via the hover tooltip and click-to-copy.
---------
Co-authored-by: Jason <farion1231@gmail.com>
- Replace activity/agentplan links with the new codingplan invite URL
(ac=MMAP8JTTCAQ2, rc=6J6FV5N2) across all 6 app presets
- Refresh partnerPromotion copy in all 4 locales: two-month 75% off plus
invite code 6J6FV5N2; correct product name from Agent Plan to Coding Plan
- README: swap the CN-mainland redirect link in EN/DE, refresh the ZH promo
copy, and add the redirect link to the JA BytePlus entry
Remove the gold star badge and the partner promotion banner for MiniMax by
deleting the isPartner flag from all 12 MiniMax presets (cn + en) across
claude, claude-desktop, codex, opencode, openclaw, and hermes.
Both the preset-selector star (gated on isPartner) and the API-key promotion
banner (gated on isPartner && partnerPromotionKey) disappear as a result.
The partnerPromotionKey and the minimax_cn/minimax_en i18n copy are kept
dormant so the partnership can be re-enabled with a single line if needed.
MiniMax stays as a regular cn_official provider, keeping its icon and theme.
Settings uses Radix tabs, which unmount inactive tab content. Every time
the About tab was reopened, AboutSection remounted and its mount effect
re-ran all six tool version probes (a `--version` subprocess plus an
npm/github/pypi request each) -- wasteful, since versions rarely change
within a session and a manual Refresh already exists.
Add a module-scoped cache (lives for the app session, survives
unmount/remount) with a 10-minute TTL:
- on remount within the TTL the cached results are reused, skipping all
probes; state is lazily initialized from the cache so the first paint
shows the values with no loading flash
- a stale cache shows the old values immediately and revalidates per
tool in the background (stale-while-revalidate)
- the Refresh button forces a re-probe; single-tool refreshes (shell
change / post-update) update cached data without resetting the TTL
- cold-cache entries start at at=0 (a "not yet fully loaded" sentinel)
so a partial cache left by a mid-probe tab switch is treated as stale
and re-fetched rather than served as if complete; the real timestamp
is only stamped once a full load finishes
The app's own version is cached too, purely to avoid a loading flash on
remount.
getVersion() is a local, millisecond call but was awaited together with
loadAllToolVersions() in a single Promise.all, so setVersion and
setIsLoadingVersion only fired after all six tool checks finished. The
version badge under the app icon therefore waited for the whole batch.
Split the mount effect into two independent chains: loadAppVersion sets
the app version (and clears its loading flag) the moment getVersion()
resolves, while loadAllToolVersions runs its own progressive fan-out.
The two no longer block each other.
Fan out the six tool version probes concurrently instead of awaiting
one sequential batch, so each tool card updates the moment its own
check finishes rather than waiting for the slowest one.
- loadAllToolVersions now calls refreshToolVersions per tool via
Promise.all, reusing the existing per-name merge and per-tool
loading flags
- card loading derives from per-tool state
(loadingTools[t] || (isLoadingTools && !resolved)) so a resolved
card leaves loading independently while the batch is still in flight
- WSL shell/flag selects reuse the per-card loading flag, matching the
install/update buttons' early-enable behavior
This also drops total wall time from the sum of six sequential probes
to the slowest single probe. get_single_tool_version_impl is already
isolated and read-only (shared HTTP client, spawn_blocking subprocess,
no shared state), so concurrent probes are safe.
- useUsageQuery: retry once + keep-last-good — show the last successful
result for up to 10min when a query fails transiently (network/timeout/
HTTP 5xx), so a single blip no longer flips the card to red. Deterministic
failures (auth, empty key, unknown provider, 4xx) surface immediately and
clear the snapshot so a stale quota can't resurface after credentials change.
- bump native balance/coding-plan/subscription request timeouts 10s -> 15s
for slow cross-border endpoints.
- coding_plan: return explicit errors ("API key is empty" / "Unknown coding
plan provider") instead of a blank failure, mirroring balance.
- add unit tests for keep-last-good and transient/deterministic classification.
Display the Fable 5 Verified banner to the right of the app name and version block on the settings About page, marking this as a special build. Center the version badge under the app name so the two rows share a common axis.
The app-side asset lives at src/assets/fable5-verified.png (imported via the @ alias and bundled by Vite); the original source banner is kept under assets/partners/banners/ alongside the other partner banners.
Official providers (Claude/Codex/Gemini/Claude Desktop) use OAuth with an intentionally empty base_url and connect via the client's default endpoint, so cc-switch has no reliable reachability target. Probing a guessed endpoint either hits the wrong target or returns a meaningless green light. Hide the connectivity button for category === 'official'; reachability stays available for copilot/codex-oauth/third-party/custom providers, which is where the old real-request probe produced false negatives. Revert the official base_url fallback added earlier — resolve_base_url is back to extract-or-error.
The 1500ms degraded threshold was too strict; normal ~1s probe latencies showed as 'slow'. Restore the original 6000ms scale (default + config panel + per-provider range). Keep the reachability-appropriate 8s timeout / 1 retry.
The provider panel health check sent a real streaming model request, which many third-party providers block (401/403/WAF), causing false negatives while only stable official endpoints passed. Replace it with a lightweight reachability probe: GET the provider base_url and treat any HTTP response (200/4xx/5xx) as reachable; only DNS/connect/TLS/timeout count as failure. Latency is the probe's TTFB.
Backend (services/stream_check.rs): rewrite ~2200 -> ~350 lines, dropping real-request building, format conversion, auth and API-path resolution while keeping per-app base_url extraction. Defaults: 8s timeout, 1 retry, 1500ms degraded threshold.
Failover invariant: the reachability check must never reset the circuit breaker (reachable != usable; a 403 host is reachable but broken for real traffic). Remove the resetCircuitBreaker call from useStreamCheck; failover failure detection stays driven solely by real proxy traffic (forwarder/circuit_breaker untouched). useResetCircuitBreaker is kept dormant for a future manual-recovery entry.
Open the check to all providers: drop the official/copilot/codex-oauth/third-party gating and the 'sends a real request' confirm dialog. For official providers whose base_url is intentionally empty, fall back to the endpoint the client actually uses (Claude -> api.anthropic.com, Codex -> chatgpt.com/backend-api/codex, Gemini -> generativelanguage). Non-official providers with a missing base_url still error to avoid a false green light. Claude Desktop Official is native 1P mode (talks to claude.ai, cc-switch not in the request path, no reliable endpoint) so its button stays hidden.
Slim StreamCheckConfig and per-provider testConfig to timeout/threshold/retries (drop test model + prompt); sync zh/en/ja/zh-TW. Retain the now-unused anthropic_to_openai/anthropic_to_gemini transform utilities and their test suites.
* 调整预设供应商按钮外观与搜索框位置
1. 调整预设供应商按钮外观,显示默认图标,大小统一;
2. 调整预设供应商搜索框位置。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(provider): 新增预设按钮外观与 inline 搜索的单元测试
覆盖:
1. 所有预设按钮固定 200px 宽度,视觉对齐一致
2. preset.icon 存在时按钮内渲染 ProviderIcon
3. preset 无 icon 且无 theme.icon 时渲染占位元素保持文字对齐
4. 点击放大镜 inline 切换搜索输入框可见性,ESC 收起并清空
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(provider-preset): responsive grid layout and search polish
- Replace fixed-width preset buttons with a responsive CSS grid (auto-fill, 150px min column)
- Add a leading placeholder to the custom button so its label aligns with iconed presets
- Close the inline search box on outside click, restoring the old Popover behavior
- Span the empty-state hint across the full grid row
- Update component tests for the new layout and behaviors
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
The Kimi For Coding preset was removed (74104946) because the coding
endpoint (api.kimi.com/coding/v1) enforces a User-Agent whitelist that
rejects Codex's default codex-cli UA with 403. The provider-level custom
User-Agent feature now lets users override the UA (to claude-cli/*) under
proxy takeover, so the preset can be restored.
- Re-add the Codex Kimi For Coding preset (openai_chat, kimi-for-coding,
256K context) in the same position it was removed from.
- Enable thinking mode by default via codexChatReasoning (supportsThinking
true, thinkingParam "thinking"), mirroring the general Kimi preset since
both target the same Moonshot model family. The proxy injects
thinking:{type:enabled} when Codex requests reasoning.
- Restore the trilingual user-manual row to "Kimi / Kimi For Coding".
Note: this preset requires proxy takeover and a whitelisted custom
User-Agent to work; the default codex-cli UA still gets 403.
Update the Codex chat preset test's Kimi expectation (kimi-k2.6 -> kimi-k2.7-code) after the Kimi K2.7 upgrade, update the Claude Desktop form test for the four-tier (Sonnet/Opus/Fable/Haiku) routes, and reformat UsageDateRangePicker imports (prettier).
Add kimi-k2.7-code pricing seed (in $0.95 / out $4.00 / cache-read $0.19
per 1M tokens, 256K context) and point all six official Moonshot Kimi
presets (Claude Code, Codex, Claude Desktop, Hermes, OpenCode, OpenClaw)
at the new model. Rename the version-tagged OpenCode/OpenClaw presets to
"Kimi K2.7 Code" and correct the OpenClaw context window to 262144.
The seed is applied via the idempotent INSERT OR IGNORE path that runs on
every startup, so existing users pick up the new pricing without a schema
migration. Kimi For Coding and Nvidia presets are intentionally untouched.
The preset search text also included websiteUrl and the shared category label, producing imprecise matches: a single category term matched the whole group, and URL fragments like "com"/"api" matched nearly everything. Restrict the search text to the display name and raw name; category labels are still used for rendering.
PopoverContent and TooltipContent used z-50, below FullScreenPanel's z-[60] opaque overlay. Popovers such as the provider preset search therefore rendered behind the panel and looked unresponsive on click. Bump both to z-[100], matching the select dropdown: above fullscreen panels, below modal dialogs (z-[110]).
The menu display name and request model columns used mismatched example
brands (DeepSeek vs Kimi), implying a display name maps to an unrelated
request model. Derive both placeholders from the row role so each row
stays brand-consistent, and route the lightweight Haiku tier to a flash
example (deepseek-v4-flash) while other tiers use deepseek-v4-pro.
- Wire claude-fable-5 as a fourth tier on both proxy paths, with a
fable -> opus -> default fallback mirroring the official downgrade.
- Whitelist the fable- prefix for the Desktop 1.12603.1+ validator.
- Clarify fallbackModelHint (zh/en/ja/zh-TW): a blank tier on
third-party endpoints forwards the literal model name and 404s.
Refs #3980, #4026, #4049.
- Reduce all four filter controls to w-[100px] h-9
- Add ChevronDown icon to date picker trigger for consistency
- Suppress focus border highlight on select triggers after close
- Enable dialog gains a checkbox (default off) to migrate existing
official sessions from the built-in "openai" bucket into the shared
"custom" bucket, with per-generation backups; failed migrations retry
at startup
- Disable dialog offers a precise restore driven by the backup ledger:
only sessions recorded as "openai" in backups are flipped back, and
sessions created while the toggle was on are never touched
- Completion marker and backup generations are bound to the canonical
Codex config dir; migrate/restore serialize on an op lock and the
marker is written conditionally inside the settings write lock
- save_settings rolls back the toggle and fails the save when the live
rewrite fails; migration additionally requires the live config to
actually route to the shared bucket (skips with live_not_unified so
refused injection or proxy takeover can't split history)
- Restore refuses to run while the toggle is (re-)enabled and reports
nothing_to_restore instead of a zero-count success; local migration
markers are now backend-owned in merge_settings_for_save so stale
frontend payloads can't resurrect them
- Settings autosave reverts optimistic form state on failure so a
failed toggle change can't be replayed by an unrelated save
- ConfirmDialog supports an optional checkbox; all four locales updated
Codex buckets resume history by the model_provider id recorded in each
session: official runs (no key, built-in "openai") and cc-switch
third-party runs (shared "custom") are mutually invisible in the resume
picker. Add an opt-in setting that runs official providers under the
shared "custom" id so future official sessions land in the same history
bucket as third-party ones. Forward-only by design: existing sessions
are not migrated.
When enabled, official live config.toml gets model_provider = "custom"
plus a [model_providers.custom] entry that mirrors the built-in openai
provider (requires_openai_auth routes auth to the ChatGPT login in
auth.json, name "OpenAI" keeps is_openai() feature gates, explicit
supports_websockets/wire_api restore built-in defaults). auth.json is
untouched.
Key invariants:
- Injection lives only in the live config: switch-away backfill strips
the exact injected shape, so stored provider configs stay clean and
turning the toggle off fully reverts on the next write.
- Toggle changes apply immediately via a takeover-aware reapply: when
the proxy owns the live config (backup/placeholder present), only the
live backup is updated, mirroring the provider-switch path.
- The takeover backup path runs the same injection so a takeover
release restores a config that still carries the unified routing.
- Injection refuses to activate a foreign [model_providers.custom]
table (e.g. stale entry with a third-party base_url) to avoid routing
ChatGPT OAuth traffic to an unknown backend.
The toggle lives under Settings → Codex App Enhancements; the
description warns that resuming old sessions across providers may fail
because encrypted_content reasoning only decrypts on the backend that
created it (upstream treats cross-provider resume as unsupported).
- LemonData: delete the provider preset from all apps (claude, claude-desktop,
codex, gemini, hermes, opencode, openclaw), remove partner promotion copy
(zh/en/ja/zh-TW), icon index/metadata entries, sponsor ads in all READMEs,
and the logo/icon PNG assets.
- SudoCode: drop the isPartner flag and partnerPromotionKey across all app
presets and remove the now-orphaned partner promotion copy; it stays as a
regular third_party provider, keeping its icon.
Replace the click-to-cycle refresh button with a Select matching the
source/model filters. The "off" option now shows a localized label
(zh/en/ja/zh-TW) instead of the cryptic "--", and changing the interval
still invalidates all usage queries for an immediate refresh.
Align the top-bar controls into two width groups: source and model
selects at 120px, refresh select and date range trigger at 150px. The
date range button moves from auto width to fixed, so its long custom
range label gets truncate + hover title, and the calendar icon is
shrink-proofed.
The provider/model filters only lived inside the request-log table, so
there was no way to see "how much did app X spend on source Y" across
the whole dashboard. Promote them to the top bar next to the app
filter, applying globally to the hero summary, trend chart, request
logs, and both stats tabs.
Backend: the five stats queries (summary, summary-by-app, trends,
provider stats, model stats) accept optional provider_name/model
filters, applied to both the detail and daily-rollup branches (the
rollup PK already carries provider_id/model/pricing_model). Sources
match by exact display name via provider_name_coalesce, so session
placeholder rows like "Claude (Session)" are selectable; models match
by effective pricing model (pricing_model falling back to model), the
same grouping key the model-stats tab uses. Request-log filtering
switches from LIKE to these exact semantics.
Frontend: two truncating dropdowns list only sources/models that have
data in the current range, with the model list cascading from the
selected source. Dynamic option values are prefix-encoded so a source
literally named "all" cannot collide with the sentinel, query keys
fall back to null instead of "all", and the option queries follow the
dashboard refresh interval (otherwise their default 30s polling drags
same-key stats queries along even with refresh disabled). The
request-log filter bar keeps only the log-specific status-code select.
Labels read "sources" rather than "providers" because direct-connect
session buckets sit alongside real providers. i18n updated across
zh/en/ja/zh-TW.
The text tabs (All / Claude Code / Codex / Gemini / OpenCode) wrapped
awkwardly in narrow windows. Render app icons via ProviderIcon instead,
mirroring AppSwitcher's icon mapping (codex reuses the openai icon),
with a LayoutGrid icon for "all". Localized labels are kept as
title/aria-label on each button.
The Desktop gateway's proxy traffic is still recorded under its own
app_type for route-takeover billing audit (the request detail panel
shows the real value), but the dashboard now folds it into `claude`
for display. A standalone "Claude Desktop" bucket only ever showed a
partial number: Desktop chat usage never passes through the proxy and
has no scannable local source, while its Code-tab sessions are just the
embedded Claude Code runtime writing into the shared ~/.claude/projects
tree — so a separate bucket misled users into reading it as Desktop's
full usage.
Backend: new `folded_app_type_sql` helper wraps the app_type column in
every dashboard read path (10 filter sites + the by-app projection) so
`= 'claude'` also matches claude-desktop and GROUP BY merges the two,
without changing bound-param counts. Dedup matching and provider-limit
checks keep exact comparison; get_request_logs folds only the WHERE
filter and keeps the raw app_type in its row projection.
Frontend: drop claude-desktop from the dashboard AppType/KNOWN_APP_TYPES
filter list, the UsageHero theme, and the now-dead appFilter locale key
in all four languages (the managed-app apps.claude-desktop label stays).
Adds test_claude_desktop_folds_into_claude_for_display.
* fix(updater): drive download/install/restart from backend to avoid hang
The 3.16/3.16.1 update flow was frontend-driven: downloadAndInstall()
then relaunchApp(). relaunch() routed through AppHandle::restart(), and
the old WebView had to keep running JS after the .app bundle was already
swapped — an unstable window that could hang the update or leave the old
version running until a manual restart.
Move the whole download -> install -> cleanup -> restart chain into a new
backend command install_update_and_restart, so it no longer depends on the
old WebView running JS after the bundle is swapped.
Platform-aware install ordering (install() behaves differently per OS):
- Windows: install() launches the external installer and exits the process
internally, so cleanup + single-instance destroy must run before install.
Surface a recovery hint on failure since the proxy may already be stopped.
- macOS/Linux: install() returns, so install first then cleanup — an install
failure no longer wrongly stops the proxy / reverts takeover.
Eliminate the restart vs single-instance race: restart_process() destroys
the single-instance lock (remove socket on macOS, ReleaseMutex on Windows)
before tauri::process::restart(), so the freshly spawned process can't
connect to the old listener and exit itself.
Also remove the now-dead frontend update plumbing (relaunchApp,
UpdateHandle, mapUpdateHandle) and surface backend errors in the toast.
Adapted from the original af4271f4 while rebasing onto #4069: the
ExitRequested handler changes were dropped entirely — the classifier from
#4069 already routes RESTART_EXIT_CODE to Tauri's default restart flow,
and the original should_restart branch (prevent_exit + async cleanup)
would have reintroduced the window-state deadlock that #4069 fixed.
install_update_and_restart bypasses ExitRequested entirely, so the two
fixes compose cleanly.
* fix(updater): clear tray icon on the direct-restart update path
restart_process re-execs via tauri::process::restart (spawn + exit(0)),
which skips Tauri's internal cleanup_before_exit and all RunEvent::Exit
plugin hooks. Window state, proxy/live restore and the single-instance
lock were already compensated explicitly; the tray icon was not.
On macOS/Linux the OS drops the status item when the process dies, so
the gap there was cosmetic at most. The real residue risk is the
Windows branch, which never reaches restart_process at all:
update.install() exits the process inside the updater plugin
(std::process::exit(0)), bypassing TrayIcon::drop — no NIM_DELETE is
sent and a stale icon lingers in the shell until hovered, the same
failure remove_tray_icon_before_exit was originally added for on the
quit path.
Call remove_tray_icon_before_exit (set_visible(false), proxied to the
main thread via run_item_main_thread) in restart_process and before
the Windows install. Deliberately not AppHandle::cleanup_before_exit():
it drops tray icons on the calling thread, which is not safe off the
main thread on macOS (NSStatusItem).
Add Unity2.ai, a high-performance AI API relay partner, as a preset for
Claude, Codex, Gemini, OpenCode, OpenClaw, Claude Desktop, and Hermes.
Each preset carries the referral signup link as apiKeyUrl.
- Register the unity2 icon via iconUrls (PNG URL import) + metadata
- Add partnerPromotion copy in zh/en/ja/zh-TW; backfill the missing
zh-TW ccsub entry
- List Unity2.ai in the sponsor section of all README locales
- Codex uses the bare base URL (gateway exposes /responses at root);
OpenCode/OpenClaw/Hermes use the /v1 chat-completions endpoint with
gpt-5.5 as the only preset model
- Trim CCSub OpenCode/OpenClaw/Hermes model lists to gpt-5.5 to match
- Normalize unity2/ccsub banners to the standard 2.41 aspect ratio
- Fold local routing toggle, model mapping, reasoning overrides and custom
User-Agent into a single collapsible advanced section, mirroring the
Claude form (auto-expands when UA is set or local routing is enabled)
- Custom User-Agent becomes configurable for native Responses providers;
it was previously reachable only when openai_chat routing was on
- Collapsed hint names local routing as the entry point for Chat
Completions / non-GPT providers
- Backfill all missing codexConfig keys in zh-TW locale
Polish the provider-level User-Agent override UI on the Claude and Codex forms.
- Add a shared CustomUserAgentField (label + input + preset dropdown + live
validation) so both forms stay in sync.
- Provide curated UA presets (Claude Code / Kilo Code families that pass
coding-plan UA whitelists per #3671); the first is Claude Code's real
`claude-cli/x (external, cli)` format. Whitelists gate on the name prefix,
not the version, so static values stay valid across upgrades.
- Expose presets via a dropdown to the right of the input (z-[200] so it
renders above the dialog layers) instead of inline chips.
- Move the field into the existing advanced/reasoning collapsibles.
- userAgent.ts mirrors the backend byte rule (reject only control chars;
non-ASCII is allowed) for a non-blocking inline hint.
- i18n for all four locales (zh/en/ja/zh-TW).
Extract a shared `parse_custom_user_agent` helper in provider.rs returning
`Result<Option<HeaderValue>>`, and reuse it in the forwarder, stream check,
and model fetch paths so detection, forwarding, and model listing all apply
the same provider-level User-Agent. Previously only the forwarder honored it,
so stream check could fail (or model listing 403) on UA-gated upstreams that
the proxy itself handled fine.
- stream_check injects the provider's custom UA on the claude/codex paths and
still skips the GitHub Copilot fingerprint UA.
- model_fetch service + command and the model-fetch.ts wrapper thread an
optional UA through to GET /v1/models.
- runtime callers silently ignore invalid values via `.ok().flatten()`
(no save-time block, so deeplink imports stay lenient).