Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
collectLimitPools,
formatDuration,
formatResetsIn,
PACE_LABEL,
remainingPercent,
type LimitAccount,
type LimitPoolWindow,
Expand All @@ -26,7 +27,6 @@ import { ResetCredits } from "./UsageLimitsSection";
import { useProviderColors } from "./usageProviders";

const DRIVER_LABEL: Partial<Record<string, string>> = { codex: "Codex", claudeAgent: "Claude" };
const PACE_LABEL = { ahead: "Ahead of pace", on: "On pace", under: "Under pace" } as const;

function accountName(account: LimitAccount) {
if (account.displayName) return account.displayName;
Expand Down Expand Up @@ -78,7 +78,9 @@ function PoolWindowCard({
readonly environmentIds: readonly string[] | null;
}) {
const navigation = useNavigation();
const nextRefill = pool.resets.find((reset) => reset.restoresPercent > 0);
// Only worth a line with several accounts; with one it restates the number and the segment.
const nextRefill =
pool.members.length > 1 ? pool.resets.find((reset) => reset.restoresPercent > 0) : undefined;
const openAccount = (account: LimitAccount) =>
navigation.navigate("SettingsSheet", {
screen: "SettingsContent",
Expand All @@ -95,26 +97,27 @@ function PoolWindowCard({
});
return (
<View className="gap-3 rounded-[24px] border-continuous bg-card p-4">
<View className="flex-row items-start justify-between gap-3">
<View className="gap-1">
<Text className="text-sm font-t3-medium text-foreground">{pool.label}</Text>
<View className="flex-row items-baseline gap-1.5">
<Text className="text-3xl font-t3-bold tabular-nums text-foreground">
{pool.remainingPercent}%
</Text>
<Text className="text-sm text-foreground-muted">left</Text>
</View>
<View className="gap-1">
<Text className="text-sm font-t3-medium text-foreground">{pool.label}</Text>
<View className="flex-row items-baseline gap-1.5">
<Text className="text-3xl font-t3-bold tabular-nums text-foreground">
{pool.remainingPercent}%
</Text>
<Text className="text-sm text-foreground-muted">left</Text>
</View>
{pool.pace ? (
<Text className="text-xs text-foreground-tertiary">{PACE_LABEL[pool.pace]}</Text>
<Text className="text-xs text-foreground-muted">{PACE_LABEL[pool.pace]}</Text>
) : null}
{nextRefill ? (
<Text className="text-xs tabular-nums text-foreground-muted">
<Text className="font-t3-medium text-foreground">
{accountName(nextRefill.member.account)}
</Text>{" "}
resets {nextRefill.at <= now ? "now" : `in ${formatDuration(nextRefill.at - now)}`},
restoring {nextRefill.restoresPercent}%
</Text>
) : null}
</View>
{nextRefill ? (
<Text className="text-xs tabular-nums text-foreground-muted">
↻ +{nextRefill.restoresPercent}%{" "}
{nextRefill.at <= now ? "now" : `in ${formatDuration(nextRefill.at - now)}`}
</Text>
) : null}
<View className="flex-row gap-1">
{pool.members.map(({ account, window }, index) => (
<Pressable
Expand Down
3 changes: 1 addition & 2 deletions apps/mobile/src/features/usage/UsageLimitsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
formatDuration,
formatResetsIn,
limitsNotice,
PACE_LABEL,
paceOf,
remainingPercent,
} from "@t3tools/shared/usageLimits";
Expand All @@ -26,8 +27,6 @@ import { serverEnvironment } from "../../state/server";
import { useAtomCommand } from "../../state/use-atom-command";
import { useProviderColors } from "./usageProviders";

const PACE_LABEL = { ahead: "ahead of pace", on: "on pace", under: "under pace" } as const;

type Driver = ServerProvider["driver"];

/** The series colour the usage chart uses for this driver, so the two views read as one. */
Expand Down
22 changes: 13 additions & 9 deletions apps/web/src/components/usage/UsageLimits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
formatDuration,
formatResetsIn,
type LimitPace,
PACE_LABEL,
paceOf,
remainingPercent,
} from "@t3tools/shared/usageLimits";
Expand All @@ -38,10 +39,10 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { UsageLimitsPooled } from "./UsageLimitsPooled";
import { PROVIDER_PRESENTATION } from "./usageProviders";

const PACE: Record<LimitPace, { readonly label: string; readonly icon: typeof GaugeIcon }> = {
ahead: { label: "Ahead of pace: spending faster than the window elapses", icon: TrendingUpIcon },
on: { label: "On pace with the window", icon: GaugeIcon },
under: { label: "Under pace: headroom left for the rest of the window", icon: TrendingDownIcon },
const PACE_ICON: Record<LimitPace, typeof GaugeIcon> = {
ahead: TrendingUpIcon,
on: GaugeIcon,
under: TrendingDownIcon,
};

/** The series colour the cost chart uses for this driver, so the two views read as one. */
Expand All @@ -51,23 +52,26 @@ export function barColor(driver: ServerProvider["driver"]): string {
return kind ? PROVIDER_PRESENTATION[kind].color : "var(--foreground)";
}

/** Pace as a glyph with the words on hover. */
export function PaceIcon({ pace }: { readonly pace: LimitPace }) {
const Icon = PACE[pace].icon;
/**
* Pace as a glyph with the words on hover, for rows whose bar already draws
* the even-spending mark the glyph summarises.
*/
function PaceIcon({ pace }: { readonly pace: LimitPace }) {
const Icon = PACE_ICON[pace];
return (
<Tooltip>
<TooltipTrigger
render={
<span
role="img"
aria-label={PACE[pace].label}
aria-label={PACE_LABEL[pace]}
className="inline-flex text-muted-foreground"
/>
}
>
<Icon className="size-3.5" aria-hidden />
</TooltipTrigger>
<TooltipPopup side="top">{PACE[pace].label}</TooltipPopup>
<TooltipPopup side="top">{PACE_LABEL[pace]}</TooltipPopup>
</Tooltip>
);
}
Expand Down
35 changes: 20 additions & 15 deletions apps/web/src/components/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type LimitPool,
type LimitPoolMember,
type LimitPoolWindow,
PACE_LABEL,
remainingPercent,
} from "@t3tools/shared/usageLimits";
import { TicketIcon } from "lucide-react";
Expand All @@ -21,13 +22,7 @@ import { getDriverOption } from "../settings/providerDriverMeta";
import { RedactedSensitiveText } from "../settings/RedactedSensitiveText";
import { Button } from "../ui/button";
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
import {
PaceIcon,
ResetCreditDialog,
barColor,
resetCreditsSummary,
useResetCredit,
} from "./UsageLimits";
import { ResetCreditDialog, barColor, resetCreditsSummary, useResetCredit } from "./UsageLimits";

/** `someone@example.com` → `SE`: enough to tell accounts apart, too little to identify one. */
function accountInitials(email: string): string {
Expand Down Expand Up @@ -468,9 +463,10 @@ function PoolBar({
}

/**
* Big pooled number and the segment bar. The bar is sorted by reset, so who
* refills next is its left edge; the exact time and share restored live in
* each segment's popover rather than a list restating the bar.
* Big pooled number, pace in words, and the segment bar. With several
* accounts the card also names who refills next and how much of the pool
* that hands back, since the big number alone cannot say. With one account
* that would only restate the number and the segment, so it is left out.
*/
function PoolWindowCard({
pool,
Expand All @@ -482,7 +478,8 @@ function PoolWindowCard({
readonly now: number;
}) {
// The soonest reset that hands anything back; an untouched account resets to no effect.
const nextRefill = pool.resets.find((reset) => reset.restoresPercent > 0);
const nextRefill =
pool.members.length > 1 ? pool.resets.find((reset) => reset.restoresPercent > 0) : undefined;
return (
<div className="grid items-center gap-x-6 gap-y-3 rounded-lg border border-border/60 p-4 md:grid-cols-[11rem_minmax(0,1fr)]">
<div className="flex flex-col gap-1">
Expand All @@ -492,12 +489,20 @@ function PoolWindowCard({
{pool.remainingPercent}%
</span>
<span className="text-sm text-muted-foreground">left</span>
{pool.pace ? <PaceIcon pace={pool.pace} /> : null}
</span>
{pool.pace ? (
<span className="text-xs text-muted-foreground">{PACE_LABEL[pool.pace]}</span>
) : null}
{nextRefill ? (
<span className="text-xs text-muted-foreground tabular-nums">
<span className="font-medium text-foreground">↻ +{nextRefill.restoresPercent}%</span>{" "}
{nextRefill.at <= now ? "now" : `in ${formatDuration(nextRefill.at - now)}`}
<span className="flex flex-wrap items-center gap-x-1 text-xs text-muted-foreground tabular-nums">
<AccountName
account={nextRefill.member.account}
className="min-w-0 truncate font-medium text-foreground"
/>
<span>
resets {nextRefill.at <= now ? "now" : `in ${formatDuration(nextRefill.at - now)}`},
restoring {nextRefill.restoresPercent}%
</span>
</span>
) : null}
</div>
Expand Down
5 changes: 3 additions & 2 deletions docs/user/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ the dialog.
**Usage → Limits** pools every subscription account it can see per provider, so with several Codex
or Claude accounts across your environments and hubs you read one number per window rather than a
list. Each window card shows how much of the pool is left and a bar with one segment per account,
ordered by which resets soonest; when the provider reports reset times, the card also says when
the next reset lands and how much it hands back. The hatched
ordered by which resets soonest. When the provider reports reset times, the card says whether you
are spending faster or slower than the window elapses, and with more than one account, which
account resets next and how much of the pool that hands back. The hatched
part of a segment is what that reset restores. Tap a segment or account row for the account's plan,
where it is signed in, and its reset time. On web, you can hover too. Codex accounts with banked
reset credits show a ticket count and the **Use reset** action in the account details. On narrow screens, numbered rows below
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/usageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,13 @@ export function elapsedShare(window: ServerProviderUsageWindow, now: number): nu

export type LimitPace = "ahead" | "on" | "under";

/** Pace in words, shared so every client describes the same state the same way. */
export const PACE_LABEL: Record<LimitPace, string> = {
ahead: "Spending faster than the clock",
on: "Spending in step with the clock",
under: "Spending slower than the clock",
};

/**
* Usage against the clock. Spending evenly leaves the same share of quota as
* there is time left in the window; within five points of that counts as on
Expand Down
Loading