Commit Graph

639 Commits

  • 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): 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): 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.
  • 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.
  • 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
  • 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
  • Merge branch 'main' into feat/gemini-proxy-integration
    # Conflicts:
    #	src-tauri/src/proxy/providers/claude.rs
    #	src/i18n/locales/en.json
    #	src/i18n/locales/ja.json
    #	src/i18n/locales/zh.json
  • feat(stream-check): refresh default models and detect model-not-found errors (#2099)
    * chore(stream-check): update default health check models to latest
    
    Replaces deprecated gpt-5.1-codex@low with gpt-5.4@low and switches
    the Gemini default from gemini-3-pro-preview to gemini-3-flash-preview
    to pick the lightest variant of the latest series for fast, low-cost
    health checks.
    
    https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte
    
    * feat(stream-check): detect model-not-found errors with dedicated toast
    
    Health check previously classified failures purely by HTTP status code,
    which meant deprecated/invalid models showed up as a generic "Not found
    (404)" error pointing users to check the Base URL — misleading when the
    URL is fine and only the test model is wrong (e.g. gpt-5.1-codex after
    it was retired).
    
    Backend: add detect_error_category() that inspects 4xx response bodies
    for model-not-found indicators (model_not_found, does not exist,
    invalid model, not_found_error, etc.) and returns a "modelNotFound"
    category. Thread the resolved test model through build_stream_check_result
    so the failed result carries it in model_used. Add StreamCheckResult
    .error_category field (serde-skipped when None).
    
    Frontend: useStreamCheck branches on errorCategory === "modelNotFound"
    before the HTTP-status fallback and renders a toast.error with the model
    name and a description pointing to Model Test Config. Add i18n keys
    (modelNotFound / modelNotFoundHint) for zh/en/ja.
    
    Tests: unit-test detect_error_category against real OpenAI/Anthropic
    error shapes, 5xx false-positive avoidance, and plain 401 auth errors.
    
    https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte
    
    * fix(stream-check): add missing error_category field in fallback
    
    The error_category field was added to StreamCheckResult in this branch
    but the fallback constructor in stream_check_all_providers was not
    updated, which broke cargo build.
    
    ---------
    
    Co-authored-by: Claude <noreply@anthropic.com>
  • fix(opencode): use json5 parser for trailing comma tolerance (#2023)
    * fix(opencode): use json5 parser for trailing comma tolerance
    
    OpenCode CLI writes opencode.json with trailing commas (valid JSONC),
    but CC Switch parsed it with serde_json (strict JSON), causing errors
    like 'trailing comma at line 35 column 3'.
    
    Switch to json5::from_str which accepts both JSON and JSONC. The json5
    crate is already a project dependency. Change error type from
    AppError::json() to AppError::Config() since json5::Error differs from
    serde_json::Error.
    
    * style(opencode): apply rustfmt to satisfy cargo fmt --check
    
    The previous commit's .map_err(...) chain exceeded rustfmt's default
    100-char max_width, breaking CI's `cargo fmt --check`. Let rustfmt
    wrap the closure body as a multi-line block. No behavior change.
    
    ---------
    
    Co-authored-by: 18067889926 <ming.flute@outlook.com>
    Co-authored-by: Jason <farion1231@gmail.com>
  • fix: preserve env vars when saving Google Official Gemini provider (#2087)
    write_gemini_live() unconditionally cleared env_map for GoogleOfficial
    auth type, discarding user-configured env vars (e.g. GEMINI_MODEL).
    Remove the env_map.clear() call so the user's settings_config.env is
    written as-is, and merge identical Packycode/Generic match arms.
  • feat: classify stream check errors with color-coded toasts
    Distinguish between "provider rejects probe" (yellow warning) and
    "genuinely broken" (red error) in health check results.
    
    Backend: add AppError::HttpStatus variant to carry structured HTTP
    status codes, populate http_status on error results, classify codes
    into short labels (e.g. "Auth rejected (401)"), and truncate overly
    long response bodies.
    
    Frontend: route 401/403/400/429/5xx to toast.warning with localized
    hints explaining the error may not indicate actual unusability; route
    404/402/connection errors to toast.error. Add i18n keys for all three
    locales (zh/en/ja).
    
    Also deduplicate check_once by reusing build_stream_check_result.
  • fix: handle root-level skill repos during installation
    When a repo itself is a single skill (SKILL.md at repo root), the
    discovery phase sets directory to the repo name, but after ZIP
    extraction (which strips the root folder), no matching subdirectory
    exists. Add a fallback to check if SKILL.md exists directly in the
    extracted temp directory before reporting SKILL_DIR_NOT_FOUND.
    
    Fixes installation of repos like zlbigger/Google-SEOs.skill.
  • 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)
  • fix(clippy): remove redundant closure in session ID parsing
    Replace `|uid| parse_session_from_user_id(uid)` with direct
    function reference to satisfy clippy::redundant_closure.
  • fix(proxy): reduce unnecessary Copilot premium interaction consumption
    - Fix request classification: treat messages containing tool_result as
      agent continuation instead of user-initiated, preventing false premium
      charges on every tool call
    - Add subagent detection via __SUBAGENT_MARKER__ and metadata._agent_
      fallback, setting x-interaction-type=conversation-subagent
    - Add deterministic x-interaction-id derived from session ID to group
      requests into a single billing interaction
    - Add orphan tool_result sanitization to prevent upstream API errors
      that could cause retries and duplicate billing
    - Reorder pipeline: classify (on original body) → sanitize → merge →
      warmup, ensuring classification sees raw tool_result semantics
    - Enable warmup downgrade by default with gpt-5-mini model
    - Enhance session ID extraction priority chain for Copilot cache keys
    - Detect infinite whitespace bug in streaming tool call arguments
  • fix(usage): remove unnecessary private IP restrictions from usage script
    SSRF protection (private IP blocking, suspicious hostname detection) was
    originally added for web-server threat models but is unnecessary for a
    local desktop app where the user already has full network access. This
    removal unblocks legitimate use cases like enterprise intranet APIs,
    Docker container addresses, and self-hosted services.
    
    Retained: HTTPS enforcement and same-origin checks which still provide
    meaningful security (protecting API keys in transit and preventing
    scripts from leaking keys to unrelated domains).
  • fix(sessions): strip OpenClaw message_id suffix and allow 2-line titles
    OpenClaw gateway injects `[message_id: UUID]` metadata at the end of
    every message, wasting display space. Strip this suffix from both title
    and summary fields.
    
    Also change session title display from single-line truncate to
    line-clamp-2, so longer titles (e.g. OpenClaw's timestamp-prefixed
    messages) can show more meaningful content across two lines.
  • feat(sessions): extract meaningful titles for Codex and OpenClaw sessions
    Previously Codex and OpenClaw sessions only showed the working directory
    basename as the title, making it hard to distinguish sessions in the same
    project. Now both providers extract the first real user message as the
    session title, matching the existing Claude Code behavior.
    
    - Codex: first user message → dir basename (skips AGENTS.md injection)
    - OpenClaw: displayName (sessions.json) → first user message → dir basename
    - Move TITLE_MAX_CHARS constant to shared utils.rs
    - Use Option<&HashMap> for OpenClaw parse_session to avoid leaky abstraction
  • fix(usage): deduplicate proxy and session log usage records
    Extract message_id from Claude API responses (msg_xxx) and use it to
    generate a shared request_id format (session:{msg_xxx}) between the
    proxy logger and session log sync. When session sync encounters the
    same request_id via INSERT OR IGNORE, it skips the duplicate.
    
    - Add message_id field to TokenUsage, extracted from Claude responses
    - Add TokenUsage::dedup_request_id() to generate shared request IDs
    - Define SESSION_REQUEST_ID_PREFIX constant to eliminate magic strings
    - Change proxy logger to INSERT OR REPLACE for richer-data-wins semantics
  • feat(pricing): add ~50 new model pricing entries and fix outdated prices
    Add pricing data for 4 new providers (Qwen, xAI Grok, Mistral, Cohere)
    and supplement existing providers (MiniMax M2.5/M2.7, GLM-5/5.1,
    Doubao Seed 2.0, MiMo V2 Pro, OpenAI o1/o3/codex-mini/gpt-5-mini/nano).
    
    Fix outdated prices for deepseek-chat, deepseek-reasoner, and kimi-k2.5.
    Fix display_name casing "Mimo" → "MiMo" for consistency.
    
    Use prepared statement in seed_model_pricing() to avoid recompiling SQL
    on each of ~130 INSERT iterations.
    
    Schema migration v8→v9: DELETE + re-seed model_pricing for existing users.
  • feat: block official provider switching during proxy takeover
    Prevent users from switching to official providers (Anthropic/OpenAI/Google)
    when proxy takeover is active, as using a proxy with official APIs may cause
    account bans.
    
    Defense-in-depth across 4 layers:
    - Backend: ProviderService::switch(), hot_switch_provider(), switch_proxy_provider command
    - Frontend: useProviderActions soft guard with error toast
    - UI: ProviderActions button disabled with ShieldAlert icon
    - Tray menu: official provider items disabled with  indicator
    
    Also warns when enabling proxy takeover while current provider is official.
  • [codex] Preserve cache_control when merging system prompts (#1946)
    * Preserve cache hints when collapsing system prompts
    
    Strict OpenAI-compatible chat backends still need fragmented Claude\nsystem prompts collapsed into one leading system message, but that\nnormalization should not silently drop stable cache hints. Preserve\nmessage-level cache_control when the merged system fragments agree,\nand fall back to omitting it when the fragments conflict.\n\nConstraint: Must keep single-system normalization for Nvidia/Qwen-style chat backends\nRejected: Always copy the first cache_control | could misrepresent conflicting cache boundaries\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: If system prompt merging changes again, preserve cache_control whenever the merged metadata is unambiguous\nTested: cargo test proxy::providers::transform --manifest-path src-tauri/Cargo.toml\nNot-tested: End-to-end prompt caching behavior against cache-aware OpenAI-compatible upstreams\nRelated: #1881
    
    * Tighten cache hint inheritance for merged system prompts
    
    The follow-up cache hint fix still treated mixed present/absent\ncache_control across fragmented system prompts as inheritable, which\nexpanded the cache scope after prompt collapse. Treat that mix as\nambiguous and only preserve cache_control when every merged fragment\nexplicitly agrees on the same value.\n\nConstraint: Must preserve strict-backend system prompt normalization from #1942\nRejected: Inherit first present cache_control | widens cache scope when later fragments were intentionally uncached\nConfidence: high\nScope-risk: narrow\nReversibility: clean\nDirective: Any future merged-system cache hint logic should treat missing cache_control as semantically significant\nTested: cargo test proxy::providers::transform --manifest-path src-tauri/Cargo.toml\nNot-tested: End-to-end upstream caching behavior against cache-aware relays\nRelated: #1881\nRelated: #1946
    
    * Keep cache-control merge regressions easy to review
    
    Reflow the two long cache-control regression assertions in transform.rs so the neighboring merge cases stay rustfmt-aligned and easier to scan.
    
    This keeps the preserved code change separate from the untracked Markdown design notes the user did not want committed.
    
    Constraint: Exclude Markdown design files from the commit while preserving the local code change
    Rejected: Include docs in the same commit | user explicitly asked to leave Markdown files out
    Confidence: high
    Scope-risk: narrow
    Reversibility: clean
    Directive: Treat this as a readability-only test change; do not infer runtime behavior changes from it
    Tested: cargo test --manifest-path src-tauri/Cargo.toml test_anthropic_to_openai_drops_ --lib
    Tested: cargo check --manifest-path src-tauri/Cargo.toml --tests
    Tested: pnpm format:check
    Tested: pnpm typecheck
    Not-tested: Full application integration and manual flows
  • 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>
  • 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.
  • Merge branch 'main' into feat/gemini-proxy-integration
    Resolve conflict in claudeProviderPresets.ts: keep both Gemini Native
    and Shengsuanyun presets.
  • chore(release): bump version to v3.13.0 and sync changelog/release notes
    Backfill post-draft changes into CHANGELOG and three-language release
    notes (en/zh/ja): 16 new Added entries, 6 Fixed entries, 1 Docs entry,
    and updated header stats (139 commits, 280 files, +31627/-3042).
  • fix: map adaptive thinking to xhigh reasoning_effort instead of high
    When thinking.type is "adaptive" (Claude's maximum thinking mode) and
    output_config.effort is absent, resolve_reasoning_effort() incorrectly
    mapped it to "high" instead of "xhigh" in OpenAI format conversions.
  • 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
  • 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
  • Merge branch 'main' into feat/gemini-proxy-integration
    # Conflicts:
    #	src-tauri/src/proxy/providers/claude.rs
    #	src-tauri/src/proxy/sse.rs
    #	src-tauri/src/services/stream_check.rs
  • feat(session-manager): extract meaningful titles for Claude sessions
    Instead of showing directory basenames for all Claude sessions, extract
    titles from JSONL content with a priority chain:
    1. custom-title metadata (set via /rename in Claude Code)
    2. First real user message (skipping /clear, /compact caveats)
    3. Directory basename (fallback)
  • 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(providers): auto-import OpenCode/OpenClaw live providers on startup
    Drops the friction of clicking the manual "Import current config" button
    for OpenCode and OpenClaw — they now match the auto-import behavior the
    previous commit added for Claude/Codex/Gemini.
    
    - New "1.6." startup block in lib.rs runs both
      import_opencode_providers_from_live and import_openclaw_providers_from_live
      on every launch. The functions are id-keyed and idempotent, so re-running
      just picks up new providers added externally to the live JSON files.
    - Both functions now use a new Database::get_provider_ids() helper
      (HashSet<String> from a single SELECT id-only query) instead of
      get_all_providers(), avoiding the N+1 endpoint sub-queries that would
      otherwise hit the startup hot path on every launch.
  • feat(providers): seed an official preset on startup for Claude/Codex/Gemini
    New and existing users now see a built-in "Claude Official" / "OpenAI
    Official" / "Google Official" entry in their provider list, so switching
    back to the official endpoint is one click away instead of buried in the
    README.
    
    - New providers_seed.rs holds the three seeds (id, name, settings_config,
      icon) keyed by AppType, with a single is_official_seed_id() helper that
      scans OFFICIAL_SEEDS so the id list has one source of truth.
    - Database::init_default_official_providers() runs once per database
      (gated by an official_providers_seeded setting flag), appends each seed
      to the end of the sort order, and never touches is_current.
    - Startup also auto-imports the live config (settings.json / auth.json /
      .env) as a "default" provider before seeding, so users with an existing
      manual config don't lose it when they click the official preset.
    - Database::has_non_official_seed_provider() replaces the get_all_providers
      call in import_default_config's gating check with an id-only scan,
      dropping the N+1 endpoint sub-queries from every startup.
  • refactor(stream-check): rename helper and drop phase markers
    Post-merge cleanup from a simplify review pass on the phase 1-4
    OpenCode/OpenClaw changes.
    
    - Rename check_once_opencode_like → check_once_without_adapter. The
      new name directly expresses the intent (bypass get_adapter) instead
      of suggesting the function is somehow "like" OpenCode.
    - Drop two "Phase 4 会美化错误消息" phase-history markers from
      docstrings; git history is the right place for them.
    - Document in resolve_opencode_base_url why its default endpoints
      cannot be merged with ProviderType::default_endpoint(): the former
      encode AI SDK package defaults (e.g. @ai-sdk/openai ships with the
      /v1 suffix) while the latter encode proxy upstream hosts. They
      happen to overlap but are two independent truth sources.
  • feat(stream-check): handle edge cases for OpenCode/OpenClaw
    Phase 4: polish the four remaining edge cases uncovered by Phase 1-3.
    
    Custom headers passthrough
    - check_claude_stream and check_gemini_stream now accept an optional
      extra_headers map which is appended after all built-in headers so it
      can override defaults (e.g. a custom User-Agent).
    - OpenClaw reads from settings_config.headers.
    - OpenCode reads from settings_config.options.headers.
    - All pre-existing Claude/Codex/Gemini call sites pass None.
    
    OpenClaw custom auth header (Longcat-style)
    - When settings_config.authHeader is true, the provider expects a
      custom auth header whose name is only known to the OpenClaw gateway
      itself. Return a dedicated openclaw_auth_header_not_supported error
      so the user sees a meaningful explanation instead of a 401.
    
    Bedrock error polish
    - The bedrock-converse-stream (OpenClaw) and @ai-sdk/amazon-bedrock
      (OpenCode) branches now explain why (SigV4 signing) and point to
      the official consoles as an alternative test path.
    
    OpenCode baseURL fallback
    - resolve_opencode_base_url: when options.baseURL is empty and the
      npm package has a canonical default endpoint (@ai-sdk/openai,
      @ai-sdk/anthropic, @ai-sdk/google), fall back to that endpoint.
      @ai-sdk/openai-compatible still requires an explicit baseURL
      because its whole purpose is to point at a custom OpenAI clone.
    
    Tests
    - 8 new unit tests covering authHeader detection, baseURL resolution
      (explicit / fallback / error), and header map extraction on both
      apps. Total stream_check tests: 18 → 26.
  • feat(stream-check): support OpenCode via npm package mapping
    Phase 3: implement stream check for OpenCode providers by mapping the
    `settings_config.npm` (AI SDK package name) to the corresponding API
    protocol and delegating to the existing stream checkers.
    
    Package mapping:
    - @ai-sdk/openai-compatible → openai_chat
    - @ai-sdk/openai            → openai_responses
    - @ai-sdk/anthropic         → anthropic (ClaudeAuth strategy)
    - @ai-sdk/google            → gemini (Google strategy)
    - @ai-sdk/amazon-bedrock    → not supported (phase 4 message polish)
    
    Note: OpenCode nests baseURL/apiKey under `settings_config.options`
    (different from OpenClaw's root-level fields) and uses `baseURL` with
    a capital L. Three new extractors (base_url / api_key / npm) encode
    these shape differences so check_opencode_stream stays symmetric with
    check_openclaw_stream.
    
    Frontend: drop the remaining `appId !== "opencode"` filter in
    ProviderList.tsx — both apps can now test providers.
  • feat(stream-check): support remaining 3 OpenClaw protocols
    Phase 2: extend check_openclaw_stream to cover the full non-Bedrock
    protocol set declared by openclawApiProtocols.
    
    - openai-responses   → check_claude_stream(api_format="openai_responses")
    - anthropic-messages → check_claude_stream(api_format="anthropic"),
      using AuthStrategy::ClaudeAuth (Bearer-only) so Claude relay
      services that reject a simultaneous x-api-key still work. Official
      Anthropic also accepts pure Bearer on /v1/messages.
    - google-generative-ai → check_gemini_stream with AuthStrategy::Google.
    
    bedrock-converse-stream still errors out but with a dedicated
    openclaw_bedrock_not_supported key; its user-facing message will be
    polished in phase 4.
    
    Each protocol now builds its own AuthInfo inside the match arm because
    the auth strategy is protocol-specific.
  • feat(stream-check): support OpenClaw openai-completions protocol
    Phase 1 of extending stream health check to OpenCode/OpenClaw apps.
    
    - Add early-dispatch path for OpenCode/OpenClaw in check_once so they
      bypass the adapter layer (which only knows Claude/Codex/Gemini
      settings_config shapes).
    - Introduce check_openclaw_stream dispatcher that reads the `api` field
      from settings_config and routes to the existing check_claude_stream
      with api_format="openai_chat" for "openai-completions". Other
      protocols return localized errors to be lit up in phases 2 and 4.
    - Extract build_stream_check_result helper to avoid duplicating the
      StreamCheckResult construction logic between the two code paths.
    - Unblock the test button for OpenClaw providers in ProviderList.tsx.
    
    OpenCode still returns the "not yet supported" error; it will be
    enabled in phase 3.
  • fix(linux): repair unresponsive UI on startup and full-screen panels
    Linux users reported the window UI (including native title bar buttons)
    couldn't receive clicks until manually maximizing and restoring the
    window. Root causes: (1) Tauri webview did not acquire focus on startup
    so first clicks were consumed by X11/Wayland click-to-activate
    (Tauri #10746, wry #637); (2) GTK surface input region failed to
    renegotiate on the visible:false + show() path under some
    WebKitGTK/compositor combinations.
    
    - Add linux_fix::nudge_main_window helper that performs set_focus plus
      a ±1px no-op resize after window show, with a 500ms reconciliation
      readback to compensate for dropped resize requests on slow
      compositors.
    - Wire the helper into every window re-show path: normal startup,
      deeplink, single_instance, tray show_main, and lightweight exit.
    - Set WEBKIT_DISABLE_COMPOSITING_MODE=1 at startup to avoid resize
      crashes and Wayland surface negotiation issues.
    - Remove data-tauri-drag-region on Linux from App.tsx header and the
      shared FullScreenPanel (used by all provider/MCP/workspace forms)
      to avoid Tauri #13440 in Wayland sessions. Extract drag-region
      constants to src/lib/platform.ts for reuse.
    
    All Rust changes are gated by #[cfg(target_os = "linux")]; frontend
    changes preserve macOS/Windows behavior via runtime isLinux() checks.
    Known limitation: tiling Wayland compositors ignore set_size, so
    GDK_BACKEND=x11 remains the user-side workaround.
  • feat: display subscription quota for Codex OAuth provider cards
    Codex OAuth (ChatGPT Plus/Pro) providers previously fell through to the
    default UsageFooter branch and showed no quota at all, while Copilot and
    official Codex providers already had a wham/usage-backed quota footer.
    
    This wires up the same five-hour / seven-day tier badges for codex_oauth
    provider cards by reusing the existing query_codex_quota function and
    SubscriptionQuotaFooter rendering, parameterized to keep both the CLI
    credential path ("codex") and the cc-switch managed OAuth path
    ("codex_oauth") working from a single source of truth.
    
    - Parameterize services::subscription::query_codex_quota with tool_label
      and expired_message; promote SubscriptionQuota constructors to
      pub(crate). The CLI path keeps its existing "codex" label and the
      "re-login with Codex CLI" message; the new path passes "codex_oauth"
      and a cc-switch-specific re-login hint.
    - Add a new get_codex_oauth_quota Tauri command in commands/codex_oauth.rs
      that resolves the ChatGPT account (explicit binding > default account
      > not_found), pulls a valid access_token from CodexOAuthManager
      (auto-refresh handled), and delegates to query_codex_quota.
    - Extract SubscriptionQuotaFooter's render body into a pure
      SubscriptionQuotaView component (props: quota / loading / refetch /
      appIdForExpiredHint / inline). The existing SubscriptionQuotaFooter
      becomes a thin wrapper with identical props and behavior, so
      CopilotQuotaFooter and the official Claude/Codex/Gemini paths are
      untouched. This avoids duplicating ~280 lines of five-state rendering.
    - Add CodexOauthQuotaFooter, a 38-line wrapper that calls the new
      useCodexOauthQuota hook and forwards to SubscriptionQuotaView.
    - ProviderCard inserts an isCodexOauth branch between isCopilot and
      isOfficial, keyed off PROVIDER_TYPES.CODEX_OAUTH (newly added to
      config/constants.ts to centralize the previously scattered string).
    - Frontend hook caches per (codex_oauth, accountId) so multiple cards
      bound to the same ChatGPT account share one fetch via react-query
      dedup; cards bound to different accounts get independent fetches.
    - No new i18n keys: existing subscription.fiveHour / sevenDay / expired /
      refresh / queryFailed / expiredHint are reused.
  • feat: add Codex OAuth (ChatGPT Plus/Pro) reverse proxy support
    Adds a new managed OAuth provider that lets Claude Code route requests
    through a user's ChatGPT Plus/Pro subscription via the chatgpt.com
    backend-api/codex endpoint.
    
    - CodexOAuthManager: OpenAI Device Code flow with multi-account support,
      JWT-based account identification, and automatic access_token refresh.
    - Reuses the generic managed-auth command surface (auth_start_login,
      auth_poll_for_account, etc.) via provider dispatch in commands/auth.rs.
    - ClaudeAdapter detects codex_oauth providers, forces the base URL to
      the ChatGPT backend, pins api_format to openai_responses, and emits
      Authorization + originator headers; the forwarder injects the dynamic
      access_token and ChatGPT-Account-Id per request.
    - transform_responses gains an is_codex_oauth path that aligns the body
      with OpenAI's codex-rs ResponsesApiRequest contract: sets store:false,
      appends reasoning.encrypted_content to include, strips max_output_tokens
      / temperature / top_p, injects default instructions/tools/parallel_tool_calls,
      and forces stream:true. Covered by 9 new unit tests plus regression
      guards for the non-Codex path.
    - Stream check reuses the same transform flag so detection matches the
      production request shape.
    - Frontend adds CodexOAuthSection + useCodexOauth hook, integrates it
      into ClaudeFormFields / ProviderForm / AuthCenterPanel, ships a new
      "Codex (ChatGPT Plus/Pro)" preset, and adds zh/en/ja i18n strings.