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>
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>
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.
- 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).
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.
The Fill Recommended button was misleading — it showed toast.success even
when most slots couldn't be filled due to model ID mismatches with the
user's configured providers.
Changes:
- Sync OMO_BUILTIN_AGENTS/CATEGORIES recommended fields with upstream
oh-my-openagent model-requirements (gpt-5.4→5.5, kimi-k2.5→claude-sonnet-4-6, etc.)
- Add toast.warning tier when unmatched >= filled, showing slot:model pairs
(e.g. "Sisyphus: claude-opus-4-7") so users know exactly what to configure
- Upgrade fillRecommendedNoMatch to also show examples
- Add fillRecommendedMostlyUnmatched i18n key (zh/en/ja/zh-TW)
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.
* Add Traditional Chinese localization
* fix: address zh-TW formatting and token units
- Format `zh-TW.json` with Prettier.
- Use Traditional Chinese `萬` and `億` units for zh-TW token summaries.
- Add usage formatting coverage for Traditional Chinese locale aliases.
---------
Co-authored-by: Jason <farion1231@gmail.com>
* refactor: replace JSON.parse(JSON.stringify()) with structuredClone and extract useTauriEvent hook
Replace all `JSON.parse(JSON.stringify())` deep copy patterns with native
`structuredClone()` across production source (9 occurrences), tests (11
occurrences), and a hand-rolled `deepClone` utility in providerConfigUtils.ts.
Add "ES2022" to tsconfig lib for type support.
Extract a `useTauriEvent` hook to eliminate the repeated Tauri event listener
boilerplate (`useEffect` + `active/disposed` flag + async `listen`) that was
duplicated across App.tsx (3 listeners) and useUsageCacheBridge.ts. The hook
handles async registration, race-condition guards, and cleanup automatically.
* fix: add compatible deepClone helper
- Add a shared deepClone helper with a structuredClone runtime guard and fallback.
- Route clone call sites through the helper.
- Preserve universal-provider-synced listener ordering and drop the dead-directory diff.
* fix: harden Tauri event handling
- Guard WebDAV sync status events against missing payloads.
- Preserve settings query invalidation ordering before showing auto-sync errors.
- Simplify useTauriEvent subscriptions to avoid dependency-driven re-listens.
---------
Co-authored-by: zcb <zhangchongbiao@qiyuanlab.com>
Co-authored-by: Jason <farion1231@gmail.com>
* 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>
* 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>
* docs: add German README translation (README_DE.md)
Adds README_DE.md, a full German translation modelled on README_JA.md,
and extends the language switcher in README.md, README_ZH.md and
README_JA.md with a Deutsch link.
* docs: remove outdated German sponsor entry
- Remove the AICoding.sh sponsor block from README_DE.md.
- Keep the German README aligned with the current sponsor list.
---------
Co-authored-by: Jason <farion1231@gmail.com>
* fix(ui): remove fixed width constraint on AppSwitcher text to prevent clipping
App names like "Claude Desktop" were being clipped by the max-w-[90px]
constraint on the text label. Changed to max-w-none so text adapts to
content width. Also removed overflow-x-hidden from the parent toolbar
container to prevent clipping of the wider content.
Fixes text truncation in the top-right app switcher segmented control.
* - fix(ui): preserve AppSwitcher compact overflow behavior
- Restore toolbar overflow clipping required by useAutoCompact.
- Cap expanded AppSwitcher labels at 120px so Claude Desktop fits while compact animation remains smooth.
---------
Co-authored-by: Jason <farion1231@gmail.com>
Remove AICoding (aicoding.sh) sponsor entry from the three README files
and the provider preset across all seven app_types (claude, codex,
gemini, hermes, opencode, openclaw, claudeDesktop). Also drop the
matching partner_promotion.aicoding string in zh/en/ja locales.
The icon (src/icons/extracted/index.ts) and the legacy provider id in
codex_history_migration.rs are kept intentionally: the former for
possible later re-use, the latter so existing users' Codex history
still gets bucketed into "custom" on upgrade.
- 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.
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.
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.
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.
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)
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
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
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.
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.
- 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).
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.
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.
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.
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
After an install/update command succeeds, re-probe the tool version: if it
still can't be detected the tool is installed-but-unrunnable (e.g. Node too
old), so report a warning toast and show "installed · can't run" with the
cause, instead of a misleading "success" plus a stale version number.
Read the backend `installed_but_broken` flag rather than matching error
text; the card's current-version line, action button (no useless "install"
for a broken tool), and a "check environment" hint all key off it. Drop the
now-redundant version-clearing setState (the refresh merge already nulls it),
flatten the action ternary, and use extractErrorMessage in the catch path.
Adds toolNotRunnable / toolActionInstalledNotRunnable / installedNotRunnable
/ toolCheckEnv i18n keys (zh/en/ja).
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.
Update All previously sent every tool to the backend as one script, where
set -e (or errorlevel on Windows) aborts the whole batch on the first
failure, skipping the rest and not refreshing versions for tools that did
succeed. Run each tool as its own call instead: failures are isolated,
each tool's version refreshes as soon as it finishes, and the result is
summarized in a toast (success / partial / failure) listing which tools
failed.
Also derive an isAnyBusy flag and disable all install/update buttons,
the refresh button and the WSL shell selectors while any action is
running, so users can't trigger concurrent global npm/pip writes.
Add the toolActionPartial i18n key (zh/en/ja).
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.
Add Hermes to the environment-check table and app-switcher list, fix the
OpenCode install command (opencode-ai), and describe the new update/install
flow. Align with the final implementation: drop the removed Install Missing
action, note that installs run silently with in-button progress, and rename
the section to Manual Install Commands (now collapsible).
List all managed apps with current/latest versions, with per-tool install
and update buttons plus an Update All action. Installs and updates now run
silently: the button shows a spinner for the full duration, versions refresh
automatically when done, and failures surface the backend error detail in a
toast.
While loading, the button keeps its label and only swaps the icon for a
spinner, so width stays constant across zh/en/ja instead of jumping when the
text changes (e.g. Update -> Updating...).
Also collapses and renames the install-commands area to Manual Install
Commands, and removes the managed-apps badge row and the Install Missing
batch button.
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.
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.
CI on main was red because two tests did not follow intentional changes
that were already merged into the codebase:
- codexChatProviderPresets.test.ts still expected the "Kimi For Coding"
preset, which was removed in 16c3ef3f. Drop the corresponding entry
from the expected presets map.
- import_export_sync.rs still asserted the legacy per-provider
model_provider id ("rightcode"), but 9fac15b8 unified Codex
third-party providers into the stable "custom" history bucket. Mirror
the assertion update already applied to provider_service.rs.
No production code changed; both fixes only update test expectations.
No-argument tool calls produced empty argument strings that passed
through both the streaming response path and the request transform
verbatim. Strict OpenAI-compatible upstreams (e.g. Minimax) reject
`arguments: ""` with a 400 "invalid function arguments json string",
while lenient ones (OpenAI, Kimi) silently treat it as an empty object.
Add canonicalize_tool_arguments / canonicalize_tool_arguments_str helpers
that coerce empty/whitespace arguments to "{}" and apply them at the four
tool-call argument sites (streaming finalize + three request/response
transform paths). Leave function_call_output content untouched, where an
empty string is valid.