Skip to content

feat(tray): show cached provider usage in the system tray menu#2184

Merged
farion1231 merged 10 commits into
farion1231:mainfrom
TuYv:feat/tray-usage-display
Apr 23, 2026
Merged

feat(tray): show cached provider usage in the system tray menu#2184
farion1231 merged 10 commits into
farion1231:mainfrom
TuYv:feat/tray-usage-display

Conversation

@TuYv
Copy link
Copy Markdown
Contributor

@TuYv TuYv commented Apr 19, 2026

Summary / 概述

Surface the cached provider usage in the system tray menu so users can see how much quota each account has left without opening the main window.

What ships (scoped to TRAY_SECTIONS today — Claude / Codex / Gemini):

  • Each app's submenu title shows the current provider + a compact usage suffix, e.g. Claude · Claude Official · 🟢 h9% w27% (5-hour window, 7-day / weekly window).
  • Color encoding via emoji prefix: 🟢 <70%, 🟠 70–89%, 🔴 ≥90% — same thresholds as the web card footer's utilizationColor.
  • Hovering or clicking the tray icon triggers a background usage refresh (10-second throttle). The main window's React Query auto-refresh also keeps the tray in sync.

Under the hood:

  • New services::usage_cache::UsageCache on AppState — in-memory, write-through from get_subscription_quota and queryProviderUsage. Rendering reads only the cache — no network requests when opening the menu.
  • Cache is ephemeral (no DB migration); repopulated by React Query auto-refresh or the tray hover trigger.
  • Refresh-triggered tray rebuilds are coalesced via an AtomicBool flag + 50 ms window, so a burst of successful usage writes only rebuilds the menu once.
  • refresh_all_usage_in_tray parallelises the per-section subscription calls and per-provider script calls with futures::future::join_all.
  • AppType gets Eq / Hash derives to serve as a HashMap key.

Design notes driven by issue discussion:

  • Dropped the "summary rows above Show Main" idea — duplicated submenu titles.
  • Dropped the per-provider inner-submenu suffix — the outer title is enough and other providers usually have no cached data.
  • Dropped the "Refresh all usage" menu item in favor of implicit refresh on hover/click — lower friction.
  • We also considered appending the reset countdown (e.g. 3h34m / 4d11h) next to each percentage, but the label got too long to stay readable. Happy to land that as a follow-up commit if you have a preferred layout — we already cache the resets_at timestamps.
  • Happy to swap emoji for plain text if preferred.

Related Issue / 关联 Issue

Fixes #2178 #2153

Screenshots / 截图

Before / 修改前 After / 修改后
image image

Checklist / 检查清单

  • `pnpm typecheck` passes
  • `pnpm format:check` passes
  • `cargo clippy` passes — no new warnings introduced; pre-existing warnings in `commands/{misc,settings}.rs`, `database/backup.rs`, `provider.rs`, `proxy/response_processor.rs` untouched
  • `cargo fmt --check` passes
  • `cargo test` — 3 new unit tests for `UsageCache` green; the pre-existing `openclaw_config::default_model_noop_write_skips_backup` flake is unrelated to this PR
  • No new i18n JSON keys needed — tray text lives in `TrayTexts::from_language` (Rust), extended for en / zh / ja

TuYv added 2 commits April 19, 2026 16:33
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.
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 farion1231#2178.
@TuYv TuYv force-pushed the feat/tray-usage-display branch from d2502dc to 7fc5751 Compare April 19, 2026 15:45
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.
@farion1231
Copy link
Copy Markdown
Owner

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4955703e67

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/src/tray.rs Outdated
Comment thread src-tauri/src/tray.rs Outdated
TuYv added a commit to TuYv/cc-switch that referenced this pull request Apr 20, 2026
…script cache

Address P2 findings from automated review on farion1231#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.
@TuYv
Copy link
Copy Markdown
Contributor Author

TuYv commented Apr 20, 2026

Addressed both P2 findings from the Codex review in 54f61a2:

  1. refresh_all_usage_in_tray now filters by settings.visible_apps before
    scheduling any subscription/script queries, so hidden apps no longer
    trigger external API calls on tray hover.
  2. format_usage_suffix only trusts the script cache when
    provider.meta.usage_script.enabled == true, and invalidates the cache
    entry otherwise, preventing indefinitely-stale labels after a user
    disables or removes a usage script.
    PTAL @farion1231 — happy to run @codex review again once you've
    had a look.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

…script cache

Address P2 findings from automated review on farion1231#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.
@TuYv TuYv force-pushed the feat/tray-usage-display branch from 54f61a2 to dab739d Compare April 20, 2026 01:33
@TuYv
Copy link
Copy Markdown
Contributor Author

TuYv commented Apr 20, 2026

Note: squashed a small internal cleanup into the same commit; the earlier reference 54f61a22 is now dab739d9. No behavior change.

TuYv added 5 commits April 21, 2026 23:16
- Add hermes field to SkillApps mock in useImportSkillsFromApps test
- Replace manual match-Ok-Some with .ok() in gemini.rs to satisfy clippy
- 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
@TuYv
Copy link
Copy Markdown
Contributor Author

TuYv commented Apr 23, 2026

已同步 upstream/main,顺带修了两个 CI 问题:
另外顺手做了个小整理:把 is_codex_oauth / codex_fast_mode_enabled 提到 Provider 上,去掉 claude.rs 和 stream_check.rs 里重复的 meta 提取;非
codex-oauth 的测试 call site 也改成语义正确的 codex_fast_mode=false。

这个 tray 用量显示这两天自己在用,感觉还挺方便的。唯一的遗憾是想把配额到期时间也放上去,但一直找不到比较优雅的展示方式(label 太长了不好看)。不知道大佬有没有什么思路,欢迎指点 🙏

…-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.
@farion1231
Copy link
Copy Markdown
Owner

已同步 upstream/main,顺带修了两个 CI 问题: 另外顺手做了个小整理:把 is_codex_oauth / codex_fast_mode_enabled 提到 Provider 上,去掉 claude.rs 和 stream_check.rs 里重复的 meta 提取;非 codex-oauth 的测试 call site 也改成语义正确的 codex_fast_mode=false。

这个 tray 用量显示这两天自己在用,感觉还挺方便的。唯一的遗憾是想把配额到期时间也放上去,但一直找不到比较优雅的展示方式(label 太长了不好看)。不知道大佬有没有什么思路,欢迎指点 🙏

先合了,回头再研究,哈哈

Copy link
Copy Markdown
Owner

@farion1231 farion1231 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢您的贡献!

@farion1231 farion1231 merged commit dc04165 into farion1231:main Apr 23, 2026
2 checks passed
ManLOK-Chu pushed a commit to ManLOK-Chu/cc-switch that referenced this pull request May 11, 2026
…n1231#2184)

* 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 farion1231#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 farion1231#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 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(tray): show cached provider usage in the system tray menu

2 participants