Commit Graph

809 Commits

Author SHA1 Message Date
Jason ce993baefa Fix Codex OAuth auth cleared on takeover when provider mis-categorized
Preserve-mode takeover routes the PROXY_MANAGED placeholder into
config.toml and must leave auth.json (the ChatGPT OAuth login) intact.
c9cadd6e covered only the None-provider write branch; the
Some(provider) branch still ran the category-based auth decision in
codex_config::write_codex_live_for_provider, whose first clause
(category == "official" && has_login_material) ignores the preserve
flag entirely. Because takeover stamps OPENAI_API_KEY = PROXY_MANAGED
into auth, codex_auth_has_login_material returns true, so a provider
stored with a stale/mis-classified "official" category (e.g. DeepSeek)
had its real auth.json overwritten with the placeholder — the access
token vanished on takeover and reappeared on cleanup.

Fix at the takeover entry instead of patching the fragile category
logic: add write_codex_takeover_live_for_provider, which under
preservation detects the PROXY_MANAGED placeholder in auth and writes
only config.toml (bearer token + catalog projection), never touching
auth.json. Gating on the placeholder is orthogonal to category, so any
mis-classification is handled. All four takeover sites
(sync-while-active, takeover_live_configs, _strict, _best_effort) now
route through it; restore (verbatim) and non-takeover provider writes
are unchanged.

Also projects the model catalog via
prepare_codex_live_config_text_with_optional_catalog, so the
model_catalog_json pointer survives takeover too.

Add a regression test: official-category provider + preserve enabled +
proxy takeover must keep auth.json byte-identical while moving the
placeholder into config.toml.
2026-06-01 11:41:05 +08:00
Jason d5328e5290 Fix Codex model catalog lost on proxy takeover-off restore
Turning proxy takeover off restores Live from a stored backup via
write_codex_live_verbatim. That path mishandled the Codex model catalog
for two backup shapes that need opposite treatment:

- Snapshot backup (read_codex_live_settings -> {auth, config}): no inline
  modelCatalog, but the config.toml text already carries the live
  model_catalog_json pointer. The old code ran catalog projection, which
  saw no specs and stripped the pointer.
- Provider-rebuilt backup (update_live_backup_from_provider): inline
  modelCatalog (DB SSOT) with a pointer-less config text. A pure verbatim
  write ignored the inline catalog and never regenerated the pointer.

The projection decision is orthogonal to auth: a provider-rebuilt backup
can pair an inline modelCatalog with empty auth.json ({}) when the API key
lives in the config's experimental_bearer_token. The empty-auth branch
raw-wrote config and skipped projection entirely, so that shape lost its
mapping too.

Decide the config.toml text once, before splitting on auth, via the new
prepare_codex_live_config_text_with_optional_catalog helper: project only
when an inline modelCatalog is present, else keep the text raw (preserving
any existing pointer). Every config-writing branch (write-auth,
delete-auth, no-auth) now applies it consistently. Add regression tests
covering all three shapes.
2026-06-01 11:41:05 +08:00
Jason 0fbba4267c Fix Codex model catalog being wiped by live-config backfill
`modelCatalog` is a cc-switch-private field whose SSOT is the database; Live's
config.toml only carries a lossy `model_catalog_json` projection. Proxy
takeover/restore cycles and the official Codex.app rewriting config.toml can
drop that projection, so `read_live_settings` reconstructs an empty catalog.
Two paths then overwrote the stored mapping with that empty Live snapshot:

- Switch-away backfill (`switch_normal` -> `restore_live_settings_for_provider_backfill`):
  now overlays the DB provider's `modelCatalog`, falling back to the
  Live-reconstructed one only when the DB has none.
- Edit dialog (`EditProviderDialog`): when editing the active Codex provider it
  preferred Live over the DB SSOT; now keeps the DB `modelCatalog` so opening +
  saving no longer clears the mapping table.

Add Rust backfill tests (preserve DB catalog when Live lacks it; keep Live
catalog when DB has none) and a frontend regression test for the edit dialog.
2026-06-01 11:39:22 +08:00
oasis 8bf1660237 fix(codex): always update model catalog JSON on provider switch (#3360)
* fix(codex): always update model catalog JSON on provider switch

Without this fix, the cc-switch-model-catalog.json file and the
model_catalog_json field in config.toml were only regenerated when
should_sync_backup was true (proxy active or backup exists). Switching
providers while the proxy was idle left the catalog pointing at the
previous provider's models, requiring a full restart to take effect.

After the existing should_sync_backup block, unconditionally call
write_codex_provider_live_with_catalog when switching a Codex provider
and the proxy is not currently active (live_taken_over == false). This
mirrors what live.rs already does during a normal provider apply.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(codex): refresh catalog on restored hot switch

---------

Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-06-01 11:38:07 +08:00
Siskon afa09e127e fix(usage): resolve per-app credentials for native balance/coding-plan queries (#3355)
* fix(usage): resolve per-app credentials for native balance/coding-plan queries

The native usage-query paths (balance + coding_plan) in
`query_provider_usage_inner` read credentials only from `env.ANTHROPIC_*`.
That matches Claude providers, but Codex stores its key in
`auth.OPENAI_API_KEY` with the base URL inside a TOML `config` string,
Hermes/OpenClaw flatten them at the top level, and OpenCode nests them under
`options`. So the card "refresh usage" / auto-query returned empty
credentials and failed ("查询失败") for those apps, even though the
config-page "Test" button worked (the frontend extracts per-app correctly).

Introduce a single per-app resolver `Provider::resolve_usage_credentials`
that mirrors the frontend `getProviderCredentials`, and route both native
branches through it. Add `extract_codex_base_url` to `codex_config` as the
canonical Codex TOML base-URL parser and make the proxy adapter delegate to
it (removing a duplicate copy). Align the frontend `getProviderCredentials`
to cover OpenCode and Claude Desktop and to use the same OpenRouter/Google
key fallbacks as the backend.

Fixes #3158
Fixes #3100
Fixes #2625

* refactor(usage): explicit AppType arms + frontend trailing-slash trim

Address two review nits on the per-app credential resolver:

- provider.rs: replace the catch-all `_` arm in resolve_usage_credentials with an explicit `AppType::Claude | AppType::ClaudeDesktop` arm, so a new AppType variant fails to compile here instead of silently defaulting to the Anthropic env shape.
- UsageScriptModal.tsx: normalize getProviderCredentials baseUrl through a single trailing-slash trim so the frontend 'Test' path matches the backend resolver (trim_end_matches), keeping front/back truly in lockstep.

No behavior change for well-formed configs; tests/typecheck/fmt/clippy clean.

* fix(usage): skip empty primary credential fields in fallback chain

`obj.get(key)` returns `Some` for a present-but-empty field, so the
`.or_else()` fallback chains for the Claude/ClaudeDesktop and Gemini api-key
lookups only skipped *absent* keys, not empty ones. Presets seed fields like
`ANTHROPIC_AUTH_TOKEN` as present-but-empty placeholders, so a provider whose
real key lives in a fallback field (`ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY`
/ `GOOGLE_API_KEY`, or `GOOGLE_API_KEY` for Gemini) resolved to an empty key on
the native balance/coding-plan path — while the frontend `a || b` (which skips
empty strings) still found it, reproducing the same Test-works / refresh-fails
divergence this PR removes.

Add a `first_non_empty` helper that skips present-but-empty values, matching
the frontend `||` semantics, and use it for both fallback chains. Tests cover
empty primary + populated fallback for Claude and Gemini.
2026-06-01 08:43:40 +08:00
Eunknight 0960fd7179 fix: Claude Desktop 官方供应商添加报错 #3402 (#3405)
* fix: Claude Desktop 官方供应商添加时缺少 ANTHROPIC_BASE_URL 报错

根因:前端 mutation 为 claude-desktop 生成随机 UUID 作为 provider id,
后端 is_official_provider 通过 id 匹配跳过校验,随机 UUID 不匹配导致
走入普通 direct 模式校验并要求 ANTHROPIC_BASE_URL。

修复:
- 前端:claude-desktop + category=official 时使用固定 id "claude-desktop-official"
- 后端:validate_provider / validate_direct_provider / validate_proxy_provider /
  apply_provider_to_paths 增加 category=="official" 兜底检查

Fixes #3402

* fix: restrict Claude Desktop official provider detection

* fix: add Claude Desktop official provider via seed

---------

Co-authored-by: 金恩光 <enguang.jin@gmail.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-05-31 23:37:31 +08:00
Zhiqi Yao 5ef72a2030 fix(codex): add multi-platform CLI discovery and static gpt-5.5 template fallback (#3382)
* fix(codex): add multi-platform CLI discovery and static gpt-5.5 template fallback for model catalog generation

When cc-switch generates a Codex model catalog for third-party providers,
it needs the gpt-5.5 model definition as a template. Previously it relied
on either ~/.codex/models_cache.json (only exists when Codex has connected
to OpenAI) or running 'codex debug models --bundled' (fails when codex
is not on the Tauri app's PATH, common in macOS GUI environments).

This commit adds a three-tier fallback:
1. Try 'codex' from PATH, then platform-specific common paths
   (/opt/homebrew/bin/codex, /usr/local/bin/codex, etc.)
2. If all CLI attempts fail, use a compile-time embedded gpt-5.5
   template (extracted from codex 0.135.0 bundled models)

Also adds tests for the static template validity and CLI candidates.

* fix(codex): discover user node codex installs

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-05-31 22:48:27 +08:00
Haoyu Wang e02a2763c2 fix: add kimi/moonshot to Anthropic tool thinking history normalizer (#3377)
* fix: add kimi/moonshot to Anthropic tool thinking history normalizer

Kimi's Anthropic-compatible endpoint requires reasoning_content in
assistant tool call messages when thinking is enabled, same as DeepSeek.
The is_reasoning_content_compatible_identifier already included kimi/moonshot
but is_anthropic_tool_thinking_history_identifier was missing them, causing
400 errors on multi-turn conversations with tool calls.

Closes #3351

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: unify reasoning vendor hints into a single SSOT list

Replace the two separately-maintained vendor identifier lists with one
shared REASONING_VENDOR_HINTS constant and a single
is_reasoning_vendor_identifier predicate. The Anthropic
tool-thinking-history matcher and the openai_chat reasoning_content
matcher previously kept independent lists, which is exactly how
kimi/moonshot ended up in one but not the other (#3351). A single source
of truth keeps the two from drifting apart again.

Also add a Kimi regression test for the Anthropic tool-history
normalization path.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-05-31 20:10:02 +08:00
Jason c9cadd6e09 Fix Codex OAuth auth being cleared during preserve-mode takeover
When "preserve official auth on switch" is enabled, proxy takeover routes
the PROXY_MANAGED placeholder into config.toml's experimental_bearer_token
and leaves auth.json (the ChatGPT OAuth login) untouched. Takeover detection
only inspected auth.json's OPENAI_API_KEY, so it never recognized this state
and returned a false negative, which led downstream paths to clobber the
preserved OAuth login.

- Detection: is_codex_live_taken_over now also matches a config.toml
  experimental_bearer_token equal to the placeholder, fixing detect/cleanup/
  restore/startup-recovery in one place.
- Cleanup: remove the config.toml bearer token only when it equals the
  placeholder (new remove_codex_experimental_bearer_token_if predicate), so a
  real third-party key is never stripped.
- Write: under preservation, the None-provider takeover path writes only
  config.toml and keeps auth.json intact, matching the provider path.
- Settings: rename the section to "Codex App Enhancements" and reword the
  description across all four locales.
- Add tests covering OAuth preservation on takeover and placeholder-only
  cleanup.
2026-05-30 22:56:36 +08:00
Jason 60a9b330e5 Refactor Codex live-write routing and cover default auth overwrite
Collapse the two duplicated write_codex_live_atomic branches in
write_codex_live_for_provider into a single should_write_auth guard.
This is behavior-preserving: `if A {X} else if B {X} else {Y}` becomes
`if A || B {X} else {Y}`.

Adapt the Codex switch tests to the new opt-in default for
preserve_codex_official_auth_on_switch (flipped off in 3f59ab37):
add an enable_codex_official_auth_preservation() test helper for the
cases that assert the auth-preserving path, and tag the official login
provider with category="official" so it routes through the official
branch rather than relying on the global preservation flag.

Add a regression test locking the default (preservation off) behavior:
switching to a third-party provider rewrites auth.json with the new
API key and discards the existing ChatGPT OAuth login. This is the
dual of the existing preserve-and-backfill test, which only covered
the opt-in path.
2026-05-30 22:09:28 +08:00
Jason f4e2c28a2b Enrich Codex proxy forwarding-error responses with context
When forward_with_retry fails for Codex endpoints (/chat/completions, /responses, /responses/compact), the handlers previously returned the bare ProxyError, surfacing a terse body to the client. Build a richer JSON error instead: the message embeds provider, model, endpoint and upstream status, and the error object carries those as structured fields alongside a stable cc_switch_* code.

Align map_proxy_error_to_status with ProxyError::into_response so the manually built responses use the canonical status codes (AuthError->401, Config/InvalidRequest->400, TransformError->422, StreamIdleTimeout->504, AlreadyRunning->409, NotRunning->503); previously these forwarder-level errors collapsed to 500.
2026-05-30 21:42:16 +08:00
Jason 3f59ab3746 Default Codex auth preservation to off (opt-in)
Flip preserve_codex_official_auth_on_switch from true to false so
third-party Codex switches overwrite auth.json by default, matching the
expectation that switching providers also swaps credentials. Users who
rely on keeping the ChatGPT login in auth.json while on a third-party
provider (for official plugins / remote login) can enable it in
Settings -> Codex Authentication.

The toggle field ships for the first time here (it is not in v3.16.0),
so no existing settings.json holds an explicit value -- every user lands
on the new default and no migration is required.

Also set the flag explicitly in the preservation unit test instead of
relying on the global default, keeping it valid now that the default is
false.
2026-05-30 21:04:23 +08:00
Jason ee69c83687 Fix garbled output and false "not runnable" in Windows version probe
The version probe ran `cmd /C "\"{path}\" --version"` on Windows. Rust's
own arg quoting plus the manual quotes produced nested quotes that cmd
(without /S) misparsed, so it reported even working tools as an
unrecognized command -> non-zero exit -> shown as "installed but not
runnable". cmd's error is emitted in the OEM code page (e.g. GBK on
zh-CN) and was decoded as UTF-8, turning it into mojibake.

- Add decode_command_output: try strict UTF-8, then OEM (GetOEMCP), then
  ANSI (GetACP) code page via MultiByteToWideChar; route all command
  output decoding through it (probes, lifecycle output, osascript,
  terminal launchers).
- Add run_windows_tool_version_command: run .exe directly (no cmd);
  invoke .cmd/.bat via `cmd /D /S /C call <quoted> --version` with
  raw_arg to bypass Rust's quoting and keep deterministic quote handling.
- Add windows-sys Win32_Globalization dependency.
2026-05-30 17:46:35 +08:00
Jason 2683af57cb Add Codex auth preservation setting 2026-05-30 14:13:58 +08:00
Jason 47232cb05d chore(release): bump version to 3.16.0 and add release notes
- Bump version to 3.16.0 in package.json, Cargo.toml, tauri.conf.json, and Cargo.lock
- Add v3.16.0 release notes (zh/en/ja) with contributor credits
2026-05-30 00:00:15 +08:00
Jason 94cc3d103a Align Claude Desktop model mapping with Claude Code three-role tiers
Claude Desktop's 3P validation only accepts claude-{sonnet,opus,haiku}-*
role IDs, so providers must map every tier. Bring the Desktop mapping flow
in line with Claude Code and fix the fallout that broke sub-agent Haiku calls.

- Proxy form: replace the dynamic route list with fixed Sonnet/Opus/Haiku
  tiers; blank tiers backfill from the first filled tier (Sonnet first) on
  submit and inherit its supports1m flag
- Backend: add a role-keyword fallback in map_proxy_request_model so dated
  official names (e.g. claude-haiku-4-5-20251001) resolve to the right tier,
  guarded by is_claude_safe_model_id so [1m]-suffixed IDs stay rejected
- Tighten is_claude_safe_model_id / isClaudeSafeRoute to reject degenerate
  role IDs like "claude-sonnet-"
- Fix the seed-effect race where normalizing empty routes to three blank
  tiers blocked the default-route backfill
- Sync switch hints, placeholders, and the zh/en/ja/zh-TW locales to the
  three-role-ID rule
- Update zh/en/ja user manual (2.1, 2.6), calling out legacy Claude IDs
  (claude-3-5-sonnet-...) as a rejected example

Tests: 282 frontend + 34 backend claude_desktop; typecheck, clippy, fmt clean.
2026-05-29 22:52:25 +08:00
Jason 3a15420770 Update default models and pricing across presets
Bump default model names project-wide: gpt-5.4 -> gpt-5.5,
gemini-3.x -> gemini-3.5-flash, glm-5 -> glm-5.1, and
grok-code-fast-1 -> grok-build-0.1 across all provider presets
(claude, codex, gemini, hermes, openclaw, opencode, universal),
Gemini config, and stream check defaults.

Pricing:
- Seed gemini-3.5-flash, gemini-3.1-flash-lite, step-3.5-flash-2603,
  doubao-seed-2.0-code, mimo-v2.5(/pro), qwen3-coder-480b, grok-build-0.1.
- Correct deepseek-v4-flash/pro, glm-5/5.1, grok pricing.
- Add repair_current_model_pricing: idempotent pass that fixes only
  rows still equal to the outdated built-in values, preserving any
  user-customized prices (seed uses INSERT OR IGNORE and cannot update
  existing rows).

Fixes from review:
- opencode: drop duplicate gemini-3.5-flash variant (unreachable via
  .find), keep the entry with the full minimal/low/medium/high set.
- Align stale display names/costs to gemini-3.5-flash (hermes, openclaw,
  opencode); openclaw cost -> {1.5, 9, 0.15} to match seed.
- i18n (zh/en/ja/zh-TW): refresh OMO category tooltips for new model
  names; fix writing tooltip to Kimi K2.5 to match its recommended.

Update tests accordingly and add a regression test asserting unique
model ids in the Google opencode preset variants.
2026-05-29 22:52:25 +08:00
Jason 2b6ede1431 Update Codex provider migration test expectations 2026-05-29 22:52:25 +08:00
Jason 0877b9e35d Upgrade default Claude Opus model to 4.8
Bump the default Opus route/model from claude-opus-4-7 to claude-opus-4-8
across provider presets (claude, claudeDesktop, hermes, openclaw, opencode,
universal), i18n locales (zh/en/ja/zh-TW), pricing seed data, and the
user-manual docs.

- Add claude-opus-4-8 pricing row ($5/$25/$0.50/$6.25); keep the 4-7 row
  for historical usage stats (seeded via INSERT OR IGNORE).
- Claude Desktop proxy: accept bidirectional opus 4-7 <-> 4-8 route alias
  during rollout so previously saved routes keep resolving.
- thinking_optimizer: route opus-4-8 through adaptive thinking and normalize
  dotted model ids (also fixes dotted 4-6/4-7 falling back to legacy).
- usage_stats: normalize Bedrock/Vertex/aggregator opus-4-8 ids to base
  pricing.

Also merge role:"system" messages into the Gemini systemInstruction in the
Anthropic->Gemini transform.
2026-05-29 22:52:25 +08:00
Q3yp 554e3b4875 fix(proxy): normalize DeepSeek Anthropic tool thinking history (#3203)
Fixes #3200
2026-05-29 22:51:12 +08:00
lipeng e605eba292 fix(deeplink): preserve custom env fields when importing Claude providers (#2928)
When importing a Claude provider via deep link, the preview dialog
correctly shows every env field carried in the `config` payload — but
on confirmation `build_claude_settings` only persisted the standard
fields mapped from URL params (`apiKey`, `endpoint`, `model`,
`haiku/sonnet/opusModel`). Any non-standard env keys such as
`ANTHROPIC_CUSTOM_HEADERS`, `API_TIMEOUT_MS`, or
`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` were silently dropped,
making the preview and the actual write diverge.

Start the env map from the inline `config` payload (best-effort
decode via the new `extract_claude_config_env` helper) and then
overlay the URL-param-derived standard fields on top, so:

- URL params remain authoritative for standard fields (unchanged)
- Custom env keys present in `config` survive the import
- Deep links without a `config` payload behave exactly as before

Adds two regression tests:
- `test_build_claude_provider_preserves_custom_env_fields` proves
  custom fields are kept and URL params override same-named config
  fields.
- `test_build_claude_provider_without_config_unchanged` guards the
  backward-compatible path (no `config` → only standard env keys).

Closes #2927

Co-authored-by: lipeng31 <lipeng31@tanyang.co>
2026-05-29 22:49:02 +08:00
in30mn1a bc1467db8d feat(usage): real-time stats refresh + fix codex sync panic on non-ASCII model names (#3027)
The usage dashboard previously only refreshed on app restart for users
who don't route through the cc-switch proxy. Two issues were involved:

1. The session-sync background task panicked when a Codex model name
   contained non-ASCII characters (e.g. `【官】glm-5.1`), because
   `normalize_codex_model` sliced `&name[name.len() - 11..]` without
   verifying char boundaries. Once the task panicked, no session logs
   were imported until the app was restarted (where startup-time
   `rollup_and_prune` happened to flush pending data).

2. Even with sync working, the dashboard only polled every 30s and
   skipped polling when the window was unfocused, so freshly-imported
   data was invisible until the next poll or window refocus.

Fixes
-----

* `normalize_codex_model`: guard the 11-byte ISO-date suffix slice with
  `is_char_boundary` + `is_ascii` checks. ASCII-only suffix means the
  date-stripping logic is correct, and non-ASCII names (which can never
  be valid date suffixes anyway) now bypass the slice safely.

* New `usage_events` module that emits `usage-log-recorded` to the
  frontend whenever `proxy_request_logs` actually gains a new row.
  Sources covered: proxy `log_request`, Claude/Codex/Gemini session
  sync, and startup `rollup_and_prune`. Notifications use a global
  `OnceLock<AppHandle>` so call sites that don't already hold an
  `AppHandle` (e.g. `UsageLogger`) can notify without signature churn.

* 200ms debounce in `notify_log_recorded` collapses bursts (a single
  Codex sync importing 3000+ entries triggers ~2 emits, not 3000) so
  the frontend's `invalidateQueries` is never spammed.

* Frontend `useUsageEventBridge` listens for the event and invalidates
  `usageKeys.all`. Hook is mounted only on `UsageDashboard`, so the
  listener is unsubscribed automatically when the user navigates away.

Verification
------------

* `cargo check` passes (existing 25 dead-code warnings in
  `commands/misc.rs` are pre-existing and unrelated).
* `tsc --noEmit` passes.
* Manually verified end-to-end: a Codex sync run that imported 3145
  entries produced 2 debounced emits, both logged as `emit
  usage-log-recorded 成功`, and the dashboard updated within ~200ms.

Behaviour notes
---------------

* `INSERT OR IGNORE` paths (Claude/Codex session sync) only notify when
  the row is actually inserted, so dedup-skipped writes don't trigger
  empty refreshes.
* Gemini's `INSERT … ON CONFLICT … DO UPDATE` path reuses the existing
  `conn.changes() > 0` check and only notifies when token counts truly
  changed.
* `rollup_and_prune` notifies once per pruning cycle (at most once per
  app start) so the dashboard reflects the new aggregate state.

Co-authored-by: in30mn1a <in30mn1a@users.noreply.github.com>
2026-05-29 22:47:51 +08:00
Jason d7a34f4247 fix(about): handle prerelease tools in version check
Use semver comparison instead of string inequality so locally-ahead prerelease builds aren't misreported as outdated; backend now fetches the full npm dist-tags and, when the local version leads latest, surfaces the tool's prerelease channel (claude=next) as latest.
2026-05-29 08:47:54 +08:00
Jason fc0433f2f4 refactor(codex): unify custom model_provider routing key to "custom"
- Always emit `model_provider = "custom"` from deep link, UniversalProvider, and the universal form modal so future writes share one stable routing key.
- Add `codex_provider_template_v1` local migration that rewrites legacy keys (aihubmix/ccswitch/...) under `[model_providers.custom]`, updates profile refs, and backs up the original settings_config under `~/.cc-switch/backups/<timestamp>/providers/`.
- Tighten history migration source detection to a whitelist plus `[model_providers.<id>]` existence check so user-authored keys are never rewritten in jsonl/state DB.
- Encode deep link name/model/endpoint through `toml_edit::Value` so display names containing quotes or backslashes no longer break the generated config.toml.
- Stabilize provider settings backup filename hash with Sha256 (was process-random SipHash).
2026-05-28 17:30:44 +08:00
Jason a2ac21d0a6 refactor(codex): stop force-rewriting user's model_provider field in live config
The one-time history migration already consolidates legacy provider IDs
into a single bucket; there is no need to normalize on every write.
This preserves user-chosen provider identities through the full
write/backup/restore cycle.
2026-05-28 11:39:54 +08:00
nanmen2 e9d84af500 fix(session): include Codex archived sessions (#2861)
Scan Codex archived_sessions alongside active sessions so archived conversations appear in the session manager.

Allow deletion from any validated Codex session root while keeping path safety checks in place.
2026-05-27 11:16:50 +08:00
Zihao Han 8e21b061cf Fix custom usage script summaries (#3129)
Co-authored-by: Zihao Han <hanhan3344@users.noreply.github.com>
2026-05-27 10:50:29 +08:00
zhizhuowq 9c2add9a09 fix(proxy): 修复 Claude 兼容模式下流式响应因空 tool_calls 数组导致 block 状态重置 (#2915)
问题:OpenAI 兼容 Provider(如 MiniMax)在发送 reasoning_content 流时,
      每个 chunk 包含 "tool_calls": [] 空数组,导致每个字符被当作独立 block 处理

根因:streaming.rs:343 的 if let Some(tool_calls) 匹配空数组进入处理分支,
      发送 content_block_stop 并重置 current_non_tool_block_type 为 None

修改:在 tool_calls 处理前添加 if !tool_calls.is_empty() 判断

示例输入:
```jsonl
{"delta": {"content": null, "reasoning_content": "用户", "role": "assistant", "tool_calls": []}}
{"delta": {"content": null, "reasoning_content": "用", "role": "assistant", "tool_calls": []}}
{"delta": {"content": null, "reasoning_content": "中文", "role": "assistant", "tool_calls": []}}
...
```
示例错误输出:
```text
event: content_block_start → index 0, type thinking
event: content_block_delta → thinking "用户"
event: content_block_stop → index 0
event: content_block_start → index 1, type thinking
event: content_block_delta → thinking "用"
event: content_block_stop → index 1
event: content_block_start → index 2, type thinking
event: content_block_delta → thinking "中文"
event: content_block_stop → index 2
...
```

Co-authored-by: WangQiang <wangqiang75@huawei.com>
2026-05-27 10:49:41 +08:00
Sphaela 864593bb09 Fix Claude Desktop Cowork egress profile (#3172) 2026-05-27 10:49:08 +08:00
滅ü 5fd3ec0d6a 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>
2026-05-27 00:05:03 +08:00
MelorTang 05ba28016c fix: sync Claude Desktop profile during proxy takeover (#3157)
* fix: sync Claude Desktop profile during proxy takeover

* fix(provider): skip Claude Desktop backup refresh during takeover

- Route Claude Desktop takeover updates directly through the 3P profile writer.
- Keep takeover startup backup state unchanged when provider metadata changes.
- Narrow platform-specific test helpers and environment setup with cfg gates.

* fix(provider): restore PathBuf import for CI tests

- Restore an unconditional PathBuf import for provider tests.

- Keep Linux cargo test builds compiling while preserving Claude Desktop cfg-gated helpers.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-05-26 23:01:33 +08:00
zhangyapu1 707a5593e5 feat: add MiMo reasoning_content support for Claude Code proxy (#2990)
* feat: add MiMo reasoning_content support for Claude Code proxy

MiMo requires reasoning_content to be passed back in multi-turn
conversations with tool calls. Add "mimo" and "xiaomimimo" to the
preserve_reasoning_content whitelist so thinking blocks are converted
to reasoning_content when proxying Claude Code → MiMo.

Also handle redacted_thinking blocks (encrypted by Claude Code across
turns) by injecting a placeholder to prevent MiMo 400 errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: support MiMo reasoning histories

- Fix the PR CI clippy failure for redacted thinking handling.
- Preserve MiMo reasoning content on the OpenAI Chat proxy path.
- Normalize Claude Desktop Anthropic thinking history for MiMo local routes.
- Reject unsupported Claude Desktop direct model remaps.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-05-26 22:55:36 +08:00
Jason d8a4292091 chore: satisfy clippy::nonminimal_bool and prettier to unblock CI
- codex_config: rewrite `!(A && !B)` as `!A || B` (De Morgan-
  equivalent) — also reads more directly as "no OAuth, or
  already has provider API key".
- UsageScriptModal: reflow conditional onto one line per prettier.
2026-05-25 22:20:34 +08:00
Jason b15d9dfa11 feat(codex): migrate third-party history provider bucket to unified "custom" ID
Codex CLI filters conversations by model_provider — after normalizing
all third-party providers to "custom", old sessions with their original
provider IDs become invisible. This migration rewrites JSONL session
files and state_5.sqlite threads to use the unified ID.

Uses a three-tier trust model for source ID collection:
- Known legacy preset whitelist (46 IDs, case-insensitive)
- cc-switch-created custom providers (identified by created_at)
- User-maintained custom providers are explicitly skipped

Adds scanned_history_files flag to prevent stale v1 markers from
blocking re-runs with the improved source ID collection logic.
2026-05-25 22:20:34 +08:00
Jason 177eef6682 fix(quota): sort ZhiPu tiers so missing nextResetTime maps to five_hour bucket
When the 5-hour bucket is at 0% utilization, ZhiPu's API omits
nextResetTime. The old i64::MAX sentinel sorted these entries last,
causing the weekly bucket to incorrectly claim the five_hour slot.
2026-05-25 22:20:34 +08:00
Jason 95f2dd4126 feat(codex): preserve OAuth login state during third-party provider switching
Codex provider switches now only write config.toml for third-party providers,
injecting the API key as experimental_bearer_token. The user's auth.json
(ChatGPT OAuth tokens) is preserved. Official providers with login material
still write auth.json normally. Backfill restores bearer tokens into stored
provider auth.OPENAI_API_KEY to maintain canonical shape.
2026-05-25 22:20:34 +08:00
Jason 88ba908bc4 feat(settings): unify unix installers on mktemp+bash, fix WSL install missing native installer
Extend the mktemp+bash installer pattern from hermes (commit 20d943be) to
claude and opencode, and remove the cfg gate that locked install_command_for
to non-Windows targets — Windows host + WSL tools now correctly route
through the POSIX install priority instead of falling back to bare npm.
Frontend "one-click install commands" displayed in the About section now
diverge per platform to match what the backend actually runs.

Backend (src-tauri/src/commands/misc.rs):
- New CLAUDE_INSTALL_UNIX and OPENCODE_INSTALL_UNIX constants use the same
  `bash -c 'tmp=$(mktemp) && curl -fsSL .../install.sh -o $tmp && bash
  $tmp; status=$?; rm -f $tmp; exit $status'` shape as HERMES_INSTALL_UNIX.
  The shared comment about why we avoid `curl | bash` is now hoisted to the
  top of all three constants — the constraint (WSL `sh -c` sub-shells
  don't inherit outer pipefail; default shell may be dash/ash) applies to
  every shell installer, not just Hermes.
- New installer_with_npm_fallback helper deduplicates the
  "official installer || npm fallback" chain construction; reuses
  chain_update_commands(LifecycleCommandShell::Posix) so the wiring matches
  the update-chain logic.
- install_command_for split into cross-platform posix_install_command_for
  + thin #[cfg(not(target_os = "windows"))] wrapper. The cfg gate had
  been hiding a real bug: on Windows hosts, the WSL branch called
  wsl_tool_action_shell_command which forwarded install actions to the
  Windows-target tool_action_shell_command, yielding bare `npm i -g` for
  claude/opencode even though they have working native POSIX installers.
- wsl_tool_action_shell_command now branches on action: Install dispatches
  to posix_install_command_for (native installer || npm chain); Update
  keeps the existing behavior. Empty install commands collapse to None so
  callers error on unsupported tools instead of running an empty command.
- build_tool_lifecycle_command pipefail comment updated: the original
  justification (covering install's `curl | bash` path) no longer applies
  to any current command, but the directive stays as defense-in-depth for
  future pipe additions.
- build_tool_action_line WSL branch comment updated to reflect the
  install/update split.

Frontend (src/components/settings/AboutSection.tsx):
- New posixScriptInstallCommand(url) helper builds the same mktemp+bash
  string template the backend uses.
- New powershellEncodedCommand(script) mirrors the backend's UTF-16 LE +
  base64 encoder (charCodeAt + String.fromCharCode(lo, hi) + btoa) so the
  PowerShell EncodedCommand shown to users matches what the backend
  executes.
- ONE_CLICK_INSTALL_COMMANDS split into POSIX_ONE_CLICK_INSTALL_COMMANDS
  and WINDOWS_ONE_CLICK_INSTALL_COMMANDS, selected by isWindows():
  POSIX shows claude/opencode/hermes with the mktemp+bash installer;
  Windows shows claude/codex/gemini/opencode/openclaw with npm (matching
  backend static_fallback_command on Windows) and hermes with the
  PowerShell EncodedCommand. Display now mirrors backend execution
  per platform, removing the show-vs-run mismatch.
- Stale comment in the probe-concurrency note mentioning "curl | bash"
  updated to "官方 installer".

Tests (src-tauri/src/commands/misc.rs):
- New wsl_install_uses_posix_install_priority covers three
  representatives: claude (positive: native installer with npm fallback),
  opencode (positive), codex (negative: no native installer so falls back
  to bare `npm i -g`). Reverse-asserts !contains("| bash") to lock out
  the old pipe form.
- claude_install_chains_native_then_npm and
  opencode_install_chains_native_then_npm strengthened with
  !parts[0].contains('|') so the native installer portion stays
  pipe-free even if future edits to the template sneak a pipe back in.

Validated:
- cargo check --tests: clean
- cargo test --lib commands::misc:: : 59 passed, 0 failed
- pnpm typecheck: 0 errors
- cargo fmt: clean
- pnpm format: clean (no files changed)
2026-05-25 22:20:34 +08:00
Jason 5de0a0dc8d feat(settings): self-update first chain, switch hermes to native installer
Promote anchored upgrades to "<bin> update || <package fallback>" for the
tools whose CLI knows how to self-update safely, and replace pip-based
hermes install/update with the official NousResearch installer scripts.
Frontend learns to detect "update reported success but version did not
change" so silent upstream no-ops surface as a soft warning.

Backend (src-tauri/src/commands/misc.rs):
- New prefers_official_update(tool, shell) per-platform allow-list:
  POSIX {claude, codex, opencode, openclaw}. Windows drops opencode
  pending anomalyco/opencode#17295 (its silent `upgrade` may prompt for
  install-source detection and deadlock our lifecycle). gemini is absent
  from both because `gemini update` is not implemented upstream
  (google-gemini/gemini-cli#18618 / #16122 OPEN).
- New chain_update_commands joins primary/fallback per shell: POSIX
  `||`, Windows `|| call` — batch transfers control to the `||` RHS
  without `call`, skipping subsequent tools in multi-tool actions.
- New LifecycleCommandShell {Posix, WindowsBatch} threads the target
  shell through tool_action_shell_command_for_shell, replacing the old
  hermes-only WSL substitution hack with a uniform mechanism.
- anchored_command_from_paths refactored: hermes anchors to
  `<bin> update`; tools in prefers_official_update chain self-update
  with the package-manager command as fallback; brew formulae remain
  brew-managed (never self-update what Homebrew owns); others use the
  prior npm/volta/bun/pnpm anchoring unchanged. Mirror on Windows.
- Hermes commands switch from pip to the official scripts. POSIX/WSL
  runs `bash -c 'tmp=\$(mktemp) && curl -fsSL .../install.sh -o \$tmp
  && bash \$tmp; status=\$?; rm -f \$tmp; exit \$status'`. The mktemp +
  two-step download avoids `curl | bash` because WSL sub-shells inherit
  neither outer `set -o pipefail` nor a guaranteed bash (default may be
  dash/ash). Windows uses `powershell -NoProfile -ExecutionPolicy Bypass
  -EncodedCommand <b64>` with a hand-rolled UTF-16 LE base64 encoder
  for `irm .../install.ps1 | iex`, hiding the PowerShell pipe from
  cmd.exe. Pip is abandoned because macOS system python3 is often 3.9
  while hermes-agent requires >=3.11, and the pyenv `python` shim may
  not exist — pip errors then misclassify as "command not found".

Frontend (src/components/settings/AboutSection.tsx):
- New versionUnchangedAfterUpdate four-AND short-circuit detects when
  the upgrade command succeeded, the tool still reports a version, but
  that version equals the pre-upgrade version while a different
  latest_version is known. Guards against upstream no-op updaters that
  return exit 0 without applying anything (openai/codex#21897).
- Soft-failure shape gains kind?: "notRunnable" | "versionUnchanged"
  so the warning toast title can disambiguate the two cases.
- Hermes install command snippet swapped from `python3 -m pip ...` to
  the official curl-bash one-liner shown to users.

i18n (zh / en / ja):
- New settings.toolActionVersionUnchangedTitle and
  settings.toolActionVersionUnchanged, synchronized across all locales.

Tests (src-tauri/src/commands/misc.rs):
- Anchored upgrade assertions updated to `<bin> update || <pkg>` shape
  across nvm / volta / bun / homebrew-npm-global / spaces / fnm
  branches; brew formula explicitly does NOT chain self-update.
- New hermes anchor tests replace hermes_has_no_npm_anchor on both
  POSIX and Windows. WSL hermes tests rewritten for the mktemp+bash
  flow with `hermes update` fallback.
- Reverse-lock tests: gemini static fallback must NOT contain
  `gemini update`; opencode Windows static fallback must NOT contain
  `opencode upgrade`; hermes commands must NOT contain
  `python`/`pip`/`powershell`/forbidden pipes.

Validated:
- cargo check --tests: clean
- pnpm typecheck: 0 errors
- cargo fmt: clean
2026-05-25 22:20:34 +08:00
Jason 7cad61be27 refactor(settings): drop CLI uninstall command hints, keep conflict diagnostics
The uninstall hint feature added in f6bdd4bb / 89e2339b printed a
copy-pasteable shell command (e.g. `brew uninstall <formula>`,
`<sibling-npm> rm -g <pkg>; [ -e bin ] && rm -f bin && rm -rf pkg_dir`)
under each detected install in the multi-install conflict panel. Even
with copy-only-never-execute UX, putting destructive commands one
button away from the user is risk we don't want to ship — `rm -rf`
under a misclassified path would be unrecoverable.

Conflict diagnostics themselves are kept: users still see the warning
banner + per-install row (source / path / version / Default badge), so
the "you have multiple installs and the upgrade only touches one"
signal survives. They just don't get a one-click destructive command.

Backend (src-tauri/src/commands/misc.rs):
- Remove `uninstall_command_from_paths` (POSIX + Windows pair).
- Remove `posix_quote_for_user_shell` and `win_quote_path_for_user_shell`
  — both were uninstall-only quoters (whitelist + cmd-only forms).
  `win_quote_path_for_batch` and `shell_single_quote` stay because the
  anchored upgrade path and `build_shell_cd_command` still use them.
- Remove `quoted_sibling_or_bare` and `quoted_sibling_or_bare_with_ext`
  thin wrappers (only callers were uninstall branches).
- Drop `uninstall_command` and `uninstall_command_needs_cmd_hint` from
  `ToolInstallation`. Drop the eager `infer_install_source` ->
  `uninstall_command_from_paths` call in `enumerate_tool_installations`
  (saves ~16μs per probe but the real point is fewer surprises).
- Remove 14 `#[test]` cases for uninstall command shape (brew /
  homebrew-npm-global / nvm / npm-fallback quoting / pnpm / volta / bun /
  hermes / system / Windows percent-path) plus 4 `win_quote_path_for_user_shell`
  helper tests and the `expect_user_shell_quoted_path` test mirror.
- Drop the `src()` test helper that was only used to feed the deleted
  uninstall tests (`infer_install_source` is still called directly by
  surviving tests).
- Cross-references in `anchored_command_from_paths` doc to its now-gone
  uninstall "dual" are stripped.

Frontend (src/components/settings/AboutSection.tsx):
- Conflicts `<ul>` keeps the diagnostic header + per-install `ToolInstallRow`
  but loses the inline command box, the trash + copy icons, the per-row
  PowerShell-call-operator hint, and the `handleCopyCommand` callback.
- Remove the `Trash2` lucide import; `Copy` stays (the install-commands
  copy button at line 1100 still uses it).

API (src/lib/api/settings.ts):
- Drop `uninstall_command` and `uninstall_command_needs_cmd_hint` from
  the `ToolInstallation` TS interface (matches the Rust struct change).

i18n (zh / en / ja):
- Drop `settings.toolUninstallCopyHint`, `settings.toolUninstallCopied`,
  `settings.toolUninstallPwshHint`. Other `tool*` and `skills.uninstall*`
  keys are untouched (Skill uninstall is a separate, non-destructive
  feature for managing the Skills repo and is unaffected).

Stats: 858 deletions, 5 insertions across 6 files.

Validated:
- cargo test --lib: 1322 passed, 0 failed
- cargo fmt --check: clean
- cargo build: clean
- cargo check --tests: clean
- pnpm typecheck: 0 errors
- JSON.parse on all three locale files: clean
2026-05-25 22:20:34 +08:00
Jason 3a77861dd9 feat(settings): backend-generated source-aware uninstall command hints
Move uninstall hint command generation from frontend (which used a lossy
`source` string and couldn't tell brew formula from homebrew-npm-global apart)
into the Rust backend, where `brew_formula_from_path` + `infer_install_source`
together produce correct commands for each install. Conflict UI now reads
`inst.uninstall_command` directly.

Backend (src-tauri/src/commands/misc.rs):
- New `uninstall_command_from_paths` (POSIX + Windows) mirrors the
  `anchored_command_from_paths` family. POSIX branches on brew formula /
  volta / bun / pnpm / nvm|fnm|mise|homebrew / system. Windows branches on
  volta / pnpm / npm.
- New `quoted_sibling_or_bare` (POSIX) + `quoted_sibling_or_bare_with_ext`
  (Windows) deduplicate the `sibling_bin().map(quote).unwrap_or_else(bare)`
  pattern across all uninstall branches.
- New `posix_quote_for_user_shell` whitelists `[A-Za-z0-9._/+=:@-]`; any
  other character routes through `shell_single_quote`. Replaces
  `quote_path_if_spaced` on the uninstall side because the fallback chain
  is destructive (`rm -rf <pkg_dir>`), so a `;` / `$` / backtick in the
  home dir would have let the chain inject extra commands.
- New `win_quote_path_for_user_shell` (no `%` 4x escape) splits from
  `win_quote_path_for_batch`. Uninstall hints are copy-paste targets, not
  `.bat + call` payloads, so the two-round percent expansion that
  motivates the 4x escape doesn't apply.
- POSIX `nvm|fnm|mise|homebrew` branch appends physical-delete fallback
  `; [ -e <bin> ] && rm -f <bin> && rm -rf <pkg_dir>` because nvm v18.20.8
  was observed to silently no-op `npm rm -g` (exit 0 with `up to date`
  message, no filesystem change). Anchored `<node_root>/{bin,lib}` layout
  assumption documented as branch-specific.
- `ToolInstallation` gains `uninstall_command: String` and
  `uninstall_command_needs_cmd_hint: bool`. The bool is computed as
  `cfg!(target_os = "windows") && uninstall_command.contains('"')` — a
  compile-time platform gate avoids false positives from POSIX
  `shell_single_quote`'s `'"'"'` escape form, which also contains `"`.
- `enumerate_tool_installations` computes `source` once and threads it
  into `uninstall_command_from_paths`, removing a redundant
  `infer_install_source` call (review N1).

Frontend (src/components/settings/AboutSection.tsx + src/lib/api/settings.ts):
- Remove the JS helpers `buildUninstallCommand`, `dirOfPath`,
  `isWindowsPath`, `shellQuote`, `TOOL_NPM_PACKAGES`. Conflict rows now
  read `inst.uninstall_command` directly.
- Render a PowerShell-call-operator hint when
  `inst.uninstall_command_needs_cmd_hint` is true. Avoids string-match
  inference from the command (which fails on macOS user names containing
  `'`, e.g. `o'leary`, because `shell_single_quote`'s escape contains `"`).
- `ToolInstallation` TS interface gains the two new fields.

i18n (zh/en/ja):
- New key `settings.toolUninstallPwshHint` documents the PowerShell
  `&` (call operator) requirement for quoted paths.

Tests: 30 new cases under `commands::misc::tests::anchored_upgrade` cover
each POSIX branch (brew formula extraction, npm anchored with fallback,
pnpm, volta, bun, hermes), strong-quoting under `;` / `'` paths, the
fallback-only-for-anchored-branches inverse guard, plus mirror Windows
helpers for the user-shell quoter and a `path%foo%` integration case
demonstrating the literal `%` preservation contract. Total
`commands::misc` test count: 68.
2026-05-25 22:20:34 +08:00
Jason 671859748d feat(settings): extend anchored tool upgrades to Windows native paths
Apply macOS-side anchored upgrades (9984eccb) to Windows. Six tools now
upgrade via an absolute path derived from the install actually hit by the
command line, instead of bare `npm i -g` landing on PATH-first npm. WSL
tools explicitly bypass anchoring (Windows-host paths invalid in distro).

Scope: 7+ Windows package managers compress to 3 idioms in practice
(Scoop/Chocolatey/winget/nvm-windows/MS Store all just install node; the
global-package layer is still npm):
- volta  -> <sibling>\volta.exe install <pkg>
- pnpm   -> <sibling>\pnpm.cmd add -g <pkg>@latest
- others -> <sibling>\npm.cmd i -g <pkg>@latest

Cross-platform:
- infer_install_source: add /volta/ (no dot) and /pnpm/ matches so
  %LOCALAPPDATA%\{Volta,pnpm} route correctly.
- parent_dir: recognize both \ and /, take rightmost (Option<usize>
  Ord lets None<Some, so rfind('\\').max(rfind('/')) auto-picks).
- npm_package_for, default_install, installs_anchored_command:
  promoted from cfg(not(windows)) to all-platforms.
- plan_command_for: merged the two cfg branches; Windows+WSL short-
  circuits to wsl_tool_action_shell_command (not static_fallback) so
  the command shown to the user matches what build_tool_action_line
  actually runs.
- ToolInstallation: add #[serde(skip)] real: PathBuf, populated by
  enumerate, read by installs_anchored_command — no more double
  canonicalize, and closes a TOCTOU window between enumerate and anchor.

Windows-only helpers (#[cfg(target_os = "windows")]):
- sibling_bin_with_ext reads fs to pick .cmd vs .exe (Node installer
  drops .cmd, Volta drops .exe; pure string can't disambiguate).
- win_double_quote shared primitive; win_quote_path_for_batch handles
  cmd token-boundary chars (space, &, (, ), ^, ;, <, >, |, ,) by
  wrapping in quotes, and handles `%` via 4x escape (%%%%) to survive
  both .bat parser expansion AND `call`'s own second percent-expansion
  pass (Microsoft `call /?`: "percent (%) expansion is performed on
  each parameter").
- wsl_tool_action_shell_command wraps tool_action_shell_command so
  hermes inside WSL gets python3||python instead of Windows-target
  py||python (py launcher is Windows-only, missing in WSL distros).

Wire-up:
- build_tool_action_line Windows: WSL -> wsl_tool_action_shell_command
  + wsl.exe wrap; native update -> enumerate + anchored + static
  fallback. Everything gets `call ` prefix (.bat needs it for .cmd
  without replacing the current script; harmless for .exe).

Tests: 33 new — 10 platform-agnostic (install_source_classification,
parent_dir_cases) + 23 cfg-gated Windows-only (anchored_upgrade_windows,
windows_helpers). Anchored expected values use a small expect_quoted_path
mirror in the test mod so assertions stay correct on Windows hosts whose
%TEMP% itself contains spaces/special chars (e.g. user accounts named
"John Doe"); the 7 hardcoded-literal win_quote_* unit tests independently
lock the quoting rules themselves, so mirror drift would be caught by
either side.

Limitation: Windows cfg-gated tests don't run on macOS cargo test.
Validated on macOS host: cargo fmt clean, clippy --all-targets 0
warnings, cargo test --lib 1322 passed. Windows CI runs only on release
tag (release.yml has windows-2022); production validation needs cargo
test on a Windows machine before broad rollout.
2026-05-25 22:20:34 +08:00
Jason c6fd2415c3 fix(settings): enforce absolute paths for all anchored upgrade branches
- misc: extend sibling_bin derivation to claude/brew/volta/bun in addition
  to npm. Return Option<String> so the anchored-path invariant cannot be
  silently violated — when bin_path has no parent directory, propagate
  None and fall back to the static command at the call site instead of
  emitting a bare command that depends on the GUI process PATH.
- misc: factor out quote_path_if_spaced to keep all five anchored
  branches consistent when the resolved path contains spaces.
- AboutSection: add preflightTools lock + try/finally around
  probeToolInstallations so fast double-clicks cannot race two probe
  rounds into concurrent executeRun calls or duplicate confirm dialogs.
- Tests: cover sibling_bin None branch and space-quoting for every
  anchored branch (claude/brew/volta/bun).
2026-05-25 22:20:34 +08:00
Jason 108dda1772 feat(settings): prefer official installer for tool install with npm fallback
Bare `npm i -g <pkg>` was the install command for every managed tool, but
Anthropic and SST now both recommend their native installers over npm. The
update path already anchors to native installs (`claude update` and friends),
so install was the last place still forcing npm — even when the user's
intent was clearly to get the upstream-recommended install.

- New `install_command_for(tool)` (non-Windows only): claude and opencode
  now run the upstream installer with a `|| npm i -g ...@latest` POSIX
  short-circuit fallback, so the tool still installs if the URL is
  unreachable.
- codex / gemini / openclaw / hermes pass through to the original static
  command — they have no independent official installer today.
- `build_tool_lifecycle_command` now opens `set -o pipefail` alongside
  `set -e`: without it, `curl ... | bash` where curl fails would have bash
  exit 0 on empty stdin, silently masking the failure and skipping the
  `||` fallback.
- 6 new `install_strategy` unit tests pin the per-tool install command
  shape, mirroring the existing anchored_upgrade regression suite.

Update routing and Windows install behavior are unchanged.
2026-05-25 22:20:34 +08:00
Jason e25682d345 fix(proxy): source takeover model fields from target provider on managed accounts
When a managed-account provider (GitHub Copilot or Codex OAuth) is taken
over by the local proxy, the *_MODEL_NAME entries written to Claude live
config used to be snapshotted from the live file on disk, which still
held the *previous* provider's values during a switch. The result was a
stale display name lingering after the switch.

Read the model snapshot from the target provider's effective settings
(common-config merged) when the provider uses managed-account auth;
preserve the existing behavior of reading from the live config for
normal providers. The three takeover entry points now resolve the
effective provider up front so common-config writes flow through.
2026-05-25 22:20:34 +08:00
Jason 014c82d28b feat(settings): anchor tool upgrades to the actual install
Rework the About tool-upgrade flow so upgrades target the install the
command line is actually using, instead of running bare `npm i -g` and
landing on PATH's first npm — which used to move upgrades to the wrong
location or fail outright when the resolved bin had no sibling npm.

Anchored upgrade command (update path, non-Windows):
- claude native installer (~/.local/share/claude/versions/) -> `claude update`
- Homebrew formula (canonical path under /Cellar/) -> `brew upgrade <formula>`
- volta / bun -> `volta install` / `bun add -g`
- Node managers with sibling npm (nvm/fnm/mise/homebrew-npm) -> that
  directory's npm with `i -g <pkg>@latest`
- system / unknown sources (e.g. opencode install.sh under ~/.opencode/bin,
  ~/go/bin) -> fall back to the bare command, surfaced as anchored=false
  so the UI shows an honest "default entry not determinable" hint instead
  of pretending the upgrade is targeted.

Multi-install confirmation:
- When the probe finds >=2 installs, pop ToolUpgradeConfirmDialog showing
  each install (source badge + path + version + Default badge) and the
  resolved command before executing. Top-level text only asserts what is
  universally true ("won't update all of them; see each tool below");
  per-card text adapts to anchored vs unanchored.
- Pending upgrade state stores the full upgrade set; the dialog only
  shows the subset that needs confirmation, so non-conflicting tools in
  a batch still get upgraded after confirm.

Unified probe command:
- Merge the previous diagnose_tool_installations and plan_tool_upgrades
  into a single probe_tool_installations returning installs + is_conflict
  + needs_confirmation + command + anchored. Diagnose button, post-upgrade
  auto-diagnose, and pre-upgrade confirmation all share one IPC. The
  diagnose button's previous Promise.all of 6 IPCs is now a single call.

resolve_path_default robustness:
- The previous lines().next() was poisoned by interactive .zshrc output
  (welcome banners, p10k instant prompts, etc.), causing default-install
  detection to fail and dropping multi-install scenarios into the
  unanchored fallback even when one install was clearly the native one.
- Replaced with first_abs_path_line that scans for the first
  absolute-path line and ignores noise, mirroring how try_get_version
  already tolerates banner noise via a regex.

Diagnostic staleness fix:
- diagnoseToolSilently now clears stale diagnostics in the else branch
  when the conflict no longer holds (previously only wrote on conflict,
  never cleared, so externally resolving a conflict left the card stuck
  on a stale list until the user clicked Diagnose).
- Auto-diagnose now triggers after any successful update, not only when
  the version didn't change, so the card reflects the latest conflict
  state even when an anchored upgrade succeeds while another install
  remains.

Other:
- Shared ToolInstallRow between the conflict list and the confirmation
  dialog removes the duplicated per-install row JSX.
- Drop the new shell_quote_path helper in favor of reusing the file's
  existing POSIX-correct shell_single_quote, gated on whitespace.
- Collapse displayName lookup into a stable module-level helper to remove
  the `as ToolName` cast and avoid recreating the closure each render.
- 17 backend unit tests covering anchored command generation across each
  source, brew formula extraction, is_conflicting thresholds, default
  install resolution, and the welcome-banner scenario.
- i18n (zh/en/ja): new keys for confirmation dialog
  title/hint/will-run/confirm-button and the unanchored hint.
2026-05-25 22:20:34 +08:00
Jason ce232a1452 feat(settings): diagnose conflicting tool installations with uninstall hints
Add a "Diagnose installs" action to the About section that enumerates every
installation of each managed CLI, flags real conflicts (diverging versions
or mixed runnable state), and lists them under the affected tool card.

- backend: enumerate_tool_installations + diagnose_tool_installations command,
  sharing build_tool_search_paths with the single-probe scan; resolve the PATH
  default via the same login shell as version probing so the "Default" marker
  matches what the command line (and an upgrade) actually targets
- frontend: global diagnose button left of Refresh, per-card conflict list,
  and a copy-only uninstall command per install -- source-aware (npm/volta/
  bun/pip) and anchored to that install's own npm to avoid removing the wrong
  node version; marked with a red trash icon, never executed
- auto-diagnose silently after an upgrade that leaves the version unchanged
  or installs something that can't run
- i18n: zh/en/ja
2026-05-25 22:20:34 +08:00
Jason 768c5f9f39 fix(misc): probe tool versions faithfully instead of masking unrunnable installs
Unify the three probe paths (try_get_version / try_get_version_wsl /
scan_cli_version) on a cross-platform ShellProbe enum that distinguishes
"not installed" (shell exit 127) from "installed but --version failed"
(e.g. Node too old to run the freshly-upgraded CLI).

The found-but-broken case is now reported as-is and no longer falls back to
scan_cli_version. Previously the fallback surfaced a stale runnable copy
installed elsewhere (e.g. a Homebrew node vs the nvm node the upgrade wrote
to), making an upgrade look like it had no effect ("succeeded but version
unchanged").

Expose the state via a structured `installed_but_broken` flag on ToolVersion
so the frontend no longer infers it by string-matching the error text. Also
fixes a WSL path that could mislabel a genuinely-missing tool as broken, and
extracts the NOT_INSTALLED constant.
2026-05-25 22:20:34 +08:00
Jason 820c4db1b0 feat(misc): broaden CLI version detection to PATH and Windows managers
Extend scan_cli_version search paths with the PATH environment variable
and common Windows Node/version-manager locations (pnpm, Volta, nvm,
Scoop, Yarn). Entries from PATH are skipped when they point at the
Microsoft\WindowsApps App Execution Alias directory, which is what
previously caused bogus launches on Windows.

Add unit tests for PATH dedup, child-dir suffix expansion, and the
WindowsApps alias skip.
2026-05-25 22:20:34 +08:00
Jason ee2d634d89 feat: add silent install/update lifecycle for managed CLI tools
Expand tool version detection to all six managed apps (including Hermes
via PyPI), and add a run_tool_lifecycle_action command that installs or
updates CLI tools silently: it captures the subprocess output and blocks
until the command actually finishes instead of popping a terminal window
(Windows uses CREATE_NO_WINDOW). On failure the last lines of stderr are
returned so the UI can surface a useful message.

Also restores the Windows version-detection path by gating try_get_version
behind cfg(not(windows)) so it no longer risks triggering the App
Execution Alias / protocol handler that previously disabled Windows.
2026-05-25 22:20:34 +08:00
Jason c12d20efd0 refactor(proxy): replace panic-prone unwrap/expect with safe patterns
Harden the proxy module against panics by removing optimistic
unwrap()/expect() calls in favor of pattern matching and graceful
fallbacks:

- copilot_optimizer/cache_injector: bind Value::Array/String directly
  instead of is_array()+as_array().unwrap(); use is_none_or and in-place
  string mutation
- hyper_client: gate the raw-write path with if-let + filter instead of
  has_cases + unwrap()
- gemini_shadow: recover poisoned RwLock via into_inner() rather than
  panicking, with warn logging
- streaming_codex_chat: replace expect("tool state exists") with
  let-else (return/continue)
- merge_tool_results: early-return when messages is absent
- sse: fall back to from_utf8_lossy on the UTF-8 boundary slice

No behavior change on the happy path; all proxy tests pass.
2026-05-25 22:20:34 +08:00