Add SuperGrok subscription usage display, following the existing
Claude Code / Codex official-subscription pattern (protocol ported
from steipete/CodexBar):
- New subscription_grok service: reads Grok CLI credentials from
~/.grok/auth.json, calls the grok.com GrokBuildBilling gRPC-web
endpoint, and parses the response via heuristic protobuf scanning
(used percent, reset time, zero-usage special case)
- Transient failures (network errors, HTTP 408, gRPC deadline/
cancelled) propagate as Err so the frontend retries and keeps the
last good value; auth failures map to Expired with a re-login hint
- Tier naming by reset distance: weekly limit, monthly, or a new
"credits" tier (i18n added for zh/en/ja/zh-TW; tray shows "c")
- New get_xai_oauth_quota command: xai_oauth providers (managed
SuperGrok OAuth accounts) query the same billing endpoint with
their bound account token; ProviderCard auto-renders the quota
footer for them and hides the usage-script entry, and the tray /
usage-script path routes xai_oauth providers to the managed
account instead of the host app's CLI credentials
- UsageScriptModal: drop the config-content heuristic for official
detection; category === "official" is the single source of truth
Claude-Session: https://claude.ai/code/session_01LSNvhEfuoJHaQLZcYQgBU5
Usage/quota queries frequently showed spurious "query failed" states that
manual refresh could not clear (#3820). Root cause: all transport-level
failures were folded into Ok(success:false), so react-query's retry never
fired and the failure body poisoned the cache as regular data.
Backend:
- balance/coding_plan/subscription services now return Err for send
failures and body-read failures (read body via bytes() before
serde_json::from_slice; reqwest's json() wraps read errors as Decode,
making error-kind checks on it dead code). Auth/4xx/parse errors stay
Ok(success:false) and surface immediately.
- Script path maps transient AppError keys (request_failed /
read_response_failed) to Err; Volcengine adds a Transient call variant.
- Command layer skips snapshot persistence, usage-cache-updated emit and
tray refresh on Err so the cache bridge cannot overwrite retained data.
- Expired-credential retry propagates transient errors instead of
rewriting them as "OAuth token has expired".
Frontend:
- resolveDisplayUsage generalized to subscription quotas; 5th param is now
an options object with a `rejected` flag: stale success data retained by
react-query across rejections is re-anchored to dataUpdatedAt and expires
through the same 10-minute keep-last-good window instead of being shown
indefinitely (a total outage used to mask longer than a single 5xx).
- useSubscriptionQuota / useCodexOauthQuota gain keep-last-good with
per-scope snapshot reset; rejected queries with no displayable value
synthesize a failure placeholder carrying the real error message so
footers keep rendering a retry entry point.
- HTTP 429 is classified transient (retry-later) alongside 5xx.
- UsageScriptModal surfaces string rejections via extractErrorMessage.
Tests: 6 backend behavior tests drive real HTTP against a local listener
to pin the Err/Ok channel semantics; frontend suite extends
keepLastGoodUsage coverage (405 passing).
* 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>
CLI-credential-based subscriptions (Claude/Codex/Gemini) read from a
single global credential file, so the quota always reflects the last
CLI login rather than a specific provider. Showing it on non-current
cards is misleading when multiple official subscriptions exist.
Apply the same isCurrent + autoQuery pattern already used by Copilot
and Codex OAuth: only query and render the quota footer when the
provider is the currently active one.
CopilotQuotaFooter and CodexOauthQuotaFooter called their hooks with a
hardcoded `enabled: true` plus an unconditional 5-minute refetch and
refetchOnWindowFocus, so non-current reverse-proxy cards kept polling
in violation of the project's "only the active provider auto-queries
on cooldown" rule. With multiple Copilot or ChatGPT accounts bound to
different cards, every card kept hitting its own usage endpoint.
Adopt the same pattern as useUsageQuery: keep `enabled` independent of
isCurrent so first-fetch and manual refresh still work, but gate
refetchInterval / refetchIntervalInBackground / refetchOnWindowFocus on
a new `autoQuery` option, and thread `isCurrent` from ProviderCard
through the footers into the hooks.
Codex OAuth (ChatGPT Plus/Pro) providers previously fell through to the
default UsageFooter branch and showed no quota at all, while Copilot and
official Codex providers already had a wham/usage-backed quota footer.
This wires up the same five-hour / seven-day tier badges for codex_oauth
provider cards by reusing the existing query_codex_quota function and
SubscriptionQuotaFooter rendering, parameterized to keep both the CLI
credential path ("codex") and the cc-switch managed OAuth path
("codex_oauth") working from a single source of truth.
- Parameterize services::subscription::query_codex_quota with tool_label
and expired_message; promote SubscriptionQuota constructors to
pub(crate). The CLI path keeps its existing "codex" label and the
"re-login with Codex CLI" message; the new path passes "codex_oauth"
and a cc-switch-specific re-login hint.
- Add a new get_codex_oauth_quota Tauri command in commands/codex_oauth.rs
that resolves the ChatGPT account (explicit binding > default account
> not_found), pulls a valid access_token from CodexOAuthManager
(auto-refresh handled), and delegates to query_codex_quota.
- Extract SubscriptionQuotaFooter's render body into a pure
SubscriptionQuotaView component (props: quota / loading / refetch /
appIdForExpiredHint / inline). The existing SubscriptionQuotaFooter
becomes a thin wrapper with identical props and behavior, so
CopilotQuotaFooter and the official Claude/Codex/Gemini paths are
untouched. This avoids duplicating ~280 lines of five-state rendering.
- Add CodexOauthQuotaFooter, a 38-line wrapper that calls the new
useCodexOauthQuota hook and forwards to SubscriptionQuotaView.
- ProviderCard inserts an isCodexOauth branch between isCopilot and
isOfficial, keyed off PROVIDER_TYPES.CODEX_OAUTH (newly added to
config/constants.ts to centralize the previously scattered string).
- Frontend hook caches per (codex_oauth, accountId) so multiple cards
bound to the same ChatGPT account share one fetch via react-query
dedup; cards bound to different accounts get independent fetches.
- No new i18n keys: existing subscription.fiveHour / sevenDay / expired /
refresh / queryFailed / expiredHint are reused.
Read Claude OAuth credentials from macOS Keychain (with file fallback)
and query the Anthropic usage API to show quota utilization inline on
official provider cards. Includes compact countdown timer for reset
windows and hides the rarely-used seven_day_sonnet tier in inline mode.