Commit Graph

138 Commits

  • 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).
  • feat(usage): add official subscription quota template with unified tier rendering
    Changes:
    - Add official_subscription template type for Claude/Codex/Gemini
    - Replace implicit 'category=official auto-query' with explicit opt-in template
    - Default disabled; users enable via usage script modal with configurable interval
    - Unify tier→label mapping across subscription and script paths via labeled_tier_parts()
    - Fix tray rendering: week aliases (seven_day/opus/sonnet) now use highest utilization
    - Add depth guard: official_subscription checks enabled flag in query_provider_usage_inner
    - Add cache invalidation symmetry: invalidate_subscription() for disabled providers
    - i18n: add templateOfficialSubscription + hint in zh/en/ja/zh-TW
    
    Backend (Rust):
    - provider.rs: add TEMPLATE_TYPE_OFFICIAL_SUBSCRIPTION branch, flatten SubscriptionQuota→UsageData
    - tray.rs: extract labeled_tier_parts() shared by both summary functions, use max_by for multi-alias groups
    - usage_cache.rs: add invalidate_subscription() method
    - Test coverage: add week-alias highest-utilization tests for both paths
    
    Frontend (TypeScript):
    - UsageScriptModal: add official_subscription to templates, auto-detect for official providers
    - ProviderCard: gate useUsageQuery with !isOfficialSubscriptionUsage, pass autoQueryInterval to footer
    - SubscriptionQuotaFooter: accept autoQueryInterval prop, default 0 (disabled)
    - constants.ts: add TEMPLATE_TYPES.OFFICIAL_SUBSCRIPTION
    
    Fixes tier rendering regression where:
    - Claude/Codex: seven_day was missed (only weekly_limit matched) → lost 7-day window in tray
    - Gemini: gemini_pro/flash/flash_lite fell through to fallback → leaked machine names
    - Multi-window (opus+sonnet): find() took first, not worst → underestimated utilization and emoji color
    
    All tests pass (cargo test + cargo clippy clean).
  • feat: 新增 S3 兼容云存储同步 (#1351)
    * Add S3 Cloud Sync design document
    
    Design for adding AWS S3 as a new Cloud Sync backend alongside WebDAV.
    Hybrid approach: extract shared sync protocol, add independent S3 transport.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * Add S3 cloud sync implementation design (reqwest + Sig V4)
    
    Updated design based on 2026-03-06 draft: switches from rust-s3 crate
    to hand-rolled AWS Sig V4 on existing reqwest for broader S3-compatible
    service support (AWS, MinIO, R2, Alibaba OSS, Tencent COS, Huawei OBS).
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * Add S3 cloud sync implementation plan (11 tasks, TDD)
    
    Detailed step-by-step plan covering: sync_protocol extraction, S3 Sig V4
    transport, settings, sync/auto-sync modules, Tauri commands, frontend
    presets/dynamic form, and i18n.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * deps: add hmac crate for S3 Sig V4 signing
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * refactor: extract sync_protocol.rs from webdav_sync.rs for shared use
    
    Move transport-agnostic sync protocol logic (constants, types, snapshot
    building, manifest validation, artifact verification, snapshot application,
    utilities) into a new shared sync_protocol module so both WebDAV and the
    upcoming S3 transport can reuse it.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix: use transport-neutral error keys in sync_protocol
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 transport layer with AWS Sig V4 signing
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3SyncSettings to AppSettings
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync module with upload/download/fetch
    
    Implements the S3 sync protocol layer (s3_sync.rs) that combines the
    shared sync_protocol with the S3 transport. Mirrors the WebDAV sync
    module structure with independent sync mutex, connection check,
    upload, download, fetch_remote_info, and sync status persistence.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 auto sync worker with debounce
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync Tauri commands and auto sync worker startup
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync TypeScript types and API layer
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync i18n translations (en/zh/ja)
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add S3 sync presets and dynamic form to sync settings
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix: preserve HTTP scheme for S3 custom endpoints (MinIO support)
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * test: add live S3 integration tests (env-var driven, --ignored)
    
    Run with: S3_TEST_AK=... S3_TEST_SK=... S3_TEST_BUCKET=... cargo test --lib services::s3::integration_tests -- --ignored
    
    Verifies test_connection, put_object, get_object, head_object, and 404
    handling against a real S3 bucket using the project's own Sig V4 signing.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * chore: remove internal design docs before PR
    
    * fix: wire S3 auto-sync to DB hook & sync UI state on async load
    
    - P1: Add s3_auto_sync::notify_db_changed call in SQLite update_hook
      so S3 auto-sync worker receives DB change signals (was only wired
      for WebDAV, leaving S3 worker idle)
    
    - P2: Add useEffect to update syncType selector when s3Config loads
      asynchronously, preventing stale "webdav" default for S3 users
    
    * fix: satisfy clippy for s3 sync
    
    * fix: address s3 sync review feedback
    
    ---------
    
    Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com>
    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • 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>
  • 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.
  • 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>
  • 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>
  • Add Chat Completions routing for Codex providers
    - Add a Codex API format selector and routing badge for Chat Completions providers.
    - Convert Codex Responses requests to upstream Chat Completions when routing is required.
    - Convert Chat Completions JSON and SSE responses back to Responses format.
    - Keep generated Codex wire_api values on Responses for Codex compatibility.
    - Add i18n labels, provider metadata handling, and focused conversion tests.
  • Fix race condition in useEffect hooks and type assertion bug (#2827)
    - Add active flag pattern to 3 useEffect hooks in App.tsx to prevent
      event listener leaks when component unmounts before async setup completes
    - Add guard check in useSettings.ts to prevent undefined from being
      stored in localStorage when payload.language is missing
    
    Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
  • refactor(claude-desktop): trim duplication in proxy and switch flows
    - services/proxy.rs: collapse 10 repeated `OpenCode | OpenClaw | Hermes |
      ClaudeDesktop` match arms into `_` fallthroughs.
    - claude_desktop_config.rs: extract a `with_rollback` closure shared by
      apply_provider_to_paths and restore_official_at_paths.
    - useProviderActions.ts: replace the triple-nested ternary picking the
      switch-success toast message with a flat let/if/else block.
    
    Net -36 lines. No behavior change; cargo test and pnpm typecheck pass.
  • feat(claude-desktop): add 3P provider switching with proxy gateway
    Adds a new ClaudeDesktop AppType that writes Claude Desktop's third-party
    inference profile under configLibrary/, sharing _meta.json with other
    launchers (Ollama-compatible) so cc-switch can coexist with them.
    
    Two switch modes:
    - direct: provider already exposes claude-* / anthropic/claude-* model
      ids on Anthropic Messages, Claude Desktop connects to it directly.
    - proxy: cc-switch's local proxy acts as the inference gateway,
      presenting only claude-* route names to Claude Desktop and mapping
      them to real upstream models. Required after Anthropic restricted
      Claude Desktop to claude-family ids.
    
    Backend:
    - New module claude_desktop_config with snapshot/rollback, official seed
      bypass, /claude-desktop/v1/{models,messages} routes, and a single
      source of truth for default proxy routes.
    - Gateway token persisted in SQLite, validated on every proxied request.
    - get_claude_desktop_status surfaces drift signals (stale models,
      missing routes, proxy stopped, base URL mismatch, missing token).
    
    Frontend:
    - Slim ClaudeDesktopProviderForm independent from ProviderForm,
      controlled by a top-level appId guard.
    - ProviderList banner consumes the status query (5s polling) and
      renders actionable diagnostics.
    - ClaudeDesktopRouteToggle in the header to start/stop the local
      gateway without touching takeover state.
    - Three-locale i18n synchronised.
  • feat(providers): add Baidu Qianfan Coding Plan for Claude Code (#2322)
    * feat(providers): add baidu qianfan coding plan presets
    
    * refactor(providers): align qianfan presets with existing format
    
    * chore(providers): narrow qianfan coding plan scope
  • feat(tray): show coding-plan usage for Kimi / Zhipu / MiniMax
    dc04165f surfaced tray usage badges for Claude/Codex/Gemini official
    OAuth only. Chinese coding-plan providers already expose 5h + weekly
    windows through coding_plan::get_coding_plan_quota, but two gaps kept
    the tray from rendering them.
    
    - format_script_summary read only data.first(), truncating the tier-
      flattened UsageResult to a single window. Detect plan_name matching
      TIER_FIVE_HOUR / TIER_WEEKLY_LIMIT and emit the "🟢 h12% w80%" layout
      used by format_subscription_summary; worst utilization drives the
      emoji. Copilot / balance / custom scripts keep the legacy single-
      bucket output via fallback.
    
    - usage_script previously required manual activation through
      UsageScriptModal. Auto-inject meta.usage_script on Claude provider
      creation when ANTHROPIC_BASE_URL matches a known coding plan, so the
      tray lights up without the user opening the modal. Does not overwrite
      existing usage_script on update.
    
    Extract the URL route table out of UsageScriptModal into a shared
    codingPlanProviders module so the modal, the creation hook, and the
    Rust coding_plan::detect_provider mirror all agree on one list.
    Add TIER_WEEKLY_LIMIT alongside TIER_FIVE_HOUR and a createUsageScript()
    factory to collapse the duplicated default fields across four call
    sites and drop the remaining stringly-typed tier names.
  • refactor(hermes): drop config health check scanner
    The Hermes config.yaml schema has stabilized and users have migrated to
    the current provider fields, so the value of scanning for model.provider
    dangling references, custom_providers shape errors, v12 migration residue
    etc. no longer justifies the maintenance surface — and the scan produces
    false positives when users keep some providers under Hermes' v12+
    providers: dict (Hermes' runtime merges both shapes, but CC Switch's
    scanner only looked at the list form).
    
    Removes the whole HermesHealthWarning type, scan_hermes_config_health
    command, HermesHealthBanner React component, useHermesHealth hook,
    warnings field on HermesWriteOutcome, and the three helper functions
    (yaml_as_non_empty_str, collect_mapping_string_keys, hermes_warning)
    that only served the scanner. Drops the matching i18n keys in
    zh/en/ja and the fixInWebUI button label that only the banner used.
  • feat(tray): show cached provider usage in the system tray menu (#2184)
    * feat: add Rust-side write-through usage cache
    
    Introduce an in-memory UsageCache on AppState that the existing usage
    query commands populate on success. The cache is read-only to the rest
    of the app today; the next commit consumes it from the tray menu.
    
    - New services::usage_cache module with split maps: subscription keyed
      by AppType, script keyed by (AppType, provider_id).
    - AppType gains Eq + Hash so it can be used as a HashMap key.
    - commands::subscription::get_subscription_quota now takes State<AppState>
      and writes through on success (signature change is invisible to the
      frontend — Tauri injects State automatically).
    - commands::provider::queryProviderUsage body extracted into an inner
      async fn; the public command wraps it with write-through, covering
      Copilot, coding-plan, balance, and generic script paths uniformly.
    
    Cache is in-memory only; auto-query interval and the upcoming tray
    refresh action rebuild it after restarts.
    
    * feat(tray): surface cached usage in the system tray menu
    
    Read UsageCache populated by the previous commit and render it in three
    places, scoped to whatever TRAY_SECTIONS covers (Claude/Codex/Gemini):
    
    1. Inline suffix on each provider submenu item
       "AnyProvider  · 🟢 5h 18% / 7d 23%"
    2. Disabled summary row per visible app under "Show Main"
       "Claude · Anthropic Official · 🟢 5h 18% / 7d 23%"
    3. "Refresh all usage" menu item that triggers get_subscription_quota +
       queryProviderUsage for every applicable provider, then rebuilds the
       tray menu via the existing refresh_tray_menu path.
    
    Color encoding uses emoji (🟢 <70% / 🟠 70-89% / 🔴 ≥90%) since Tauri 2
    tray labels are plain text. Missing cache entry leaves the label
    unchanged — tray never issues network requests when opened. Three new
    i18n-ready strings live in TrayTexts (en/zh/ja), following the existing
    pattern for tray text.
    
    Closes #2178.
    
    * feat(usage): bridge tray UsageCache writes to frontend React Query
    
    Why: tray hover triggers backend-only refresh that wrote to UsageCache but
    never notified the frontend, leaving main UI stale while tray showed fresh
    numbers. Emit a payload-carrying event after each cache write so React Query
    can setQueryData directly, keeping both views in sync without duplicate fetches.
    
    * fix(tray): skip hidden apps on hover refresh and drop stale disabled-script cache
    
    Address P2 findings from automated review on #2184:
    
    1. refresh_all_usage_in_tray now filters TRAY_SECTIONS by settings.visible_apps
       before scheduling subscription/script queries, matching create_tray_menu and
       preventing wasted external API calls (and rate-limit/auth-error log noise)
       for apps the user has hidden.
    
    2. format_usage_suffix only trusts the script cache when provider.meta.usage_script
       is still enabled; when a script is disabled/removed the cached suffix is now
       invalidated so the tray label no longer shows stale data indefinitely.
    
    * refactor: consolidate codex provider helpers and fix test semantics
    
    - Add Provider::is_codex_oauth() and Provider::codex_fast_mode_enabled()
      to eliminate duplicated meta extraction in claude.rs and stream_check.rs
    - Fix non-codex-oauth tests to pass codex_fast_mode=false (was true, harmless
      but semantically misleading)
    - Remove redundant is_dir() guard after resolve_skill_source_dir already
      guarantees the returned path is a directory
    
    * style: apply cargo fmt
    
    * fix(tray): reflect failed refreshes in cache and support Gemini flash-lite
    
    Follow-up to the tray usage-display feature addressing review feedback:
    
    - Write snapshots for both Ok(success:false) and Err paths in
      queryProviderUsage / get_subscription_quota so stale success data
      no longer persists across failed refreshes; the original Err is
      still returned to the frontend onError handler.
    - Include gemini_flash_lite tier in the tray summary with label "l".
      Matches the frontend SubscriptionQuotaFooter and keeps the worst
      emoji correct when lite is the highest utilization.
    - Add TIER_GEMINI_PRO / _FLASH / _FLASH_LITE constants in
      services/subscription.rs and reuse them in classify_gemini_model
      and sort_order.
    - Extract Provider::has_usage_script_enabled() to remove the
      duplicated meta.usage_script chain at two call sites.
    - Use db.get_provider_by_id in refresh_all_usage_in_tray instead of
      materialising the full provider map, and parallelise subscription
      and script futures via futures::future::join.
    - Narrow refresh_all_usage_in_tray to each section's effective
      current provider (script if enabled, else subscription when the
      provider is official). Hover refreshes now issue at most
      TRAY_SECTIONS.len() outbound requests.
    - Add 10 unit tests in tray::tests covering Claude/Codex h/w dispatch,
      Gemini p/f/l dispatch (including lite-only and lite-worst cases),
      and success/failure guards.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(skills): prevent duplicate imports when import button is double-clicked (#2211)
    Closes #2139
    
    Two related defects let the installed-skills count balloon when users
    tap the import confirm button multiple times — either deliberately or
    because the button is still clickable while a slow import is in flight:
    
    - The confirm button only disabled itself while `selected.size === 0`,
      so it stayed clickable during a pending mutation. Each extra click
      triggered another `importFromApps` mutation.
    - `useImportSkillsFromApps` appended the server response to the
      installed cache without deduping, so re-firing the mutation stacked
      the same skills into the list again.
    
    Disable the confirm (and cancel) buttons while the mutation is pending
    — matching the `isRestoring` / `isDeleting` pattern already used by
    `RestoreSkillsDialog` — and merge success payloads by
    `InstalledSkill.id` so repeated results overwrite rather than
    accumulate.
    
    The merge is extracted as a pure `mergeImportedSkills` reducer to make
    the behaviour unit-testable and to short-circuit on an empty payload,
    returning the existing reference so React Query does not notify
    subscribers about a no-op cache update.
  • feat(hermes): launch dashboard from toolbar when Web UI is offline
    When the Hermes Web UI probe fails, the toolbar entry now opens an info
    confirm dialog offering to run `hermes dashboard` in the user's preferred
    terminal. Accepting spawns it via a temp bash/batch script; `hermes
    dashboard` itself opens the browser once ready, so we do not poll.
    The Memory panel and Health banner keep the existing toast behavior.
    
    Also corrects the stale `hermes web` hint in the offline toast (the real
    command is `hermes dashboard`) and reorders Linux terminal detection to
    try `which` before stat'ing /usr/bin, /bin, /usr/local/bin.
  • fix(header): stop auto-compact from latching after maximize
    useAutoCompact cached normalWidthRef = el.scrollWidth on every
    non-compact resize, but per DOM spec scrollWidth === clientWidth
    when content fits. Maximizing the window (content no longer
    overflows) therefore wrote the container width into
    normalWidthRef, making it impossible to re-enter compact when
    the window was restored to its original size.
    
    Move the assignment inside the overflow branch so the cache is
    only written at the actual compact threshold, where scrollWidth
    reflects the real content width.
  • feat(hermes): memory enable switch + clearer migration warning copy
    Replaces the greyed-out "Memory is disabled" banner with a real Switch
    at the top of each memory tab. Users can now toggle Hermes' memory/user
    blobs without leaving CC Switch; the underlying write goes through the
    merge-aware `set_memory_enabled`, so budgets and external-provider
    settings survive toggle operations. The new `useToggleHermesMemoryEnabled`
    mutation invalidates the limits query so the Switch state and the
    amber disabled-hint update in lockstep.
    
    Reworks the `schema_migrated_v12` health banner copy to match the
    simplified "CC Switch only manages custom_providers" posture — it now
    tells users to reconcile migrated dict entries via Hermes Web UI,
    instead of the earlier (and now inaccurate) "CC Switch reads both".
  • feat(settings): add Hermes config dir override with data-driven dispatch
    Adds a dedicated Hermes row to the directory-override settings so users
    can point CC Switch at alternate Hermes config locations (e.g. a second
    profile directory for work/personal split). `get_config_dir` on the
    Rust side already supports hermes; this just wires up the frontend row.
    
    Wiring it through `useDirectorySettings` revealed a scaling problem:
    every supported app required five parallel ternary chains across
    `computeDefaultConfigDir`, `updateDirectory`, `browseDirectory`,
    `resetDirectory`, and `updateDirectoryState`. Replaces those with two
    lookup tables (`APP_DIRECTORY_META`, `DIRECTORY_KEY_TO_SETTINGS_FIELD`)
    so adding the next app is two entries, not fifteen edit sites.
    
    Drive-by cleanup from the same touch:
    * `resetAllDirectories` takes a `ResolvedAppDirectoryOverrides` object
      instead of five positional optional strings.
    * `setResolvedDirs` returns the same reference when the sanitized
      value is unchanged, so no-op edits don't cascade renders.
    
    Also lands all i18n updates for this series (`hermesConfigDir` and
    placeholder, Memory section's enable/disable/toggleFailed copy, and
    the reworded `schemaMigratedV12` warning) in zh/en/ja together.
  • feat(hermes): replace Prompts entry with Memory panel
    Hermes has no slash-prompt concept (templates live as Skills), so the
    Prompts tab for the Hermes app was always empty. Swap the toolbar Book
    button for a Brain button that opens a new Memory panel editing
    ~/.hermes/memories/{MEMORY,USER}.md — Hermes' first-class memory store
    which its Web UI exposes only as on/off toggles, never as an editor.
    
    The panel shows each file in its own tab with a character-budget bar
    read from config.yaml's nested memory.* section (memory_char_limit /
    user_char_limit, default 2200 / 1375). Edits are written atomically;
    Hermes picks them up on the next session start per MemoryStore.
    
    Also extract useDarkMode to src/hooks/useDarkMode.ts — the codebase
    already repeats the same MutationObserver pattern in 12+ places; this
    PR introduces the shared hook and uses it once, leaving the migration
    of the other copies to a follow-up.
  • refactor(hermes): delegate deep config to Hermes Web UI
    Slim the Hermes surface in CC Switch to match its core positioning —
    cross-client provider switching and shared MCP/prompts/skills — and
    delegate deep configuration (model, agent, env, skills, cron, logs)
    to the Hermes Web UI at http://127.0.0.1:9119.
    
    - Drop AgentPanel/EnvPanel/ModelPanel and their mutation commands,
      hooks, types, and i18n keys across zh/en/ja.
    - Add open_hermes_web_ui Tauri command that probes /api/status and
      launches the URL in the system browser. Hermes injects its own
      session token into the returned HTML, so CC Switch doesn't need
      to touch auth.
    - Surface the launcher from the Hermes toolbar and the health banner
      via a shared useOpenHermesWebUI() hook; the offline error code is
      defined once per side and referenced across the contract.
    - Keep read-only access to model.provider so ProviderList can still
      highlight the active supplier; apply_switch_defaults continues to
      write the top-level model section when switching providers.
    
    Net diff: +152 / -1253.
  • fix(hermes): show active provider and wire add/enable/remove actions
    Switching a Hermes provider previously only fired a toast because the frontend treated Hermes as non-additive (unlike backend AppType::is_additive_mode, which lists OpenCode | OpenClaw | Hermes) and relied on the unused is_current DB flag for highlighting. Align the UI model with the backend:
    
    - Include Hermes in ProviderActions' isAdditiveMode so the main button switches between "Add" and "Remove".
    - Drive the "current" highlight from model.provider (via useHermesModelConfig) instead of the DB is_current field; model.provider is Hermes's real SSOT for the active provider.
    - Reuse OpenClaw's set-as-default button slot to expose an "Enable" action on Hermes that calls switchProvider, so providers already in config can be activated without re-adding. switch_normal + apply_switch_defaults already atomically update custom_providers and model.provider, so no backend change is needed.
    - Invalidate liveProviderIds + modelConfig + health in parallel after add/update/delete/switch via a new invalidateHermesProviderCaches helper, replacing four copies of three sequential awaits.
  • feat: add Hermes UI components, presets, and config panels (Phase 8)
    - Add 7 provider presets (OpenRouter, Anthropic, OpenAI, Google, DeepSeek, Together, Nous)
    - Create HermesFormFields + useHermesFormState for provider form integration
    - Create Model/Agent/Env config panels with save/load functionality
    - Create HermesHealthBanner for config warnings
    - Add hermes icon (violet winged H) to icon system
    - Integrate into App.tsx: 3 new view types (hermesModel/hermesAgent/hermesEnv),
      sidebar buttons (Brain/Bot/KeyRound), health banner, session support
    - Integrate into ProviderForm: presets, form state, key validation, rendering
    - Integrate into AddProviderDialog: universal tab exclusion, providerKey, base_url extraction
    - Add i18n keys for all Hermes UI (zh/en/ja)
  • feat: add Hermes frontend types, API layer, and hooks (Phase 7)
    - Add "hermes" to AppId union type and all exhaustive Record<AppId>
    - Add HermesModelConfig, HermesAgentConfig, HermesEnvConfig types
    - Add hermes field to VisibleApps, McpApps, ProxyTakeoverStatus
    - Create src/lib/api/hermes.ts with Tauri invoke wrappers
    - Create src/hooks/useHermes.ts with 5 query + 3 mutation hooks
    - Register hermes in APP_IDS, APP_ICON_MAP (violet color scheme)
    - Split MCP_SKILLS_APP_IDS into MCP_APP_IDS (includes hermes) and
      SKILLS_APP_IDS (excludes hermes, since Hermes has no Skills support)
    - Wire hermes additive-mode into App.tsx (remove/duplicate handlers),
      ProviderList.tsx (live provider ID query + In Config badge),
      mutations.ts (cache invalidation on switch/add/delete)
    - Add Hermes checkbox to McpFormModal
    - Add basic hermes i18n keys (en/zh/ja)
  • fix: surface backend error details in proxy toast messages
    The takeover.failed i18n template lacked the {{detail}} placeholder
    and three useProxyStatus onError callbacks omitted the detail variable,
    so proxy start/stop/takeover failures all displayed a generic message
    regardless of the underlying cause.
  • fix(claude-plugin): sync current provider config to settings.json (#1905)
    * fix(claude-plugin): sync current provider config to settings.json on toggle enable
    
    - Extract syncClaudePluginIfChanged to share logic between autoSaveSettings and saveSettings
    - Fix P1: enableClaudePluginIntegration toggle in General tab now actually syncs ~/.claude/settings.json
    - Fix P2: check syncCurrentProvidersLiveSafe() return value and show toast on failure
    - Fix P3: sync providers on both enable and disable, not just enable
    - Fix P4: avoid double syncCurrentProvidersLiveSafe when plugin toggle + dir change happen together
    - Remove duplicate comment
    - Add missing providersApi.getCurrent/getAll mocks in tests
    
    * style: reformat after rebase onto main
    
    Prettier flagged a line-break introduced by the openclaw directory
    change (from main) after rebase.
    
    * fix(claude-plugin): read prev enabled state from live cache to avoid stale closure
    
    syncClaudePluginIfChanged compared enabled against data?.enableClaudePluginIntegration
    captured in a useCallback closure. After invalidateQueries + refetch, the React
    Query cache is up to date, but the consuming hook's closure does not see the new
    value until React re-renders. Quick on->off toggles could therefore skip
    applyClaudePluginConfig, leaving ~/.claude/config.json in the previously enabled
    state even though settings.json was persisted as disabled.
    
    Read the previous value synchronously from queryClient.getQueryData(["settings"])
    before saveMutation.mutateAsync(), then pass it to the helper as prevEnabled.
    getQueryData bypasses the closure and reflects the live cache at call time.
    
    Test covers the race: closure data stays at false while the cache reports true;
    the helper must still call applyClaudePluginConfig({ official: true }).
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • feat(stream-check): refresh default models and detect model-not-found errors (#2099)
    * chore(stream-check): update default health check models to latest
    
    Replaces deprecated gpt-5.1-codex@low with gpt-5.4@low and switches
    the Gemini default from gemini-3-pro-preview to gemini-3-flash-preview
    to pick the lightest variant of the latest series for fast, low-cost
    health checks.
    
    https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte
    
    * feat(stream-check): detect model-not-found errors with dedicated toast
    
    Health check previously classified failures purely by HTTP status code,
    which meant deprecated/invalid models showed up as a generic "Not found
    (404)" error pointing users to check the Base URL — misleading when the
    URL is fine and only the test model is wrong (e.g. gpt-5.1-codex after
    it was retired).
    
    Backend: add detect_error_category() that inspects 4xx response bodies
    for model-not-found indicators (model_not_found, does not exist,
    invalid model, not_found_error, etc.) and returns a "modelNotFound"
    category. Thread the resolved test model through build_stream_check_result
    so the failed result carries it in model_used. Add StreamCheckResult
    .error_category field (serde-skipped when None).
    
    Frontend: useStreamCheck branches on errorCategory === "modelNotFound"
    before the HTTP-status fallback and renders a toast.error with the model
    name and a description pointing to Model Test Config. Add i18n keys
    (modelNotFound / modelNotFoundHint) for zh/en/ja.
    
    Tests: unit-test detect_error_category against real OpenAI/Anthropic
    error shapes, 5xx false-positive avoidance, and plain 401 auth errors.
    
    https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte
    
    * fix(stream-check): add missing error_category field in fallback
    
    The error_category field was added to StreamCheckResult in this branch
    but the fallback constructor in stream_check_all_providers was not
    updated, which broke cargo build.
    
    ---------
    
    Co-authored-by: Claude <noreply@anthropic.com>
  • feat: classify stream check errors with color-coded toasts
    Distinguish between "provider rejects probe" (yellow warning) and
    "genuinely broken" (red error) in health check results.
    
    Backend: add AppError::HttpStatus variant to carry structured HTTP
    status codes, populate http_status on error results, classify codes
    into short labels (e.g. "Auth rejected (401)"), and truncate overly
    long response bodies.
    
    Frontend: route 401/403/400/429/5xx to toast.warning with localized
    hints explaining the error may not indicate actual unusability; route
    404/402/connection errors to toast.error. Add i18n keys for all three
    locales (zh/en/ja).
    
    Also deduplicate check_once by reusing build_stream_check_result.
  • feat: block official provider switching during proxy takeover
    Prevent users from switching to official providers (Anthropic/OpenAI/Google)
    when proxy takeover is active, as using a proxy with official APIs may cause
    account bans.
    
    Defense-in-depth across 4 layers:
    - Backend: ProviderService::switch(), hot_switch_provider(), switch_proxy_provider command
    - Frontend: useProviderActions soft guard with error toast
    - UI: ProviderActions button disabled with ShieldAlert icon
    - Tray menu: official provider items disabled with  indicator
    
    Also warns when enabling proxy takeover while current provider is official.
  • 添加应用级别窗口按钮,以改善linux wayland下系统窗口按钮失效的问题 (#1119)
    * feat(window): add app-level window controls with settings toggle
    
    Add a persistent settings toggle to enable app-level minimize/maximize/close controls and hide system decorations when enabled, providing a Wayland-friendly fallback for broken native titlebar interactions.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
    
    * fix(window): restrict app-level window controls to Linux only and fix startup flicker
    
    - Guard useAppWindowControls with isLinux() in App.tsx so it's always
      false on macOS/Windows even if persisted as true
    - Wrap set_decorations call in lib.rs with #[cfg(target_os = "linux")]
    - Only show the toggle in WindowSettings on Linux
    - Skip setDecorations effect while settingsData is still loading to
      prevent the Rust-side decoration state from being overridden by the
      undefined->false fallback, which caused a brief title bar flicker
    
    ---------
    
    Co-authored-by: wzk <wx13571681304@outlook.com>
    Co-authored-by: Cursor <cursoragent@cursor.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix(session-manager): improve session search accuracy and Chinese support
    - Pre-filter sessions by provider before indexing to prevent result
      truncation when FlexSearch limit cuts across providers
    - Switch tokenizer from "forward" to "full" for Chinese substring matching
    - Preserve FlexSearch relevance ranking when search query is present
  • fix(notifications): remove duplicate toast when switching to proxy providers
    When switching to Copilot/ChatGPT/OpenAI-format providers with the proxy
    not running, two toasts appeared: a "proxy required" warning followed by
    a "switch success" toast. Unify the post-switch toast logic so that all
    provider types show a single success toast, and skip it entirely when
    a proxy-required warning was already shown.
  • feat: integrate skills.sh search for discovering skills from public registry
    Add skills.sh API integration allowing users to search and install from
    a catalog of 91K+ agent skills directly within CC Switch. The search
    results are converted to DiscoverableSkill objects and reuse the existing
    install pipeline. Includes fallback directory search for repos where
    skills are nested in subdirectories, and filters out non-GitHub sources.
  • feat: add skill update detection via SHA-256 content hashing
    - Add content_hash and updated_at fields to skills table (DB migration v6→v7)
    - Compute directory content hash on install/import/restore for version tracking
    - Add check_updates command: downloads repos, compares hashes, returns update list
    - Add update_skill command: backs up old files, re-downloads and replaces SSOT
    - Backfill content_hash for existing skills on first update check
    - Add "Check Updates" button and per-skill update badge/button in UnifiedSkillsPanel
    - Add i18n keys for zh/en/ja
  • fix: allow provider switch without proxy, show warning instead of blocking
    Remove the hard block that prevented switching to providers requiring
    proxy (OpenAI format, Copilot, full URL mode) when the proxy is not
    running. Now the switch proceeds with a warning toast. Also deduplicate
    the proxy hint info toast so it doesn't appear alongside the warning.
  • feat(provider): additive provider key lifecycle & fix openclaw serializer panic (#1724)
    * feat(provider): support additive provider key lifecycle management
    
    Add `addToLive` parameter to add_provider so callers can opt out of
    writing to the live config (e.g. when duplicating an inactive provider).
    Add `originalId` parameter to update_provider to support provider key
    renames — the old key is removed from live config before the new one
    is written.
    
    Frontend: ProviderForm now exposes provider-key input for openclaw app
    type, and EditProviderDialog forwards originalId on save. Deep-link
    import passes addToLive=true to preserve existing behavior.
    
    * test(provider): add integration tests for additive provider key flows
    
    Cover openclaw provider duplication scenario to verify that a generated
    provider key is assigned automatically. Add MSW handlers for
    get_openclaw_live_provider_ids, get_openclaw_default_model,
    scan_openclaw_config_health, and check_env_conflicts endpoints.
    Update EditProviderDialog mock to pass originalId alongside provider.
    
    * fix(openclaw): replace json-five serializer to prevent panic on empty collections
    
    json-five 0.3.1 panics when pretty-printing nested empty maps/arrays.
    Switch value_to_rt_value() to serde_json::to_string_pretty() which
    produces valid JSON5 output without the panic. Add regression test for
    removing the last provider (empty providers map).
    
    * style: apply rustfmt formatting to proxy and provider modules
    
    Reformat chained .header() calls in ClaudeAdapter and StreamCheckService
    for consistent alignment. Reorder imports alphabetically in stream_check.
    Fix trailing whitespace in transform.rs and merge import lines in
    provider/mod.rs.
    
    * style: fix clippy warnings in live.rs and tray.rs
    
    * refactor(provider): simplify live_config_managed and deduplicate tolerant live config checks
    
    - Change live_config_managed from Option<bool> to bool with #[serde(default)]
    - Extract repeated tolerant live config query into check_live_config_exists helper
    - Fix duplicate key generation to also check live-only provider IDs
    - Fix updateProvider test to match new { provider, originalId } call signature
    - Add streaming_responses test type annotation for compiler inference
    
    * fix(provider): distinguish legacy providers from db-only when tolerating live config errors
    
    Change `ProviderMeta.live_config_managed` from `bool` to `Option<bool>`
    to introduce a three-state semantic:
    - `Some(true)`: provider has been written to live config
    - `Some(false)`: explicitly db-only, never written to live config
    - `None`: legacy data or unknown state (pre-existing providers)
    
    Previously, legacy providers defaulted to `live_config_managed = false`
    via `#[serde(default)]`, which silently swallowed live config parse
    errors. This could mask genuine configuration issues for providers that
    had actually been synced to live config before the field was introduced.
    
    Now, only providers with an explicit `Some(false)` marker tolerate parse
    errors; legacy `None` providers surface errors as before, preserving
    safety for already-managed configurations.
    
    Also wrap the `ensureQueryData` call for live provider IDs during
    duplication in a try/catch so that a malformed config file shows a
    user-facing toast instead of silently failing.
    
    Add tests for both the legacy error propagation path and the frontend
    duplication failure scenario.
    
    * refactor(provider): unify OMO variant updates with atomic file-then-db writes and rollback
    
    Consolidate the duplicated omo/omo-slim update branches into a single
    match on the variant. Write the OMO config file from the in-memory
    provider state *before* persisting to the database, so a file-write or
    plugin-sync failure leaves the database unchanged. If `add_plugin`
    fails after the config file is already written, roll back to the
    previous on-disk contents via snapshot/restore.
    
    Also:
    - `sync_all_providers_to_live` now skips db-only providers
      (`live_config_managed == Some(false)`) instead of attempting to write
      them to live config.
    - `import_{opencode,openclaw}_providers_from_live` mark imported
      providers as `live_config_managed: Some(true)` so they are correctly
      recognized during subsequent syncs.
    - Extract OmoService helpers: `profile_data_from_provider`,
      `snapshot_config_file`, `restore_config_file`, `write_profile_config`,
      and the new public `write_provider_config_to_file`.
    - Add 9 new tests covering sync skip, legacy restore, import marking,
      OMO persistence, file-write failure, and plugin-sync rollback.
    
    * fix(provider): fix additive provider delete/switch regressions and redundancy
    
    - fix(delete): replace stale live_config_managed flag check with
      check_live_config_exists so providers written to live before the
      flag-flip logic was introduced are still cleaned up on delete
    - fix(switch): make write_live_with_common_config return Err instead of
      silently returning Ok when config structure is invalid, preventing
      live_config_managed from being incorrectly flipped to true
    - fix(update): block provider key rename for OMO/OMO Slim categories to
      prevent orphaned current-state markers breaking OMO file syncs
    - fix(switch): flip live_config_managed to true after successful live
      write for DB-only additive providers so sync_all_providers_to_live
      includes them on future syncs; roll back live write if DB update fails
    - refactor(delete): merge symmetric OMO/OMO-Slim blocks into single
      match-on-variant path; hoist DB read to top of additive branch
    - refactor(remove_from_live_config): merge OMO/OMO-Slim if/else-if
      into single match-on-variant path
    - refactor(switch_normal): merge two OMO/OMO-Slim if blocks into one
      OpenCode guard with (enable, disable) variant pair
    - fix(update): remove redundant duplicate return Ok(true) after OMO
      current-state write
    
    * fix(test): use preferred_filename after OMO field rename
    
    The merge from main brought in #1746 which renamed
    OmoVariant.filename → preferred_filename, but the test helper
    omo_config_path() was not updated, breaking compilation of all
    new provider tests.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • refactor(proxy): transparent header forwarding via hyper client (#1714)
    * style(frontend): reformat provider forms, constants and hooks
    
    Apply prettier formatting across 5 frontend files. No logic changes.
    
    Changed files:
    - AddProviderDialog.tsx: reformat generic type annotation and callback
    - ClaudeFormFields.tsx: consolidate multi-line useState and Collapsible props
    - CodexConfigSections.tsx: expand single-line React imports to multi-line,
      collapse removeCodexTopLevelField() call
    - constants.ts: merge TemplateType into single line
    - useSkills.ts: expand single-line TanStack Query imports to multi-line,
      reformat uninstallSkill mutationFn chain
    
    * deps(proxy): add hyper ecosystem crates and manual decompression libs
    
    reqwest internally normalizes all header names to lowercase and does not
    preserve insertion order, causing proxied requests to differ from the
    original client requests. To achieve transparent header forwarding with
    original casing and order, introduce lower-level hyper HTTP client libs.
    
    New dependencies:
    - hyper-util 0.1: TokioExecutor + legacy Client with
      preserve_header_case support for HTTP/1.1
    - hyper-rustls 0.27: rustls-based TLS connector for hyper
    - http 1 / http-body 1 / http-body-util 0.1: HTTP type crates for
      hyper 1.x request/response construction
    - flate2 1: manual gzip/deflate decompression (replaces reqwest auto)
    - brotli 7: manual brotli decompression
    
    Changed dependencies:
    - serde_json: enable preserve_order feature to keep JSON field order
    - reqwest: drop gzip feature to prevent reqwest from overriding the
      client's original accept-encoding header
    
    * refactor(proxy): use hyper client for header-case preserving forwarding
    
    Previously the proxy used reqwest for all upstream requests. reqwest
    normalizes header names to lowercase and reorders them internally,
    making proxied requests distinguishable from direct CLI requests.
    Some upstream providers are sensitive to these differences.
    
    This commit replaces reqwest with a hyper-based HTTP client on the
    default (non-proxy) path, achieving wire-level header fidelity:
    
    Server layer (server.rs):
    - Replace axum::serve with a manual hyper HTTP/1.1 accept loop
    - Enable preserve_header_case(true) so incoming header casing is
      captured in a HeaderCaseMap extension on each request
    - Bridge hyper requests to axum Router via tower::Service
    
    New hyper client module (hyper_client.rs):
    - Lazy-initialized hyper-util Client with preserve_header_case
    - ProxyResponse enum wrapping both hyper::Response and reqwest::Response
      behind a unified interface (status, headers, bytes, bytes_stream)
    - send_request() builds requests with ordered HeaderMap + case map
    
    Request handlers (handlers.rs):
    - Switch from (HeaderMap, Json<Value>) extractors to raw
      axum::extract::Request to preserve Extensions (containing the
      HeaderCaseMap from the accept loop)
    - Pass extensions through the forwarding chain
    
    Forwarder (forwarder.rs):
    - Remove HEADER_BLACKLIST array; replace with ordered header iteration
      that preserves original header sequence and casing
    - Build ordered_headers by iterating client headers, skipping only
      auth/host/content-length, and inserting auth headers at the original
      authorization position to maintain order
    - Handle anthropic-beta (ensure claude-code-20250219 tag) and
      anthropic-version (passthrough or default) inline during iteration
    - Remove should_force_identity_encoding() — accept-encoding is now
      transparently forwarded to upstream
    - Use hyper client by default; fall back to reqwest only when an
      HTTP/SOCKS5 proxy tunnel is configured
    
    Provider adapters (adapter.rs, claude.rs, codex.rs, gemini.rs):
    - Replace add_auth_headers(RequestBuilder) -> RequestBuilder with
      get_auth_headers(AuthInfo) -> Vec<(HeaderName, HeaderValue)>
    - Adapters now return header pairs instead of mutating a reqwest builder
    - Claude adapter: merge Anthropic/ClaudeAuth/Bearer into single branch;
      move Copilot fingerprint headers into get_auth_headers
    
    Response processing (response_processor.rs):
    - Add manual decompression (gzip/deflate/brotli via flate2 + brotli)
      for non-streaming responses, since reqwest auto-decompression is now
      disabled to allow accept-encoding passthrough
    - Add compressed-SSE warning log for streaming responses
    - Accept ProxyResponse instead of reqwest::Response
    
    HTTP client (http_client.rs):
    - Disable reqwest auto-decompression (.no_gzip/.no_brotli/.no_deflate)
      on both global and per-provider clients
    
    Streaming adapters (streaming.rs, streaming_responses.rs):
    - Generalize stream error type from reqwest::Error to generic E: Error
    
    Misc:
    - log_codes.rs: add SRV-005 (ACCEPT_ERR) and SRV-006 (CONN_ERR)
    - stream_check.rs: reformat copilot header lines
    - transform.rs: fix trailing whitespace alignment
    
    * fix(lint): resolve 35 clippy warnings across Rust codebase
    
    Fix all clippy warnings reported by `cargo clippy --lib`:
    
    - codex_config.rs: fix doc_overindented_list_items (3 spaces -> 2)
    - commands/copilot.rs: inline format args in 2 log::error! calls
    - commands/provider.rs: inline format args in 3 map_err closures
    - proxy/hyper_client.rs: inline format arg in log::debug! call
    - proxy/providers/copilot_auth.rs: inline format args in 16 locations
      (log macros, format! in headers, error constructors)
    - proxy/thinking_optimizer.rs: inline format args in 2 log::info! calls
    - services/skill.rs: inline format args in log::debug! call
    - services/webdav_sync.rs: inline format args in 6 format! calls
      (version compat messages, download limit messages)
    - services/webdav_sync/archive.rs: inline format args in 2 format! calls
    - session_manager/providers/opencode.rs: inline format args in
      source_path format!
    
    All fixes use the clippy::uninlined_format_args suggestion pattern:
      format!("msg: {}", var)  ->  format!("msg: {var}")
    
    * deps(proxy): add raw HTTP write and native TLS cert dependencies
    
    Add crates required for the raw TCP/TLS write path that bypasses
    hyper's header encoder to preserve original header name casing:
    
    - httparse: parse raw TCP peek bytes to capture header casings
    - tokio-rustls + rustls: direct TLS connections for raw write path
    - webpki-roots: Mozilla CA bundle baseline
    - rustls-native-certs: load system keychain CAs (trusts proxy MITM
      certificates from Clash, mitmproxy, etc.)
    
    * fix(proxy): address code review feedback on response handling
    
    Fixes from PR #1714 code review:
    
    - Extract `read_decoded_body()` and `strip_entity_headers_for_rebuilt_body()`
      in response_processor to properly clean content-encoding/content-length
      headers after decompression
    - Reuse `read_decoded_body()` in handlers.rs for Claude transform path,
      ensuring compressed responses are decoded before format conversion
    - Make `build_proxy_url_from_config()` public so forwarder can pass proxy
      URL to the hyper raw write path
    - Add `has_system_proxy_env()` utility with test coverage
    - Add 50ms backoff after accept() failures in server.rs to prevent
      tight-loop CPU spin on transient socket errors
    
    * feat(proxy): implement raw TCP/TLS write with HTTP CONNECT tunnel
    
    Rewrite hyper_client with a two-tier strategy for header case preservation:
    
    Primary path (raw write):
    - Peek raw TCP bytes in server.rs to capture OriginalHeaderCases before
      hyper lowercases them
    - Build raw HTTP/1.1 request bytes with exact original header name casing
    - Write directly to TLS stream, then use WriteFilter to let hyper parse
      the response while discarding its duplicate request writes
    - Support HTTP CONNECT tunneling through upstream proxies, so header case
      is preserved even when a proxy (Clash, V2Ray) is configured
    
    Fallback path (hyper-util Client):
    - Used when OriginalHeaderCases is empty or raw write fails
    - Configured with title_case_headers(true) for best-effort casing
    
    TLS improvements:
    - Load native system certificates alongside webpki roots so proxy MITM
      CAs (installed in system keychain) are trusted through CONNECT tunnels
    
    Key types added:
    - OriginalHeaderCases: maps lowercase name → original wire-casing bytes
    - WriteFilter<S>: AsyncRead+AsyncWrite wrapper that discards writes
    - connect_via_proxy(): HTTP CONNECT tunnel establishment
    - ExtensionDebugMarker: diagnostic marker for extension chain debugging
    
    * refactor(proxy): route requests through hyper with proxy-aware forwarding
    
    Rework forwarder request dispatch to always prefer the hyper raw write
    path (header case preservation) over reqwest:
    
    Request routing:
    - HTTP/HTTPS proxy: hyper raw write through CONNECT tunnel (case preserved)
    - SOCKS5 proxy: reqwest fallback (CONNECT not supported for SOCKS5)
    - No proxy: hyper raw write direct connection
    
    Header handling improvements:
    - Replace host header in-place at original position instead of
      skip-and-append, preserving client's header ordering
    - Preserve client's original accept-encoding for transparent passthrough;
      only force identity encoding when transform path needs decompression
    - Add should_force_identity_encoding() to centralize the decision
    - Remove hardcoded 'br, gzip, deflate' override that masked client values
    
    Proxy URL resolution (priority order):
    1. Provider-specific proxy config (if enabled)
    2. Global proxy URL configured in CC Switch
    3. Direct connection (no proxy)
    
    * chore(proxy): remove dead code, redundant tests and debug scaffolding
    
    - Inline should_force_identity_encoding() (was just `needs_transform`)
      and delete its 5 test cases
    - Remove ExtensionDebugMarker diagnostic type
    - Remove unused has_system_proxy_env() and its test
    - Remove strip_entity_headers test
    - Simplify hyper path: remove redundant is_socks_proxy ternary
    - Update hyper_client module doc to reflect CONNECT tunnel support
    
    * fix(proxy): block direct-connect fallback and complete CONNECT tunnel support
    
    * feat(hooks): improve proxy requirement warnings with specific reasons
    
    - Remove redundant OpenAI format hint toast messages
    - Add detailed reason detection for proxy requirements (OpenAI Chat, OpenAI Responses, full URL mode)
    - Update i18n files with new reason-specific keys
    
    * style(*): format code with prettier
    
    - Remove extra whitespace in http_client.rs
    - Fix formatting issues in useProviderActions.ts
    
    * fix(proxy): post-merge fixes for forward return type and clippy warnings
    
    - Restore forward() return type to (ProxyResponse, Option<String>)
      to pass claude_api_format through to callers
    - Inline format args in log::warn! macro (clippy::uninlined_format_args)
    - Suppress too_many_arguments for check_claude_stream
    
    * refactor(proxy): preserve original header wire order and add non-streaming body timeout
    
    - Rewrite build_raw_request to emit headers in original
      client-sent sequence instead of hash-map order
    - Remove unused OriginalHeaderCases::get_all method
    - Add body_timeout to read_decoded_body to prevent
      requests hanging when upstream stalls after headers
  • fix: 修复 Copilot 作为 Claude 时 OpenAI 模型的 Responses 分流 (#1735)
    * fix: route copilot claude openai models to responses
    
    * fix(i18n): add copilotProxyHint translation key for all locales
    
    The copilotProxyHint message was using inline defaultValue with Chinese
    text, which would show Chinese to English and Japanese users. Added
    proper translation keys in zh/en/ja locale files and removed the
    hardcoded defaultValue fallback.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • feat(proxy): add full URL mode and refactor endpoint rewriting (#1561)
    * feat(proxy): add full URL mode and refactor endpoint rewriting
    
    - Add `isFullUrl` provider meta to treat base_url as complete API endpoint
    - Remove hardcoded `?beta=true` from Claude adapter, pass through from client
    - Refactor forwarder endpoint rewriting with proper query string handling
    - Block provider switching when proxy is required but not running
    - Add full URL toggle UI in endpoint field with i18n (zh/en/ja)
    
    * refactor(proxy): remove beta query handling
    
    * fix(proxy): strip beta query when rewriting Claude endpoints
    
    * feat(codex): complete full URL support
    
    * refactor(ui): refine full URL endpoint hint
  • feat(skills): 优化技能安装/卸载的缓存更新策略 (#1573)
    - 修改安装、卸载、导入、ZIP安装等操作的缓存更新逻辑,从invalidateQueries改为直接setQueryData
    - 为已安装和可发现技能查询添加keepPreviousData和staleTime: Infinity配置
    - 修复会话管理页面布局滚动问题,添加min-h-0防止内容溢出
  • feat(copilot): add GitHub Copilot reverse proxy support (#930)
    * refactor(toolsearch): replace binary patch with ENABLE_TOOL_SEARCH env var toggle
    
    - Remove toolsearch_patch.rs binary patching mechanism (~590 lines)
      - Delete `toolsearch_patch.rs` and `commands/toolsearch.rs`
      - Remove auto-patch startup logic and command registration from lib.rs
      - Remove `tool_search_bypass` field from settings.rs
      - Remove frontend settings ToggleRow, useSettings hook sync logic, and API methods
      - Clean up zh/en/ja i18n keys (notifications + settings)
    
    - Add ENABLE_TOOL_SEARCH toggle to Claude provider form
      - Add checkbox in CommonConfigEditor.tsx (alongside teammates toggle)
      - When enabled, writes `"env": { "ENABLE_TOOL_SEARCH": "true" }`
      - When disabled, removes the key; takes effect on provider switch
      - Add zh/en/ja i18n key: `claudeConfig.enableToolSearch`
    
    Claude Code 2.1.76+ natively supports this env var, eliminating the need for binary patching.
    
    * feat(claude): add effortLevel high toggle to provider form
    
    - Add "high-effort thinking" checkbox to Claude provider config form
    - When checked, writes `"effortLevel": "high"`; when unchecked, removes the field
    - Add zh/en/ja i18n translations
    
    * refactor(claude): remove deprecated alwaysThinking toggle
    
    - Claude Code now enables extended thinking by default; alwaysThinkingEnabled is a no-op
    - Thinking control is now handled via effortLevel (added in prior commit)
    - Remove state, switch case, and checkbox UI from CommonConfigEditor
    - Clean up alwaysThinking i18n keys across zh/en/ja locales
    
    * feat(opencode): add setCacheKey: true to all provider presets
    
    - Add setCacheKey: true to options in all 33 regular presets
    - Add setCacheKey: true to OPENCODE_DEFAULT_CONFIG for custom providers
    - Exclude 2 OMO presets (Oh My OpenCode / Slim) which have their own config mechanism
    
    Closes #1523
    
    * fix(codex): resolve 1M context window toggle causing MCP editor flicker
    
    - Add localValueRef to short-circuit duplicate CodeMirror updateListener callbacks,
      breaking the React state → CodeMirror → stale onChange → React state feedback loop
    - Use localValueRef.current in handleContextWindowToggle and handleCompactLimitChange
      to avoid stale closure reads
    - Change compact limit input from type="number" to type="text" with inputMode="numeric"
      to remove unnecessary spinner buttons
    
    * feat(codex): add 1M context window toggle utilities and i18n keys
    
    - Add extractCodexTopLevelInt, setCodexTopLevelInt, removeCodexTopLevelField
      TOML helpers in providerConfigUtils.ts
    - Add i18n keys for contextWindow1M, autoCompactLimit in zh/en/ja locales
    
    * feat(claude): collapse model mapping fields by default
    
    - Wrap 5 model mapping inputs in a Collapsible, collapsed by default
    - Auto-expand when any model value is present (including preset-filled)
    - Show hint text when collapsed explaining most users need no config
    - Add zh/en/ja i18n keys for toggle label and collapsed hint
    - Use variant={null} to avoid ghost button hover style clash in dark mode
    
    * feat(claude): merge advanced fields into single collapsible section
    
    - Merge API format, auth field, and model mapping into a unified "Advanced Options" collapsible
    - Extend smart-expand logic to detect non-default values across all advanced fields
    - Preserve model mapping sub-header and hint with a separator line
    - Update zh/en/ja i18n keys (advancedOptionsToggle, advancedOptionsHint, modelMappingLabel, modelMappingHint)
    
    * feat(copilot): add GitHub Copilot reverse proxy support
    
    Add GitHub Copilot as a Claude provider variant with OAuth device code
    authentication and Anthropic ↔ OpenAI format transformation.
    
    Backend:
    - Add CopilotAuthManager for GitHub OAuth device code flow
    - Implement Copilot token auto-refresh (60s before expiry)
    - Persist GitHub token to ~/.cc-switch/copilot_auth.json
    - Add ProviderType::GitHubCopilot and AuthStrategy::GitHubCopilot
    - Modify forwarder to use /chat/completions for Copilot
    - Add Copilot-specific headers (Editor-Version, Editor-Plugin-Version)
    
    Frontend:
    - Add CopilotAuthSection component for OAuth UI
    - Add useCopilotAuth hook for OAuth state management
    - Auto-copy user code to clipboard and open browser
    - Use 8-second polling interval to avoid GitHub rate limits
    - Skip API Key validation for Copilot providers
    - Add GitHub Copilot preset with claude-sonnet-4 model
    
    Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
    
    * fix(copilot): remove is_expired() calls from tests
    
    Remove references to deleted is_expired() method in test code.
    Only is_expiring_soon() is needed for token refresh logic.
    
    Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
    
    * feat(copilot): add real-time model listing from Copilot API
    
    - Add fetch_models() to CopilotAuthManager calling GET /models endpoint
    - Add copilot_get_models Tauri command
    - Add copilotGetModels() frontend API wrapper
    - Modify ClaudeFormFields to show model dropdown for Copilot providers
      - Fetches available models on component mount when isCopilotPreset
      - Groups models by vendor (Anthropic, OpenAI, Google, etc.)
      - Input + dropdown button combo allows both manual entry and selection
      - Non-Copilot providers keep original plain Input behavior
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat(copilot): add usage query integration
    
    - Add Copilot usage API integration (fetch_usage method)
    - Add copilot_get_usage Tauri command
    - Add GitHub Copilot template in usage query modal
    - Unify naming: copilot → github_copilot
    - Add constants management (TEMPLATE_TYPES, PROVIDER_TYPES)
    - Improve error handling with detailed error messages
    - Add database migration (v5 → v6) for template type update
    - Add i18n translations (zh, en, ja)
    - Improve type safety with TemplateType
    - Apply code formatting (cargo fmt, prettier)
    
    * 修复github 登录和注销问题 ,模型选择问题
    
    * feat(copilot): add multi-account support for GitHub Copilot
    
    - Add multi-account storage structure with v1 to v2 migration
    - Add per-account token caching and auto-refresh
    - Add new Tauri commands for account management
    - Integrate account selection in Proxy forwarder
    - Add account selection UI in CopilotAuthSection
    - Save githubAccountId to ProviderMeta
    - Add i18n translations for multi-account features (zh/en/ja)
    
    * 修复用量查询Reset字段出现多余字符
    
    * refactor(auth-binding): introduce generic provider auth binding primitives
    
    - add shared authBinding types in Rust and TypeScript while keeping githubAccountId as a compatibility field\n- resolve Copilot token, models, and usage through provider-bound account lookup instead of only the implicit default account\n- fix the Unix build regression in settings.rs by restoring std::io::Write for write_all()\n- remove the accidental .github ignore entry and drop leftover Copilot form debug logs\n- keep the first migration step non-breaking by writing both authBinding and the legacy githubAccountId field from the form
    
    * refactor(auth-service): add managed auth command surface and explicit default account state
    
    - introduce generic managed auth commands and frontend auth API wrappers for provider-scoped login, status, account listing, removal, logout, and default-account selection\n- store an explicit Copilot default_account_id instead of relying on HashMap iteration order, and use it consistently for fallback token/model/usage resolution\n- sort managed accounts deterministically and surface default-account state to the UI\n- refactor the Copilot form hook to wrap a generic useManagedAuth implementation while preserving the existing component contract\n- add default-account controls to the Copilot auth section and extend Copilot auth status serialization/tests for the new state
    
    * feat(auth-center): add a dedicated settings entrypoint for managed OAuth accounts
    
    - add an Auth Center tab to Settings so managed OAuth accounts are no longer hidden inside individual provider forms\n- introduce a first AuthCenterPanel that hosts GitHub Copilot account management as the initial managed auth provider\n- keep the provider form experience intact while establishing a global account-management surface for future providers such as OpenAI\n- validate that the new settings tab works cleanly with the generic managed auth hook and existing Copilot account controls
    
    * feat(add-provider): expose managed OAuth sources alongside universal providers
    
    - add an OAuth tab to the Add Provider flow so managed auth sources sit beside app-specific and universal providers\n- reuse the new Auth Center panel inside the dialog, keeping account management discoverable during provider creation\n- make the dialog footer adapt to the OAuth tab so account setup does not pretend to create a provider directly\n- align the add-provider UX with the new architecture where OAuth accounts are global assets and providers bind to them later
    
    * fix(auth-reliability): harden managed auth persistence and refresh behavior
    
    - replace direct Copilot auth store writes with private temp-file writes and atomic rename semantics, and document the local token storage limitation\n- add per-account refresh locks plus a double-check path so concurrent requests do not stampede GitHub token refresh\n- surface legacy migration failures through auth status, expose them in the UI, and add translated copy for the new account-state labels\n- stop writing the legacy githubAccountId field from the provider form while keeping compatibility reads in place\n- add logout error recovery and Copilot model-load toasts so auth failures are no longer silently swallowed
    
    * refactor(copilot-detection): prefer provider type before URL fallbacks
    
    - update forwarder endpoint rewriting to treat providerType as the primary GitHub Copilot signal\n- keep githubcopilot.com string matching only as a compatibility fallback for older provider records without providerType\n- reduce one more path where Copilot behavior depended purely on URL heuristics
    
    * fix(copilot-auth): add cancel button to error state in CopilotAuthSection
    
    - 错误状态下仅有"重试"按钮,用户无法退出(如不可恢复的 403 未订阅错误)
    - 新增"取消"按钮,复用已有的 cancelAuth 逻辑重置为 idle 状态
    
    * 修复打包后github账号头像显示异常
    
    * 修复github copilot 来源的模型测试报错
    
    * feat(copilot-preset): add default model presets for GitHub Copilot
    
    - 补充 Copilot 预设的默认模型配置,用户选完预设即可直接使用
    - ANTHROPIC_MODEL: claude-opus-4.6
    - ANTHROPIC_DEFAULT_HAIKU_MODEL: claude-haiku-4.5
    - ANTHROPIC_DEFAULT_SONNET_MODEL: claude-sonnet-4.6
    - ANTHROPIC_DEFAULT_OPUS_MODEL: claude-opus-4.6
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
    Co-authored-by: 周梦泽 <mengze.zhou@dafeng-tech.com>
    Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
  • feat(skills): add restore and delete for skill backups
    Introduce list/restore/delete commands for skill backups created during
    uninstall. Restore copies files back to SSOT, saves the DB record, and
    syncs to the current app with rollback on failure. Delete removes the
    backup directory after a confirmation dialog. ConfirmDialog gains a
    configurable zIndex prop to support nested dialog stacking.
  • feat: add Tool Search domain restriction bypass with active-installation patching
    Resolve the active `claude` command from PATH and apply an equal-length
    byte patch to remove the domain whitelist check. Backups are stored in
    ~/.cc-switch/toolsearch-backups/ (SHA-256 of path) so they survive
    Claude Code version upgrades. The patch auto-reapplies on app startup
    when the setting is enabled.
    
    Frontend checks PatchResult.success and rolls back the setting on failure.
  • fix: replace implicit app inference with explicit selection for Skills import and sync
    Skills import previously inferred app enablement from filesystem presence,
    causing incorrect multi-app activation when the same skill directory existed
    under multiple app paths. Now the frontend submits explicit app selections
    via ImportSkillSelection, and schema migration preserves a snapshot of
    legacy app mappings to avoid lossy reconstruction.
    
    Also adds reconciliation to sync_to_app (removes disabled/orphaned symlinks)
    and MCP sync_all_enabled (removes disabled servers from live config).
  • fix: sync session search index with query data to refresh list after deletion
    Replace useRef+useEffect async index rebuild with useMemo so the
    FlexSearch index and the sessions array always reference the same data.
    This ensures filtered search results update immediately when a session
    is deleted via TanStack Query setQueryData.