Commit Graph

400 Commits

Author SHA1 Message Date
Jason 142c8c1da7 feat(providers): rebrand CTok to ETok across presets, icons, and docs
The vendor renamed its domain, API endpoints, and trademark from CTok to
ETok. Update all user-facing branding accordingly:

- Presets (claude, claude-desktop, codex, gemini, hermes, openclaw,
  opencode): ctok.ai -> etok.ai, api.ctok.ai -> api.etok.ai, internal
  provider id ctok -> etok, display name CTok.ai -> ETok.ai
- Icons: replace inline placeholder SVG with the new ETok brand PNG,
  moved from `icons` to `iconUrls`; key ctok -> etok; banner logo
  ctok.png -> etok.png
- README (en/zh/ja/de) partner banner: new logo, links, and copy
- i18n (en/ja/zh/zh-TW): partnerPromotion key ctok -> etok

Keep "ctok" in the Codex history-migration whitelist as a legacy id and
add "etok", so existing users' local session history stays correctly
bucketed after the rename.
2026-06-22 10:28:10 +08:00
Xinyu Kuo 0bb3b7515a feat(usage): support importing model pricing from models.dev (#4079)
* feat(usage): support importing model pricing from models.dev

Add an "Import from models.dev" button to the Add Pricing panel that
fetches https://models.dev/api.json, lists priced models sorted by
release date (newest 50 by default, full-text search across ~4800),
and bulk-imports the selected entries through the same
update_model_pricing command as manual entry.

- Normalize imported model IDs to match the backend's
  clean_model_id_for_pricing rules (strip vendor prefix, lowercase,
  truncate ':' suffix, map '@' to '-', drop the [1m] marker) so the
  stored rows actually match cost-attribution lookups
- Dedupe selections that collapse to the same model_id and report
  skipped duplicates in the success toast
- Invalidate usage queries on settled (not just success) so partial
  import failures still refresh the pricing list
- Keep ESC inside the picker's search input from closing the dialog
  and discarding the selection
- Add i18n keys for zh/en/zh-TW/ja and unit tests for the
  normalization, price formatting and flattening logic

Fixes #4017

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(usage): match scoped cost backfill against raw model aliases

The scoped backfill selected zero-cost rows via exact SQL string match,
but log columns store raw model strings (route prefixes, :free variants,
date suffixes), so alias rows were skipped until the next full backfill
on startup. Filter rows in Rust with the same model_pricing_candidates
normalization used by the pricing lookup; pricing decision logic is
untouched. Pre-existing gap from schema v11, surfaced by bulk import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(usage): restrict models.dev pricing import to a single model

Each update_model_pricing call triggers a backfill pass that loads
every zero-cost usage row before filtering by model, so bulk-importing
N entries scaled as selectedModels x allZeroCostLogs full scans.
Re-importing pricing is rare, so drop the batch path instead of
optimizing it: the picker is now single-select, one import runs exactly
one update_model_pricing call and one backfill pass. This also removes
the normalized-ID dedup logic and the useImportModelPricing hook in
favor of the existing useUpdateModelPricing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 22:01:56 +08:00
Jason c678374c59 chore(presets): update Volcengine Ark Coding Plan invite link and promo copy
- Replace activity/agentplan links with the new codingplan invite URL
  (ac=MMAP8JTTCAQ2, rc=6J6FV5N2) across all 6 app presets
- Refresh partnerPromotion copy in all 4 locales: two-month 75% off plus
  invite code 6J6FV5N2; correct product name from Agent Plan to Coding Plan
- README: swap the CN-mainland redirect link in EN/DE, refresh the ZH promo
  copy, and add the redirect link to the JA BytePlus entry
2026-06-14 23:03:31 +08:00
Jason a5903d8600 feat(health-check): replace real-LLM probe with HTTP reachability check
The provider panel health check sent a real streaming model request, which many third-party providers block (401/403/WAF), causing false negatives while only stable official endpoints passed. Replace it with a lightweight reachability probe: GET the provider base_url and treat any HTTP response (200/4xx/5xx) as reachable; only DNS/connect/TLS/timeout count as failure. Latency is the probe's TTFB.

Backend (services/stream_check.rs): rewrite ~2200 -> ~350 lines, dropping real-request building, format conversion, auth and API-path resolution while keeping per-app base_url extraction. Defaults: 8s timeout, 1 retry, 1500ms degraded threshold.

Failover invariant: the reachability check must never reset the circuit breaker (reachable != usable; a 403 host is reachable but broken for real traffic). Remove the resetCircuitBreaker call from useStreamCheck; failover failure detection stays driven solely by real proxy traffic (forwarder/circuit_breaker untouched). useResetCircuitBreaker is kept dormant for a future manual-recovery entry.

Open the check to all providers: drop the official/copilot/codex-oauth/third-party gating and the 'sends a real request' confirm dialog. For official providers whose base_url is intentionally empty, fall back to the endpoint the client actually uses (Claude -> api.anthropic.com, Codex -> chatgpt.com/backend-api/codex, Gemini -> generativelanguage). Non-official providers with a missing base_url still error to avoid a false green light. Claude Desktop Official is native 1P mode (talks to claude.ai, cc-switch not in the request path, no reliable endpoint) so its button stays hidden.

Slim StreamCheckConfig and per-provider testConfig to timeout/threshold/retries (drop test model + prompt); sync zh/en/ja/zh-TW. Retain the now-unused anthropic_to_openai/anthropic_to_gemini transform utilities and their test suites.
2026-06-14 21:21:45 +08:00
Jason 4f8a79c273 feat: add Claude Fable 5 model mapping across Claude Code and Desktop
- Wire claude-fable-5 as a fourth tier on both proxy paths, with a
  fable -> opus -> default fallback mirroring the official downgrade.
- Whitelist the fable- prefix for the Desktop 1.12603.1+ validator.
- Clarify fallbackModelHint (zh/en/ja/zh-TW): a blank tier on
  third-party endpoints forwards the literal model name and 404s.

Refs #3980, #4026, #4049.
2026-06-13 19:12:34 +08:00
Jason eab6bfd20c feat(codex): add opt-in migration and ledger-based restore for unified session history
- Enable dialog gains a checkbox (default off) to migrate existing
  official sessions from the built-in "openai" bucket into the shared
  "custom" bucket, with per-generation backups; failed migrations retry
  at startup
- Disable dialog offers a precise restore driven by the backup ledger:
  only sessions recorded as "openai" in backups are flipped back, and
  sessions created while the toggle was on are never touched
- Completion marker and backup generations are bound to the canonical
  Codex config dir; migrate/restore serialize on an op lock and the
  marker is written conditionally inside the settings write lock
- save_settings rolls back the toggle and fails the save when the live
  rewrite fails; migration additionally requires the live config to
  actually route to the shared bucket (skips with live_not_unified so
  refused injection or proxy takeover can't split history)
- Restore refuses to run while the toggle is (re-)enabled and reports
  nothing_to_restore instead of a zero-count success; local migration
  markers are now backend-owned in merge_settings_for_save so stale
  frontend payloads can't resurrect them
- Settings autosave reverts optimistic form state on failure so a
  failed toggle change can't be replayed by an unrelated save
- ConfirmDialog supports an optional checkbox; all four locales updated
2026-06-12 23:35:01 +08:00
Jason 948d762792 feat(codex): add unified session history toggle for official providers
Codex buckets resume history by the model_provider id recorded in each
session: official runs (no key, built-in "openai") and cc-switch
third-party runs (shared "custom") are mutually invisible in the resume
picker. Add an opt-in setting that runs official providers under the
shared "custom" id so future official sessions land in the same history
bucket as third-party ones. Forward-only by design: existing sessions
are not migrated.

When enabled, official live config.toml gets model_provider = "custom"
plus a [model_providers.custom] entry that mirrors the built-in openai
provider (requires_openai_auth routes auth to the ChatGPT login in
auth.json, name "OpenAI" keeps is_openai() feature gates, explicit
supports_websockets/wire_api restore built-in defaults). auth.json is
untouched.

Key invariants:
- Injection lives only in the live config: switch-away backfill strips
  the exact injected shape, so stored provider configs stay clean and
  turning the toggle off fully reverts on the next write.
- Toggle changes apply immediately via a takeover-aware reapply: when
  the proxy owns the live config (backup/placeholder present), only the
  live backup is updated, mirroring the provider-switch path.
- The takeover backup path runs the same injection so a takeover
  release restores a config that still carries the unified routing.
- Injection refuses to activate a foreign [model_providers.custom]
  table (e.g. stale entry with a third-party base_url) to avoid routing
  ChatGPT OAuth traffic to an unknown backend.

The toggle lives under Settings → Codex App Enhancements; the
description warns that resuming old sessions across providers may fail
because encrypted_content reasoning only decrypts on the backend that
created it (upstream treats cross-provider resume as unsupported).
2026-06-12 10:40:51 +08:00
Jason 4f355970e1 chore(presets): remove LemonData provider and demote SudoCode to regular provider
- LemonData: delete the provider preset from all apps (claude, claude-desktop,
  codex, gemini, hermes, opencode, openclaw), remove partner promotion copy
  (zh/en/ja/zh-TW), icon index/metadata entries, sponsor ads in all READMEs,
  and the logo/icon PNG assets.
- SudoCode: drop the isPartner flag and partnerPromotionKey across all app
  presets and remove the now-orphaned partner promotion copy; it stays as a
  regular third_party provider, keeping its icon.
2026-06-11 23:15:11 +08:00
Jason 22ecd2d611 feat(usage): turn refresh interval into a select and align control widths
Replace the click-to-cycle refresh button with a Select matching the
source/model filters. The "off" option now shows a localized label
(zh/en/ja/zh-TW) instead of the cryptic "--", and changing the interval
still invalidates all usage queries for an immediate refresh.

Align the top-bar controls into two width groups: source and model
selects at 120px, refresh select and date range trigger at 150px. The
date range button moves from auto width to fixed, so its long custom
range label gets truncate + hover title, and the calendar icon is
shrink-proofed.
2026-06-11 22:42:26 +08:00
Jason a75f479576 feat(usage): lift provider/model filters to dashboard-wide scope
The provider/model filters only lived inside the request-log table, so
there was no way to see "how much did app X spend on source Y" across
the whole dashboard. Promote them to the top bar next to the app
filter, applying globally to the hero summary, trend chart, request
logs, and both stats tabs.

Backend: the five stats queries (summary, summary-by-app, trends,
provider stats, model stats) accept optional provider_name/model
filters, applied to both the detail and daily-rollup branches (the
rollup PK already carries provider_id/model/pricing_model). Sources
match by exact display name via provider_name_coalesce, so session
placeholder rows like "Claude (Session)" are selectable; models match
by effective pricing model (pricing_model falling back to model), the
same grouping key the model-stats tab uses. Request-log filtering
switches from LIKE to these exact semantics.

Frontend: two truncating dropdowns list only sources/models that have
data in the current range, with the model list cascading from the
selected source. Dynamic option values are prefix-encoded so a source
literally named "all" cannot collide with the sentinel, query keys
fall back to null instead of "all", and the option queries follow the
dashboard refresh interval (otherwise their default 30s polling drags
same-key stats queries along even with refresh disabled). The
request-log filter bar keeps only the log-specific status-code select.
Labels read "sources" rather than "providers" because direct-connect
session buckets sit alongside real providers. i18n updated across
zh/en/ja/zh-TW.
2026-06-11 22:42:26 +08:00
Jason c7efa77ad9 refactor(usage): fold claude-desktop into claude in the dashboard
The Desktop gateway's proxy traffic is still recorded under its own
app_type for route-takeover billing audit (the request detail panel
shows the real value), but the dashboard now folds it into `claude`
for display. A standalone "Claude Desktop" bucket only ever showed a
partial number: Desktop chat usage never passes through the proxy and
has no scannable local source, while its Code-tab sessions are just the
embedded Claude Code runtime writing into the shared ~/.claude/projects
tree — so a separate bucket misled users into reading it as Desktop's
full usage.

Backend: new `folded_app_type_sql` helper wraps the app_type column in
every dashboard read path (10 filter sites + the by-app projection) so
`= 'claude'` also matches claude-desktop and GROUP BY merges the two,
without changing bound-param counts. Dedup matching and provider-limit
checks keep exact comparison; get_request_logs folds only the WHERE
filter and keeps the raw app_type in its row projection.

Frontend: drop claude-desktop from the dashboard AppType/KNOWN_APP_TYPES
filter list, the UsageHero theme, and the now-dead appFilter locale key
in all four languages (the managed-app apps.claude-desktop label stays).

Adds test_claude_desktop_folds_into_claude_for_display.
2026-06-11 22:42:26 +08:00
Peng Steam c1aa6c3917 feat(providers): add preset search and sorting (#3975) 2026-06-11 17:14:53 +08:00
Jason daa5595f36 feat(presets): add Unity2.ai partner provider across seven apps
Add Unity2.ai, a high-performance AI API relay partner, as a preset for
Claude, Codex, Gemini, OpenCode, OpenClaw, Claude Desktop, and Hermes.
Each preset carries the referral signup link as apiKeyUrl.

- Register the unity2 icon via iconUrls (PNG URL import) + metadata
- Add partnerPromotion copy in zh/en/ja/zh-TW; backfill the missing
  zh-TW ccsub entry
- List Unity2.ai in the sponsor section of all README locales
- Codex uses the bare base URL (gateway exposes /responses at root);
  OpenCode/OpenClaw/Hermes use the /v1 chat-completions endpoint with
  gpt-5.5 as the only preset model
- Trim CCSub OpenCode/OpenClaw/Hermes model lists to gpt-5.5 to match
- Normalize unity2/ccsub banners to the standard 2.41 aspect ratio
2026-06-11 11:05:32 +08:00
Jason e776160912 feat(provider-form): consolidate codex form into advanced options section
- Fold local routing toggle, model mapping, reasoning overrides and custom
  User-Agent into a single collapsible advanced section, mirroring the
  Claude form (auto-expands when UA is set or local routing is enabled)
- Custom User-Agent becomes configurable for native Responses providers;
  it was previously reachable only when openai_chat routing was on
- Collapsed hint names local routing as the entry point for Chat
  Completions / non-GPT providers
- Backfill all missing codexConfig keys in zh-TW locale
2026-06-11 08:29:29 +08:00
Jason 596019505f feat(provider-form): custom User-Agent presets dropdown in advanced settings
Polish the provider-level User-Agent override UI on the Claude and Codex forms.

- Add a shared CustomUserAgentField (label + input + preset dropdown + live
  validation) so both forms stay in sync.
- Provide curated UA presets (Claude Code / Kilo Code families that pass
  coding-plan UA whitelists per #3671); the first is Claude Code's real
  `claude-cli/x (external, cli)` format. Whitelists gate on the name prefix,
  not the version, so static values stay valid across upgrades.
- Expose presets via a dropdown to the right of the input (z-[200] so it
  renders above the dialog layers) instead of inline chips.
- Move the field into the existing advanced/reasoning collapsibles.
- userAgent.ts mirrors the backend byte rule (reject only control chars;
  non-ASCII is allowed) for a non-blocking inline hint.
- i18n for all four locales (zh/en/ja/zh-TW).
2026-06-11 08:29:29 +08:00
Jason e8b07cb2a5 feat(usage): claude-desktop filter and pricing-model audit display
- add claude-desktop to AppType/KNOWN_APP_TYPES and the dashboard app
  filter; it was hidden because its rows looked like pure failure
  noise, which was the app_type attribution bug fixed on the backend
- request detail panel now shows the requested model and the pricing
  model when they differ from the response model, making route-takeover
  bills auditable from the UI
- locale keys added for zh/en/ja/zh-TW
2026-06-11 08:29:29 +08:00
Jason fa17194d84 feat(presets): add CCSub provider across six apps
Add CCSub, a multi-model aggregator partner, as a preset for Claude, Codex, OpenCode, OpenClaw, Claude Desktop, and Hermes. Each preset carries the referral signup link as apiKeyUrl.

- Register the ccsub icon via iconUrls (1.1MB SVG URL import) + metadata
- Add partnerPromotion copy in zh/en/ja
- List CCSub in the sponsor section of all README locales
- Use gpt-5.5 and gemini-3.1-pro as the OpenAI/Gemini model ids
2026-06-08 22:04:16 +08:00
Jason 5c36ae066b fix(providers): only block explicit official providers under proxy takeover
The proxy-takeover block previously fell back to the isOfficial heuristic
(empty base_url / missing key) when category was absent. That misjudged
custom providers whose endpoint lives in meta or whose fields are simply
unfilled: their switch button got disabled, making users think the config
was broken. That extra UI block was also "virtual" — the executor in
useProviderActions only ever honored category === "official", so the
front end blocked more than the backend would enforce.

Gate the block solely on explicit category === "official", matching the
executor and unifying both verdicts on a single source of truth.

Also rework the blocked-state UI:
- drop the red "blocked" badge for a plain disabled Enable button
- move title/cursor onto a wrapper span (disabled buttons set
  pointer-events:none, so an on-button title/cursor never fired)
- replace the account-ban warning tooltip with a lighter hint
  (provider.blockedByProxyHint), four locales kept in sync
2026-06-07 20:56:35 +08:00
Jason 473f21971d feat(usage): add official subscription quota template with unified tier rendering
Changes:
- Add official_subscription template type for Claude/Codex/Gemini
- Replace implicit 'category=official auto-query' with explicit opt-in template
- Default disabled; users enable via usage script modal with configurable interval
- Unify tier→label mapping across subscription and script paths via labeled_tier_parts()
- Fix tray rendering: week aliases (seven_day/opus/sonnet) now use highest utilization
- Add depth guard: official_subscription checks enabled flag in query_provider_usage_inner
- Add cache invalidation symmetry: invalidate_subscription() for disabled providers
- i18n: add templateOfficialSubscription + hint in zh/en/ja/zh-TW

Backend (Rust):
- provider.rs: add TEMPLATE_TYPE_OFFICIAL_SUBSCRIPTION branch, flatten SubscriptionQuota→UsageData
- tray.rs: extract labeled_tier_parts() shared by both summary functions, use max_by for multi-alias groups
- usage_cache.rs: add invalidate_subscription() method
- Test coverage: add week-alias highest-utilization tests for both paths

Frontend (TypeScript):
- UsageScriptModal: add official_subscription to templates, auto-detect for official providers
- ProviderCard: gate useUsageQuery with !isOfficialSubscriptionUsage, pass autoQueryInterval to footer
- SubscriptionQuotaFooter: accept autoQueryInterval prop, default 0 (disabled)
- constants.ts: add TEMPLATE_TYPES.OFFICIAL_SUBSCRIPTION

Fixes tier rendering regression where:
- Claude/Codex: seven_day was missed (only weekly_limit matched) → lost 7-day window in tray
- Gemini: gemini_pro/flash/flash_lite fell through to fallback → leaked machine names
- Multi-window (opus+sonnet): find() took first, not worst → underestimated utilization and emoji color

All tests pass (cargo test + cargo clippy clean).
2026-06-05 19:03:40 +08:00
Allen Xu 03a9296c1f fix: polish usage statistics ui (#3426)
* fix: improve usage statistics ui

* chore: remove unused token suffix translation

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-06-05 08:12:17 +08:00
@nothingness0db nothingness0db 0527002cca feat(usage): add OpenCode session usage sync (#3215)
* feat(usage): add OpenCode session usage sync

Add OpenCode as a fourth app type in the usage statistics system.
Reads per-message token data from opencode's local SQLite database
(~/.local/share/opencode/opencode.db) and imports into proxy_request_logs.

- New session_usage_opencode.rs module following Codex/Gemini pattern
- Parses assistant message.data JSON for tokens, cost, model
- Adds "opencode" to AppType union and filter tabs
- Updates dedup filters to include opencode_session data_source
- Adds i18n keys for all 4 locales

* fix(usage): add opencode to UsageHero title themes

* fix: respect XDG_DATA_HOME and platform defaults for OpenCode DB path

- Support OPENCODE_DB env var override (absolute and relative paths)
- Use ~/Library/Application Support/opencode/ on macOS
- Use XDG_DATA_HOME/opencode/ when set
- Fall back to ~/.local/share/opencode/ on Linux
- Rename misleading test to test_parse_message_data_ignores_role

* fix(usage): use ~/.local/share/opencode on all platforms

OpenCode relies on xdg-basedir, which ignores macOS/Windows conventions,
so its DB always lives at ~/.local/share/opencode. The previous macOS
default pointed at ~/Library/Application Support/opencode, which does not
exist, making the sync a silent no-op for macOS users without
XDG_DATA_HOME set.

* fix(usage): include opencode -wal mtime in freshness check

OpenCode runs its SQLite DB in WAL mode; new commits land in the -wal
file and the main DB file's mtime only advances on checkpoint. Keying
the freshness gate solely on opencode.db could skip newly written
sessions until a checkpoint occurred. Take the max of the db and -wal
mtimes instead.

* style(usage): apply cargo fmt to opencode session sync

Fixes Backend Checks cargo fmt --check failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(usage): ignore empty OPENCODE_DB and XDG_DATA_HOME env vars

An empty OPENCODE_DB collapsed the path to the data dir (dropping the opencode.db filename); an empty XDG_DATA_HOME produced a relative "opencode" path. Treat empty strings as unset, per the XDG spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(usage): harden OpenCode session sync error handling and labeling

- Map '_opencode_session' provider_id to 'OpenCode (Session)' display name
- Return accurate inserted flag from insert_opencode_message (was always true)
- Do not advance file/session sync_state when a session errors, so failed
  inserts are retried next run instead of being permanently skipped
- Surface per-message insert failures into the sync result errors
- Add opencode_session data-source icon and i18n labels (zh/zh-TW/en/ja)
- Add provider-stats labeling test for opencode session rows

* fix(usage): only import finalized OpenCode messages

An in-progress assistant message holds partial tokens; OpenCode updates the same message_id with final values later. Since request_id is fixed and the insert uses INSERT OR IGNORE, a partial row could never be corrected. Skip messages without time.completed so each turn is imported once with final usage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(usage): keep OpenCode incomplete sessions retryable

---------

Co-authored-by: Eira Hazel <kip3vx9ma@mozmail.com>
Co-authored-by: Eria hazel <git config --global user.email your@email.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-06-04 22:26:24 +08:00
Keith Yu 2a24da517f feat: 新增 S3 兼容云存储同步 (#1351)
* Add S3 Cloud Sync design document

Design for adding AWS S3 as a new Cloud Sync backend alongside WebDAV.
Hybrid approach: extract shared sync protocol, add independent S3 transport.

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

* Add S3 cloud sync implementation design (reqwest + Sig V4)

Updated design based on 2026-03-06 draft: switches from rust-s3 crate
to hand-rolled AWS Sig V4 on existing reqwest for broader S3-compatible
service support (AWS, MinIO, R2, Alibaba OSS, Tencent COS, Huawei OBS).

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

* Add S3 cloud sync implementation plan (11 tasks, TDD)

Detailed step-by-step plan covering: sync_protocol extraction, S3 Sig V4
transport, settings, sync/auto-sync modules, Tauri commands, frontend
presets/dynamic form, and i18n.

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

* deps: add hmac crate for S3 Sig V4 signing

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

* refactor: extract sync_protocol.rs from webdav_sync.rs for shared use

Move transport-agnostic sync protocol logic (constants, types, snapshot
building, manifest validation, artifact verification, snapshot application,
utilities) into a new shared sync_protocol module so both WebDAV and the
upcoming S3 transport can reuse it.

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

* fix: use transport-neutral error keys in sync_protocol

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

* feat: add S3 transport layer with AWS Sig V4 signing

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

* feat: add S3SyncSettings to AppSettings

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

* feat: add S3 sync module with upload/download/fetch

Implements the S3 sync protocol layer (s3_sync.rs) that combines the
shared sync_protocol with the S3 transport. Mirrors the WebDAV sync
module structure with independent sync mutex, connection check,
upload, download, fetch_remote_info, and sync status persistence.

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

* feat: add S3 auto sync worker with debounce

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

* feat: add S3 sync Tauri commands and auto sync worker startup

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

* feat: add S3 sync TypeScript types and API layer

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

* feat: add S3 sync i18n translations (en/zh/ja)

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

* feat: add S3 sync presets and dynamic form to sync settings

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

* fix: preserve HTTP scheme for S3 custom endpoints (MinIO support)

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

* test: add live S3 integration tests (env-var driven, --ignored)

Run with: S3_TEST_AK=... S3_TEST_SK=... S3_TEST_BUCKET=... cargo test --lib services::s3::integration_tests -- --ignored

Verifies test_connection, put_object, get_object, head_object, and 404
handling against a real S3 bucket using the project's own Sig V4 signing.

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

* chore: remove internal design docs before PR

* fix: wire S3 auto-sync to DB hook & sync UI state on async load

- P1: Add s3_auto_sync::notify_db_changed call in SQLite update_hook
  so S3 auto-sync worker receives DB change signals (was only wired
  for WebDAV, leaving S3 worker idle)

- P2: Add useEffect to update syncType selector when s3Config loads
  asynchronously, preventing stale "webdav" default for S3 users

* fix: satisfy clippy for s3 sync

* fix: address s3 sync review feedback

---------

Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-06-04 22:18:51 +08:00
Jason 6692343d1e Add media fallback rectifier for text-only models
Replace unsupported Anthropic image blocks with an [Unsupported Image] marker when routed models are text-only or upstream rejects image input.

Add rectifier settings for media fallback and heuristic model detection, wire the controls into the settings UI, and cover the sanitizer and forwarder gates with regression tests.
2026-06-04 11:40:16 +08:00
Sleepwf 73073454cd fix(i18n): align Chinese VS Code wording with English and Japanese (#3228) 2026-06-03 15:10:06 +08:00
Jason aeaa016cae Simplify Codex takeover notice copy and match hint styling
Restyle the proxy-takeover notice in the Codex editor from the boxed
Alert to the amber inline-hint style used by the endpoint hints, and drop
implementation jargon (127.0.0.1 / PROXY_MANAGED) that users could not
interpret. The notice and the auth/config hints now simply state that the
form shows the stored provider config rather than the proxy-managed live
config. i18n synced across en/zh/ja/zh-TW.
2026-06-01 11:41:05 +08:00
Jason a04e72a267 Fix Codex edit dialog masking live OAuth during proxy takeover
The reported "OAuth access token disappears when enabling Codex proxy
takeover" was a display artifact, not data loss: auth.json on disk kept
the OAuth login the whole time. During takeover the edit dialog falls
back to the stored provider config (so it does not surface the proxy
placeholder), which for a third-party provider shows that provider's own
key instead of the live auth.json, making the OAuth token look gone.

Thread an isProxyTakeover flag from App through ProviderForm into the
Codex editor and show an explicit notice plus storage-aware auth/config
hints clarifying that the form displays the stored provider config while
the live config is temporarily managed by the proxy. Drop the
proxy-running condition so the notice shows whenever takeover is active,
even with the proxy stopped.

Add a regression test asserting the dialog does not read live settings
during takeover and renders the database config. i18n synced across
en/zh/ja/zh-TW.
2026-06-01 11:41:05 +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 41433cfab2 Add Codex restart hint after provider switch 2026-05-30 21:19:30 +08:00
Jason 2683af57cb Add Codex auth preservation setting 2026-05-30 14:13:58 +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 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
Jason e71b90916b Add SudoCode partner provider presets 2026-05-29 11:11:46 +08:00
Jason 32b30e43c0 Add AtlasCloud partner provider presets 2026-05-29 09:46:00 +08:00
Jason 9ef141902e Add APINebula partner provider presets 2026-05-29 09:18:59 +08:00
Jason 8302f1e36c Add APIKEY.FUN partner provider presets 2026-05-28 23:43:48 +08:00
Jason af60c7ed2c feat(codex): add remote compaction toggle for third-party providers
Write model_providers name as "OpenAI" to let Codex attempt remote
compaction through compatible endpoints. Hidden for official providers.
2026-05-28 11:57:54 +08:00
Jason 6b0dd3c4e9 fix(omo): sync recommended models with upstream and improve Fill Recommended feedback
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)
2026-05-27 23:08:11 +08:00
Kairos Duan 3c3d417457 Enable Codex goals in provider templates (#3089)
* Enable Codex goals in provider templates

* feat: add Codex goal mode toggle

- Remove forced goals=true from Codex provider presets and custom templates.
- Add a Codex provider editor switch that updates [features].goals on demand.
- Update docs, i18n, and regression coverage for the optional Goal mode flow.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-05-27 11:31:31 +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
Jason 910ca3b40e chore(sponsors): drop AICoding partner from README, presets and i18n
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.
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 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 ea604a1827 fix(settings): surface "installed but not runnable" state in tool cards
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).
2026-05-25 22:20:34 +08:00
Jason f8b4d67b21 fix(settings): run batch updates per-tool and lock actions while busy
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).
2026-05-25 22:20:34 +08:00
Jason e3df86587d feat(settings): expand About page into a tool management panel
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.
2026-05-25 22:20:34 +08:00
Jason 44d9aabbf3 feat: adaptive reasoning detection for Codex Chat providers
Auto-detect each Chat-routed Codex provider's reasoning interface from
its name, base URL, and model, then inject the matching thinking
parameter without manual configuration:

- Platform-first inference (OpenRouter, SiliconFlow) overrides model
  rules, since the same model exposes different reasoning controls
  depending on the hosting platform.
- Effort tiers are forwarded only to providers that support them
  (DeepSeek, OpenRouter, and StepFun's step-3.5-flash-2603); on/off-only
  providers (Kimi, GLM, Qwen, MiniMax, MiMo, SiliconFlow) drop the level
  instead of sending a field the upstream rejects.
- OpenRouter uses the native reasoning:{effort} object, clamps max to
  xhigh (its enum has no max), and forwards an explicit effort:"none" so
  reasoning can be turned off.
- StepFun falls back to inference so per-model effort support is honored
  (the static preset would have forced effort on step-3.5-flash too).

Includes the Codex provider-form reasoning controls, i18n strings
(zh/en/ja), and response-side reasoning extraction.
2026-05-25 22:20:33 +08:00