* feat: add Rust-side write-through usage cache
Introduce an in-memory UsageCache on AppState that the existing usage
query commands populate on success. The cache is read-only to the rest
of the app today; the next commit consumes it from the tray menu.
- New services::usage_cache module with split maps: subscription keyed
by AppType, script keyed by (AppType, provider_id).
- AppType gains Eq + Hash so it can be used as a HashMap key.
- commands::subscription::get_subscription_quota now takes State<AppState>
and writes through on success (signature change is invisible to the
frontend — Tauri injects State automatically).
- commands::provider::queryProviderUsage body extracted into an inner
async fn; the public command wraps it with write-through, covering
Copilot, coding-plan, balance, and generic script paths uniformly.
Cache is in-memory only; auto-query interval and the upcoming tray
refresh action rebuild it after restarts.
* feat(tray): surface cached usage in the system tray menu
Read UsageCache populated by the previous commit and render it in three
places, scoped to whatever TRAY_SECTIONS covers (Claude/Codex/Gemini):
1. Inline suffix on each provider submenu item
"AnyProvider · 🟢 5h 18% / 7d 23%"
2. Disabled summary row per visible app under "Show Main"
"Claude · Anthropic Official · 🟢 5h 18% / 7d 23%"
3. "Refresh all usage" menu item that triggers get_subscription_quota +
queryProviderUsage for every applicable provider, then rebuilds the
tray menu via the existing refresh_tray_menu path.
Color encoding uses emoji (🟢 <70% / 🟠 70-89% / 🔴 ≥90%) since Tauri 2
tray labels are plain text. Missing cache entry leaves the label
unchanged — tray never issues network requests when opened. Three new
i18n-ready strings live in TrayTexts (en/zh/ja), following the existing
pattern for tray text.
Closes#2178.
* feat(usage): bridge tray UsageCache writes to frontend React Query
Why: tray hover triggers backend-only refresh that wrote to UsageCache but
never notified the frontend, leaving main UI stale while tray showed fresh
numbers. Emit a payload-carrying event after each cache write so React Query
can setQueryData directly, keeping both views in sync without duplicate fetches.
* fix(tray): skip hidden apps on hover refresh and drop stale disabled-script cache
Address P2 findings from automated review on #2184:
1. refresh_all_usage_in_tray now filters TRAY_SECTIONS by settings.visible_apps
before scheduling subscription/script queries, matching create_tray_menu and
preventing wasted external API calls (and rate-limit/auth-error log noise)
for apps the user has hidden.
2. format_usage_suffix only trusts the script cache when provider.meta.usage_script
is still enabled; when a script is disabled/removed the cached suffix is now
invalidated so the tray label no longer shows stale data indefinitely.
* refactor: consolidate codex provider helpers and fix test semantics
- Add Provider::is_codex_oauth() and Provider::codex_fast_mode_enabled()
to eliminate duplicated meta extraction in claude.rs and stream_check.rs
- Fix non-codex-oauth tests to pass codex_fast_mode=false (was true, harmless
but semantically misleading)
- Remove redundant is_dir() guard after resolve_skill_source_dir already
guarantees the returned path is a directory
* style: apply cargo fmt
* fix(tray): reflect failed refreshes in cache and support Gemini flash-lite
Follow-up to the tray usage-display feature addressing review feedback:
- Write snapshots for both Ok(success:false) and Err paths in
queryProviderUsage / get_subscription_quota so stale success data
no longer persists across failed refreshes; the original Err is
still returned to the frontend onError handler.
- Include gemini_flash_lite tier in the tray summary with label "l".
Matches the frontend SubscriptionQuotaFooter and keeps the worst
emoji correct when lite is the highest utilization.
- Add TIER_GEMINI_PRO / _FLASH / _FLASH_LITE constants in
services/subscription.rs and reuse them in classify_gemini_model
and sort_order.
- Extract Provider::has_usage_script_enabled() to remove the
duplicated meta.usage_script chain at two call sites.
- Use db.get_provider_by_id in refresh_all_usage_in_tray instead of
materialising the full provider map, and parallelise subscription
and script futures via futures::future::join.
- Narrow refresh_all_usage_in_tray to each section's effective
current provider (script if enabled, else subscription when the
provider is official). Hover refreshes now issue at most
TRAY_SECTIONS.len() outbound requests.
- Add 10 unit tests in tray::tests covering Claude/Codex h/w dispatch,
Gemini p/f/l dispatch (including lite-only and lite-worst cases),
and success/failure guards.
---------
Co-authored-by: Jason <farion1231@gmail.com>
Skills import previously inferred app enablement from filesystem presence,
causing incorrect multi-app activation when the same skill directory existed
under multiple app paths. Now the frontend submits explicit app selections
via ImportSkillSelection, and schema migration preserves a snapshot of
legacy app mappings to avoid lossy reconstruction.
Also adds reconciliation to sync_to_app (removes disabled/orphaned symlinks)
and MCP sync_all_enabled (removes disabled servers from live config).
v3.10.3 introduced HOME env priority on Windows for test isolation,
which caused database path to change when HOME differs from USERPROFILE
(common in Git/MSYS environments), making providers appear to disappear.
Changes:
- Use CC_SWITCH_TEST_HOME for test isolation instead of HOME
- Add legacy fallback to detect v3.10.3 database location on Windows
- Add logging for legacy path detection to aid debugging
When proxy is active, switching providers only updated the database flags
but not the live backup. This caused the wrong provider config to be
restored when stopping the proxy.
Added `update_live_backup_from_provider()` method to ProxyService that
generates backup from provider's settings_config instead of reading from
live files (which are already taken over by proxy).
The previous cleanup logic only sent a shutdown signal but didn't wait
for the proxy server to actually stop. This caused a race condition
where the app would exit before cleanup completed, leaving Live configs
in an inconsistent state.
Changes:
- Add `server_handle` field to ProxyServer to track the spawned task
- Modify `stop()` to wait for server task completion (5s timeout)
- Add 100ms delay before process exit to ensure I/O flush
- Export ProxyService and fix test files that were missing proxy_service field
This commit refactors all tests to work with the new database-based
architecture, replacing the previous JSON config approach.
Key changes:
- Add Database export to lib.rs for test access
- Create test helper functions in support.rs:
- create_test_state(): Creates empty test state with fresh DB
- create_test_state_with_config(): Migrates JSON config to DB
- Fix environment isolation in provider_service tests:
- provider_service_switch_missing_provider_returns_error
- provider_service_switch_codex_missing_auth_returns_error
- Replace ignored export tests with working alternatives:
- export_sql_writes_to_target_path (tests Database::export_sql)
- export_sql_returns_error_for_invalid_path (tests error handling)
- Update error type matching to align with current implementation
All tests now:
- Use isolated test environments (test_mutex + reset_test_fs)
- Access data via Database API instead of RwLock<MultiAppConfig>
- Work with SQLite persistence layer
- Pass without environment pollution or race conditions
Fixes test compilation errors after database migration.
Key improvements:
- Extract switch_provider_internal() returning AppError for better testability
- Fix backup mtime inheritance: use read+write instead of fs::copy to ensure latest backup survives cleanup
- Add 15+ integration tests covering provider commands, atomic writes, and rollback scenarios
- Expose write_codex_live_atomic, AppState, and test hooks in public API
- Extract tests/support.rs with isolated HOME and mutex utilities
Test coverage:
- Provider switching with live config backfill and MCP sync
- Codex atomic write success and failure rollback
- Backup retention policy with proper mtime ordering
- Negative cases: missing auth field, invalid provider ID