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:
co-authored by
ABorakati
Claude Sonnet 4.6
Mohamed Boudra
lumingjun
parent
2b0740ff84
commit
acea7f3d24
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user