Commit Graph

1152 Commits

  • Fix Codex model catalog being wiped by live-config backfill
    `modelCatalog` is a cc-switch-private field whose SSOT is the database; Live's
    config.toml only carries a lossy `model_catalog_json` projection. Proxy
    takeover/restore cycles and the official Codex.app rewriting config.toml can
    drop that projection, so `read_live_settings` reconstructs an empty catalog.
    Two paths then overwrote the stored mapping with that empty Live snapshot:
    
    - Switch-away backfill (`switch_normal` -> `restore_live_settings_for_provider_backfill`):
      now overlays the DB provider's `modelCatalog`, falling back to the
      Live-reconstructed one only when the DB has none.
    - Edit dialog (`EditProviderDialog`): when editing the active Codex provider it
      preferred Live over the DB SSOT; now keeps the DB `modelCatalog` so opening +
      saving no longer clears the mapping table.
    
    Add Rust backfill tests (preserve DB catalog when Live lacks it; keep Live
    catalog when DB has none) and a frontend regression test for the edit dialog.
  • fix(usage): resolve per-app credentials for native balance/coding-plan queries (#3355)
    * fix(usage): resolve per-app credentials for native balance/coding-plan queries
    
    The native usage-query paths (balance + coding_plan) in
    `query_provider_usage_inner` read credentials only from `env.ANTHROPIC_*`.
    That matches Claude providers, but Codex stores its key in
    `auth.OPENAI_API_KEY` with the base URL inside a TOML `config` string,
    Hermes/OpenClaw flatten them at the top level, and OpenCode nests them under
    `options`. So the card "refresh usage" / auto-query returned empty
    credentials and failed ("查询失败") for those apps, even though the
    config-page "Test" button worked (the frontend extracts per-app correctly).
    
    Introduce a single per-app resolver `Provider::resolve_usage_credentials`
    that mirrors the frontend `getProviderCredentials`, and route both native
    branches through it. Add `extract_codex_base_url` to `codex_config` as the
    canonical Codex TOML base-URL parser and make the proxy adapter delegate to
    it (removing a duplicate copy). Align the frontend `getProviderCredentials`
    to cover OpenCode and Claude Desktop and to use the same OpenRouter/Google
    key fallbacks as the backend.
    
    Fixes #3158
    Fixes #3100
    Fixes #2625
    
    * refactor(usage): explicit AppType arms + frontend trailing-slash trim
    
    Address two review nits on the per-app credential resolver:
    
    - provider.rs: replace the catch-all `_` arm in resolve_usage_credentials with an explicit `AppType::Claude | AppType::ClaudeDesktop` arm, so a new AppType variant fails to compile here instead of silently defaulting to the Anthropic env shape.
    - UsageScriptModal.tsx: normalize getProviderCredentials baseUrl through a single trailing-slash trim so the frontend 'Test' path matches the backend resolver (trim_end_matches), keeping front/back truly in lockstep.
    
    No behavior change for well-formed configs; tests/typecheck/fmt/clippy clean.
    
    * fix(usage): skip empty primary credential fields in fallback chain
    
    `obj.get(key)` returns `Some` for a present-but-empty field, so the
    `.or_else()` fallback chains for the Claude/ClaudeDesktop and Gemini api-key
    lookups only skipped *absent* keys, not empty ones. Presets seed fields like
    `ANTHROPIC_AUTH_TOKEN` as present-but-empty placeholders, so a provider whose
    real key lives in a fallback field (`ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY`
    / `GOOGLE_API_KEY`, or `GOOGLE_API_KEY` for Gemini) resolved to an empty key on
    the native balance/coding-plan path — while the frontend `a || b` (which skips
    empty strings) still found it, reproducing the same Test-works / refresh-fails
    divergence this PR removes.
    
    Add a `first_non_empty` helper that skips present-but-empty values, matching
    the frontend `||` semantics, and use it for both fallback chains. Tests cover
    empty primary + populated fallback for Claude and Gemini.
  • fix: Claude Desktop 官方供应商添加报错 #3402 (#3405)
    * fix: Claude Desktop 官方供应商添加时缺少 ANTHROPIC_BASE_URL 报错
    
    根因:前端 mutation 为 claude-desktop 生成随机 UUID 作为 provider id,
    后端 is_official_provider 通过 id 匹配跳过校验,随机 UUID 不匹配导致
    走入普通 direct 模式校验并要求 ANTHROPIC_BASE_URL。
    
    修复:
    - 前端:claude-desktop + category=official 时使用固定 id "claude-desktop-official"
    - 后端:validate_provider / validate_direct_provider / validate_proxy_provider /
      apply_provider_to_paths 增加 category=="official" 兜底检查
    
    Fixes #3402
    
    * fix: restrict Claude Desktop official provider detection
    
    * fix: add Claude Desktop official provider via seed
    
    ---------
    
    Co-authored-by: 金恩光 <enguang.jin@gmail.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • Fix Codex OAuth auth being cleared during preserve-mode takeover
    When "preserve official auth on switch" is enabled, proxy takeover routes
    the PROXY_MANAGED placeholder into config.toml's experimental_bearer_token
    and leaves auth.json (the ChatGPT OAuth login) untouched. Takeover detection
    only inspected auth.json's OPENAI_API_KEY, so it never recognized this state
    and returned a false negative, which led downstream paths to clobber the
    preserved OAuth login.
    
    - Detection: is_codex_live_taken_over now also matches a config.toml
      experimental_bearer_token equal to the placeholder, fixing detect/cleanup/
      restore/startup-recovery in one place.
    - Cleanup: remove the config.toml bearer token only when it equals the
      placeholder (new remove_codex_experimental_bearer_token_if predicate), so a
      real third-party key is never stripped.
    - Write: under preservation, the None-provider takeover path writes only
      config.toml and keeps auth.json intact, matching the provider path.
    - Settings: rename the section to "Codex App Enhancements" and reword the
      description across all four locales.
    - Add tests covering OAuth preservation on takeover and placeholder-only
      cleanup.
  • Default Codex auth preservation to off (opt-in)
    Flip preserve_codex_official_auth_on_switch from true to false so
    third-party Codex switches overwrite auth.json by default, matching the
    expectation that switching providers also swaps credentials. Users who
    rely on keeping the ChatGPT login in auth.json while on a third-party
    provider (for official plugins / remote login) can enable it in
    Settings -> Codex Authentication.
    
    The toggle field ships for the first time here (it is not in v3.16.0),
    so no existing settings.json holds an explicit value -- every user lands
    on the new default and no migration is required.
    
    Also set the flag explicitly in the preservation unit test instead of
    relying on the global default, keeping it valid now that the default is
    false.
  • Align Claude Desktop model mapping with Claude Code three-role tiers
    Claude Desktop's 3P validation only accepts claude-{sonnet,opus,haiku}-*
    role IDs, so providers must map every tier. Bring the Desktop mapping flow
    in line with Claude Code and fix the fallout that broke sub-agent Haiku calls.
    
    - Proxy form: replace the dynamic route list with fixed Sonnet/Opus/Haiku
      tiers; blank tiers backfill from the first filled tier (Sonnet first) on
      submit and inherit its supports1m flag
    - Backend: add a role-keyword fallback in map_proxy_request_model so dated
      official names (e.g. claude-haiku-4-5-20251001) resolve to the right tier,
      guarded by is_claude_safe_model_id so [1m]-suffixed IDs stay rejected
    - Tighten is_claude_safe_model_id / isClaudeSafeRoute to reject degenerate
      role IDs like "claude-sonnet-"
    - Fix the seed-effect race where normalizing empty routes to three blank
      tiers blocked the default-route backfill
    - Sync switch hints, placeholders, and the zh/en/ja/zh-TW locales to the
      three-role-ID rule
    - Update zh/en/ja user manual (2.1, 2.6), calling out legacy Claude IDs
      (claude-3-5-sonnet-...) as a rejected example
    
    Tests: 282 frontend + 34 backend claude_desktop; typecheck, clippy, fmt clean.
  • Rename OpenCode Go preset to drop model suffix
    Simplify the display name from "OpenCode Go (DeepSeek V4 Flash)" to
    "OpenCode Go" in both the Claude Code and Claude Desktop preset lists.
  • Add referral param to ShengSuanYun website links
    The websiteUrl for ShengSuanYun presets pointed at the bare domain while
    only apiKeyUrl carried the from=CH_4HHXMRYF referral code. Append the same
    referral param to websiteUrl across all provider presets so the in-app
    'open website' jump is also attributed to the channel.
  • Update default models and pricing across presets
    Bump default model names project-wide: gpt-5.4 -> gpt-5.5,
    gemini-3.x -> gemini-3.5-flash, glm-5 -> glm-5.1, and
    grok-code-fast-1 -> grok-build-0.1 across all provider presets
    (claude, codex, gemini, hermes, openclaw, opencode, universal),
    Gemini config, and stream check defaults.
    
    Pricing:
    - Seed gemini-3.5-flash, gemini-3.1-flash-lite, step-3.5-flash-2603,
      doubao-seed-2.0-code, mimo-v2.5(/pro), qwen3-coder-480b, grok-build-0.1.
    - Correct deepseek-v4-flash/pro, glm-5/5.1, grok pricing.
    - Add repair_current_model_pricing: idempotent pass that fixes only
      rows still equal to the outdated built-in values, preserving any
      user-customized prices (seed uses INSERT OR IGNORE and cannot update
      existing rows).
    
    Fixes from review:
    - opencode: drop duplicate gemini-3.5-flash variant (unreachable via
      .find), keep the entry with the full minimal/low/medium/high set.
    - Align stale display names/costs to gemini-3.5-flash (hermes, openclaw,
      opencode); openclaw cost -> {1.5, 9, 0.15} to match seed.
    - i18n (zh/en/ja/zh-TW): refresh OMO category tooltips for new model
      names; fix writing tooltip to Kimi K2.5 to match its recommended.
    
    Update tests accordingly and add a regression test asserting unique
    model ids in the Google opencode preset variants.
  • Upgrade default Claude Opus model to 4.8
    Bump the default Opus route/model from claude-opus-4-7 to claude-opus-4-8
    across provider presets (claude, claudeDesktop, hermes, openclaw, opencode,
    universal), i18n locales (zh/en/ja/zh-TW), pricing seed data, and the
    user-manual docs.
    
    - Add claude-opus-4-8 pricing row ($5/$25/$0.50/$6.25); keep the 4-7 row
      for historical usage stats (seeded via INSERT OR IGNORE).
    - Claude Desktop proxy: accept bidirectional opus 4-7 <-> 4-8 route alias
      during rollout so previously saved routes keep resolving.
    - thinking_optimizer: route opus-4-8 through adaptive thinking and normalize
      dotted model ids (also fixes dotted 4-6/4-7 falling back to legacy).
    - usage_stats: normalize Bedrock/Vertex/aggregator opus-4-8 ids to base
      pricing.
    
    Also merge role:"system" messages into the Gemini systemInstruction in the
    Anthropic->Gemini transform.
  • feat(usage): real-time stats refresh + fix codex sync panic on non-ASCII model names (#3027)
    The usage dashboard previously only refreshed on app restart for users
    who don't route through the cc-switch proxy. Two issues were involved:
    
    1. The session-sync background task panicked when a Codex model name
       contained non-ASCII characters (e.g. `【官】glm-5.1`), because
       `normalize_codex_model` sliced `&name[name.len() - 11..]` without
       verifying char boundaries. Once the task panicked, no session logs
       were imported until the app was restarted (where startup-time
       `rollup_and_prune` happened to flush pending data).
    
    2. Even with sync working, the dashboard only polled every 30s and
       skipped polling when the window was unfocused, so freshly-imported
       data was invisible until the next poll or window refocus.
    
    Fixes
    -----
    
    * `normalize_codex_model`: guard the 11-byte ISO-date suffix slice with
      `is_char_boundary` + `is_ascii` checks. ASCII-only suffix means the
      date-stripping logic is correct, and non-ASCII names (which can never
      be valid date suffixes anyway) now bypass the slice safely.
    
    * New `usage_events` module that emits `usage-log-recorded` to the
      frontend whenever `proxy_request_logs` actually gains a new row.
      Sources covered: proxy `log_request`, Claude/Codex/Gemini session
      sync, and startup `rollup_and_prune`. Notifications use a global
      `OnceLock<AppHandle>` so call sites that don't already hold an
      `AppHandle` (e.g. `UsageLogger`) can notify without signature churn.
    
    * 200ms debounce in `notify_log_recorded` collapses bursts (a single
      Codex sync importing 3000+ entries triggers ~2 emits, not 3000) so
      the frontend's `invalidateQueries` is never spammed.
    
    * Frontend `useUsageEventBridge` listens for the event and invalidates
      `usageKeys.all`. Hook is mounted only on `UsageDashboard`, so the
      listener is unsubscribed automatically when the user navigates away.
    
    Verification
    ------------
    
    * `cargo check` passes (existing 25 dead-code warnings in
      `commands/misc.rs` are pre-existing and unrelated).
    * `tsc --noEmit` passes.
    * Manually verified end-to-end: a Codex sync run that imported 3145
      entries produced 2 debounced emits, both logged as `emit
      usage-log-recorded 成功`, and the dashboard updated within ~200ms.
    
    Behaviour notes
    ---------------
    
    * `INSERT OR IGNORE` paths (Claude/Codex session sync) only notify when
      the row is actually inserted, so dedup-skipped writes don't trigger
      empty refreshes.
    * Gemini's `INSERT … ON CONFLICT … DO UPDATE` path reuses the existing
      `conn.changes() > 0` check and only notifies when token counts truly
      changed.
    * `rollup_and_prune` notifies once per pruning cycle (at most once per
      app start) so the dashboard reflects the new aggregate state.
    
    Co-authored-by: in30mn1a <in30mn1a@users.noreply.github.com>
  • fix(about): handle prerelease tools in version check
    Use semver comparison instead of string inequality so locally-ahead prerelease builds aren't misreported as outdated; backend now fetches the full npm dist-tags and, when the local version leads latest, surfaces the tool's prerelease channel (claude=next) as latest.
  • refactor(codex): unify custom model_provider routing key to "custom"
    - Always emit `model_provider = "custom"` from deep link, UniversalProvider, and the universal form modal so future writes share one stable routing key.
    - Add `codex_provider_template_v1` local migration that rewrites legacy keys (aihubmix/ccswitch/...) under `[model_providers.custom]`, updates profile refs, and backs up the original settings_config under `~/.cc-switch/backups/<timestamp>/providers/`.
    - Tighten history migration source detection to a whitelist plus `[model_providers.<id>]` existence check so user-authored keys are never rewritten in jsonl/state DB.
    - Encode deep link name/model/endpoint through `toml_edit::Value` so display names containing quotes or backslashes no longer break the generated config.toml.
    - Stabilize provider settings backup filename hash with Sha256 (was process-random SipHash).
  • feat(codex): add remote compaction toggle for third-party providers
    Write model_providers name as "OpenAI" to let Codex attempt remote
    compaction through compatible endpoints. Hidden for official providers.
  • fix(omo): sync recommended models with upstream and improve Fill Recommended feedback
    The Fill Recommended button was misleading — it showed toast.success even
    when most slots couldn't be filled due to model ID mismatches with the
    user's configured providers.
    
    Changes:
    - Sync OMO_BUILTIN_AGENTS/CATEGORIES recommended fields with upstream
      oh-my-openagent model-requirements (gpt-5.4→5.5, kimi-k2.5→claude-sonnet-4-6, etc.)
    - Add toast.warning tier when unmatched >= filled, showing slot:model pairs
      (e.g. "Sisyphus: claude-opus-4-7") so users know exactly what to configure
    - Upgrade fillRecommendedNoMatch to also show examples
    - Add fillRecommendedMostlyUnmatched i18n key (zh/en/ja/zh-TW)
  • Enable Codex goals in provider templates (#3089)
    * Enable Codex goals in provider templates
    
    * feat: add Codex goal mode toggle
    
    - Remove forced goals=true from Codex provider presets and custom templates.
    - Add a Codex provider editor switch that updates [features].goals on demand.
    - Update docs, i18n, and regression coverage for the optional Goal mode flow.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • feat(i18n): add Traditional Chinese localization (#3093)
    * Add Traditional Chinese localization
    
    * fix: address zh-TW formatting and token units
    
    - Format `zh-TW.json` with Prettier.
    - Use Traditional Chinese `萬` and `億` units for zh-TW token summaries.
    - Add usage formatting coverage for Traditional Chinese locale aliases.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • refactor: replace JSON deep copy with deepClone helper and extract useTauriEvent hook (#3140)
    * refactor: replace JSON.parse(JSON.stringify()) with structuredClone and extract useTauriEvent hook
    
    Replace all `JSON.parse(JSON.stringify())` deep copy patterns with native
    `structuredClone()` across production source (9 occurrences), tests (11
    occurrences), and a hand-rolled `deepClone` utility in providerConfigUtils.ts.
    Add "ES2022" to tsconfig lib for type support.
    
    Extract a `useTauriEvent` hook to eliminate the repeated Tauri event listener
    boilerplate (`useEffect` + `active/disposed` flag + async `listen`) that was
    duplicated across App.tsx (3 listeners) and useUsageCacheBridge.ts. The hook
    handles async registration, race-condition guards, and cleanup automatically.
    
    * fix: add compatible deepClone helper
    
    - Add a shared deepClone helper with a structuredClone runtime guard and fallback.
    - Route clone call sites through the helper.
    - Preserve universal-provider-synced listener ordering and drop the dead-directory diff.
    
    * fix: harden Tauri event handling
    
    - Guard WebDAV sync status events against missing payloads.
    - Preserve settings query invalidation ordering before showing auto-sync errors.
    - Simplify useTauriEvent subscriptions to avoid dependency-driven re-listens.
    
    ---------
    
    Co-authored-by: zcb <zhangchongbiao@qiyuanlab.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix: sync Claude Desktop profile during proxy takeover (#3157)
    * fix: sync Claude Desktop profile during proxy takeover
    
    * fix(provider): skip Claude Desktop backup refresh during takeover
    
    - Route Claude Desktop takeover updates directly through the 3P profile writer.
    - Keep takeover startup backup state unchanged when provider metadata changes.
    - Narrow platform-specific test helpers and environment setup with cfg gates.
    
    * fix(provider): restore PathBuf import for CI tests
    
    - Restore an unconditional PathBuf import for provider tests.
    
    - Keep Linux cargo test builds compiling while preserving Claude Desktop cfg-gated helpers.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(ui): remove fixed width constraint on AppSwitcher text to prevent clipping (#3161)
    * fix(ui): remove fixed width constraint on AppSwitcher text to prevent clipping
    
    App names like "Claude Desktop" were being clipped by the max-w-[90px]
    constraint on the text label. Changed to max-w-none so text adapts to
    content width. Also removed overflow-x-hidden from the parent toolbar
    container to prevent clipping of the wider content.
    
    Fixes text truncation in the top-right app switcher segmented control.
    
    * - fix(ui): preserve AppSwitcher compact overflow behavior
    
    - Restore toolbar overflow clipping required by useAutoCompact.
    
    - Cap expanded AppSwitcher labels at 120px so Claude Desktop fits while compact animation remains smooth.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • chore(sponsors): drop AICoding partner from README, presets and i18n
    Remove AICoding (aicoding.sh) sponsor entry from the three README files
    and the provider preset across all seven app_types (claude, codex,
    gemini, hermes, opencode, openclaw, claudeDesktop). Also drop the
    matching partner_promotion.aicoding string in zh/en/ja locales.
    
    The icon (src/icons/extracted/index.ts) and the legacy provider id in
    codex_history_migration.rs are kept intentionally: the former for
    possible later re-use, the latter so existing users' Codex history
    still gets bucketed into "custom" on upgrade.
  • chore: satisfy clippy::nonminimal_bool and prettier to unblock CI
    - codex_config: rewrite `!(A && !B)` as `!A || B` (De Morgan-
      equivalent) — also reads more directly as "no OAuth, or
      already has provider API key".
    - UsageScriptModal: reflow conditional onto one line per prettier.
  • feat(codex): preserve OAuth login state during third-party provider switching
    Codex provider switches now only write config.toml for third-party providers,
    injecting the API key as experimental_bearer_token. The user's auth.json
    (ChatGPT OAuth tokens) is preserved. Official providers with login material
    still write auth.json normally. Backfill restores bearer tokens into stored
    provider auth.OPENAI_API_KEY to maintain canonical shape.
  • feat(settings): unify unix installers on mktemp+bash, fix WSL install missing native installer
    Extend the mktemp+bash installer pattern from hermes (commit 20d943be) to
    claude and opencode, and remove the cfg gate that locked install_command_for
    to non-Windows targets — Windows host + WSL tools now correctly route
    through the POSIX install priority instead of falling back to bare npm.
    Frontend "one-click install commands" displayed in the About section now
    diverge per platform to match what the backend actually runs.
    
    Backend (src-tauri/src/commands/misc.rs):
    - New CLAUDE_INSTALL_UNIX and OPENCODE_INSTALL_UNIX constants use the same
      `bash -c 'tmp=$(mktemp) && curl -fsSL .../install.sh -o $tmp && bash
      $tmp; status=$?; rm -f $tmp; exit $status'` shape as HERMES_INSTALL_UNIX.
      The shared comment about why we avoid `curl | bash` is now hoisted to the
      top of all three constants — the constraint (WSL `sh -c` sub-shells
      don't inherit outer pipefail; default shell may be dash/ash) applies to
      every shell installer, not just Hermes.
    - New installer_with_npm_fallback helper deduplicates the
      "official installer || npm fallback" chain construction; reuses
      chain_update_commands(LifecycleCommandShell::Posix) so the wiring matches
      the update-chain logic.
    - install_command_for split into cross-platform posix_install_command_for
      + thin #[cfg(not(target_os = "windows"))] wrapper. The cfg gate had
      been hiding a real bug: on Windows hosts, the WSL branch called
      wsl_tool_action_shell_command which forwarded install actions to the
      Windows-target tool_action_shell_command, yielding bare `npm i -g` for
      claude/opencode even though they have working native POSIX installers.
    - wsl_tool_action_shell_command now branches on action: Install dispatches
      to posix_install_command_for (native installer || npm chain); Update
      keeps the existing behavior. Empty install commands collapse to None so
      callers error on unsupported tools instead of running an empty command.
    - build_tool_lifecycle_command pipefail comment updated: the original
      justification (covering install's `curl | bash` path) no longer applies
      to any current command, but the directive stays as defense-in-depth for
      future pipe additions.
    - build_tool_action_line WSL branch comment updated to reflect the
      install/update split.
    
    Frontend (src/components/settings/AboutSection.tsx):
    - New posixScriptInstallCommand(url) helper builds the same mktemp+bash
      string template the backend uses.
    - New powershellEncodedCommand(script) mirrors the backend's UTF-16 LE +
      base64 encoder (charCodeAt + String.fromCharCode(lo, hi) + btoa) so the
      PowerShell EncodedCommand shown to users matches what the backend
      executes.
    - ONE_CLICK_INSTALL_COMMANDS split into POSIX_ONE_CLICK_INSTALL_COMMANDS
      and WINDOWS_ONE_CLICK_INSTALL_COMMANDS, selected by isWindows():
      POSIX shows claude/opencode/hermes with the mktemp+bash installer;
      Windows shows claude/codex/gemini/opencode/openclaw with npm (matching
      backend static_fallback_command on Windows) and hermes with the
      PowerShell EncodedCommand. Display now mirrors backend execution
      per platform, removing the show-vs-run mismatch.
    - Stale comment in the probe-concurrency note mentioning "curl | bash"
      updated to "官方 installer".
    
    Tests (src-tauri/src/commands/misc.rs):
    - New wsl_install_uses_posix_install_priority covers three
      representatives: claude (positive: native installer with npm fallback),
      opencode (positive), codex (negative: no native installer so falls back
      to bare `npm i -g`). Reverse-asserts !contains("| bash") to lock out
      the old pipe form.
    - claude_install_chains_native_then_npm and
      opencode_install_chains_native_then_npm strengthened with
      !parts[0].contains('|') so the native installer portion stays
      pipe-free even if future edits to the template sneak a pipe back in.
    
    Validated:
    - cargo check --tests: clean
    - cargo test --lib commands::misc:: : 59 passed, 0 failed
    - pnpm typecheck: 0 errors
    - cargo fmt: clean
    - pnpm format: clean (no files changed)
  • feat(settings): self-update first chain, switch hermes to native installer
    Promote anchored upgrades to "<bin> update || <package fallback>" for the
    tools whose CLI knows how to self-update safely, and replace pip-based
    hermes install/update with the official NousResearch installer scripts.
    Frontend learns to detect "update reported success but version did not
    change" so silent upstream no-ops surface as a soft warning.
    
    Backend (src-tauri/src/commands/misc.rs):
    - New prefers_official_update(tool, shell) per-platform allow-list:
      POSIX {claude, codex, opencode, openclaw}. Windows drops opencode
      pending anomalyco/opencode#17295 (its silent `upgrade` may prompt for
      install-source detection and deadlock our lifecycle). gemini is absent
      from both because `gemini update` is not implemented upstream
      (google-gemini/gemini-cli#18618 / #16122 OPEN).
    - New chain_update_commands joins primary/fallback per shell: POSIX
      `||`, Windows `|| call` — batch transfers control to the `||` RHS
      without `call`, skipping subsequent tools in multi-tool actions.
    - New LifecycleCommandShell {Posix, WindowsBatch} threads the target
      shell through tool_action_shell_command_for_shell, replacing the old
      hermes-only WSL substitution hack with a uniform mechanism.
    - anchored_command_from_paths refactored: hermes anchors to
      `<bin> update`; tools in prefers_official_update chain self-update
      with the package-manager command as fallback; brew formulae remain
      brew-managed (never self-update what Homebrew owns); others use the
      prior npm/volta/bun/pnpm anchoring unchanged. Mirror on Windows.
    - Hermes commands switch from pip to the official scripts. POSIX/WSL
      runs `bash -c 'tmp=\$(mktemp) && curl -fsSL .../install.sh -o \$tmp
      && bash \$tmp; status=\$?; rm -f \$tmp; exit \$status'`. The mktemp +
      two-step download avoids `curl | bash` because WSL sub-shells inherit
      neither outer `set -o pipefail` nor a guaranteed bash (default may be
      dash/ash). Windows uses `powershell -NoProfile -ExecutionPolicy Bypass
      -EncodedCommand <b64>` with a hand-rolled UTF-16 LE base64 encoder
      for `irm .../install.ps1 | iex`, hiding the PowerShell pipe from
      cmd.exe. Pip is abandoned because macOS system python3 is often 3.9
      while hermes-agent requires >=3.11, and the pyenv `python` shim may
      not exist — pip errors then misclassify as "command not found".
    
    Frontend (src/components/settings/AboutSection.tsx):
    - New versionUnchangedAfterUpdate four-AND short-circuit detects when
      the upgrade command succeeded, the tool still reports a version, but
      that version equals the pre-upgrade version while a different
      latest_version is known. Guards against upstream no-op updaters that
      return exit 0 without applying anything (openai/codex#21897).
    - Soft-failure shape gains kind?: "notRunnable" | "versionUnchanged"
      so the warning toast title can disambiguate the two cases.
    - Hermes install command snippet swapped from `python3 -m pip ...` to
      the official curl-bash one-liner shown to users.
    
    i18n (zh / en / ja):
    - New settings.toolActionVersionUnchangedTitle and
      settings.toolActionVersionUnchanged, synchronized across all locales.
    
    Tests (src-tauri/src/commands/misc.rs):
    - Anchored upgrade assertions updated to `<bin> update || <pkg>` shape
      across nvm / volta / bun / homebrew-npm-global / spaces / fnm
      branches; brew formula explicitly does NOT chain self-update.
    - New hermes anchor tests replace hermes_has_no_npm_anchor on both
      POSIX and Windows. WSL hermes tests rewritten for the mktemp+bash
      flow with `hermes update` fallback.
    - Reverse-lock tests: gemini static fallback must NOT contain
      `gemini update`; opencode Windows static fallback must NOT contain
      `opencode upgrade`; hermes commands must NOT contain
      `python`/`pip`/`powershell`/forbidden pipes.
    
    Validated:
    - cargo check --tests: clean
    - pnpm typecheck: 0 errors
    - cargo fmt: clean
  • refactor(settings): drop CLI uninstall command hints, keep conflict diagnostics
    The uninstall hint feature added in f6bdd4bb / 89e2339b printed a
    copy-pasteable shell command (e.g. `brew uninstall <formula>`,
    `<sibling-npm> rm -g <pkg>; [ -e bin ] && rm -f bin && rm -rf pkg_dir`)
    under each detected install in the multi-install conflict panel. Even
    with copy-only-never-execute UX, putting destructive commands one
    button away from the user is risk we don't want to ship — `rm -rf`
    under a misclassified path would be unrecoverable.
    
    Conflict diagnostics themselves are kept: users still see the warning
    banner + per-install row (source / path / version / Default badge), so
    the "you have multiple installs and the upgrade only touches one"
    signal survives. They just don't get a one-click destructive command.
    
    Backend (src-tauri/src/commands/misc.rs):
    - Remove `uninstall_command_from_paths` (POSIX + Windows pair).
    - Remove `posix_quote_for_user_shell` and `win_quote_path_for_user_shell`
      — both were uninstall-only quoters (whitelist + cmd-only forms).
      `win_quote_path_for_batch` and `shell_single_quote` stay because the
      anchored upgrade path and `build_shell_cd_command` still use them.
    - Remove `quoted_sibling_or_bare` and `quoted_sibling_or_bare_with_ext`
      thin wrappers (only callers were uninstall branches).
    - Drop `uninstall_command` and `uninstall_command_needs_cmd_hint` from
      `ToolInstallation`. Drop the eager `infer_install_source` ->
      `uninstall_command_from_paths` call in `enumerate_tool_installations`
      (saves ~16μs per probe but the real point is fewer surprises).
    - Remove 14 `#[test]` cases for uninstall command shape (brew /
      homebrew-npm-global / nvm / npm-fallback quoting / pnpm / volta / bun /
      hermes / system / Windows percent-path) plus 4 `win_quote_path_for_user_shell`
      helper tests and the `expect_user_shell_quoted_path` test mirror.
    - Drop the `src()` test helper that was only used to feed the deleted
      uninstall tests (`infer_install_source` is still called directly by
      surviving tests).
    - Cross-references in `anchored_command_from_paths` doc to its now-gone
      uninstall "dual" are stripped.
    
    Frontend (src/components/settings/AboutSection.tsx):
    - Conflicts `<ul>` keeps the diagnostic header + per-install `ToolInstallRow`
      but loses the inline command box, the trash + copy icons, the per-row
      PowerShell-call-operator hint, and the `handleCopyCommand` callback.
    - Remove the `Trash2` lucide import; `Copy` stays (the install-commands
      copy button at line 1100 still uses it).
    
    API (src/lib/api/settings.ts):
    - Drop `uninstall_command` and `uninstall_command_needs_cmd_hint` from
      the `ToolInstallation` TS interface (matches the Rust struct change).
    
    i18n (zh / en / ja):
    - Drop `settings.toolUninstallCopyHint`, `settings.toolUninstallCopied`,
      `settings.toolUninstallPwshHint`. Other `tool*` and `skills.uninstall*`
      keys are untouched (Skill uninstall is a separate, non-destructive
      feature for managing the Skills repo and is unaffected).
    
    Stats: 858 deletions, 5 insertions across 6 files.
    
    Validated:
    - cargo test --lib: 1322 passed, 0 failed
    - cargo fmt --check: clean
    - cargo build: clean
    - cargo check --tests: clean
    - pnpm typecheck: 0 errors
    - JSON.parse on all three locale files: clean
  • feat(settings): backend-generated source-aware uninstall command hints
    Move uninstall hint command generation from frontend (which used a lossy
    `source` string and couldn't tell brew formula from homebrew-npm-global apart)
    into the Rust backend, where `brew_formula_from_path` + `infer_install_source`
    together produce correct commands for each install. Conflict UI now reads
    `inst.uninstall_command` directly.
    
    Backend (src-tauri/src/commands/misc.rs):
    - New `uninstall_command_from_paths` (POSIX + Windows) mirrors the
      `anchored_command_from_paths` family. POSIX branches on brew formula /
      volta / bun / pnpm / nvm|fnm|mise|homebrew / system. Windows branches on
      volta / pnpm / npm.
    - New `quoted_sibling_or_bare` (POSIX) + `quoted_sibling_or_bare_with_ext`
      (Windows) deduplicate the `sibling_bin().map(quote).unwrap_or_else(bare)`
      pattern across all uninstall branches.
    - New `posix_quote_for_user_shell` whitelists `[A-Za-z0-9._/+=:@-]`; any
      other character routes through `shell_single_quote`. Replaces
      `quote_path_if_spaced` on the uninstall side because the fallback chain
      is destructive (`rm -rf <pkg_dir>`), so a `;` / `$` / backtick in the
      home dir would have let the chain inject extra commands.
    - New `win_quote_path_for_user_shell` (no `%` 4x escape) splits from
      `win_quote_path_for_batch`. Uninstall hints are copy-paste targets, not
      `.bat + call` payloads, so the two-round percent expansion that
      motivates the 4x escape doesn't apply.
    - POSIX `nvm|fnm|mise|homebrew` branch appends physical-delete fallback
      `; [ -e <bin> ] && rm -f <bin> && rm -rf <pkg_dir>` because nvm v18.20.8
      was observed to silently no-op `npm rm -g` (exit 0 with `up to date`
      message, no filesystem change). Anchored `<node_root>/{bin,lib}` layout
      assumption documented as branch-specific.
    - `ToolInstallation` gains `uninstall_command: String` and
      `uninstall_command_needs_cmd_hint: bool`. The bool is computed as
      `cfg!(target_os = "windows") && uninstall_command.contains('"')` — a
      compile-time platform gate avoids false positives from POSIX
      `shell_single_quote`'s `'"'"'` escape form, which also contains `"`.
    - `enumerate_tool_installations` computes `source` once and threads it
      into `uninstall_command_from_paths`, removing a redundant
      `infer_install_source` call (review N1).
    
    Frontend (src/components/settings/AboutSection.tsx + src/lib/api/settings.ts):
    - Remove the JS helpers `buildUninstallCommand`, `dirOfPath`,
      `isWindowsPath`, `shellQuote`, `TOOL_NPM_PACKAGES`. Conflict rows now
      read `inst.uninstall_command` directly.
    - Render a PowerShell-call-operator hint when
      `inst.uninstall_command_needs_cmd_hint` is true. Avoids string-match
      inference from the command (which fails on macOS user names containing
      `'`, e.g. `o'leary`, because `shell_single_quote`'s escape contains `"`).
    - `ToolInstallation` TS interface gains the two new fields.
    
    i18n (zh/en/ja):
    - New key `settings.toolUninstallPwshHint` documents the PowerShell
      `&` (call operator) requirement for quoted paths.
    
    Tests: 30 new cases under `commands::misc::tests::anchored_upgrade` cover
    each POSIX branch (brew formula extraction, npm anchored with fallback,
    pnpm, volta, bun, hermes), strong-quoting under `;` / `'` paths, the
    fallback-only-for-anchored-branches inverse guard, plus mirror Windows
    helpers for the user-shell quoter and a `path%foo%` integration case
    demonstrating the literal `%` preservation contract. Total
    `commands::misc` test count: 68.
  • fix(settings): enforce absolute paths for all anchored upgrade branches
    - misc: extend sibling_bin derivation to claude/brew/volta/bun in addition
      to npm. Return Option<String> so the anchored-path invariant cannot be
      silently violated — when bin_path has no parent directory, propagate
      None and fall back to the static command at the call site instead of
      emitting a bare command that depends on the GUI process PATH.
    - misc: factor out quote_path_if_spaced to keep all five anchored
      branches consistent when the resolved path contains spaces.
    - AboutSection: add preflightTools lock + try/finally around
      probeToolInstallations so fast double-clicks cannot race two probe
      rounds into concurrent executeRun calls or duplicate confirm dialogs.
    - Tests: cover sibling_bin None branch and space-quoting for every
      anchored branch (claude/brew/volta/bun).
  • feat(settings): anchor tool upgrades to the actual install
    Rework the About tool-upgrade flow so upgrades target the install the
    command line is actually using, instead of running bare `npm i -g` and
    landing on PATH's first npm — which used to move upgrades to the wrong
    location or fail outright when the resolved bin had no sibling npm.
    
    Anchored upgrade command (update path, non-Windows):
    - claude native installer (~/.local/share/claude/versions/) -> `claude update`
    - Homebrew formula (canonical path under /Cellar/) -> `brew upgrade <formula>`
    - volta / bun -> `volta install` / `bun add -g`
    - Node managers with sibling npm (nvm/fnm/mise/homebrew-npm) -> that
      directory's npm with `i -g <pkg>@latest`
    - system / unknown sources (e.g. opencode install.sh under ~/.opencode/bin,
      ~/go/bin) -> fall back to the bare command, surfaced as anchored=false
      so the UI shows an honest "default entry not determinable" hint instead
      of pretending the upgrade is targeted.
    
    Multi-install confirmation:
    - When the probe finds >=2 installs, pop ToolUpgradeConfirmDialog showing
      each install (source badge + path + version + Default badge) and the
      resolved command before executing. Top-level text only asserts what is
      universally true ("won't update all of them; see each tool below");
      per-card text adapts to anchored vs unanchored.
    - Pending upgrade state stores the full upgrade set; the dialog only
      shows the subset that needs confirmation, so non-conflicting tools in
      a batch still get upgraded after confirm.
    
    Unified probe command:
    - Merge the previous diagnose_tool_installations and plan_tool_upgrades
      into a single probe_tool_installations returning installs + is_conflict
      + needs_confirmation + command + anchored. Diagnose button, post-upgrade
      auto-diagnose, and pre-upgrade confirmation all share one IPC. The
      diagnose button's previous Promise.all of 6 IPCs is now a single call.
    
    resolve_path_default robustness:
    - The previous lines().next() was poisoned by interactive .zshrc output
      (welcome banners, p10k instant prompts, etc.), causing default-install
      detection to fail and dropping multi-install scenarios into the
      unanchored fallback even when one install was clearly the native one.
    - Replaced with first_abs_path_line that scans for the first
      absolute-path line and ignores noise, mirroring how try_get_version
      already tolerates banner noise via a regex.
    
    Diagnostic staleness fix:
    - diagnoseToolSilently now clears stale diagnostics in the else branch
      when the conflict no longer holds (previously only wrote on conflict,
      never cleared, so externally resolving a conflict left the card stuck
      on a stale list until the user clicked Diagnose).
    - Auto-diagnose now triggers after any successful update, not only when
      the version didn't change, so the card reflects the latest conflict
      state even when an anchored upgrade succeeds while another install
      remains.
    
    Other:
    - Shared ToolInstallRow between the conflict list and the confirmation
      dialog removes the duplicated per-install row JSX.
    - Drop the new shell_quote_path helper in favor of reusing the file's
      existing POSIX-correct shell_single_quote, gated on whitespace.
    - Collapse displayName lookup into a stable module-level helper to remove
      the `as ToolName` cast and avoid recreating the closure each render.
    - 17 backend unit tests covering anchored command generation across each
      source, brew formula extraction, is_conflicting thresholds, default
      install resolution, and the welcome-banner scenario.
    - i18n (zh/en/ja): new keys for confirmation dialog
      title/hint/will-run/confirm-button and the unanchored hint.
  • feat(settings): diagnose conflicting tool installations with uninstall hints
    Add a "Diagnose installs" action to the About section that enumerates every
    installation of each managed CLI, flags real conflicts (diverging versions
    or mixed runnable state), and lists them under the affected tool card.
    
    - backend: enumerate_tool_installations + diagnose_tool_installations command,
      sharing build_tool_search_paths with the single-probe scan; resolve the PATH
      default via the same login shell as version probing so the "Default" marker
      matches what the command line (and an upgrade) actually targets
    - frontend: global diagnose button left of Refresh, per-card conflict list,
      and a copy-only uninstall command per install -- source-aware (npm/volta/
      bun/pip) and anchored to that install's own npm to avoid removing the wrong
      node version; marked with a red trash icon, never executed
    - auto-diagnose silently after an upgrade that leaves the version unchanged
      or installs something that can't run
    - i18n: zh/en/ja
  • fix(settings): surface "installed but not runnable" state in tool cards
    After an install/update command succeeds, re-probe the tool version: if it
    still can't be detected the tool is installed-but-unrunnable (e.g. Node too
    old), so report a warning toast and show "installed · can't run" with the
    cause, instead of a misleading "success" plus a stale version number.
    
    Read the backend `installed_but_broken` flag rather than matching error
    text; the card's current-version line, action button (no useless "install"
    for a broken tool), and a "check environment" hint all key off it. Drop the
    now-redundant version-clearing setState (the refresh merge already nulls it),
    flatten the action ternary, and use extractErrorMessage in the catch path.
    
    Adds toolNotRunnable / toolActionInstalledNotRunnable / installedNotRunnable
    / toolCheckEnv i18n keys (zh/en/ja).
  • fix(settings): run batch updates per-tool and lock actions while busy
    Update All previously sent every tool to the backend as one script, where
    set -e (or errorlevel on Windows) aborts the whole batch on the first
    failure, skipping the rest and not refreshing versions for tools that did
    succeed. Run each tool as its own call instead: failures are isolated,
    each tool's version refreshes as soon as it finishes, and the result is
    summarized in a toast (success / partial / failure) listing which tools
    failed.
    
    Also derive an isAnyBusy flag and disable all install/update buttons,
    the refresh button and the WSL shell selectors while any action is
    running, so users can't trigger concurrent global npm/pip writes.
    
    Add the toolActionPartial i18n key (zh/en/ja).
  • feat(settings): expand About page into a tool management panel
    List all managed apps with current/latest versions, with per-tool install
    and update buttons plus an Update All action. Installs and updates now run
    silently: the button shows a spinner for the full duration, versions refresh
    automatically when done, and failures surface the backend error detail in a
    toast.
    
    While loading, the button keeps its label and only swaps the icon for a
    spinner, so width stays constant across zh/en/ja instead of jumping when the
    text changes (e.g. Update -> Updating...).
    
    Also collapses and renames the install-commands area to Manual Install
    Commands, and removes the managed-apps badge row and the Install Missing
    batch button.
  • feat: add silent install/update lifecycle for managed CLI tools
    Expand tool version detection to all six managed apps (including Hermes
    via PyPI), and add a run_tool_lifecycle_action command that installs or
    updates CLI tools silently: it captures the subprocess output and blocks
    until the command actually finishes instead of popping a terminal window
    (Windows uses CREATE_NO_WINDOW). On failure the last lines of stderr are
    returned so the UI can surface a useful message.
    
    Also restores the Windows version-detection path by gating try_get_version
    behind cfg(not(windows)) so it no longer risks triggering the App
    Execution Alias / protocol handler that previously disabled Windows.
  • feat: adaptive reasoning detection for Codex Chat providers
    Auto-detect each Chat-routed Codex provider's reasoning interface from
    its name, base URL, and model, then inject the matching thinking
    parameter without manual configuration:
    
    - Platform-first inference (OpenRouter, SiliconFlow) overrides model
      rules, since the same model exposes different reasoning controls
      depending on the hosting platform.
    - Effort tiers are forwarded only to providers that support them
      (DeepSeek, OpenRouter, and StepFun's step-3.5-flash-2603); on/off-only
      providers (Kimi, GLM, Qwen, MiniMax, MiMo, SiliconFlow) drop the level
      instead of sending a field the upstream rejects.
    - OpenRouter uses the native reasoning:{effort} object, clamps max to
      xhigh (its enum has no max), and forwards an explicit effort:"none" so
      reasoning can be turned off.
    - StepFun falls back to inference so per-model effort support is honored
      (the static preset would have forced effort on step-3.5-flash too).
    
    Includes the Codex provider-form reasoning controls, i18n strings
    (zh/en/ja), and response-side reasoning extraction.
  • feat: remove Kimi For Coding preset from Codex
    The Kimi For Coding preset pins users to a single coding-only model
    (kimi-for-coding), restricting model selection. Drop it from the Codex
    presets and trim the corresponding row in the trilingual user manual.
    The general Kimi preset (Moonshot platform) is kept, and presets for
    other apps are unchanged.
  • fix: reclassify ClaudeAPI as aggregator to re-enable model test
    ClaudeAPI supports the health/stream check, but the third_party
    category triggers the isClaudeThirdParty gate in ProviderCard that
    disables the model test button for third-party Claude providers.
    Recategorizing as aggregator restores the test action; the partner
    star is unaffected since it is driven by isPartner, not category.
    
    Applied to both the Claude Code and Claude Desktop presets.
  • refactor: rewrite Codex local routing toggle hints for clarity
    Reframe OFF/ON hints as action guidance (when to enable) rather than
    scenario descriptions, mirroring the Claude Desktop model mapping copy
    style. Synced across zh/en/ja locales and the component-level
    defaultValue fallback. The switch label "Needs Local Routing" is kept
    unchanged to preserve the routing-layer semantics specific to Codex.
  • feat: add Chat Completions routing for 22 Codex third-party presets
    Enable openai_chat routing with explicit model catalogs across the major
    Chinese/Asian providers (DeepSeek, Zhipu GLM, Kimi, MiniMax, Baidu Qianfan,
    Bailian, StepFun, Volcengine Agent Plan, BytePlus, DouBaoSeed, ModelScope,
    Longcat, BaiLing, Xiaomi MiMo, SiliconFlow, Novita AI, Nvidia, etc.). Each
    preset declares its context window so the UI can size catalog rows when the
    preset is picked.
    
    Also lands two consistency fixes uncovered along the way:
    - Include setCodexCatalogModels in resetCodexConfig's useCallback deps to
      match the new third parameter it consumes.
    - Realign TheRouter Codex test to the "custom" model_provider bucket
      established by the recent third-party unification; the previous assertion
      predated that refactor and had been failing on HEAD.
  • feat: unify Codex third-party providers into stable "custom" history bucket
    Codex filters resume history by `model_provider`, so switching between
    provider-specific ids like `rightcode` and `aihubmix` made past sessions
    appear to vanish. Collapse all third-party providers into a single
    stable bucket so cross-switch history stays visible.
    
    - Normalize live `model_provider` to "custom" on every Codex write
      (reserved built-in ids like openai/ollama are preserved).
    - Add device-level one-shot migration that rewrites historical JSONL
      session files and the `state_5.sqlite` threads table from legacy
      provider ids into the "custom" bucket. Backs up originals under
      `~/.cc-switch/backups/codex-history-provider-migration-v1/` and uses
      the SQLite Backup API for the state DB.
    - Record completion in `settings.json` under `localMigrations` so the
      migration is strictly idempotent across launches.
    - Update Codex provider preset templates to emit `model_provider = "custom"`
      out of the box.
  • fix: prevent infinite render loop in Codex model catalog
    Break bidirectional sync cycle between catalogRows (child) and catalogModels (parent):
    - Remove catalogModels from child→parent effect dependencies
    - Track last sent data in ref to avoid redundant callbacks
    - Sync ref when parent pushes new data to prevent false positives
    
    Fixes severe UI jittering when adding/editing catalog entries.