feat: live multi-provider quota panel (#1278)

* feat: live provider quota panel (Claude + Codex)

Adds Claude and Codex plan usage to the context window percentage circle tooltip, querying their respective APIs directly using existing CLI auth tokens.

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

* fix: address review feedback - restore quota trigger, guard NaN date

- Re-add triggerFetch() on agent idle/error in agent status subscription
- Guard formatResetsAtLabel against NaN from malformed API date strings

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

* fix: resolve composer conflict and stub subscribe in websocket tests

* feat(quota): add cursor, copilot, zai, grok, and kimi quota UI and improve type safety

* feat(quota): make Claude and Codex pluggable, and map additional quota/credits metrics

* security(quota): fix shell injection vulnerability by migrating exec to execFile

* revert: restore original scripts/dev.ps1 and packages/desktop/scripts/dev.ps1

* Fix i18n resource test expectation

* Fix quota tooltip empty-provider state

* Preserve zero values in Grok quota

* Fix empty quota fetch state

* Ignore quota frames in relay reconnect tests

* Persist refreshed Codex tokens to source auth file

* fix(quota): read Claude OAuth credentials from the macOS login Keychain

On macOS, Claude Code stores its OAuth credential in the login Keychain
(generic-password item, service "Claude Code-credentials"), not in
~/.claude/.credentials.json — that file usually does not exist there, so the
Claude provider's existsSync check failed and macOS users silently got no
Claude quota. Read it via the macOS `security` CLI (its ACL only trusts
/usr/bin/security to decrypt; a native Keychain read would prompt), and stay
read-only there since Claude Code owns the refresh/persist. Linux and Windows
keep using ~/.claude/.credentials.json unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix quota keychain timeout and local host reconciliation

* Fix terminal notification test quota stub

* Ignore quota frames in terminal notification tests

* fix(quota): bypass Claude token refresh on macOS Keychain-backed logins

* fix(dev): update local dev daemon port to 6768 on Windows scripts/dev.ps1

* fix(dev): bypass Unix dev-daemon.sh on Windows in dev.ps1

* fix(client): handle and dispatch provider_quota event in DaemonClient

* fix(desktop): fix PowerShell quote stripping in desktop dev.ps1 config seeding

* fix(desktop): execute config update via temp file to avoid PowerShell quoting bugs

* fix(desktop): fix concurrently command invocation on Windows in dev.ps1

* fix(desktop): resolve path escaping and environment setting syntax errors in dev.ps1 for Windows

* Add on-demand provider usage views

* Fix Cursor usage billing dates

* Move provider usage renderer coverage to e2e

* Use provider manifest for quota fetchers

* Add provider usage fetch timeouts

* Reshape provider usage fetchers

---------

Co-authored-by: ABorakati <ABorakati@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Co-authored-by: lumingjun <lumingjun@bytedance.com>
This commit is contained in:
Aditya Borakati
2026-06-20 12:19:50 +08:00
committed by GitHub
co-authored by ABorakati Claude Sonnet 4.6 Mohamed Boudra lumingjun
parent 2b0740ff84
commit acea7f3d24
59 changed files with 3944 additions and 132 deletions
@@ -0,0 +1,105 @@
import { useCallback, useMemo } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useSessionStore } from "@/stores/session-store";
import { providerUsageCopy } from "./copy";
import type { ProviderUsageListPayload, ProviderUsageView } from "./types";
export const PROVIDER_USAGE_STALE_TIME_MS = 5 * 60 * 1000;
type ProviderUsageClient = Pick<DaemonClient, "listProviderUsage">;
export function providerUsageQueryKey(serverId: string | null | undefined) {
return ["providerUsage", serverId ?? ""] as const;
}
async function fetchProviderUsage(client: ProviderUsageClient): Promise<ProviderUsageListPayload> {
return client.listProviderUsage();
}
interface UseProviderUsageOptions {
enabled?: boolean;
}
export function useProviderUsage(
serverId: string | null | undefined,
options: UseProviderUsageOptions = {},
): {
view: ProviderUsageView;
refresh: () => Promise<void>;
canFetch: boolean;
} {
const queryClient = useQueryClient();
const client = useHostRuntimeClient(serverId ?? "");
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
const supportsProviderUsage = useSessionStore(
(state) => state.sessions[serverId ?? ""]?.serverInfo?.features?.providerUsageList === true,
);
const queryKey = useMemo(() => providerUsageQueryKey(serverId), [serverId]);
const canFetch = Boolean(serverId && client && isConnected && supportsProviderUsage);
const enabled = Boolean((options.enabled ?? true) && canFetch);
const queryFn = useCallback(async () => {
if (!client) {
throw new Error(providerUsageCopy.clientUnavailable);
}
return fetchProviderUsage(client);
}, [client]);
const query = useQuery({
queryKey,
queryFn,
enabled,
staleTime: PROVIDER_USAGE_STALE_TIME_MS,
refetchOnMount: true,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
});
const refresh = useCallback(async () => {
await queryClient.invalidateQueries({ queryKey });
if (!canFetch) {
return;
}
await queryClient.fetchQuery({
queryKey,
queryFn,
staleTime: PROVIDER_USAGE_STALE_TIME_MS,
});
}, [canFetch, queryClient, queryFn, queryKey]);
const view = useMemo<ProviderUsageView>(() => {
if (!serverId || !client || !isConnected) {
return { kind: "error", message: providerUsageCopy.hostUnavailable };
}
if (!supportsProviderUsage) {
return { kind: "error", message: providerUsageCopy.hostUpgradeRequired };
}
if (query.data) {
return {
kind: "ready",
payload: query.data,
isRefreshing: query.isFetching,
};
}
if (query.isError) {
return {
kind: "error",
message: query.error instanceof Error ? query.error.message : String(query.error),
};
}
return { kind: "loading" };
}, [
client,
isConnected,
query.data,
query.error,
query.isError,
query.isFetching,
serverId,
supportsProviderUsage,
]);
return { view, refresh, canFetch };
}