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<Record<string, string>> = { 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 ( <View className="gap-1"> <View className="flex-row items-baseline justify-between gap-3"> <Text className="text-sm text-foreground">{window.label}</Text> <Text className="text-sm font-t3-medium tabular-nums text-foreground">{used}%</Text> </View> <View className="h-3 justify-center"> <View className="h-1.5 flex-row overflow-hidden rounded-full bg-subtle"> <View className={ 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, ]} /> <View style={{ flex: 100 - used }} /> </View> {elapsed !== null ? ( <View className="absolute top-0 bottom-0 w-px bg-foreground" style={{ left: `${elapsed * 100}%`, opacity: 0.6 }} /> ) : null} </View> {pace || resetsIn ? ( <View className="flex-row justify-between gap-3"> <Text className="text-xs text-foreground-tertiary">{pace ? PACE_LABEL[pace] : ""}</Text> <Text className="text-xs tabular-nums text-foreground-tertiary">{resetsIn ?? ""}</Text> </View> ) : null} </View> );} /** 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 ( <View className={props.first ? "gap-3 p-4" : "gap-3 border-t border-border-subtle p-4"}> <View className="flex-row items-center gap-2"> <ProviderIcon provider={props.driver} size={16} /> <View className="min-w-0 flex-1 flex-row items-baseline gap-2"> <Text className="text-base font-t3-medium text-foreground">{props.label}</Text> {props.instanceLabel !== props.label ? ( <Text className="shrink text-xs text-foreground-tertiary" numberOfLines={1}> · {props.instanceLabel} </Text> ) : null} {props.detail ? ( <Text className="shrink text-sm text-foreground-muted" numberOfLines={1}> · {props.detail} </Text> ) : null} </View> </View> {notice ? ( <Text className="text-sm text-foreground-muted">{notice}</Text> ) : ( <View className="gap-3"> {limits.windows.map((window) => ( <WindowRow key={window.id} window={window} color={color} now={now} /> ))} </View> )} {props.footer} </View> );} const OUTCOME_TEXT: Record<ProviderConsumeResetCreditOutcome, string> = { 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<string | null>(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 ( <View className="flex-row flex-wrap items-center gap-x-3 gap-y-1"> <Text className="text-xs tabular-nums text-foreground-tertiary">{summary}</Text> {credits.availableCount > 0 ? ( <Pressable accessibilityRole="button" accessibilityState={{ disabled: busy }} disabled={busy} onPress={confirm} className="rounded-full bg-subtle-strong px-3 py-1.5" > <Text className="text-sm font-t3-medium text-foreground"> {busy ? "Using credit…" : "Use a reset credit"} </Text> </Pressable> ) : null} {status ? <Text className="text-sm text-foreground">{status}</Text> : null} </View> );} 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 ( <AccountLimits driver={provider.driver} label={DRIVER_LABEL[provider.driver] ?? String(provider.driver)} instanceLabel={providerLimitsLabel(provider, (driver) => DRIVER_LABEL[driver])} detail={provider.auth.label} limits={provider.usageLimits} now={now} first={props.first} footer={ credits ? ( <ResetCredits environmentId={environmentId} instanceId={provider.instanceId} credits={credits} now={now} /> ) : 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 ( <AccountLimits driver={account.driver} label={DRIVER_LABEL[account.driver] ?? String(account.driver)} instanceLabel="CLI Proxy" detail={account.plan} limits={account.usageLimits} now={props.now} first={props.first} /> );} /** * 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<readonly string[]>([]); // 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 ( <Text className="py-16 text-center text-base text-foreground-muted"> No provider on a connected environment reports subscription limits. </Text> ); } return ( <> {props.failedLabels.length > 0 ? ( <View className="rounded-[16px] border-continuous bg-card px-4 py-3"> <Text className="text-sm text-foreground-muted"> {props.failedLabels.join(", ")} could not refresh limits. Showing the last known values. </Text> </View> ) : null} {groups.map((group) => ( <SettingsSection key={group.environmentId} title={group.environmentLabel ?? "Providers"} card > {group.providers.map((provider, index) => ( <ProviderLimits key={provider.instanceId} provider={provider} environmentId={group.environmentId} now={now} first={index === 0} /> ))} </SettingsSection> ))} {sources.map((source) => ( <SettingsSection key={source.key} card> {source.error ? ( <Text className="p-4 text-sm text-foreground-muted">{source.error}</Text> ) : source.accounts.length === 0 ? ( <Text className="p-4 text-sm text-foreground-muted"> {source.hiddenAccountCount > 0 ? "All accounts are shown by connected providers." : "No accounts reported."} </Text> ) : ( source.accounts.map((account, index) => ( <SourceAccountLimits key={account.id} account={account} now={now} first={index === 0} /> )) )} </SettingsSection> ))} </> );}
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<Record<string, string>> = { 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 (
<View className="gap-1">
<View className="flex-row items-baseline justify-between gap-3">
<Text className="text-sm text-foreground">{window.label}</Text>
<Text className="text-sm font-t3-medium tabular-nums text-foreground">{used}%</Text>
</View>
<View className="h-3 justify-center">
<View className="h-1.5 flex-row overflow-hidden rounded-full bg-subtle">
<View
className={
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,
]}
/>
<View style={{ flex: 100 - used }} />
</View>
{elapsed !== null ? (
<View
className="absolute top-0 bottom-0 w-px bg-foreground"
style={{ left: `${elapsed * 100}%`, opacity: 0.6 }}
/>
) : null}
</View>
{pace || resetsIn ? (
<View className="flex-row justify-between gap-3">
<Text className="text-xs text-foreground-tertiary">{pace ? PACE_LABEL[pace] : ""}</Text>
<Text className="text-xs tabular-nums text-foreground-tertiary">{resetsIn ?? ""}</Text>
</View>
) : null}
</View>
);
}
/** 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 (
<View className={props.first ? "gap-3 p-4" : "gap-3 border-t border-border-subtle p-4"}>
<View className="flex-row items-center gap-2">
<ProviderIcon provider={props.driver} size={16} />
<View className="min-w-0 flex-1 flex-row items-baseline gap-2">
<Text className="text-base font-t3-medium text-foreground">{props.label}</Text>
{props.instanceLabel !== props.label ? (
<Text className="shrink text-xs text-foreground-tertiary" numberOfLines={1}>
· {props.instanceLabel}
</Text>
) : null}
{props.detail ? (
<Text className="shrink text-sm text-foreground-muted" numberOfLines={1}>
· {props.detail}
</Text>
) : null}
</View>
</View>
{notice ? (
<Text className="text-sm text-foreground-muted">{notice}</Text>
) : (
<View className="gap-3">
{limits.windows.map((window) => (
<WindowRow key={window.id} window={window} color={color} now={now} />
))}
</View>
)}
{props.footer}
</View>
);
}
const OUTCOME_TEXT: Record<ProviderConsumeResetCreditOutcome, string> = {
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<string | null>(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 (
<View className="flex-row flex-wrap items-center gap-x-3 gap-y-1">
<Text className="text-xs tabular-nums text-foreground-tertiary">{summary}</Text>
{credits.availableCount > 0 ? (
<Pressable
accessibilityRole="button"
accessibilityState={{ disabled: busy }}
disabled={busy}
onPress={confirm}
className="rounded-full bg-subtle-strong px-3 py-1.5"
>
<Text className="text-sm font-t3-medium text-foreground">
{busy ? "Using credit…" : "Use a reset credit"}
</Text>
</Pressable>
) : null}
{status ? <Text className="text-sm text-foreground">{status}</Text> : null}
</View>
);
}
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 (
<AccountLimits
driver={provider.driver}
label={DRIVER_LABEL[provider.driver] ?? String(provider.driver)}
instanceLabel={providerLimitsLabel(provider, (driver) => DRIVER_LABEL[driver])}
detail={provider.auth.label}
limits={provider.usageLimits}
now={now}
first={props.first}
footer={
credits ? (
<ResetCredits
environmentId={environmentId}
instanceId={provider.instanceId}
credits={credits}
now={now}
/>
) : 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 (
<AccountLimits
driver={account.driver}
label={DRIVER_LABEL[account.driver] ?? String(account.driver)}
instanceLabel="CLI Proxy"
detail={account.plan}
limits={account.usageLimits}
now={props.now}
first={props.first}
/>
);
}
/**
* 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<readonly string[]>([]);
// 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 (
<Text className="py-16 text-center text-base text-foreground-muted">
No provider on a connected environment reports subscription limits.
</Text>
);
}
return (
<>
{props.failedLabels.length > 0 ? (
<View className="rounded-[16px] border-continuous bg-card px-4 py-3">
<Text className="text-sm text-foreground-muted">
{props.failedLabels.join(", ")} could not refresh limits. Showing the last known values.
</Text>
</View>
) : null}
{groups.map((group) => (
<SettingsSection
key={group.environmentId}
title={group.environmentLabel ?? "Providers"}
card
>
{group.providers.map((provider, index) => (
<ProviderLimits
key={provider.instanceId}
provider={provider}
environmentId={group.environmentId}
now={now}
first={index === 0}
/>
))}
</SettingsSection>
))}
{sources.map((source) => (
<SettingsSection key={source.key} card>
{source.error ? (
<Text className="p-4 text-sm text-foreground-muted">{source.error}</Text>
) : source.accounts.length === 0 ? (
<Text className="p-4 text-sm text-foreground-muted">
{source.hiddenAccountCount > 0
? "All accounts are shown by connected providers."
: "No accounts reported."}
</Text>
) : (
source.accounts.map((account, index) => (
<SourceAccountLimits
key={account.id}
account={account}
now={now}
first={index === 0}
/>
))
)}
</SettingsSection>
))}
</>
);
}