Commit Graph

121 Commits

  • feat(codex): add opt-in migration and ledger-based restore for unified session history
    - Enable dialog gains a checkbox (default off) to migrate existing
      official sessions from the built-in "openai" bucket into the shared
      "custom" bucket, with per-generation backups; failed migrations retry
      at startup
    - Disable dialog offers a precise restore driven by the backup ledger:
      only sessions recorded as "openai" in backups are flipped back, and
      sessions created while the toggle was on are never touched
    - Completion marker and backup generations are bound to the canonical
      Codex config dir; migrate/restore serialize on an op lock and the
      marker is written conditionally inside the settings write lock
    - save_settings rolls back the toggle and fails the save when the live
      rewrite fails; migration additionally requires the live config to
      actually route to the shared bucket (skips with live_not_unified so
      refused injection or proxy takeover can't split history)
    - Restore refuses to run while the toggle is (re-)enabled and reports
      nothing_to_restore instead of a zero-count success; local migration
      markers are now backend-owned in merge_settings_for_save so stale
      frontend payloads can't resurrect them
    - Settings autosave reverts optimistic form state on failure so a
      failed toggle change can't be replayed by an unrelated save
    - ConfirmDialog supports an optional checkbox; all four locales updated
  • feat(codex): add unified session history toggle for official providers
    Codex buckets resume history by the model_provider id recorded in each
    session: official runs (no key, built-in "openai") and cc-switch
    third-party runs (shared "custom") are mutually invisible in the resume
    picker. Add an opt-in setting that runs official providers under the
    shared "custom" id so future official sessions land in the same history
    bucket as third-party ones. Forward-only by design: existing sessions
    are not migrated.
    
    When enabled, official live config.toml gets model_provider = "custom"
    plus a [model_providers.custom] entry that mirrors the built-in openai
    provider (requires_openai_auth routes auth to the ChatGPT login in
    auth.json, name "OpenAI" keeps is_openai() feature gates, explicit
    supports_websockets/wire_api restore built-in defaults). auth.json is
    untouched.
    
    Key invariants:
    - Injection lives only in the live config: switch-away backfill strips
      the exact injected shape, so stored provider configs stay clean and
      turning the toggle off fully reverts on the next write.
    - Toggle changes apply immediately via a takeover-aware reapply: when
      the proxy owns the live config (backup/placeholder present), only the
      live backup is updated, mirroring the provider-switch path.
    - The takeover backup path runs the same injection so a takeover
      release restores a config that still carries the unified routing.
    - Injection refuses to activate a foreign [model_providers.custom]
      table (e.g. stale entry with a third-party base_url) to avoid routing
      ChatGPT OAuth traffic to an unknown backend.
    
    The toggle lives under Settings → Codex App Enhancements; the
    description warns that resuming old sessions across providers may fail
    because encrypted_content reasoning only decrypts on the backend that
    created it (upstream treats cross-provider resume as unsupported).
  • 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>
  • 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>
  • feat: adaptive reasoning detection for Codex Chat providers
    Auto-detect each Chat-routed Codex provider's reasoning interface from
    its name, base URL, and model, then inject the matching thinking
    parameter without manual configuration:
    
    - Platform-first inference (OpenRouter, SiliconFlow) overrides model
      rules, since the same model exposes different reasoning controls
      depending on the hosting platform.
    - Effort tiers are forwarded only to providers that support them
      (DeepSeek, OpenRouter, and StepFun's step-3.5-flash-2603); on/off-only
      providers (Kimi, GLM, Qwen, MiniMax, MiMo, SiliconFlow) drop the level
      instead of sending a field the upstream rejects.
    - OpenRouter uses the native reasoning:{effort} object, clamps max to
      xhigh (its enum has no max), and forwards an explicit effort:"none" so
      reasoning can be turned off.
    - StepFun falls back to inference so per-model effort support is honored
      (the static preset would have forced effort on step-3.5-flash too).
    
    Includes the Codex provider-form reasoning controls, i18n strings
    (zh/en/ja), and response-side reasoning extraction.
  • feat: unify Codex third-party providers into stable "custom" history bucket
    Codex filters resume history by `model_provider`, so switching between
    provider-specific ids like `rightcode` and `aihubmix` made past sessions
    appear to vanish. Collapse all third-party providers into a single
    stable bucket so cross-switch history stays visible.
    
    - Normalize live `model_provider` to "custom" on every Codex write
      (reserved built-in ids like openai/ollama are preserved).
    - Add device-level one-shot migration that rewrites historical JSONL
      session files and the `state_5.sqlite` threads table from legacy
      provider ids into the "custom" bucket. Backs up originals under
      `~/.cc-switch/backups/codex-history-provider-migration-v1/` and uses
      the SQLite Backup API for the state DB.
    - Record completion in `settings.json` under `localMigrations` so the
      migration is strictly idempotent across launches.
    - Update Codex provider preset templates to emit `model_provider = "custom"`
      out of the box.
  • fix: 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.
  • 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.
  • 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 使用 Copilot/Codex OAuth 供应商
    - 放开本地路由托管 OAuth 供应商校验,允许动态 Token
    - 新增 Claude Desktop Copilot/Codex 预设与账号选择
    - 添加 OAuth proxy 回归测试
  • refactor(claude-desktop): drop displayName from model route schema
    Claude Desktop's new model menu reads model IDs directly and ignores the
    display_name field, so a separate displayName slot added UI noise without
    any product value. Collapse the routeId / model / displayName tuple down
    to routeId / model, and let the route ID carry the user-visible name
    through a non-editable claude- prefix rendered next to the input.
    
    Drop display_name from ClaudeDesktopModelRoute, ClaudeDesktopDefaultRoute,
    and ResolvedModelRoute on the Rust side plus the matching TS interfaces,
    stop emitting it in /v1/models responses, derive route IDs from upstream
    model IDs when picked via the model dropdown, and update zh/en/ja copy to
    describe the new two-field layout.
  • 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(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: Add Codex OAuth FAST mode toggle (#2210)
    * Add Codex OAuth FAST mode toggle
    
    * fix(codex-oauth): default FAST mode to off to avoid surprise quota burn
    
    service_tier="priority" consumes ChatGPT subscription quota at a higher
    rate. Users must now opt in explicitly rather than inherit FAST mode
    silently when this feature ships.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • 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.
  • 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)
  • feat(proxy): Gemini Native API proxy integration (#1918)
    * refactor(proxy): extract take_sse_block helper with CRLF delimiter support
    
    Replace inline `buffer.find("\n\n")` SSE splitting logic across streaming,
    streaming_responses, response_handler, and response_processor with a shared
    `take_sse_block` function that handles both `\n\n` and `\r\n\r\n` delimiters.
    
    * feat(proxy): add Gemini Native URL builder and full-URL resolver
    
    Introduce gemini_url module that normalizes legacy Gemini/OpenAI-compatible
    base URLs into canonical models/*:generateContent endpoints. Supports both
    structured Gemini URLs (auto-normalized) and opaque relay URLs (pass-through
    with query params only).
    
    * feat(proxy): add Gemini Native schema, shadow store, transform, and streaming
    
    - gemini_schema: Gemini generateContent request/response type definitions
    - gemini_shadow: session-scoped shadow store for thinking signature and
      tool-call state replay across streaming chunks
    - transform_gemini: bidirectional Anthropic Messages ↔ Gemini Native
      request/response conversion with thinking block and tool-use support
    - streaming_gemini: Gemini SSE → Anthropic SSE streaming adapter with
      incremental thinking/text/tool_use delta emission
    
    * feat(proxy): wire Gemini Native format into proxy core and Claude adapter
    
    Integrate gemini_native api_format throughout the proxy pipeline:
    - ClaudeAdapter: detect Gemini provider type, Google/GoogleOAuth auth
      strategies, and suppress Anthropic-specific headers for Gemini targets
    - Forwarder: Gemini URL resolution, shadow store threading, endpoint
      rewriting to models/*:generateContent with stream/non-stream variants
    - Handlers: route Gemini streaming through streaming_gemini adapter and
      non-streaming through transform_gemini converter
    - Server/State: add GeminiShadowStore to shared ProxyState
    - StreamCheck: support gemini_native health check with proper auth headers
    
    * feat(ui): add Gemini Native provider preset and api format option
    
    - Add gemini_native to ClaudeApiFormat type and ProviderMeta.apiFormat
    - Add "Gemini Native" provider preset with default Google AI endpoints
    - Show Gemini-specific endpoint hints and full-URL mode guidance
    - Add gemini_native option to API format selector in ClaudeFormFields
    - Add i18n strings for zh/en/ja
    
    * feat(proxy): add Gemini Native tool argument rectification
    
    * feat(proxy): update Gemini streaming and transformation logic
    
    * fix(proxy): align shadow turns to tail on client history truncation
    
    * fix: revert unrelated cache_key change in claude proxy transform
    
    Restore .unwrap_or(&provider.id) fallback for cache_key to match main
    branch behavior. Only gemini_native related changes should be in this branch.
    
    * Prevent Gemini review regressions in streaming and tool rectification
    
    PR #1918 review feedback exposed two correctness issues in the Gemini Native adapter path. Gemini SSE buffering was still using lossy UTF-8 decoding, which could corrupt split multibyte payloads and drop streamed output. Tool arg rectification also removed top-level parameters eagerly, which broke tools that legitimately define a parameters field.
    
    This change moves Gemini SSE buffering onto the existing append_utf8_safe path and makes parameters flattening conditional on the schema actually expecting nested extraction. The old Skill rectification path stays intact, and new regression tests cover both the preserved parameters case and UTF-8-split JSON payloads.
    
    Constraint: Existing PR #1918 review feedback must be fixed without staging unrelated local docs and artifact files
    Rejected: Keep String::from_utf8_lossy in Gemini SSE buffering | corrupts split multibyte payloads and can drop JSON chunks
    Rejected: Always preserve the parameters wrapper | regresses the existing nested-parameters rectification path for Skill-style tools
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Keep Gemini SSE buffering on the UTF-8-safe accumulator path and only unwrap parameters when the target schema does not declare it as a legitimate field
    Tested: cargo fmt --manifest-path src-tauri/Cargo.toml --all; cargo test --manifest-path src-tauri/Cargo.toml preserves_utf8_boundaries_when_json_payload_spans_chunks; cargo test --manifest-path src-tauri/Cargo.toml gemini_to_anthropic_rectifies_tool_args_from_schema_hints; cargo test --manifest-path src-tauri/Cargo.toml rectifies_streamed_skill_args_from_nested_parameters; cargo test --manifest-path src-tauri/Cargo.toml gemini_to_anthropic_preserves_legitimate_parameters_arg
    Not-tested: Full src-tauri test suite; live end-to-end Gemini relay traffic against upstream services
    
    * Keep Gemini tool replay stable across Claude request boundaries
    
    Claude Code follow-up requests were still falling back to locally reconstructed functionCall parts, which dropped Gemini thought signatures and triggered INVALID_ARGUMENT errors from the official Gemini API. The replay path needed to survive real Claude request boundaries, not just idealized in-process test flows.
    
    This change makes Claude requests reuse X-Claude-Code-Session-Id as the shadow session key, records streamed Gemini tool turns before tool_use events are fully drained, and matches assistant tool_use turns to shadow state by tool_use id and normalized tool name before positional fallback. Together these fixes keep thoughtSignature-bearing Gemini tool calls available for the next request in the loop.
    
    Constraint: Claude Code sends a stable X-Claude-Code-Session-Id header while metadata.session_id may be absent on follow-up requests
    Rejected: Rely on metadata-only Claude session extraction | generated fresh session ids and broke cross-request shadow replay
    Rejected: Record Gemini shadow only after streaming completes | loses the race when the client sends the next request immediately after tool_use
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Preserve Gemini shadow continuity across requests by keying Claude sessions from the header first and persisting tool-call shadow before yielding tool_use events downstream
    Tested: cargo fmt --manifest-path src-tauri/Cargo.toml --all; cargo test --manifest-path src-tauri/Cargo.toml test_extract_session_from_claude_header; cargo test --manifest-path src-tauri/Cargo.toml test_extract_session_from_claude_header_precedes_metadata; cargo test --manifest-path src-tauri/Cargo.toml stores_tool_shadow_before_tool_use_events_are_fully_drained; cargo test --manifest-path src-tauri/Cargo.toml shadow_replay_matches_tool_use_turn_by_id_when_position_drifts; cargo test --manifest-path src-tauri/Cargo.toml shadow_replay_aligns_to_latest_turns_after_client_truncation
    Not-tested: Full src-tauri test suite without test filters; live end-to-end Gemini relay after this exact commit hash
    
    * style: apply cargo fmt to pass Backend Checks CI
    
    Wrap prompt_cache_key chained call across lines per rustfmt default
    formatting. Pure formatting change, no behavior difference.
    
    * fix(proxy/gemini): synthesize unique ids for no-id tool calls + enforce object params schema
    
    P1 — Parallel tool calls without Gemini-assigned ids no longer collapse.
    Gemini 2.x native parallel `functionCall` entries may omit the `id` field.
    The previous `merge_tool_call_snapshots` fell back to matching by `name`,
    which silently merged two parallel calls to the same function into one
    entry — dropping the first call's args. The non-streaming path and shadow
    store further bottlenecked on empty-string ids: multiple `tool_use` blocks
    shared the same id, and `tool_name_by_id.get("")` could only return one
    mapping, causing later `tool_result` round-trips to fail with
    `Unable to resolve Gemini functionResponse.name` or bind to the wrong tool.
    
    Fix: introduce `synthesize_tool_call_id()` producing `gemini_synth_<uuid>`.
    Both streaming and non-streaming response paths now guarantee every
    Anthropic-visible tool_use carries a unique id. `merge_tool_call_snapshots`
    matches by id first, falling back to the `parts` array position (for the
    cumulative-streaming case) while preserving the synthesized id across
    chunks. `convert_message_content_to_parts` detects the synthetic prefix
    and strips the id from outbound `functionCall`/`functionResponse` so the
    internal identifier never leaks upstream. `shadow_parts` performs the
    same strip when replaying a recorded assistant turn.
    
    P2 — Vertex AI rejects empty `parameters` schemas. When an Anthropic tool
    arrives with missing or empty `input_schema`, the proxy used to emit
    `"parameters": {}` (no `type`), which fails Vertex AI validation with
    `functionDeclaration parameters schema should be of type OBJECT`.
    Contrary to the automated-review suggestion, the fix is not to omit
    `parameters` (that too is rejected) but to normalize to the canonical
    empty-object form `{type: "object", properties: {}}`.
    Refs: google-gemini/generative-ai-python#423, BerriAI/litellm#5055.
    
    Fix: new `ensure_object_schema` helper in `gemini_schema` promotes
    missing `type` to `"object"` and adds empty `properties` when absent,
    while leaving atomic (non-object) schemas untouched.
    
    Tests: seven new regressions covering parallel no-id calls, cumulative
    chunk id reuse, synthetic-id round-trip both directions, shadow replay
    id stripping, and the three Vertex-AI schema shapes.
    
    The two existing wrapper functions (`gemini_to_anthropic` and
    `gemini_to_anthropic_with_shadow`) gain `#[allow(dead_code)]` to clear
    a pre-existing clippy -D warnings failure — they are part of the public
    transform API surface and intentionally kept for future callers.
    
    Addresses Codex review P1/P2 on #1918.
    
    * fix(proxy/gemini): narrow URL normalization + guard empty OAuth access_token
    
    P2a — Preserve opaque relay URLs that contain `/v1/models/` prefixes.
    
    `should_normalize_gemini_full_url` previously flagged any full URL whose
    path merely contained `/v1beta/models/` or `/v1/models/` as a structured
    Gemini endpoint, forcing rewrite to `.../v1beta/models/{model}:method`.
    This silently dropped legitimate relay route segments (e.g.
    `https://relay.example/v1/models/invoke` → `.../v1beta/models/...:generateContent`,
    losing `/invoke`) and sent traffic to the wrong upstream path.
    
    Replace the bare `contains(...)` checks with
    `matches_structured_gemini_models_path`, which requires the
    `/models/` segment to be followed by a canonical Gemini method call
    (`*:generateContent` or `*:streamGenerateContent`). The
    `matches_bare_gemini_models_path` helper is generalized (and renamed) to
    handle both `/v1beta/models/` and `/v1/models/` alongside the original
    bare `/models/` shape.
    
    P2b — Reject empty Gemini OAuth access_tokens before they reach the
    bearer header.
    
    `GeminiAdapter::parse_oauth_credentials` accepts refresh-token-only JSON
    (and surfaces `{"access_token": "", ...}` for expired credentials) with
    `access_token` defaulting to `""`. The Claude adapter's GeminiCli branch
    then called `AuthInfo::with_access_token(key, creds.access_token)`
    unconditionally, so the bearer-header builder at
    `AuthStrategy::GoogleOAuth` resolved to `Authorization: Bearer ` — a
    deterministic 401 from upstream.
    
    CC Switch does not currently exchange the refresh_token for a fresh
    access_token (`OAuthCredentials::needs_refresh` / `can_refresh` are
    annotated `#[allow(dead_code)]`). Until that exists, only attach
    `access_token` when it is non-empty; fall back to plain GoogleOAuth
    strategy with the raw key and log a warn pointing users at
    `~/.gemini/oauth_creds.json` so the failure mode is observable.
    
    Tests:
    - gemini_url.rs: three new regressions — opaque `/v1/models/invoke`,
      opaque `/v1beta/models/route`, and the positive counter-case where a
      structured `/v1/models/...:generateContent` path still normalizes.
    - claude.rs: three new `test_extract_auth_gemini_cli_*` tests covering
      refresh-only JSON, empty-string access_token JSON, and the valid-JSON
      pass-through.
    
    All 839 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex review P2 findings on #1918.
    
    * fix(proxy/gemini): treat empty-string functionCall id as missing in streaming path
    
    Follow-up to the earlier P1 fix: some Gemini relays serialize an absent
    functionCall id as `"id": ""` instead of omitting the field. The
    non-streaming `extract_tool_call_meta` already filters these via
    `.filter(|s| !s.is_empty())`, but the streaming counterpart
    `extract_tool_calls` passed the empty string straight through
    `function_call.get("id").and_then(|v| v.as_str())` into
    `GeminiToolCallMeta::new`, producing a `Some("")` id.
    
    Downstream, `merge_tool_call_snapshots` would then match two parallel
    no-id calls against each other on their shared empty-string id,
    collapsing them into a single snapshot (silent data loss for the first
    call) and emitting an Anthropic `tool_use.id: ""` that breaks tool_result
    correlation on the Claude Code client.
    
    Fix:
    - `extract_tool_calls`: apply the same `filter(|s| !s.is_empty())` guard
      used in the non-streaming path so empty strings become `None` before
      reaching the shadow meta.
    - `merge_tool_call_snapshots`: defensively collapse any incoming
      `Some("")` to `None` up front — keeps the "missing vs present" invariant
      local to the merge step for future callers that might build
      `GeminiToolCallMeta` by hand.
    
    Tests (2 new, both in streaming_gemini):
    - `parallel_empty_string_id_calls_are_treated_as_missing_and_preserved`
      covers two parallel calls with explicit `"id": ""` — asserts both
      surface, no empty tool_use id leaks, and each gets a unique
      `gemini_synth_` id.
    - `single_empty_string_id_tool_call_gets_synthesized_id` covers the
      non-parallel degraded-relay case.
    
    All 841 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex follow-up P1 on #1918.
    
    * fix(proxy/gemini): gate generic REST path suffixes behind Google host whitelist
    
    `should_normalize_gemini_full_url` previously treated any full URL whose
    path ends with `/v1`, `/v1/models`, `/models`, `/v1/openai`, or `/openai`
    as a structured Gemini endpoint and rewrote it to
    `/v1beta/models/{model}:generateContent`. These are ubiquitous REST
    conventions — opaque relays such as `https://relay.example/custom/v1`
    legitimately use them for fixed endpoints — so the rewrite silently
    routed traffic to the wrong upstream path.
    
    Split the predicate into two layers:
    
    - **Unconditional**: `matches_structured_gemini_models_path` (i.e. a
      `/models/...:generateContent` method call anywhere in the path), the
      Google-specific `/v1beta*` family, and the deep OpenAI-compat paths
      (`/v1beta/openai/chat/completions`, `/openai/chat/completions`, and
      their `responses` siblings). These remain host-agnostic because the
      path grammar itself is Gemini-specific.
    - **Google-host gated**: `/v1`, `/v1/models`, `/models`, `/v1/openai`,
      `/openai`. Only normalized when the host is one of
      `generativelanguage.googleapis.com`, `aiplatform.googleapis.com`, or a
      real `*-aiplatform.googleapis.com` Vertex regional endpoint. The match
      is exact/suffix (not `contains`), so lookalike hosts like
      `aiplatform.example.com` are correctly treated as opaque relays.
    
    Tests (8 new in `gemini_url::tests`):
    - Four opaque-relay cases: `/custom/v1`, `/custom/models`,
      `/custom/v1/models`, `/custom/openai` — all preserved as-is.
    - Three Google-host counter-cases: `/v1`, `/models`, and
      `us-central1-aiplatform.googleapis.com/v1` still normalize.
    - One lookalike safety case: `aiplatform.example.com/v1` is NOT
      treated as Google.
    
    All 849 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex review P2 on #1918.
    
    * fix(proxy/gemini): align shadow id with client-visible id in non-streaming path
    
    When Gemini returns a `functionCall` without an id (common in 2.x
    parallel calls), `gemini_to_anthropic_with_shadow_and_hints` previously
    generated TWO independent synthesized UUIDs:
    
      1. Line 186-197 — synthesized id `A` used for the Anthropic-visible
         `content[tool_use].id` returned to the client.
      2. Line 850-881 — `extract_tool_call_meta` independently synthesized
         id `B ≠ A`, which populated `shadow_turn.tool_calls[i].id`.
    
    `shadow_content` (line 225-228, cloned from `rectified_parts`) retained
    the original missing/empty id. Result: the client sees id `A`, the
    shadow store holds id `B`.
    
    On the next turn, `convert_messages_to_contents` builds
    `tool_name_by_id` from `build_tool_name_map_from_shadow_turns`, which
    uses `tool_calls[i].id` — so the map contains `B → name` but not
    `A → name`. When the client sends back `tool_result(tool_use_id=A)`,
    resolution fails with:
    
      Unable to resolve Gemini functionResponse.name for tool_use_id `A`
    
    This affects both truncated histories (client sends only the
    tool_result) and full histories (shadow-replay branch at line 342-354
    skips `convert_message_content_to_parts`, so the assistant tool_use
    block never registers id `A` itself).
    
    Fix: make `rectified_parts` the single source of truth. After
    `rectify_tool_call_parts`, run a pre-pass that writes
    `synthesize_tool_call_id()` back into any `functionCall` that lacks a
    non-empty id. All three readers — the content builder (186-197), the
    shadow_content clone (225-228), and `extract_tool_call_meta` — then
    observe the same id. `shadow_parts()` already strips synthesized ids on
    replay (line 616-628), so the internal identifier never leaks to
    Gemini upstream.
    
    This mirrors the streaming path, which already has single-source-of-
    truth semantics via `tool_call_snapshots` in `streaming_gemini.rs` —
    no change needed there.
    
    Tests (5 new in `transform_gemini::tests`):
    - `non_stream_shadow_id_matches_client_visible_id`: asserts
      `response.content[0].id == shadow.tool_calls[0].id ==
      shadow.assistant_content.parts[0].functionCall.id`.
    - `non_stream_missing_id_scenario_a_truncated_history_resolves`: turn 2
      sends only `[tool_result(id=A)]`; resolution must succeed.
    - `non_stream_missing_id_scenario_b_full_history_replay_resolves`: turn 2
      sends `[assistant(tool_use=A), tool_result(A)]`; shadow-replay branch
      strips the synth id from outgoing `functionCall` while still
      resolving the subsequent `tool_result`.
    - `non_stream_preserves_original_gemini_id_when_present`: regression —
      genuine Gemini ids flow through unchanged.
    - `non_stream_synthesized_id_not_leaked_to_gemini_via_shadow_replay`:
      defensive — shadow-replay path must strip synth ids from both
      `functionCall.id` and `functionResponse.id`.
    
    All 854 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex follow-up P1 on #1918.
    
    * refactor(proxy/gemini): share build_anthropic_usage between stream and non-stream paths
    
    `streaming_gemini::anthropic_usage_from_gemini` and
    `transform_gemini::build_anthropic_usage` were byte-for-byte identical
    (32 lines each) — both converting Gemini `usageMetadata` into the
    Anthropic `usage` shape including `cache_read_input_tokens` mapping.
    
    Promote the non-streaming version to `pub(crate)` and reuse it from the
    streaming SSE converter. Removes ~30 lines of duplication and guarantees
    the two paths cannot drift apart.
    
    No behavioral change; all 854 lib tests pass; cargo fmt + clippy -D
    warnings clean.
    
    * fix(proxy/gemini): gate /v1beta behind Google host + normalize models/ model id prefix
    
    Two related P2 corrections to the Gemini Native URL surface, both
    folding into the existing Google-host-whitelist architecture.
    
    ## P2a — `/v1beta` suffix should not unconditionally trigger rewrite
    
    `should_normalize_gemini_full_url` placed `/v1beta` and `/v1beta/models`
    in the unconditional layer on the reasoning that `/v1beta` is
    Google-specific. In practice an opaque relay fronting a non-Gemini
    service at `https://relay.example/custom/v1beta` would still be
    silently rewritten to `/v1beta/models/{model}:generateContent`,
    breaking the deployment.
    
    Move `/v1beta`, `/v1beta/models`, and `/v1beta/openai` into the
    Google-host gated layer alongside `/v1`, `/models`, and friends. The
    unconditional layer now only accepts paths whose grammar is
    intrinsically Gemini — `/models/...:generateContent` method calls and
    the deep OpenAI-compat endpoints like `/openai/chat/completions` and
    `/openai/responses`. Pasted AI-Studio URLs such as
    `https://generativelanguage.googleapis.com/v1beta` still normalize
    because the host matches the whitelist.
    
    ## P2b — `model: "models/gemini-2.5-pro"` produced doubled path prefix
    
    Gemini SDKs (and the official `list_models` response) commonly surface
    model ids in resource-name form `models/gemini-2.5-pro`. Raw
    interpolation into `format!("/v1beta/models/{model}:...")` produced
    `/v1beta/models/models/gemini-2.5-pro:streamGenerateContent` which
    upstream rejects — yielding false-negative health checks for otherwise
    valid provider configs.
    
    Introduce `normalize_gemini_model_id(&str) -> &str` in `gemini_url`
    as the single source of truth: strips an optional leading `/` then an
    optional `models/` prefix, leaving bare ids untouched. Apply in the
    three call sites that build a Gemini method URL:
    - `services/stream_check.rs::resolve_claude_stream_url` (unified path)
    - `services/stream_check.rs::check_gemini_stream` (Gemini-only path)
    - `proxy/forwarder.rs::rewrite_claude_transform_endpoint` (production)
    
    Tests (9 new):
    - `gemini_url`: 3 regressions for opaque vs Google-host `/v1beta*`
      handling + 5 unit tests pinning `normalize_gemini_model_id` behavior
      (strip prefix, leave bare id, preserve nested slashes past the one
      stripped prefix, tolerate leading slash, pass through empty input).
    - `stream_check`: one end-to-end regression confirming
      `models/gemini-2.5-pro` collapses to the expected single-prefix URL.
    - `forwarder`: one end-to-end regression on the production rewrite
      path.
    
    All 864 lib tests pass; cargo fmt + clippy -D warnings clean.
    
    Addresses Codex P2 feedback on #1918.
    
    * fix(proxy/gemini): trim API key before provider-type detection and OAuth parsing
    
    Leading whitespace on a copied oauth_creds.json (e.g. trailing newline
    when the user copies the file content as-is) would slip past the
    `starts_with("ya29.") || starts_with('{')` prefix check in
    `ClaudeAdapter::provider_type`, causing the provider to be misclassified
    as raw-API-key Gemini and fall back to `x-goog-api-key` with the raw
    JSON as the key — which upstream rejects with 401.
    
    The frontend's `handleApiKeyChange` already trims on keystrokes but
    deep-link imports, the JSON editor, and live-config backfill all bypass
    that path. Trim at every backend extraction point so the coverage is
    uniform:
    
    - `ClaudeAdapter::extract_key` (5 env / fallback branches) gets
      `.map(str::trim)` before `.filter(|s| !s.is_empty())` so that
      whitespace-only values are also treated as missing.
    - `GeminiAdapter::extract_key_raw` gets the same chain (including
      the `.filter` it was missing before).
    - `GeminiAdapter::parse_oauth_credentials` gets a defensive
      `let key = key.trim();` at the entry as a belt-and-suspenders guard.
    
    Adds two regression tests covering JSON and bare `ya29.` keys with
    leading newline/space.
    
    * fix(proxy/gemini): gate generic REST suffix stripping behind Google host in non-full-URL mode
    
    `build_gemini_native_url` unconditionally stripped `/v1`, `/v1beta`,
    `/models`, and `/openai` suffixes from the base path regardless of
    host. This worked for Google's own endpoints but silently rewrote
    third-party relay URLs like `https://relay.example/custom/v1` to
    `.../custom/v1beta/models/...`, breaking any relay that mounts its
    Gemini-compatible namespace under a versioned prefix.
    
    The result was also asymmetric with the previously-fixed full-URL
    branch: toggling the "full URL" switch changed the outbound URL for
    the same base_url, which is exactly the kind of invisible behavior
    that makes debugging proxy deployments painful.
    
    Align `normalize_gemini_base_path` with
    `should_normalize_gemini_full_url`'s layered model:
    
    - Unconditional: `/models/...:method` structured paths and deep
      OpenAI-compat endpoints (`/openai/chat/completions`,
      `/openai/responses` and their versioned variants) — these are
      unambiguous Gemini-specific grammar on any host.
    - Google-host gated: generic `/v1`, `/v1beta`, `/models`, `/openai`
      suffixes only get stripped on `generativelanguage.googleapis.com`,
      `aiplatform.googleapis.com`, or `*-aiplatform.googleapis.com`.
      Other hosts preserve the prefix verbatim so relays keep their
      intended routing.
    
    Adds seven regression tests for the non-full-URL flow: opaque relay
    preservation (v1 / v1beta / models / openai suffix variants), Google
    host normalization (counter-case), and boundary cases (structured
    method path and deep OpenAI-compat endpoint stripped regardless of
    host).
    
    Test count: 864 -> 873.
    
    * Revert "fix(proxy/gemini): gate generic REST suffix stripping behind Google host in non-full-URL mode"
    
    This reverts commit d19ff09cb7.
    
    * test(proxy/gemini): pin non-full-URL versioned relay base stripping
    
    Adds two regression tests that lock in the intentional asymmetry
    between full-URL and non-full-URL modes:
    
    - Full-URL mode: opaque base path (e.g. `https://relay.example/custom/v1beta`)
      is preserved verbatim. Already covered by
      `preserves_opaque_full_url_with_bare_v1beta_suffix`.
    - Non-full-URL mode: base path MUST strip `/v1`, `/v1beta`, etc. so the
      standard `/v1beta/models/{model}:method` endpoint can be appended
      without producing a doubled `/v1beta/v1beta/models/...` path.
    
    The non-full-URL contract is "base URL + cc-switch appends the
    canonical Gemini endpoint". A user who needs a relay's custom
    namespace (e.g. `/v1/models/...`) must use full-URL mode and paste
    the complete method path. This commit adds regression coverage so a
    future attempt to mirror full-URL's host-whitelist gating into
    `normalize_gemini_base_path` will fail the test suite immediately.
    
    * chore(lint): address clippy 1.95 findings in existing modules
    
    CI upgraded to Rust 1.95 and flagged ten pre-existing warnings that
    older toolchains did not enforce. None relate to the Gemini proxy
    integration PR itself but they block CI on the feature branch, so
    clean them up here as a separate commit for easy review:
    
    collapsible_match:
    - proxy/providers/gemini_schema.rs: `"items" if value.is_object()`
      match guard instead of nested if.
    - proxy/providers/transform_responses.rs: fold
      `map_responses_stop_reason`'s `"completed"` / `"incomplete"` arms
      into match guards, relying on the existing `_ => "end_turn"` fall-
      through for non-matching guard conditions (semantics preserved).
    - services/session_usage_codex.rs: fold
      `"session_meta" if state.session_id.is_none()` guard, relying on
      the existing `_ => {}` fall-through.
    
    unnecessary_sort_by:
    - services/provider/endpoints.rs: `sort_by_key(|ep| Reverse(ep.added_at))`.
    - services/skill.rs (backup list): same Reverse idiom on `created_at`.
    - services/skill.rs (skill listings x2): `sort_by_key(|s| s.name.to_lowercase())`.
    
    useless_conversion:
    - services/skill.rs: drop the explicit `.into_iter()` on `zip`'s argument.
    
    while_let_loop:
    - services/webdav_auto_sync.rs: `while let Some(wait_for) = ...`
      instead of `loop { let Some(...) = ... else { break }; ... }`.
    
    All changes are mechanical and preserve behavior. `cargo test --lib`
    remains green (868 passed).
    
    * fix(proxy/gemini): reconcile synthesized tool-call ids with later real ids + preserve thoughtSignature
    
    Three related findings on `streaming_gemini.rs` for Gemini's cumulative
    `streamGenerateContent` stream, all centered on `merge_tool_call_snapshots`:
    
    1. (P1) Match upgraded tool-call IDs by position.
       When Gemini delivers a `functionCall` without an id on chunk 1
       (cc-switch synthesizes `gemini_synth_*`) and then upgrades it to a
       real id on chunk 2, the `Some(incoming_id)` branch only matched by
       id and missed the existing synthesized snapshot. A second entry
       would be pushed, yielding duplicate `tool_use` content blocks at
       stream end — one with the synthesized id, one with the real id —
       which could trigger duplicate tool execution and break tool_result
       correlation. Add a positional fallback: when no id match exists but
       the same-position slot holds a synthesized id, merge into it.
       `or(preserved_id)` already lets the real id win the merge.
    
    2. (P2) Preserve prior thoughtSignature when merging snapshots.
       `tool_call_snapshots[index] = tool_call` overwrote the slot
       entirely, dropping any `thoughtSignature` captured on an earlier
       chunk if the current cumulative snapshot omitted it. Since
       `build_shadow_assistant_parts` writes `thoughtSignature` into the
       shadow turn from `tool_call.thought_signature`, a dropped signature
       would cause later replay requests to Gemini to be rejected with
       invalid-signature errors. Preserve the existing signature when the
       incoming chunk does not carry one.
    
    3. (P2) Document the part-order streaming trade-off.
       All `tool_use` content blocks are emitted after the final text
       `content_block_stop`, so interleaved [text, functionCall, text,
       functionCall] parts arrive at the Anthropic client as [text(concat),
       tool_use, tool_use] — different from the non-streaming transformer,
       which preserves part order. This is intentional given the cumulative
       snapshot model and the consumers we target (claude-code-like clients
       don't depend on strict interleaving for tool execution correctness).
       Add a block comment at the flush site describing the trade-off and
       what a strict-order fix would entail, so this isn't rediscovered as
       a bug later.
    
    Regression tests:
    - upgraded_real_id_merges_into_existing_synthesized_snapshot
    - thought_signature_preserved_when_later_chunk_omits_it
    
    Test count: 868 -> 870. clippy 1.95 clean. fmt clean.
    
    * fix(proxy/gemini): prefer exact tool-call id over normalized-name fallback
    
    The shadow-turn matcher used a three-branch `||` chain (id / full name /
    normalized name). When two tools share a suffix (e.g. `server_a:search`
    and `server_b:search`), the normalized-name clause could short-circuit
    on an earlier turn whose id is actually wrong for the incoming tool_use,
    mis-routing replay state (functionCall id / thoughtSignature) for later
    tool_result resolution.
    
    Split matching into two layers: when the incoming message carries any
    tool_use ids, run id-based lookup first and return on the earliest hit.
    Only fall back to full-name / normalized-name matching when the incoming
    ids are absent or none of them resolve.
    
    Add two regressions:
    
    - shadow_replay_prefers_exact_id_match_over_normalized_name_collision
      Two shadow turns with colliding normalized names and two assistant
      messages whose ids cross the positional order; asserts each message
      replays the id-correct shadow turn (including thoughtSignature).
    
    - shadow_replay_falls_back_to_name_when_ids_absent
      Shadow turn with no id and incoming tool_use with an empty id;
      asserts the name fallback still populates the replayed part.
    
    ---------
    
    Co-authored-by: Jason <farion1231@gmail.com>
  • refactor: remove per-provider proxy config feature
    The per-provider proxy configuration (meta.proxyConfig) is removed
    because its scope is too narrow and covered by global proxy settings
    and proxy takeover mode. Users can achieve the same result via the
    global proxy panel.
    
    Changes:
    - Remove ProviderProxyConfig type (frontend TS + backend Rust)
    - Remove ProviderAdvancedConfig proxy UI block, keep testConfig/pricingConfig
    - Simplify http_client: delete build_proxy_url_from_config,
      build_client_for_provider, get_for_provider
    - Simplify forwarder/stream_check/model_fetch to use global client
    - Remove i18n keys (en/zh/ja)
    - Fix pre-existing test bug in transform.rs (extra None arg)
  • Stop sending prompt cache keys on Claude chat conversions (#2003)
    Responses conversions still use promptCacheKey, but chat completions now stay a pure shape transform. This keeps Claude -> chat requests aligned with providers that do not understand the field and keeps stream checks consistent with production behavior.
    
    Constraint: Issue #1919 requires removing prompt_cache_key from Claude -> OpenAI Chat requests
    Rejected: Add a runtime toggle for chat injection | requested behavior is unconditional removal
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Keep promptCacheKey limited to Claude -> Responses conversions unless a provider-specific contract is proven
    Tested: cargo test anthropic_to_openai
    Tested: cargo test anthropic_to_responses_with_cache_key
    Tested: cargo test transform_claude_request_for_api_format_responses
    Not-tested: Full src-tauri test suite
    Related: #1919
  • 添加应用级别窗口按钮,以改善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>
  • Let Kaku users launch sessions from their chosen terminal (#1954) (#1983)
    Kaku is a WezTerm-derived macOS terminal, so reusing the existing WezTerm-compatible launch path keeps the change small while making it selectable in settings and session resume flows.
    
    Constraint: Kaku support should stay macOS-only and avoid introducing a separate launcher model
    Rejected: Treat Kaku as a silent WezTerm fallback | users could not explicitly choose it in settings
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Keep Kaku on the shared WezTerm-compatible launch path unless upstream drops the start-compatible CLI
    Tested: pnpm typecheck; pnpm format:check; cargo check --manifest-path src-tauri/Cargo.toml; cargo fmt --manifest-path src-tauri/Cargo.toml --check; cargo test --manifest-path src-tauri/Cargo.toml --lib session_manager::terminal::tests
    Not-tested: End-to-end launch against a locally installed Kaku.app
    Related: #1954
  • feat(common-config): show first-run notice dialog when editing providers
    Display a one-time informational dialog explaining the Common Config
    Snippet feature when users first open the add/edit provider form.
    Uses a derived isOpen state from settings to avoid race conditions.
    Adds commonConfigConfirmed flag to both TS and Rust settings types.
  • feat(welcome): show first-run welcome dialog on fresh install
    Introduce a one-time welcome dialog that explains CC Switch's workflow
    to new users: how their existing config is preserved as a "default"
    provider and how the bundled "Official" preset enables one-click revert.
    Upgrade users are excluded by checking is_providers_empty() at startup
    and never see the dialog.
    
    Persistence follows the existing *_confirmed convention in AppSettings
    (proxy/usage/stream_check/failover), stored in settings.json. The field
    is only written when the user explicitly clicks the confirm button,
    keeping its semantics strictly about user acknowledgement.
    
    Also adds two reusable DAO helpers:
    - Database::is_providers_empty for fresh-install detection, using
      EXISTS(SELECT 1) for a short-circuit query.
    - Database::get_bool_flag accepting "true" | "1", with
      init_default_official_providers migrated to use it.
    
    Dialog copy in zh/en/ja uses conditional phrasing so it stays
    accurate whether or not existing live config was found.
  • feat: add skill storage location toggle between CC Switch and ~/.agents/skills
    Allow users to choose between storing skills in CC Switch's managed
    directory (~/.cc-switch/skills/) or the Agent Skills open standard
    directory (~/.agents/skills/). Includes migration logic that safely
    moves files before updating settings, with confirmation dialog for
    non-empty installations.
  • feat: add Token Plan quota query for Kimi, Zhipu GLM, and MiniMax
    Add a new "Token Plan" template type in the usage query panel that
    natively queries quota/usage from Chinese coding plan providers
    (Kimi For Coding, Zhipu GLM, MiniMax) without requiring custom scripts.
    
    - Rust backend: new coding_plan service with provider-specific API
      queries (Kimi /v1/usages, Zhipu /api/monitor/usage/quota/limit,
      MiniMax /coding_plan/remains) normalized into UsageResult
    - Frontend: Token Plan template in UsageScriptModal with auto-detection
      of provider based on ANTHROPIC_BASE_URL pattern matching
    - Follows the same pattern as GitHub Copilot template (dedicated API
      path in queryProviderUsage, no JS script needed)
  • 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(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: 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.
  • feat: add authHeader field to OpenClawProviderConfig and reuse type in form state
    Add optional `authHeader` boolean to support vendor-specific auth headers
    (e.g. Longcat). Refactor `resetOpenclawState` to use the shared
    `OpenClawProviderConfig` type instead of an inline duplicate definition.
  • feat: add OpenClaw User-Agent toggle, default off
    Add a switch in the OpenClaw provider form to optionally send a browser
    User-Agent header. The toggle defaults to off — only providers that
    explicitly include headers in their preset or config will have it enabled.
    
    Remove the previous auto-injection logic that force-added User-Agent on
    every preset load and new provider creation.
  • feat: show failover toggle independently on main page with confirm dialog
    Add enableFailoverToggle setting to control failover toggle visibility
    on the main page, decoupled from proxy takeover state. First-time
    enable shows a ConfirmDialog (same pattern as proxy toggle). The toggle
    row is placed in the Auto Failover accordion section in settings.
  • feat: apply common config as runtime overlay instead of materialized merge
    Common config snippets are now dynamically overlaid when writing live
    files, rather than being pre-merged into provider snapshots at edit time.
    This ensures that updating a snippet immediately takes effect for the
    current provider and automatically propagates to other providers on
    their next switch.
    
    Key changes:
    - Add write_live_with_common_config() overlay pipeline
    - Strip common config from live before backfilling provider snapshots
    - Normalize provider snapshots on save to keep them snippet-free
    - Add explicit commonConfigEnabled flag in ProviderMeta (Option<bool>)
    - Migrate legacy providers on snippet save (infer flag from subset check)
    - Add Codex TOML snippet validation in set_common_config_snippet
    - Stabilize onConfigChange callbacks with useCallback in ProviderForm
  • feat: add confirmation dialog for WebDAV auto-sync toggle
    Show a one-time traffic warning when users first enable auto-sync,
    persisted via autoSyncConfirmed flag in settings.
  • feat: add dual-layer versioning to WebDAV sync (protocol v2 + db-v6)
    Separate protocol version from database compatibility version in WebDAV
    sync paths. Upload writes to v2/db-v6/<profile>, download falls back to
    legacy v2/<profile> when current path has no data. Extend manifest with
    optional dbCompatVersion field and add legacy layout detection to UI.
  • feat: overhaul OpenClaw config panels with JSON5 round-trip write engine
    - Add json-five crate for JSON5 serialization preserving comments and formatting
    - Rewrite openclaw_config.rs with comment-preserving JSON5 read/write engine
    - Add Tauri commands: get_openclaw_live_provider, write_openclaw_config_section
    - Redesign EnvPanel as full JSON editor with structured error handling
    - Add tools.profile selection (minimal/coding/messaging/full) to ToolsPanel
    - Add legacy timeout migration support to AgentsDefaultsPanel
    - Add OpenClawHealthBanner component for config validation warnings
    - Add supporting hooks, mutations, utility functions, and unit tests
  • refactor: deduplicate and improve OpenAI Responses API conversion
    - Extract shared map_responses_stop_reason and build_anthropic_usage_from_responses into transform_responses.rs as pub(crate)
    - Align cache token extraction priority: OpenAI nested details as fallback, direct Anthropic fields as override
    - Extract resolve_content_index helper to eliminate 3x copy-paste in streaming_responses.rs
    - Add streaming reasoning/thinking event handlers (response.reasoning.delta/done)
    - Add explanatory comment to transform_response heuristic detection
    - Add openai_responses to api_format doc comment and needs_transform test
    - Add explicit no-op match arms for lifecycle events
    - Add promptCacheKey to TS ProviderMeta type
    - Update toast i18n key to be generic for both OpenAI formats (zh/en/ja)
  • feat: add OpenAI Responses API format conversion (api_format = "openai_responses")
    Support Anthropic ↔ OpenAI Responses API format conversion alongside existing
    Chat Completions conversion. The Responses API uses a flat input/output structure
    with lifted function_call/function_call_output items and named SSE lifecycle events.
  • feat: add first-run confirmation dialog for stream check
    Show an informational dialog when users first click the health check
    button, explaining its limitations (OAuth providers, relay services,
    Bedrock). The dialog persists the confirmation in settings so it only
    appears once per device.
  • revert: restore full config overwrite + Common Config Snippet (revert 992dda5c)
    Revert the partial key-field merging refactoring introduced in 992dda5c,
    along with two dependent commits (24fa8a18, 87604b18) that referenced
    the now-removed ClaudeQuickToggles component.
    
    The whitelist-based partial merge approach had critical issues:
    - Non-whitelisted custom fields were lost during provider switching
    - Backfill permanently stripped non-key fields from the database
    - Whitelist required constant maintenance to track upstream changes
    
    This restores the proven "full config overwrite + Common Config Snippet"
    architecture where each provider stores its complete configuration and
    shared settings are managed via a separate snippet mechanism.
    
    Reverted commits:
    - 24fa8a18: context-aware JSON editor hint + hide quick toggles
    - 87604b18: hide ClaudeQuickToggles when creating
    - 992dda5c: partial key-field merging refactoring
    
    Restored:
    - Full config snapshot write (write_live_snapshot) for Claude/Codex/Gemini
    - Full config backfill (settings_config = live_config)
    - Common Config Snippet UI and backend commands
    - 6 frontend components/hooks for common config editing
    - configApi barrel export and DB snippet methods
    
    Removed:
    - ClaudeQuickToggles component
    - write_live_partial / backfill_key_fields / patch_claude_live
    - All KEY_FIELDS constants
  • feat: Add AWS Bedrock Provider Support (AKSK & API Key) (#1047)
    * Add AWS Bedrock provider integration design document
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * Add AWS Bedrock provider implementation plan
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * Update implementation plan: add OpenCode Bedrock support
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add cloud_provider category to ProviderCategory type
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add AWS Bedrock (AKSK) Claude Code provider preset with tests
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add AWS Bedrock (API Key) Claude Code provider preset with tests
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add AWS Bedrock OpenCode provider preset with @ai-sdk/amazon-bedrock
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * docs: add AWS Bedrock provider feature summary for PR
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * chore: remove internal planning documents
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * docs: add AWS Bedrock support to README (EN/ZH/JA)
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * Add AWS Bedrock UI merge design document
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * Add AWS Bedrock UI merge implementation plan
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: skip optional template values in validation
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: support isSecret template fields and hide base URL for Bedrock
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: add Bedrock validation, cleanup, and isBedrock prop in ProviderForm
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * feat: extend TemplateValueConfig and merge Bedrock presets
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix: mask Bedrock API Key as secret and support GovCloud regions
    
    - Add isSecret: true to BEDROCK_API_KEY template value
    - Update region regex to support multi-segment regions (us-gov-west-1)
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * style: replace AWS icon with updated logo
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * style: replace AWS icon with updated logo
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * style: replace AWS icon with new PNG image
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix: address code review findings
    
    - Fix AWS icon: use SVG with embedded <image> instead of raw <img> tag
    - Hide duplicate ApiKeySection for Bedrock (auth via template fields only)
    - Guard settingsConfig cleanup against unresolved template placeholders
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * chore: remove planning documents
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * refactor: address PR review - split Bedrock into two presets, restore SVG icon
    
    Based on maintainer review feedback on PR #1047:
    
    1. Split merged "AWS Bedrock" back into two separate presets:
       - "AWS Bedrock (AKSK)": uses AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
       - "AWS Bedrock (API Key)": uses top-level apiKey field via standard UI input
    
    2. Restore aws.svg to pure vector SVG (was PNG-in-SVG)
    
    3. Remove all Bedrock-specific logic from shared components:
       - Remove isBedrock prop from ClaudeFormFields
       - Remove Bedrock validation/cleanup blocks from ProviderForm
       - Remove optional/isSecret from TemplateValueConfig
       - Remove optional skip from useTemplateValues
    
    4. Add cloud_provider category handling:
       - Skip API Key/Base URL required validation
       - Hide Speed Test and Base URL for cloud_provider
       - Hide API format selector for cloud_provider (always Anthropic)
       - Show API Key input only when config has apiKey field
    
    5. Fix providerConfigUtils to support top-level apiKey:
       - getApiKeyFromConfig: check config.apiKey before env fields
       - setApiKeyInConfig: write to config.apiKey when present
       - hasApiKeyField: detect top-level apiKey property
    
    6. Add OpenClaw Bedrock preset (bedrock-converse-stream protocol)
    
    7. Update model IDs:
       - Sonnet: global.anthropic.claude-sonnet-4-6
       - Opus: global.anthropic.claude-opus-4-6-v1
       - Haiku: global.anthropic.claude-haiku-4-5-20251001-v1:0
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix(test): align Bedrock API Key test assertions with preset implementation
    
    The API Key preset was refactored to use standard UI input (apiKey: "")
    instead of template variables, but the tests were not updated accordingly.
    
    ---------
    
    Co-authored-by: root <root@ip-10-0-11-189.ap-northeast-1.compute.internal>
    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • refactor(provider): switch from full config overwrite to partial key-field merging (#1098)
    * refactor(provider): switch from full config overwrite to partial key-field merging
    
    Replace the provider switching mechanism for Claude/Codex/Gemini from
    full settings_config overwrite to partial key-field replacement, preserving
    user's non-provider settings (plugins, MCP, permissions, etc.) across switches.
    
    - Add write_live_partial() with per-app implementations for Claude (JSON env
      merge), Codex (auth replace + TOML partial merge), and Gemini (env merge)
    - Add backfill_key_fields() to extract only provider-specific fields when
      saving live config back to provider entries
    - Update switch_normal, sync_current_to_live, add, update to use partial merge
    - Remove common config snippet feature for Claude/Codex/Gemini (no longer
      needed with partial merging); preserve OMO common config
    - Delete 6 frontend files (3 components + 3 hooks), clean up 11 modified files
    - Remove backend extract_common_config_* methods, 3 Tauri commands,
      CommonConfigSnippets struct, and related migration code
    - Update integration tests to validate key-field-only backfill behavior
    
    * refactor(cleanup): remove dead code and redundant MCP sync after partial-merge refactor
    
    - Remove ConfigService legacy full-overwrite sync methods (~150 lines)
    - Remove redundant McpService::sync_all_enabled from switch_normal
    - Switch proxy fallback recovery from write_live_snapshot to write_live_partial
    - Remove dead ProviderService::write_gemini_live wrapper
    - Update tests to reflect partial-merge behavior (MCP preserved, not re-synced)
    
    * feat(claude): add Quick Toggles for common Claude Code preferences
    
    Add checkbox toggles for hideAttribution, alwaysThinking, and
    enableTeammates that write directly to the live settings file via
    RFC 7396 JSON Merge Patch. Mirror changes to the form editor using
    form.watch for reactive updates.
    
    * fix(provider): add missing key fields to partial-merge constants
    
    Add provider-specific fields verified against official docs to prevent
    key residue or loss during provider switching:
    
    - Claude: CLAUDE_CODE_SUBAGENT_MODEL (env), model (top-level)
    - Codex: review_model, plan_mode_reasoning_effort
    - Gemini: GOOGLE_API_KEY (official alternative to GEMINI_API_KEY)
    
    * fix(provider): expand partial-merge key fields for Bedrock, Vertex, Foundry and behavior settings
    
    Add missing env/top-level fields to CLAUDE_KEY_ENV_FIELDS and
    CLAUDE_KEY_TOP_LEVEL so that provider switching correctly replaces
    (and clears) credentials and flags for AWS Bedrock, Google Vertex AI,
    Microsoft Foundry, and provider behavior overrides like max output
    tokens and prompt caching.
    
    * feat(provider): add auth field selector for Claude providers (AUTH_TOKEN / API_KEY)
    
    Allow users to choose between ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY
    when creating or editing custom Claude providers, persisted in meta.apiKeyField.
    
    * refactor(preset): remove AiHubMix hardcoded API_KEY in favor of generic auth selector
    
    AiHubMix was the only preset that hardcoded ANTHROPIC_API_KEY before the
    generic auth field selector was introduced. Now that users can freely
    choose between AUTH_TOKEN and API_KEY via the UI, remove the special-case
    and default AiHubMix to the standard ANTHROPIC_AUTH_TOKEN.
  • feat(backup): add independent backup panel, configurable policy, and rename support
    Extract backup & restore into a standalone AccordionItem in Advanced settings.
    Add configurable auto-backup interval (disabled/6h/12h/24h/48h/7d) and retention
    count (3-50) via settings. Add per-backup rename with inline editing UI.
  • feat(settings): add first-run confirmation dialogs for proxy and usage features
    Prevent accidental activation of advanced features by showing a one-time
    info dialog. Once confirmed, the flag is persisted in settings.json and
    the dialog never appears again.
    
    - Proxy: confirmation when toggling proxy server ON for the first time
    - Usage: confirmation when enabling usage query inside UsageScriptModal
    - Enhanced ConfirmDialog with "info" variant (blue icon + default button)
    - Added i18n translations for zh, en, ja
  • feat(settings): add enableLocalProxy toggle to gate main page proxy UI
    New users often accidentally trigger ProxyToggle/FailoverToggle on the
    main page. Add a settings toggle (default off) so the proxy controls
    only appear when explicitly enabled. The proxy service start/stop in
    settings remains independent of this visibility flag.