1958 Commits

  • feat(session-manager): show source file name in session detail header (#4113)
    * 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>
  • fix(codex): restore cached tool call fields (#4160)
    * fix(codex): restore cached tool call fields
    
    * refactor(codex): merge duplicate enrich loops in chat history
    
    enrich_call_item_from_cache copied the fill-if-empty loop for
    reasoning_content/reasoning. The two loops are identical and key
    order is irrelevant, so fold both key sets into a single loop.
    
    Pure refactor, no behavior change; codex_chat_history tests pass.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • feat(proxy): strip effort params when thinking:disabled for DeepSeek endpoint (#4239)
    DeepSeek's Anthropic-compatible endpoint rejects requests where
    thinking.type=disabled coexists with effort parameters, returning
    HTTP 400. This breaks Claude Code 2.1.166+ sub-agents (Workflow/Dynamic
    Workflow), which hardcode thinking:disabled.
    
    Rather than overriding thinking:disabled, remove the conflicting effort
    parameters (output_config.effort / reasoning_effort) to respect Claude
    Code's intent — sub-agents don't need to display reasoning.
    
    Fixes: https://github.com/deepseek-ai/DeepSeek-V3/issues/1397
    
    Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  • docs(guides): add trilingual Codex unified session-history guide
    New zh/en/ja guide for the unified Codex session-history toggle: what opt-in migration (on enable) and ledger-based restore (on disable) actually do, why session data is never truly deleted (tag-only rewrite + automatic backups), and how to verify files on disk versus just being filed under another provider drawer. Includes a symptom reference table for the common "my sessions are gone" misunderstanding and on-disk verification commands for macOS/Linux/Windows.
    
    Link the guide as the lead item in the "Usage Guides" section of the v3.16.3 release notes (zh/en/ja).
  • chore(release): prepare v3.16.3
    - Bump version to 3.16.3 (package.json, tauri.conf.json, Cargo.toml, Cargo.lock)
    - Add CHANGELOG entry for v3.16.3 (59 commits since v3.16.2)
    - Add trilingual release notes (zh / en / ja) under docs/release-notes/
  • chore(presets): update Volcengine Ark Coding Plan invite link and promo copy
    - 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
  • chore(presets): drop isPartner flag from MiniMax presets across all apps
    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.
  • fix(updater): prevent codex self-update from breaking npm platform-dispatch installs
    Codex ships as an npm platform-dispatch package (JS launcher @openai/codex + per-platform binary optional deps like @openai/codex-darwin-arm64). The upgrade chain ran `<bin>/codex update || <bin>/npm i -g @openai/codex@latest`, which can leave codex throwing "Missing optional dependency @openai/codex-darwin-arm64":
    
    - `codex update` on an npm install is a bare `npm install -g @openai/codex` that prints success and exits 0 even when the platform binary fails to land, short-circuiting the `||` npm fallback.
    
    - The npm fallback is a no-op when version==latest and only targets the main package, so it can never re-land the missing platform binary.
    
    Fixes: (1) remove codex from prefers_official_update (Posix+Windows) so npm-managed codex no longer runs the false-success `codex update`; (2) add a runnable=false gate in installs_anchored_command emitting an uninstall+install self-heal — the only repair that re-lands the platform binary; (3) narrow by source/real to npm-managed sources (nvm/fnm/mise/homebrew, non-brew-formula) so broken brew-formula/volta/bun installs fall back to their own anchored commands instead of being mis-repaired with npm. Reuses the existing enumerate runnable signal; no new FS probing.
  • perf(about): cache tool version probes across tab switches with a TTL
    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.
  • fix(about): decouple app version badge from tool version probes
    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.
  • feat(about): render tool version checks progressively per tool
    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.
  • fix(usage): improve usage-query resilience and error surfacing
    - 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.
  • feat(about): add Fable 5 Verified banner to About section
    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.
  • fix(health-check): disable connectivity check for official providers, restore 6s degraded threshold
    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.
  • feat(health-check): replace real-LLM probe with HTTP reachability check
    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.
  • 调整预设供应商按钮外观与搜索框位置 (#4183)
    * 调整预设供应商按钮外观与搜索框位置
    
    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>
  • fix(macos): prevent duplicate provider terminal sessions (#4156)
    * fix(macos): prevent duplicate provider terminals
    
    * fix(macos): keep Ghostty fallback on AppleScript failure
    
    ---------
    
    Co-authored-by: thisTom <19346741+thisTom@users.noreply.github.com>
  • feat(codex): restore Kimi For Coding preset with thinking on by default
    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.
  • test: align preset tests with Kimi K2.7 and Fable model tiers
    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).
  • feat(presets): add GLM 5.1 context window for AtlasCloud Codex preset
    Declare the 200000-token context window for zai-org/glm-5.1, matching the other GLM 5.1 preset entries.
  • chore: remove stray nested preset file accidentally committed in #667
    cc-switch-main/src/config/universalProviderPresets.ts was an outdated duplicate of the real preset file, introduced by mistake in #667 and referenced by no code. Remove the orphaned nested directory.
  • feat: add Kimi K2.7 Code model and upgrade official Kimi presets
    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.
  • fix(providers): scope preset search to provider names only
    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.
  • fix(ui): raise popover/tooltip z-index above fullscreen panels
    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]).
  • fix(ui): make Claude Desktop model-mapping placeholders role-consistent
    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.
  • feat: add Claude Fable 5 model mapping across Claude Code and Desktop
    - 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.
  • fix(usage): compact toolbar controls and unify visual style
    - 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
  • feat(codex): add opt-in migration and ledger-based restore for unified session history
    - 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
  • feat(codex): add unified session history toggle for official providers
    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).
  • chore(presets): remove LemonData provider and demote SudoCode to regular provider
    - 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.
  • feat(usage): turn refresh interval into a select and align control widths
    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.
  • feat(usage): lift provider/model filters to dashboard-wide scope
    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.
  • feat(usage): replace app filter text labels with app icons
    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.
  • refactor(usage): fold claude-desktop into claude in the dashboard
    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(proxy): preserve Codex OAuth auth token on takeover (#3789)
    * fix(proxy): preserve Codex OAuth auth token on takeover
    
    * style(proxy): format Codex OAuth takeover fix
    
    * fix(proxy): unconditionally inject AUTH_TOKEN placeholder for codex takeover
    
    The preserve-if-exists condition left #3784 unfixed on three paths:
    hot-switch passes the provider's settings (presets carry no
    ANTHROPIC_AUTH_TOKEN key), fresh installs never had the key, and live
    configs already stripped by older releases stay stripped.
    
    - Fold the bool parameter into the policy enum as
      ManagedAccount { keep_auth_token } so every construction site
      declares intent
    - Decide via !is_github_copilot() within the managed branch so
      URL-only codex providers (no provider_type meta) are covered,
      matching the predicate family used for policy selection
    - Inject the placeholder unconditionally instead of only when the
      key pre-exists; Copilot behavior is unchanged (API_KEY only)
    - Pin the previously uncovered cases with tests: codex without a
      pre-existing key, URL-only codex, and Copilot removing a stale
      AUTH_TOKEN
    
    ---------
    
    Co-authored-by: codeasier <liuyekang@huawei.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • Add claude-mythos-5 model to schema (#4077)
    Insert a new 'claude-mythos-5' model tuple into src-tauri/src/database/schema.rs. The tuple ("claude-mythos-5", "Claude Mythos 5", "10", "50", "1.00", "12.50") is added to the models list (placed before the Claude 4.8 series) to register the Mythos 5 model with the Database schema.
  • fix(proxy): harden takeover-residue recovery across config-dir switches (#4076)
    Changing app_config_dir relocates the SQLite database, so a restart
    triggered while proxy takeover is active used to strand the live
    configs: the new instance reads a fresh DB with no live backups, the
    first-run import then persisted the PROXY_MANAGED placeholder as the
    `default` provider, and the no-backup recovery path wrote that
    placeholder right back to the live files — leaving Claude/Codex/Gemini
    pointed at a dead local proxy with no automatic way out.
    
    Three orthogonal fixes, defense in depth:
    
    - restart_app now awaits cleanup_before_exit() before app.restart().
      Since #4069 the ExitRequested handler intentionally defers restart
      requests to Tauri's default re-exec without custom cleanup, which is
      correct for same-DB restarts but not for this command's dir-change
      use case: only the old instance holds the backups needed to restore
      the taken-over live files, so it must restore them while its event
      loop is still alive.
    - import_default_config refuses to import a live config that is under
      proxy takeover (placeholder detected), instead of persisting it as
      the current provider.
    - restore_live_from_ssot_for_app validates that the current provider's
      settings_config does not itself contain takeover placeholders before
      writing it back; polluted SSOT now falls through to the placeholder
      cleanup fallback.
    
    Regression tests cover the import guard and the no-backup recovery
    path (the latter fails before this change by writing PROXY_MANAGED
    back to live).
  • fix(updater): drive download/install/restart from backend to avoid hang (#4074)
    * 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).
  • fix: prevent deadlock when relaunching after in-app update (#4069)
    The updater's relaunch() (and app.restart()) triggers ExitRequested
    with code RESTART_EXIT_CODE, which the handler treated as a regular
    exit: it called api.prevent_exit() and spawned an async cleanup task.
    
    However Tauri silently ignores prevent_exit() for restart requests
    (see ExitRequestApi::prevent_exit docs), so the event loop keeps
    shutting down regardless and fires every plugin's RunEvent::Exit hook.
    Two threads then deadlock:
    
    - the spawned cleanup task runs save_window_state on a tokio worker,
      holding the window-state plugin's internal mutex while querying
      window geometry, which dispatches to the main thread and blocks;
    - the main thread, already inside the plugin's own RunEvent::Exit
      hook, blocks on that same mutex.
    
    The app freezes forever on the restarting screen with the update
    already installed; force-quit + reopen comes back on the new version
    (#3998). Confirmed on macOS by sampling the frozen process: main
    thread parked in tauri_plugin_window_state save_window_state mutex
    lock, tokio worker parked in is_maximized -> mpsc recv.
    
    Fix: classify exit requests (None / restart / user exit) and let
    restart requests fall through untouched to Tauri's default restart
    flow (RunEvent::Exit -> re-exec). On that path window state is saved
    by the plugin's exit hook on the main thread, the tray icon is
    cleaned up by Tauri's internal cleanup_before_exit, and proxy/live
    config restore is unnecessary because the new instance takes over
    immediately. The regular exit path (tray quit) is unchanged.
    
    With the fix, a simulated updater relaunch (request_restart) re-execs
    the new process in under 200ms, 3/3 runs; normal quit still performs
    full cleanup.
    
    Co-authored-by: thisTom <19346741+thisTom@users.noreply.github.com>
  • feat(presets): add Unity2.ai partner provider across seven apps
    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
  • fix(proxy): aggregate mislabeled SSE bodies in transform fallback (#2234)
    The Claude/Codex format-transform non-stream branch returned an opaque 422
    "Failed to parse upstream response" whenever a 2xx upstream body was not
    valid JSON. The common case: MaaS gateways force-stream a stream:false
    request and return an SSE body with a non-SSE Content-Type, defeating the
    header-only is_sse() check.
    
    On serde failure, sniff for SSE and aggregate the chunks into a single
    JSON, then run the existing converter so clients still receive a valid
    non-stream response.
    
    - chat_sse_to_response_value: aggregate chat.completion.chunk SSE
      (content / reasoning / refusal / tool_calls / legacy function_call),
      tool_calls index-keyed via BTreeMap to avoid unbounded densification,
      first-wins finish_reason, message-snapshot override, completeness and
      error-event guards; synthesize an id when the upstream omits one
    - responses_sse_to_response_value: process the residual trailing block,
      tolerating truncation and skipping it once a completed event was seen
    - enrich remaining parse failures with content-type / content-encoding /
      body-snippet diagnostics
    - deflate: try zlib (RFC 9110) before raw; keep the content-encoding
      header for unsupported encodings
    - gate zero-usage rows on the Claude transform path
  • feat(provider-form): consolidate codex form into advanced options section
    - 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
  • feat(provider-form): custom User-Agent presets dropdown in advanced settings
    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).
  • feat(proxy): honor custom User-Agent across stream check and model fetch
    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).
  • fix: omit customUserAgent when provider category is official
    Stale custom UA values from non-official presets were persisted even
    after switching to an official preset, silently altering request headers.