import { useAtomValue } from "@effect/atom-react"; import type { EnvironmentId, ProviderConsumeResetCreditOutcome, ProviderInstanceId, ServerProvider, ServerProviderResetCredits, ServerProviderUsageWindow, UsageLimitSourceAccount, UsageProviderKind, } from "@t3tools/contracts"; import { collectLimitSources, collectLimitsGroups, elapsedShare, formatDuration, formatResetsIn, limitsNotice, paceOf, providerLimitsLabel, } from "@t3tools/shared/usageLimits"; import { type ReactNode, useState } from "react"; import { Alert, Pressable, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; import { ProviderIcon } from "../../components/ProviderIcon"; import { environmentPresentations } from "../../state/presentation"; import { serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; import { SettingsSection } from "../settings/components/SettingsSection"; import { useProviderColors } from "./usageProviders"; const PACE_LABEL = { ahead: "ahead of pace", on: "on pace", under: "under pace" } as const; const DRIVER_LABEL: Partial> = { codex: "Codex", claudeAgent: "Claude" }; type Driver = ServerProvider["driver"]; /** The series colour the usage chart uses for this driver, so the two views read as one. */ function useBarColor(driver: Driver): string | null { const colors = useProviderColors(); const kind: UsageProviderKind | null = driver === "codex" ? "codex" : driver === "claudeAgent" ? "claude" : null; return kind ? colors[kind] : null; } /** * One window as a bar spanning its whole duration: the fill is quota spent, * the hairline is how far into the window the clock is. Pace sits under the * left edge, the countdown under the right, so a row reads in one glance. */ function WindowRow(props: { readonly window: ServerProviderUsageWindow; readonly color: string | null; readonly now: number; }) { const { window, now } = props; const used = Math.round(Math.max(0, Math.min(100, window.usedPercent))); const elapsed = elapsedShare(window, now); const pace = paceOf(window, now); const resetsIn = formatResetsIn(window, now); return ( {window.label} {used}% = 90 ? "h-full rounded-full bg-destructive" : used >= 70 ? "h-full rounded-full bg-warning" : "h-full rounded-full bg-foreground" } style={[ { flex: used }, used < 70 && props.color ? { backgroundColor: props.color } : null, ]} /> {elapsed !== null ? ( ) : null} {pace || resetsIn ? ( {pace ? PACE_LABEL[pace] : ""} {resetsIn ?? ""} ) : null} ); } /** One account: icon, name and plan on a single line, then its windows. */ function AccountLimits(props: { readonly driver: Driver; readonly label: string; readonly instanceLabel: string; readonly detail: string | undefined; readonly limits: ServerProvider["usageLimits"]; readonly now: number; readonly first: boolean; readonly footer?: ReactNode; }) { const { limits, now } = props; const color = useBarColor(props.driver); if (!limits) return null; const notice = limitsNotice(limits); return ( {props.label} {props.instanceLabel !== props.label ? ( · {props.instanceLabel} ) : null} {props.detail ? ( · {props.detail} ) : null} {notice ? ( {notice} ) : ( {limits.windows.map((window) => ( ))} )} {props.footer} ); } const OUTCOME_TEXT: Record = { reset: "Reset applied. Your windows have cleared.", nothingToReset: "Nothing to reset right now.", noCredit: "No reset credit left.", alreadyRedeemed: "That credit was already redeemed.", }; /** * Banked reset credits with a confirmed redeem action. Redeeming spends a * credit the provider granted the user, so it goes through the native * confirm alert rather than firing on a bare tap. */ function ResetCredits(props: { readonly environmentId: EnvironmentId; readonly instanceId: ProviderInstanceId; readonly credits: ServerProviderResetCredits; readonly now: number; }) { const { environmentId, instanceId, credits, now } = props; const consume = useAtomCommand(serverEnvironment.consumeResetCredit, { reportFailure: false, }); const [busy, setBusy] = useState(false); const [status, setStatus] = useState(null); if (credits.availableCount === 0 && status === null) return null; const expiresIn = credits.nextExpiresAt ? formatDuration(Date.parse(credits.nextExpiresAt) - now) : null; const summary = credits.availableCount === 0 ? "No reset credits banked" : `${credits.availableCount} ${credits.availableCount === 1 ? "reset credit" : "reset credits"} banked${ expiresIn ? ` · next expires in ${expiresIn}` : "" }`; const redeem = async () => { setBusy(true); setStatus(null); const result = await consume({ environmentId, input: { instanceId } }); setBusy(false); if (result._tag === "Success") { setStatus(OUTCOME_TEXT[result.value.outcome]); return; } setStatus( "error" in result.cause && result.cause.error instanceof Error ? result.cause.error.message : "Could not use the reset credit.", ); }; const confirm = () => { Alert.alert( "Use a reset credit?", "This redeems one credit on your account and clears the current rate-limit windows. It cannot be undone.", [ { text: "Cancel", style: "cancel" }, { text: "Use credit", onPress: () => void redeem() }, ], ); }; return ( {summary} {credits.availableCount > 0 ? ( {busy ? "Using credit…" : "Use a reset credit"} ) : null} {status ? {status} : null} ); } function ProviderLimits(props: { readonly provider: ServerProvider; readonly environmentId: EnvironmentId; readonly now: number; readonly first: boolean; }) { const { provider, environmentId, now } = props; const credits = provider.usageLimits?.resetCredits; return ( DRIVER_LABEL[driver])} detail={provider.auth.label} limits={provider.usageLimits} now={now} first={props.first} footer={ credits ? ( ) : undefined } /> ); } /** Emails stay off the phone screen; the plan and driver identify the row. */ function SourceAccountLimits(props: { readonly account: UsageLimitSourceAccount; readonly now: number; readonly first: boolean; }) { const { account } = props; return ( ); } /** * Re-probes every provider (and usage-limit source) on each connected * environment; the fresh snapshots then arrive over the config stream. * Countdowns and pace anchor to `now` rather than ticking, so a refresh also * re-anchors the clock: quota and elapsed time move together, or not at all. * Environments whose probe failed are named, since their rows keep showing * the previous quota with nothing else to say so. */ export function useRefreshLimits() { const presentations = useAtomValue(environmentPresentations.presentationsAtom); const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, { reportFailure: false, }); const [now, setNow] = useState(() => Date.now()); const [refreshing, setRefreshing] = useState(false); const [failedLabels, setFailedLabels] = useState([]); // Always toggles `refreshing`, even with nothing to probe: Android's // RefreshControl keeps its spinner up until it sees true then false. const refresh = async () => { const connected = [...presentations].filter( ([, presentation]) => presentation.connection.phase === "connected", ); setRefreshing(true); try { const results = await Promise.all( connected.map(([environmentId]) => refreshProviders({ environmentId, input: {} })), ); setFailedLabels( connected .filter((_, index) => results[index]?._tag === "Failure") .map(([, presentation]) => presentation.entry.target.label), ); } finally { setNow(Date.now()); setRefreshing(false); } }; return { now, refreshing, failedLabels, refresh }; } /** * Subscription quota windows from every connected environment's providers, * read from the config each environment already streams. */ export function UsageLimitsSection(props: { readonly now: number; readonly failedLabels: readonly string[]; }) { const { now } = props; const presentations = useAtomValue(environmentPresentations.presentationsAtom); const groups = collectLimitsGroups(presentations); const sources = collectLimitSources(presentations); if (groups.length === 0 && sources.length === 0) { return ( No provider on a connected environment reports subscription limits. ); } return ( <> {props.failedLabels.length > 0 ? ( {props.failedLabels.join(", ")} could not refresh limits. Showing the last known values. ) : null} {groups.map((group) => ( {group.providers.map((provider, index) => ( ))} ))} {sources.map((source) => ( {source.error ? ( {source.error} ) : source.accounts.length === 0 ? ( {source.hiddenAccountCount > 0 ? "All accounts are shown by connected providers." : "No accounts reported."} ) : ( source.accounts.map((account, index) => ( )) )} ))} ); }