Commit Graph

117 Commits

  • 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>
  • [codex] fix Zhipu coding plan presets (#3524)
    * fix(presets): update Zhipu coding plan endpoints
    
    * fix(model-fetch): probe /models on versioned /vN base URLs
    
    The model-list probe assumed any base URL not ending in /v1 needs /v1/models appended. For providers whose base URL already ends in a version segment like /v4 (Zhipu/Z.AI GLM Coding Plan at .../api/coding/paas/v4), this produced .../v4/v1/models which 404s, so the "Fetch models" button always failed.
    
    Detect a trailing /v{N} version segment and probe {base}/models first, keeping /v1/models as a fallback candidate for non-/v1 versions. Fixes Codex/OpenCode/OpenClaw/Hermes GLM presets and any other vN-style endpoint, with no behavior change for /v1 or non-versioned URLs.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • Fix Codex edit dialog masking live OAuth during proxy takeover
    The reported "OAuth access token disappears when enabling Codex proxy
    takeover" was a display artifact, not data loss: auth.json on disk kept
    the OAuth login the whole time. During takeover the edit dialog falls
    back to the stored provider config (so it does not surface the proxy
    placeholder), which for a third-party provider shows that provider's own
    key instead of the live auth.json, making the OAuth token look gone.
    
    Thread an isProxyTakeover flag from App through ProviderForm into the
    Codex editor and show an explicit notice plus storage-aware auth/config
    hints clarifying that the form displays the stored provider config while
    the live config is temporarily managed by the proxy. Drop the
    proxy-running condition so the notice shows whenever takeover is active,
    even with the proxy stopped.
    
    Add a regression test asserting the dialog does not read live settings
    during takeover and renders the database config. i18n synced across
    en/zh/ja/zh-TW.
  • 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: 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>
  • 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.
  • 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.
  • 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>
  • 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.
  • test: align stale tests with merged Codex preset and bucket changes
    CI on main was red because two tests did not follow intentional changes
    that were already merged into the codebase:
    
    - codexChatProviderPresets.test.ts still expected the "Kimi For Coding"
      preset, which was removed in 16c3ef3f. Drop the corresponding entry
      from the expected presets map.
    - import_export_sync.rs still asserted the legacy per-provider
      model_provider id ("rightcode"), but 9fac15b8 unified Codex
      third-party providers into the stable "custom" history bucket. Mirror
      the assertion update already applied to provider_service.rs.
    
    No production code changed; both fixes only update test expectations.
  • 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.
  • fix: Codex model catalog WYSIWYG and config consolidation
    - Remove mergeCodexDefaultCatalogModelForSave implicit injection (P1)
      The model mapping table is now the single source of truth; no hidden
      entries are prepended on save.
    
    - Sync first catalog row model into config.toml on save
      Ensures Codex default request model matches the table's first entry
      instead of retaining a stale template value.
    
    - Remove API Format selector from CodexFormFields (P3)
      wire_api is always 'responses'; the selector confused users into
      thinking they were changing the upstream protocol. Only the 'Needs
      Local Routing' toggle remains.
    
    - Add restart hint to model mapping i18n text (P2)
      model_catalog_json is loaded at Codex startup; users are now informed
      that a restart is needed after changes.
    
    - Unify write_codex_live_with_catalog helper (P4)
      Replaces three scattered prepare+write call sites in config.rs,
      provider/live.rs, and proxy.rs with a single entry point.
    
    - Clean up useCodexConfigState dead state (P3 follow-up)
      Remove codexModelName, codexContextWindow, codexAutoCompactLimit and
      their handlers/effects since no component consumes them after the UI
      consolidation.
  • fix(skills): install correct skill from skills.sh search results (#2784)
    * fix(skills): install correct skill from skills.sh search results
    
    When multiple skills share the same directory name across different repos,
    SkillCard was passing directory to onInstall/onUninstall, causing handleInstall
    to always match the first result. Switch to using the unique key field
    (directory:repoOwner:repoName) for precise identification.
    
    * test(skills): add regression test for skills.sh install by key
    
    Verifies that clicking install on the second card when two skills share
    the same directory name correctly installs the second skill, not the first.
    
    ---------
    
    Co-authored-by: mrzhao <mrzhao@iflytek.com>
  • fix(usage): reduce price input step to 0.0001 for sub-cent precision (#2793)
    The step was 0.01, preventing input of prices like DeepSeek's cache read
    cost ($0.0028/million tokens). Extract step value to a constant and apply
    to all four price fields.
    
    Closes #2503
  • feat: add Xiaomi MiMo Token Plan presets (#2803)
    * feat: add Xiaomi MiMo token plan presets
    
    * fix: update Xiaomi MiMo provider presets
    
    * fix: align MiMo V2.5 model specs with official documentation
    
    - Update maxTokens from 32000 to 131072 (128K) for mimo-v2.5-pro and mimo-v2.5
    - Update contextWindow from 262144 to 1048576 (1M) for mimo-v2.5
    - Aligns with official specs from Xiaomi MiMo documentation
    - Ensures consistency between OpenClaw and OpenCode presets
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • Add Claude Desktop official preset
    - Add Claude Desktop Official to the Claude Desktop preset list.
    - Treat selected official presets as official mode in the form.
    - Cover the official preset with a preset-order regression test.
  • refactor(presets): render presets in array order and prioritize partners
    Remove the category-based grouping logic from ProviderPresetSelector,
    letting the array position in each preset config file be the single
    source of truth for display order. Move partner presets (PatewayAI,
    火山Agentplan, BytePlus, DouBaoSeed) right after Shengsuanyun across
    all 6 config files so they appear earlier in the UI.
  • feat(codex-oauth): fetch model list from ChatGPT backend on demand
    - Add `get_codex_oauth_models` Tauri command reusing the managed OAuth
      access token to hit `chatgpt.com/backend-api/codex/models`; HTTP and
      multi-shape JSON parsing live in `services::codex_oauth_models` so the
      command stays thin.
    - Unify the Claude form's "fetch models" button across normal / Copilot /
      Codex OAuth presets, drop the auto-load effect for Copilot in favor of
      explicit clicks, and guard against stale responses with a requestId ref.
    - Add Vitest coverage for both Copilot and Codex OAuth paths asserting no
      request on mount and the correct account id on click; add Rust unit
      tests for the four model-list payload shapes.
  • feat(claude-code): role-based model mapping with display names and 1M flag
    - Replace the four flat env inputs with a Sonnet/Opus/Haiku role table.
      Each row exposes ANTHROPIC_DEFAULT_*_MODEL plus a new display name
      field ANTHROPIC_DEFAULT_*_MODEL_NAME, and Sonnet/Opus gain a
      "Declare 1M" checkbox that toggles the [1M] suffix.
    - Strip the [1M] context-capability marker before forwarding non-Copilot
      requests upstream. Copilot keeps its existing [1m]->-1m normalization.
    - Claude Desktop import now consumes ANTHROPIC_DEFAULT_*_MODEL_NAME as
      label_override, closing the Claude Code -> Claude Desktop displayName
      pipeline; add_route's merge logic is shared between hashmap branches.
    - Unify the [1M] marker as ONE_M_CONTEXT_MARKER across
      claude_desktop_config and proxy::model_mapper; rename the strip
      helper to strip_one_m_suffix_for_upstream.
    - Collapse useModelState's seven duplicated useState initializers and
      the useEffect parse block into a single parseModelsFromConfig call.
    - Add tests/hooks/useModelState.test.tsx and a Claude Desktop import
      test covering Kimi K2 -> label_override. i18n (en/ja/zh) updated.
  • refactor(claude-desktop): lock route IDs to sonnet/opus/haiku roles
    Adapt to Claude Desktop 1.6259.1+ fail-all validation which only
    accepts claude-(sonnet|opus|haiku)-* route IDs. Branded model names
    (DeepSeek, Kimi, GLM, etc.) now live in a new labelOverride field
    instead of being embedded in route IDs.
    
    - Backend auto-repairs legacy unsafe routes to the next free
      sonnet/opus/haiku slot instead of erroring
    - Frontend swaps the free-form route input for a role dropdown plus
      menu display name field
    - Add CLAUDE_DESKTOP_ROLE_ROUTE_IDS as the single source of truth
      for role-to-route mapping; presets and form both consume it
    - Drop the dead displayName alias on ClaudeDesktopModelRoute and the
      ineffective /v1/models display_name injection (UI ignores it)
    - Update i18n (en/ja/zh) and form focus test for the new fields
  • - 修复 Claude Desktop 模型输入框失焦
    - 为动态模型行添加稳定 rowId,避免编辑模型 ID 时重挂载
    
    - 增加模型映射和直连模型列表焦点保持回归测试
  • 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.
  • Fix/一键配置失效 (#2249)
    * style(FailoverQueueManager): 显示供应商备注信息
    
    * style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示
    
    * style(FailoverQueueManager): 显示供应商备注信息
    
    * style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示
    
    * style(FailoverQueueManager): 更新供应商备注信息的显示样式
    
    * style(FailoverQueueItem): 添加条件序列化以优化供应商备注字段
    
    * fix: 优化模型状态管理,确保配置更新时正确引用最新设置
    
    * fix(skill): improve error handling for skill source directory resolution
    
    Co-authored-by: Copilot <copilot@github.com>
    
    * fix(gemini): simplify project directory retrieval in scan_sessions function
    
    * fix(useModelState): optimize latestConfigRef assignment in useModelState hook
    
    * fix(useModelState): remove unnecessary blank line in useModelState hook
    
    ---------
    
    Co-authored-by: Copilot <copilot@github.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.
  • test: sync stale fixtures and isolate openclaw env tests
    Three unrelated test failures surfaced after rebase:
    
    - McpFormModal expected the apps boolean set without `hermes`; Hermes MCP
      support is now wired, so the fixture must include `hermes: false`.
    - therouter Gemini preset was bumped to `gemini-3.1-pro` in a later
      commit; update the assertions to match current config.
    - openclaw_config tests mutate process-level `CC_SWITCH_TEST_HOME` and
      `HOME` inside a module-local Mutex, but hermes_config does the same
      under its own separate Mutex. Running both modules in parallel let the
      env races corrupt hermes_config's `with_test_home`. Tag the four
      env-mutating openclaw tests with `#[serial]` so they serialize across
      modules via serial_test's process-wide default key.
  • 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(presets): migrate all aggregator and Bedrock presets to Claude Opus 4.7
    - OpenClaw: replace opus-4-6 with opus-4-7 across 17 aggregator presets
      (id, name, primary, modelCatalog); AWS Bedrock entry rewritten to new
      SKU anthropic.claude-opus-4-7 (drops -v1 and dated suffix per official
      4.7 model card) and pricing corrected to $5/$25/$0.50/$6.25 during the
      SKU swap, aligning with schema.rs source of truth
    - OpenCode: same replacement for 13 aggregators plus
      OPENCODE_PRESET_MODEL_VARIANTS entries for @ai-sdk/amazon-bedrock and
      @ai-sdk/anthropic, plus AWS Bedrock provider models map
    - OpenRouter / TheRouter / GitHub Copilot in claudeProviderPresets use
      dot-style id; update to anthropic/claude-opus-4.7 (missed by 509d2250)
    - omo: switch agent/category recommended to opus-4-7; replace key in
      OMO_BACKGROUND_TASK_PLACEHOLDER priority map
    - hermes_config.rs: update doc comments and test fixtures to opus-4-7;
      Hermes ModelPanel placeholder and i18n defaultHint examples follow
    - i18n unspecifiedHigh category description bumped to 'Claude Opus 4.7
      max variant' to match omo recommended
    - Test fixtures updated: therouter preset assertion and opencode Bedrock
      variant lookup now check for opus-4-7
    - Sonnet 4.6 / Haiku 4.5 untouched - no official 4.7 release for them
  • 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(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(usage): refine usage dashboard UI and date range picker (#2002)
    * feat(usage): enhance usage stats backend and query hooks
    
    * feat(usage): redesign calendar date range picker with auto-switch and simplified layout
    
    * refactor(usage): streamline dashboard layout and stats components
    
    * refactor(usage): compact request log table with merged cache/multiplier columns and centered layout
    
    * feat(i18n): add cache short labels and usage stats translations for zh/en/ja
    
    * Align usage dashboard stats with range boundaries
    
    The usage dashboard mixed second-precision detail rows with day-level rollups, which caused custom half-day ranges to overcount historical rollup data and left the request log paginator on stale pages after top-level filter changes.
    
    This change limits rollups to fully covered local days, aligns multi-day trend buckets with natural local days, and resets request log pagination when the dashboard range or app filter changes.
    
    Constraint: usage_daily_rollups stores only daily aggregates after pruning old detail rows
    Rejected: Include partial boundary rollups proportionally | historical intra-day detail is unavailable after pruning
    Rejected: Force RequestLogTable remount on range change | would discard local draft filters unnecessarily
    Confidence: high
    Scope-risk: moderate
    Reversibility: clean
    Directive: Keep summary, trends, provider stats, and model stats on the same rollup-boundary rules
    Tested: cargo test --manifest-path src-tauri/Cargo.toml usage_stats
    Tested: pnpm exec vitest run tests/components/RequestLogTable.test.tsx
    Tested: pnpm typecheck
    Not-tested: Manual UI validation in the Tauri app
    
    * Preserve full-day usage filters at minute precision
    
    The latest review surfaced two interaction bugs in the usage dashboard: rollup-backed stats undercounted end days selected via the minute-precision picker, and immediate select changes accidentally applied unsubmitted text drafts from the request log filters.
    
    This change treats 23:59 as a fully selected local end day for rollup inclusion and narrows select-side state syncing so app/status updates do not commit provider/model drafts.
    
    Constraint: The custom range picker emits minute-precision timestamps, while rollups are stored at day granularity
    Rejected: Require exact 23:59:59 end timestamps | unreachable from the current picker UI
    Rejected: Rebuild applied filters from the full draft state on select changes | silently commits unsaved text input
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Keep request-log text fields on explicit apply semantics even when select filters remain immediate
    Tested: cargo test --manifest-path src-tauri/Cargo.toml usage_stats
    Tested: pnpm exec vitest run tests/components/RequestLogTable.test.tsx
    Tested: pnpm typecheck
    Not-tested: Manual Tauri dashboard interaction
    
    * refactor(usage): move range presets into date picker, single-row layout
    
    - UsageDateRangePicker: add preset shortcuts (今天/1d/7d/14d/30d) inside
      popover top; clicking a preset applies immediately and closes popover
    - UsageDashboard: collapse to single row (app filters + refresh + picker);
      remove standalone preset buttons and summary stats bar
    - RequestLogTable: replace static Calendar badge with interactive
      UsageDateRangePicker via onRangeChange prop; single filter row
    
    * Keep usage pagination regression coverage aligned with the rendered UI
    
    The new regression test was asserting a non-existent pagination label and page summary text, so it failed before it could verify the real page-reset behavior. This commit switches the assertions to the numbered pagination buttons that the component actually renders and validates the reset through the query hook arguments.
    
    Constraint: RequestLogTable exposes numbered pagination buttons, not a "Next page" label or "2 / 6" summary text
    Rejected: Add synthetic pagination labels solely for the test | would couple production markup to a test-only assumption
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Prefer pagination assertions that follow the rendered controls or hook inputs instead of invented summary text
    Tested: pnpm vitest run tests/components/RequestLogTable.test.tsx; pnpm typecheck; pnpm test:unit
    
    * refactor(usage): clean up dead code and polish date range picker
    
    - Remove unused exports MAX_CUSTOM_USAGE_RANGE_SECONDS,
      timestampToLocalDatetime, and localDatetimeToTimestamp from
      usageRange.ts (replaced by the calendar picker)
    - Deduplicate getPresetLabel from UsageDashboard and
      UsageDateRangePicker into shared getUsageRangePresetLabel helper
    - Add aria-label, aria-current and aria-pressed to calendar day
      buttons so screen readers can disambiguate same-numbered days
      across adjacent months
    - Drop unused cacheReadShort and cacheWriteShort i18n keys (zh/en/ja);
      the request log table renders R/W prefixes inline
    - Align customRangeHint copy with the removed 30-day limit by
      dropping "up to 30 days" wording (zh/en/ja)
    
    * fix(usage): align rollup cutoff to local midnight to keep days complete
    
    `rollup_and_prune` previously used `Utc::now() - retain_days * 86400`
    as the cutoff. Because rollups are bucketed by *local* date and detail
    rows below the cutoff are pruned, an unaligned cutoff left the youngest
    rolled-up day half-rolled-up and half-pruned. Combined with the new
    `compute_rollup_date_bounds` boundary trimming (which excludes any
    rollup day not fully covered by the requested range), custom range
    queries that touch that day silently under-count summary, trend,
    provider, and model stats.
    
    Fix the invariant at the source: snap the cutoff to the next local
    midnight after `(now - retain_days)`. Every rollup row now reflects a
    complete local day, so the boundary trimmer's all-or-nothing assumption
    holds.
    
    Includes unit tests for the cutoff math (typical case + already-on-
    midnight case). DST gap is handled defensively by bumping forward by
    an hour.
    
    Addresses Codex P2 review finding on PR #2002.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • Restore first-class OMO Slim council support (#1981) (#1982)
    cc-switch could already persist arbitrary OMO Slim agent keys and top-level fields, but the built-in metadata and UI copy still reflected the pre-council agent set. This made the upstream council feature look unsupported and pushed users toward manual JSON-only setup.
    
    Promote council to a built-in OMO Slim agent, add copy that points top-level plugin settings at Other Fields, and lock the behavior with regression tests.
    
    Constraint: oh-my-opencode-slim exposes council through both agents.council and top-level council config
    Rejected: Add a dedicated council editor UI now | too much surface area for issue #1981
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Keep OMO Slim built-in agent metadata aligned with upstream agent additions before shipping UI support
    Tested: pnpm exec vitest run tests/utils/omoConfig.test.ts tests/components/OmoFormFields.mergeCustomModelsIntoStore.test.ts
    Tested: pnpm typecheck
    Not-tested: End-to-end validation against a live oh-my-opencode-slim installation
    Related: farion1231/cc-switch#1981
  • feat(provider): add TheRouter presets for Claude, Codex, and Gemini (#1891)
    * feat(provider): add TheRouter presets for Claude and Codex
    
    * feat(provider): add TheRouter Gemini preset
    
    ---------
    
    Co-authored-by: max <me19@qq.com>
  • fix: guard migrations against missing tables and fix highlighted text assertion
    - Make migrate_v6_to_v7 check skills table existence before ALTER
    - Make migrate_v7_to_v8 check model_pricing table existence before UPDATE
    - Fix SessionManagerPage test: use getByRole heading instead of getAllByText
      which breaks when highlightText splits text across <mark> elements
  • fix: update tests for InstalledSkill new fields and missing hook mocks
    - Add content_hash and updated_at fields to 4 InstalledSkill literals in skill_sync.rs
    - Add useCheckSkillUpdates and useUpdateSkill to UnifiedSkillsPanel test mock
    - Suppress unused import warning in auto_launch.rs test module
  • 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>
  • feat: add bulk delete for session manager (#1693)
    * feat: add bulk delete for session manager
    
    * fix: address batch delete review issues
    
    * fix: keep session list in sync after batch delete
  • fix: preserve WebDAV password display and validate MKCOL 405 (#1685)
    * fix: preserve WebDAV password display and validate MKCOL 405
    
    * fix: scope WebDAV password preservation to post-save refresh
  • 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
  • chore(release): consolidate v3.12.3 release notes, changelog and test fixes
    Merge previously unreleased v3.12.4 content into v3.12.3:
    - CHANGELOG: combine [Unreleased] into [3.12.3], clear [Unreleased]
    - Release notes (zh/en/ja): add Copilot proxy, macOS signing,
      Reasoning Effort, OpenCode SQLite, Codex 1M toggle, Disable
      Auto-Upgrade toggle, and contributor thanks
    - Fix test mocks for skill backup/restore hooks
    - Fix schema migration test missing providers table
    - Fix TempHome to save/restore CC_SWITCH_TEST_HOME env var
  • feat(skills): auto-backup skill files before uninstall
    Create a local backup under ~/.cc-switch/skill-backups/ before removing
    skill directories. The backup includes all skill files and a meta.json
    with original skill metadata. Old backups are pruned to keep at most 20.
    The backup path is returned to the frontend and shown in the success
    toast. Bump version to 3.12.3.