* 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>
* fix(presets): update Zhipu coding plan endpoints
* fix(model-fetch): probe /models on versioned /vN base URLs
The model-list probe assumed any base URL not ending in /v1 needs /v1/models appended. For providers whose base URL already ends in a version segment like /v4 (Zhipu/Z.AI GLM Coding Plan at .../api/coding/paas/v4), this produced .../v4/v1/models which 404s, so the "Fetch models" button always failed.
Detect a trailing /v{N} version segment and probe {base}/models first, keeping /v1/models as a fallback candidate for non-/v1 versions. Fixes Codex/OpenCode/OpenClaw/Hermes GLM presets and any other vN-style endpoint, with no behavior change for /v1 or non-versioned URLs.
---------
Co-authored-by: Jason <farion1231@gmail.com>
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.
`modelCatalog` is a cc-switch-private field whose SSOT is the database; Live's
config.toml only carries a lossy `model_catalog_json` projection. Proxy
takeover/restore cycles and the official Codex.app rewriting config.toml can
drop that projection, so `read_live_settings` reconstructs an empty catalog.
Two paths then overwrote the stored mapping with that empty Live snapshot:
- Switch-away backfill (`switch_normal` -> `restore_live_settings_for_provider_backfill`):
now overlays the DB provider's `modelCatalog`, falling back to the
Live-reconstructed one only when the DB has none.
- Edit dialog (`EditProviderDialog`): when editing the active Codex provider it
preferred Live over the DB SSOT; now keeps the DB `modelCatalog` so opening +
saving no longer clears the mapping table.
Add Rust backfill tests (preserve DB catalog when Live lacks it; keep Live
catalog when DB has none) and a frontend regression test for the edit dialog.
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.
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.
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.
* 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>
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.
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.
Enable openai_chat routing with explicit model catalogs across the major
Chinese/Asian providers (DeepSeek, Zhipu GLM, Kimi, MiniMax, Baidu Qianfan,
Bailian, StepFun, Volcengine Agent Plan, BytePlus, DouBaoSeed, ModelScope,
Longcat, BaiLing, Xiaomi MiMo, SiliconFlow, Novita AI, Nvidia, etc.). Each
preset declares its context window so the UI can size catalog rows when the
preset is picked.
Also lands two consistency fixes uncovered along the way:
- Include setCodexCatalogModels in resetCodexConfig's useCallback deps to
match the new third parameter it consumes.
- Realign TheRouter Codex test to the "custom" model_provider bucket
established by the recent third-party unification; the previous assertion
predated that refactor and had been failing on HEAD.
- Remove mergeCodexDefaultCatalogModelForSave implicit injection (P1)
The model mapping table is now the single source of truth; no hidden
entries are prepended on save.
- Sync first catalog row model into config.toml on save
Ensures Codex default request model matches the table's first entry
instead of retaining a stale template value.
- Remove API Format selector from CodexFormFields (P3)
wire_api is always 'responses'; the selector confused users into
thinking they were changing the upstream protocol. Only the 'Needs
Local Routing' toggle remains.
- Add restart hint to model mapping i18n text (P2)
model_catalog_json is loaded at Codex startup; users are now informed
that a restart is needed after changes.
- Unify write_codex_live_with_catalog helper (P4)
Replaces three scattered prepare+write call sites in config.rs,
provider/live.rs, and proxy.rs with a single entry point.
- Clean up useCodexConfigState dead state (P3 follow-up)
Remove codexModelName, codexContextWindow, codexAutoCompactLimit and
their handlers/effects since no component consumes them after the UI
consolidation.
* fix(skills): install correct skill from skills.sh search results
When multiple skills share the same directory name across different repos,
SkillCard was passing directory to onInstall/onUninstall, causing handleInstall
to always match the first result. Switch to using the unique key field
(directory:repoOwner:repoName) for precise identification.
* test(skills): add regression test for skills.sh install by key
Verifies that clicking install on the second card when two skills share
the same directory name correctly installs the second skill, not the first.
---------
Co-authored-by: mrzhao <mrzhao@iflytek.com>
The step was 0.01, preventing input of prices like DeepSeek's cache read
cost ($0.0028/million tokens). Extract step value to a constant and apply
to all four price fields.
Closes#2503
* feat: add Xiaomi MiMo token plan presets
* fix: update Xiaomi MiMo provider presets
* fix: align MiMo V2.5 model specs with official documentation
- Update maxTokens from 32000 to 131072 (128K) for mimo-v2.5-pro and mimo-v2.5
- Update contextWindow from 262144 to 1048576 (1M) for mimo-v2.5
- Aligns with official specs from Xiaomi MiMo documentation
- Ensures consistency between OpenClaw and OpenCode presets
---------
Co-authored-by: Jason <farion1231@gmail.com>
- Add Claude Desktop Official to the Claude Desktop preset list.
- Treat selected official presets as official mode in the form.
- Cover the official preset with a preset-order regression test.
Remove the category-based grouping logic from ProviderPresetSelector,
letting the array position in each preset config file be the single
source of truth for display order. Move partner presets (PatewayAI,
火山Agentplan, BytePlus, DouBaoSeed) right after Shengsuanyun across
all 6 config files so they appear earlier in the UI.
- Add `get_codex_oauth_models` Tauri command reusing the managed OAuth
access token to hit `chatgpt.com/backend-api/codex/models`; HTTP and
multi-shape JSON parsing live in `services::codex_oauth_models` so the
command stays thin.
- Unify the Claude form's "fetch models" button across normal / Copilot /
Codex OAuth presets, drop the auto-load effect for Copilot in favor of
explicit clicks, and guard against stale responses with a requestId ref.
- Add Vitest coverage for both Copilot and Codex OAuth paths asserting no
request on mount and the correct account id on click; add Rust unit
tests for the four model-list payload shapes.
- Replace the four flat env inputs with a Sonnet/Opus/Haiku role table.
Each row exposes ANTHROPIC_DEFAULT_*_MODEL plus a new display name
field ANTHROPIC_DEFAULT_*_MODEL_NAME, and Sonnet/Opus gain a
"Declare 1M" checkbox that toggles the [1M] suffix.
- Strip the [1M] context-capability marker before forwarding non-Copilot
requests upstream. Copilot keeps its existing [1m]->-1m normalization.
- Claude Desktop import now consumes ANTHROPIC_DEFAULT_*_MODEL_NAME as
label_override, closing the Claude Code -> Claude Desktop displayName
pipeline; add_route's merge logic is shared between hashmap branches.
- Unify the [1M] marker as ONE_M_CONTEXT_MARKER across
claude_desktop_config and proxy::model_mapper; rename the strip
helper to strip_one_m_suffix_for_upstream.
- Collapse useModelState's seven duplicated useState initializers and
the useEffect parse block into a single parseModelsFromConfig call.
- Add tests/hooks/useModelState.test.tsx and a Claude Desktop import
test covering Kimi K2 -> label_override. i18n (en/ja/zh) updated.
Adapt to Claude Desktop 1.6259.1+ fail-all validation which only
accepts claude-(sonnet|opus|haiku)-* route IDs. Branded model names
(DeepSeek, Kimi, GLM, etc.) now live in a new labelOverride field
instead of being embedded in route IDs.
- Backend auto-repairs legacy unsafe routes to the next free
sonnet/opus/haiku slot instead of erroring
- Frontend swaps the free-form route input for a role dropdown plus
menu display name field
- Add CLAUDE_DESKTOP_ROLE_ROUTE_IDS as the single source of truth
for role-to-route mapping; presets and form both consume it
- Drop the dead displayName alias on ClaudeDesktopModelRoute and the
ineffective /v1/models display_name injection (UI ignores it)
- Update i18n (en/ja/zh) and form focus test for the new fields
Adds a new ClaudeDesktop AppType that writes Claude Desktop's third-party
inference profile under configLibrary/, sharing _meta.json with other
launchers (Ollama-compatible) so cc-switch can coexist with them.
Two switch modes:
- direct: provider already exposes claude-* / anthropic/claude-* model
ids on Anthropic Messages, Claude Desktop connects to it directly.
- proxy: cc-switch's local proxy acts as the inference gateway,
presenting only claude-* route names to Claude Desktop and mapping
them to real upstream models. Required after Anthropic restricted
Claude Desktop to claude-family ids.
Backend:
- New module claude_desktop_config with snapshot/rollback, official seed
bypass, /claude-desktop/v1/{models,messages} routes, and a single
source of truth for default proxy routes.
- Gateway token persisted in SQLite, validated on every proxied request.
- get_claude_desktop_status surfaces drift signals (stale models,
missing routes, proxy stopped, base URL mismatch, missing token).
Frontend:
- Slim ClaudeDesktopProviderForm independent from ProviderForm,
controlled by a top-level appId guard.
- ProviderList banner consumes the status query (5s polling) and
renders actionable diagnostics.
- ClaudeDesktopRouteToggle in the header to start/stop the local
gateway without touching takeover state.
- Three-locale i18n synchronised.
Closes#2139
Two related defects let the installed-skills count balloon when users
tap the import confirm button multiple times — either deliberately or
because the button is still clickable while a slow import is in flight:
- The confirm button only disabled itself while `selected.size === 0`,
so it stayed clickable during a pending mutation. Each extra click
triggered another `importFromApps` mutation.
- `useImportSkillsFromApps` appended the server response to the
installed cache without deduping, so re-firing the mutation stacked
the same skills into the list again.
Disable the confirm (and cancel) buttons while the mutation is pending
— matching the `isRestoring` / `isDeleting` pattern already used by
`RestoreSkillsDialog` — and merge success payloads by
`InstalledSkill.id` so repeated results overwrite rather than
accumulate.
The merge is extracted as a pure `mergeImportedSkills` reducer to make
the behaviour unit-testable and to short-circuit on an empty payload,
returning the existing reference so React Query does not notify
subscribers about a no-op cache update.
Three unrelated test failures surfaced after rebase:
- McpFormModal expected the apps boolean set without `hermes`; Hermes MCP
support is now wired, so the fixture must include `hermes: false`.
- therouter Gemini preset was bumped to `gemini-3.1-pro` in a later
commit; update the assertions to match current config.
- openclaw_config tests mutate process-level `CC_SWITCH_TEST_HOME` and
`HOME` inside a module-local Mutex, but hermes_config does the same
under its own separate Mutex. Running both modules in parallel let the
env races corrupt hermes_config's `with_test_home`. Tag the four
env-mutating openclaw tests with `#[serial]` so they serialize across
modules via serial_test's process-wide default key.
Adds a dedicated Hermes row to the directory-override settings so users
can point CC Switch at alternate Hermes config locations (e.g. a second
profile directory for work/personal split). `get_config_dir` on the
Rust side already supports hermes; this just wires up the frontend row.
Wiring it through `useDirectorySettings` revealed a scaling problem:
every supported app required five parallel ternary chains across
`computeDefaultConfigDir`, `updateDirectory`, `browseDirectory`,
`resetDirectory`, and `updateDirectoryState`. Replaces those with two
lookup tables (`APP_DIRECTORY_META`, `DIRECTORY_KEY_TO_SETTINGS_FIELD`)
so adding the next app is two entries, not fifteen edit sites.
Drive-by cleanup from the same touch:
* `resetAllDirectories` takes a `ResolvedAppDirectoryOverrides` object
instead of five positional optional strings.
* `setResolvedDirs` returns the same reference when the sanitized
value is unchanged, so no-op edits don't cascade renders.
Also lands all i18n updates for this series (`hermesConfigDir` and
placeholder, Memory section's enable/disable/toggleFailed copy, and
the reworded `schemaMigratedV12` warning) in zh/en/ja together.
- OpenClaw: replace opus-4-6 with opus-4-7 across 17 aggregator presets
(id, name, primary, modelCatalog); AWS Bedrock entry rewritten to new
SKU anthropic.claude-opus-4-7 (drops -v1 and dated suffix per official
4.7 model card) and pricing corrected to $5/$25/$0.50/$6.25 during the
SKU swap, aligning with schema.rs source of truth
- OpenCode: same replacement for 13 aggregators plus
OPENCODE_PRESET_MODEL_VARIANTS entries for @ai-sdk/amazon-bedrock and
@ai-sdk/anthropic, plus AWS Bedrock provider models map
- OpenRouter / TheRouter / GitHub Copilot in claudeProviderPresets use
dot-style id; update to anthropic/claude-opus-4.7 (missed by 509d2250)
- omo: switch agent/category recommended to opus-4-7; replace key in
OMO_BACKGROUND_TASK_PLACEHOLDER priority map
- hermes_config.rs: update doc comments and test fixtures to opus-4-7;
Hermes ModelPanel placeholder and i18n defaultHint examples follow
- i18n unspecifiedHigh category description bumped to 'Claude Opus 4.7
max variant' to match omo recommended
- Test fixtures updated: therouter preset assertion and opencode Bedrock
variant lookup now check for opus-4-7
- Sonnet 4.6 / Haiku 4.5 untouched - no official 4.7 release for them
- Add "hermes" to AppId union type and all exhaustive Record<AppId>
- Add HermesModelConfig, HermesAgentConfig, HermesEnvConfig types
- Add hermes field to VisibleApps, McpApps, ProxyTakeoverStatus
- Create src/lib/api/hermes.ts with Tauri invoke wrappers
- Create src/hooks/useHermes.ts with 5 query + 3 mutation hooks
- Register hermes in APP_IDS, APP_ICON_MAP (violet color scheme)
- Split MCP_SKILLS_APP_IDS into MCP_APP_IDS (includes hermes) and
SKILLS_APP_IDS (excludes hermes, since Hermes has no Skills support)
- Wire hermes additive-mode into App.tsx (remove/duplicate handlers),
ProviderList.tsx (live provider ID query + In Config badge),
mutations.ts (cache invalidation on switch/add/delete)
- Add Hermes checkbox to McpFormModal
- Add basic hermes i18n keys (en/zh/ja)
* fix(claude-plugin): sync current provider config to settings.json on toggle enable
- Extract syncClaudePluginIfChanged to share logic between autoSaveSettings and saveSettings
- Fix P1: enableClaudePluginIntegration toggle in General tab now actually syncs ~/.claude/settings.json
- Fix P2: check syncCurrentProvidersLiveSafe() return value and show toast on failure
- Fix P3: sync providers on both enable and disable, not just enable
- Fix P4: avoid double syncCurrentProvidersLiveSafe when plugin toggle + dir change happen together
- Remove duplicate comment
- Add missing providersApi.getCurrent/getAll mocks in tests
* style: reformat after rebase onto main
Prettier flagged a line-break introduced by the openclaw directory
change (from main) after rebase.
* fix(claude-plugin): read prev enabled state from live cache to avoid stale closure
syncClaudePluginIfChanged compared enabled against data?.enableClaudePluginIntegration
captured in a useCallback closure. After invalidateQueries + refetch, the React
Query cache is up to date, but the consuming hook's closure does not see the new
value until React re-renders. Quick on->off toggles could therefore skip
applyClaudePluginConfig, leaving ~/.claude/config.json in the previously enabled
state even though settings.json was persisted as disabled.
Read the previous value synchronously from queryClient.getQueryData(["settings"])
before saveMutation.mutateAsync(), then pass it to the helper as prevEnabled.
getQueryData bypasses the closure and reflects the live cache at call time.
Test covers the race: closure data stays at false while the cache reports true;
the helper must still call applyClaudePluginConfig({ official: true }).
---------
Co-authored-by: Jason <farion1231@gmail.com>
* feat(usage): enhance usage stats backend and query hooks
* feat(usage): redesign calendar date range picker with auto-switch and simplified layout
* refactor(usage): streamline dashboard layout and stats components
* refactor(usage): compact request log table with merged cache/multiplier columns and centered layout
* feat(i18n): add cache short labels and usage stats translations for zh/en/ja
* Align usage dashboard stats with range boundaries
The usage dashboard mixed second-precision detail rows with day-level rollups, which caused custom half-day ranges to overcount historical rollup data and left the request log paginator on stale pages after top-level filter changes.
This change limits rollups to fully covered local days, aligns multi-day trend buckets with natural local days, and resets request log pagination when the dashboard range or app filter changes.
Constraint: usage_daily_rollups stores only daily aggregates after pruning old detail rows
Rejected: Include partial boundary rollups proportionally | historical intra-day detail is unavailable after pruning
Rejected: Force RequestLogTable remount on range change | would discard local draft filters unnecessarily
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep summary, trends, provider stats, and model stats on the same rollup-boundary rules
Tested: cargo test --manifest-path src-tauri/Cargo.toml usage_stats
Tested: pnpm exec vitest run tests/components/RequestLogTable.test.tsx
Tested: pnpm typecheck
Not-tested: Manual UI validation in the Tauri app
* Preserve full-day usage filters at minute precision
The latest review surfaced two interaction bugs in the usage dashboard: rollup-backed stats undercounted end days selected via the minute-precision picker, and immediate select changes accidentally applied unsubmitted text drafts from the request log filters.
This change treats 23:59 as a fully selected local end day for rollup inclusion and narrows select-side state syncing so app/status updates do not commit provider/model drafts.
Constraint: The custom range picker emits minute-precision timestamps, while rollups are stored at day granularity
Rejected: Require exact 23:59:59 end timestamps | unreachable from the current picker UI
Rejected: Rebuild applied filters from the full draft state on select changes | silently commits unsaved text input
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep request-log text fields on explicit apply semantics even when select filters remain immediate
Tested: cargo test --manifest-path src-tauri/Cargo.toml usage_stats
Tested: pnpm exec vitest run tests/components/RequestLogTable.test.tsx
Tested: pnpm typecheck
Not-tested: Manual Tauri dashboard interaction
* refactor(usage): move range presets into date picker, single-row layout
- UsageDateRangePicker: add preset shortcuts (今天/1d/7d/14d/30d) inside
popover top; clicking a preset applies immediately and closes popover
- UsageDashboard: collapse to single row (app filters + refresh + picker);
remove standalone preset buttons and summary stats bar
- RequestLogTable: replace static Calendar badge with interactive
UsageDateRangePicker via onRangeChange prop; single filter row
* Keep usage pagination regression coverage aligned with the rendered UI
The new regression test was asserting a non-existent pagination label and page summary text, so it failed before it could verify the real page-reset behavior. This commit switches the assertions to the numbered pagination buttons that the component actually renders and validates the reset through the query hook arguments.
Constraint: RequestLogTable exposes numbered pagination buttons, not a "Next page" label or "2 / 6" summary text
Rejected: Add synthetic pagination labels solely for the test | would couple production markup to a test-only assumption
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Prefer pagination assertions that follow the rendered controls or hook inputs instead of invented summary text
Tested: pnpm vitest run tests/components/RequestLogTable.test.tsx; pnpm typecheck; pnpm test:unit
* refactor(usage): clean up dead code and polish date range picker
- Remove unused exports MAX_CUSTOM_USAGE_RANGE_SECONDS,
timestampToLocalDatetime, and localDatetimeToTimestamp from
usageRange.ts (replaced by the calendar picker)
- Deduplicate getPresetLabel from UsageDashboard and
UsageDateRangePicker into shared getUsageRangePresetLabel helper
- Add aria-label, aria-current and aria-pressed to calendar day
buttons so screen readers can disambiguate same-numbered days
across adjacent months
- Drop unused cacheReadShort and cacheWriteShort i18n keys (zh/en/ja);
the request log table renders R/W prefixes inline
- Align customRangeHint copy with the removed 30-day limit by
dropping "up to 30 days" wording (zh/en/ja)
* fix(usage): align rollup cutoff to local midnight to keep days complete
`rollup_and_prune` previously used `Utc::now() - retain_days * 86400`
as the cutoff. Because rollups are bucketed by *local* date and detail
rows below the cutoff are pruned, an unaligned cutoff left the youngest
rolled-up day half-rolled-up and half-pruned. Combined with the new
`compute_rollup_date_bounds` boundary trimming (which excludes any
rollup day not fully covered by the requested range), custom range
queries that touch that day silently under-count summary, trend,
provider, and model stats.
Fix the invariant at the source: snap the cutoff to the next local
midnight after `(now - retain_days)`. Every rollup row now reflects a
complete local day, so the boundary trimmer's all-or-nothing assumption
holds.
Includes unit tests for the cutoff math (typical case + already-on-
midnight case). DST gap is handled defensively by bumping forward by
an hour.
Addresses Codex P2 review finding on PR #2002.
---------
Co-authored-by: Jason <farion1231@gmail.com>
cc-switch could already persist arbitrary OMO Slim agent keys and top-level fields, but the built-in metadata and UI copy still reflected the pre-council agent set. This made the upstream council feature look unsupported and pushed users toward manual JSON-only setup.
Promote council to a built-in OMO Slim agent, add copy that points top-level plugin settings at Other Fields, and lock the behavior with regression tests.
Constraint: oh-my-opencode-slim exposes council through both agents.council and top-level council config
Rejected: Add a dedicated council editor UI now | too much surface area for issue #1981
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep OMO Slim built-in agent metadata aligned with upstream agent additions before shipping UI support
Tested: pnpm exec vitest run tests/utils/omoConfig.test.ts tests/components/OmoFormFields.mergeCustomModelsIntoStore.test.ts
Tested: pnpm typecheck
Not-tested: End-to-end validation against a live oh-my-opencode-slim installation
Related: farion1231/cc-switch#1981
- Make migrate_v6_to_v7 check skills table existence before ALTER
- Make migrate_v7_to_v8 check model_pricing table existence before UPDATE
- Fix SessionManagerPage test: use getByRole heading instead of getAllByText
which breaks when highlightText splits text across <mark> elements
- Add content_hash and updated_at fields to 4 InstalledSkill literals in skill_sync.rs
- Add useCheckSkillUpdates and useUpdateSkill to UnifiedSkillsPanel test mock
- Suppress unused import warning in auto_launch.rs test module
Remove the hard block that prevented switching to providers requiring
proxy (OpenAI format, Copilot, full URL mode) when the proxy is not
running. Now the switch proceeds with a warning toast. Also deduplicate
the proxy hint info toast so it doesn't appear alongside the warning.
* feat(provider): support additive provider key lifecycle management
Add `addToLive` parameter to add_provider so callers can opt out of
writing to the live config (e.g. when duplicating an inactive provider).
Add `originalId` parameter to update_provider to support provider key
renames — the old key is removed from live config before the new one
is written.
Frontend: ProviderForm now exposes provider-key input for openclaw app
type, and EditProviderDialog forwards originalId on save. Deep-link
import passes addToLive=true to preserve existing behavior.
* test(provider): add integration tests for additive provider key flows
Cover openclaw provider duplication scenario to verify that a generated
provider key is assigned automatically. Add MSW handlers for
get_openclaw_live_provider_ids, get_openclaw_default_model,
scan_openclaw_config_health, and check_env_conflicts endpoints.
Update EditProviderDialog mock to pass originalId alongside provider.
* fix(openclaw): replace json-five serializer to prevent panic on empty collections
json-five 0.3.1 panics when pretty-printing nested empty maps/arrays.
Switch value_to_rt_value() to serde_json::to_string_pretty() which
produces valid JSON5 output without the panic. Add regression test for
removing the last provider (empty providers map).
* style: apply rustfmt formatting to proxy and provider modules
Reformat chained .header() calls in ClaudeAdapter and StreamCheckService
for consistent alignment. Reorder imports alphabetically in stream_check.
Fix trailing whitespace in transform.rs and merge import lines in
provider/mod.rs.
* style: fix clippy warnings in live.rs and tray.rs
* refactor(provider): simplify live_config_managed and deduplicate tolerant live config checks
- Change live_config_managed from Option<bool> to bool with #[serde(default)]
- Extract repeated tolerant live config query into check_live_config_exists helper
- Fix duplicate key generation to also check live-only provider IDs
- Fix updateProvider test to match new { provider, originalId } call signature
- Add streaming_responses test type annotation for compiler inference
* fix(provider): distinguish legacy providers from db-only when tolerating live config errors
Change `ProviderMeta.live_config_managed` from `bool` to `Option<bool>`
to introduce a three-state semantic:
- `Some(true)`: provider has been written to live config
- `Some(false)`: explicitly db-only, never written to live config
- `None`: legacy data or unknown state (pre-existing providers)
Previously, legacy providers defaulted to `live_config_managed = false`
via `#[serde(default)]`, which silently swallowed live config parse
errors. This could mask genuine configuration issues for providers that
had actually been synced to live config before the field was introduced.
Now, only providers with an explicit `Some(false)` marker tolerate parse
errors; legacy `None` providers surface errors as before, preserving
safety for already-managed configurations.
Also wrap the `ensureQueryData` call for live provider IDs during
duplication in a try/catch so that a malformed config file shows a
user-facing toast instead of silently failing.
Add tests for both the legacy error propagation path and the frontend
duplication failure scenario.
* refactor(provider): unify OMO variant updates with atomic file-then-db writes and rollback
Consolidate the duplicated omo/omo-slim update branches into a single
match on the variant. Write the OMO config file from the in-memory
provider state *before* persisting to the database, so a file-write or
plugin-sync failure leaves the database unchanged. If `add_plugin`
fails after the config file is already written, roll back to the
previous on-disk contents via snapshot/restore.
Also:
- `sync_all_providers_to_live` now skips db-only providers
(`live_config_managed == Some(false)`) instead of attempting to write
them to live config.
- `import_{opencode,openclaw}_providers_from_live` mark imported
providers as `live_config_managed: Some(true)` so they are correctly
recognized during subsequent syncs.
- Extract OmoService helpers: `profile_data_from_provider`,
`snapshot_config_file`, `restore_config_file`, `write_profile_config`,
and the new public `write_provider_config_to_file`.
- Add 9 new tests covering sync skip, legacy restore, import marking,
OMO persistence, file-write failure, and plugin-sync rollback.
* fix(provider): fix additive provider delete/switch regressions and redundancy
- fix(delete): replace stale live_config_managed flag check with
check_live_config_exists so providers written to live before the
flag-flip logic was introduced are still cleaned up on delete
- fix(switch): make write_live_with_common_config return Err instead of
silently returning Ok when config structure is invalid, preventing
live_config_managed from being incorrectly flipped to true
- fix(update): block provider key rename for OMO/OMO Slim categories to
prevent orphaned current-state markers breaking OMO file syncs
- fix(switch): flip live_config_managed to true after successful live
write for DB-only additive providers so sync_all_providers_to_live
includes them on future syncs; roll back live write if DB update fails
- refactor(delete): merge symmetric OMO/OMO-Slim blocks into single
match-on-variant path; hoist DB read to top of additive branch
- refactor(remove_from_live_config): merge OMO/OMO-Slim if/else-if
into single match-on-variant path
- refactor(switch_normal): merge two OMO/OMO-Slim if blocks into one
OpenCode guard with (enable, disable) variant pair
- fix(update): remove redundant duplicate return Ok(true) after OMO
current-state write
* fix(test): use preferred_filename after OMO field rename
The merge from main brought in #1746 which renamed
OmoVariant.filename → preferred_filename, but the test helper
omo_config_path() was not updated, breaking compilation of all
new provider tests.
---------
Co-authored-by: Jason <farion1231@gmail.com>
* feat(proxy): add full URL mode and refactor endpoint rewriting
- Add `isFullUrl` provider meta to treat base_url as complete API endpoint
- Remove hardcoded `?beta=true` from Claude adapter, pass through from client
- Refactor forwarder endpoint rewriting with proper query string handling
- Block provider switching when proxy is required but not running
- Add full URL toggle UI in endpoint field with i18n (zh/en/ja)
* refactor(proxy): remove beta query handling
* fix(proxy): strip beta query when rewriting Claude endpoints
* feat(codex): complete full URL support
* refactor(ui): refine full URL endpoint hint
Create a local backup under ~/.cc-switch/skill-backups/ before removing
skill directories. The backup includes all skill files and a meta.json
with original skill metadata. Old backups are pruned to keep at most 20.
The backup path is returned to the frontend and shown in the success
toast. Bump version to 3.12.3.