Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7eb49ab
feat(usage): show reserve and deficit pace on Limits
cursoragent Sep 10, 2026
548dce3
fix(usage): keep Limits pace verdict readable
cursoragent Sep 10, 2026
8b8ba6a
fix(usage): mark pace with a colored pill, not a 1
cursoragent Sep 10, 2026
fb13de3
fix(usage): tuck pace percent beside leftover quota
cursoragent Sep 10, 2026
faf72c4
fix(usage): match CodexBar even-spend pace rounding
cursoragent Sep 10, 2026
420c3da
fix(usage): keep the pace tick tall and make it thinner
cursoragent Sep 10, 2026
5e4c6e6
fix(usage): drop repeating account labels on single-account Limits
cursoragent Sep 10, 2026
2df926f
fix(usage): address Limits pace review comments
cursoragent Sep 10, 2026
4f6eac3
refactor(usage): keep Limits pace to even-spend math and labels
cursoragent Sep 10, 2026
fbf8764
fix(usage): pin single-account pool pace and match Limits docs
cursoragent Sep 10, 2026
214799d
docs(usage): keep numbered Limits rows to narrow multi-account bars
cursoragent Sep 10, 2026
27d375c
fix(usage): keep the existing Limits account row
cursoragent Sep 10, 2026
d41d87d
fix(usage): overshoot the Limits pace tick and match its color
cursoragent Sep 10, 2026
7aa257e
fix(usage): overshoot the Limits pace tick by 10%
cursoragent Sep 10, 2026
a947549
fix(usage): keep Limits pace labels muted
cursoragent Sep 10, 2026
59a00fe
fix(usage): keep the mobile Limits segment number above the tick
cursoragent Sep 11, 2026
bfb4a87
feat(usage): average Limits pace across pooled accounts
cursoragent Sep 11, 2026
dec6095
docs(usage): call the Limits pace chip a pace figure
cursoragent Sep 11, 2026
556653d
merge: sync Limits pace branch with upstream main
cursoragent Sep 14, 2026
a9b22b5
merge: sync Limits pace branch with upstream main
cursoragent Sep 28, 2026
cf8b578
Merge branch 'main' into cursor/usage-limits-pacing-f418
baanish Sep 28, 2026
c35da71
fix(usage): keep the even-pace hairline and show mobile direction
cursoragent Sep 28, 2026
440b932
fix(usage): align pooled pace with the quota it describes
cursoragent Sep 28, 2026
3ac110d
fix(usage): mark even pace with a grey tick
cursoragent Sep 30, 2026
65c7be4
fix(usage): call the pace mark expected pace
cursoragent Sep 30, 2026
50fbc74
docs(usage): plain wording for expected pace
cursoragent Sep 30, 2026
cf63c8f
fix(usage): use the composer pace line on Limits
cursoragent Sep 30, 2026
98aec80
fix(usage): render the composer pace tooltip on Limits
cursoragent Sep 30, 2026
58584be
fix(usage): draw the shared pace mark as a grey tick
cursoragent Sep 30, 2026
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
66 changes: 55 additions & 11 deletions apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import {
collectLimitAccounts,
collectLimitNotices,
collectLimitPools,
evenPaceRemainingPercent,
formatAllowancePace,
formatDuration,
formatResetsIn,
paceDetail,
remainingPercent,
type LimitAccount,
type LimitPoolWindow,
Expand All @@ -26,7 +29,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 @@ -79,6 +81,7 @@ function PoolWindowCard({
}) {
const navigation = useNavigation();
const nextRefill = pool.resets.find((reset) => reset.restoresPercent > 0);
const paceReadout = pool.paceDetail ? formatAllowancePace(pool.paceDetail) : null;
const openAccount = (account: LimitAccount) =>
navigation.navigate("SettingsSheet", {
screen: "SettingsContent",
Expand All @@ -105,8 +108,17 @@ function PoolWindowCard({
<Text className="text-sm text-foreground-muted">left</Text>
</View>
</View>
{pool.pace ? (
<Text className="text-xs text-foreground-tertiary">{PACE_LABEL[pool.pace]}</Text>
{paceReadout ? (
<Text
className={
pool.paceDetail?.status === "deficit"
? "max-w-[11rem] text-right text-xs text-red-500"
: "max-w-[11rem] text-right text-xs text-emerald-500"
}
accessibilityLabel={paceReadout.explanation}
>
{paceReadout.percent}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium usage/UsageLimitsPooled.tsx:116

The header displays the same bare value such as 20% for both reserve and deficit, so single-account cards do not communicate whether the pace is ahead or behind. The status distinction is currently available only through accessibilityLabel and the separate tick; include paceReadout.marker in the visible readout.

Suggested change
{paceReadout.percent}
{`${paceReadout.percent} ${paceReadout.marker}`}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/usage/UsageLimitsPooled.tsx around line 116:

The header displays the same bare value such as `20%` for both reserve and deficit, so single-account cards do not communicate whether the pace is ahead or behind. The status distinction is currently available only through `accessibilityLabel` and the separate tick; include `paceReadout.marker` in the visible readout.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is invalid, it would render as 24% 24% in reserve.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

</Text>
) : null}
</View>
{nextRefill ? (
Expand All @@ -118,20 +130,39 @@ function PoolWindowCard({
<View className="flex-row gap-1">
{pool.columns.map(({ account, window }, index) => {
if (!window) return <View key={account.key} className="h-7 min-w-0 flex-1" />;
const rowPace = paceDetail(window, now);
return (
<Pressable
key={account.key}
accessibilityRole="button"
accessibilityLabel={`Segment ${index + 1}, ${accountName(account)}, ${remainingPercent(window)}% left`}
accessibilityLabel={`Segment ${index + 1}, ${accountName(account)}, ${remainingPercent(window)}% left${rowPace ? `, ${formatAllowancePace(rowPace).marker}` : ""}`}
accessibilityHint="Show account details"
onPress={() => openAccount(account)}
className="h-7 min-w-0 flex-1 overflow-hidden rounded-md bg-subtle"
className="h-7 min-w-0 flex-1 overflow-visible rounded-md bg-transparent"
>
<AccountSegment
remaining={remainingPercent(window)}
color={color}
pending={Boolean(window.resetsAt)}
/>
<View className="absolute inset-0 overflow-hidden rounded-md bg-subtle">
<AccountSegment
remaining={remainingPercent(window)}
color={color}
pending={Boolean(window.resetsAt)}
/>
</View>
{rowPace ? (
<View
pointerEvents="none"
className={
rowPace.status === "deficit"
? "absolute z-10 w-0.5 rounded-full bg-red-500"
: "absolute z-10 w-0.5 rounded-full bg-emerald-500"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
style={{
left: `${evenPaceRemainingPercent(rowPace)}%`,
marginLeft: -1,
top: "-5%",
height: "110%",
}}
/>
) : null}
<View pointerEvents="none" className="absolute inset-0 items-center justify-center">
<Text className="text-xs font-t3-medium tabular-nums text-foreground">
{index + 1}
Expand Down Expand Up @@ -177,7 +208,9 @@ function PoolWindowCard({
) : null}
{credits ? (
<>
{resetsIn ? <Text className="text-xs text-foreground-tertiary">·</Text> : null}
{resetsIn ? (
<Text className="text-xs text-foreground-tertiary">·</Text>
) : null}
<SymbolView name="ticket" size={13} tintColorClassName="accent-icon" />
<Text className="text-xs font-t3-medium tabular-nums text-foreground">
{credits}
Expand Down Expand Up @@ -279,6 +312,9 @@ export function UsageLimitAccountScreen({ route }: AccountScreenProps) {
?.windows.find((candidate) => candidate.id === windowId && candidate.kind === windowKind);
const window = pool?.members.find((member) => member.account.key === accountKey)?.window;
const reset = pool?.resets.find((candidate) => candidate.member.account.key === accountKey);
const accountPaceDetail = window ? paceDetail(window, now) : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium usage/UsageLimitsPooled.tsx:316

The displayed accountPace becomes stale after a refresh or streamed quota update, because paceDetail(window, now) uses the navigation-time now value rather than a current clock value. As elapsed time changes, the reserve/deficit is calculated against the old timestamp and remains incorrect until the screen is reopened. Use a live-updating current time for this calculation instead of the route snapshot.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/usage/UsageLimitsPooled.tsx around line 316:

The displayed `accountPace` becomes stale after a refresh or streamed quota update, because `paceDetail(window, now)` uses the navigation-time `now` value rather than a current clock value. As elapsed time changes, the reserve/deficit is calculated against the old timestamp and remains incorrect until the screen is reopened. Use a live-updating current time for this calculation instead of the route snapshot.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reset timer doesn't live update so this would either need that to live update too, or the pace vs reset timer would be desynced

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

const accountPace =
account && window && accountPaceDetail ? formatAllowancePace(accountPaceDetail) : null;
const [revealed, setRevealed] = useState(false);
return (
<View collapsable={false} className="flex-1 bg-sheet">
Expand Down Expand Up @@ -329,6 +365,14 @@ export function UsageLimitAccountScreen({ route }: AccountScreenProps) {
<Text className="text-3xl font-t3-bold tabular-nums text-foreground">
{remainingPercent(window)}% left
</Text>
{accountPace ? (
<Text
className="text-sm text-foreground-muted"
accessibilityLabel={accountPace.explanation}
>
{accountPace.marker}
</Text>
) : null}
{window.resetsAt ? (
<Text selectable className="text-sm text-foreground-muted">
Resets{" "}
Expand Down
52 changes: 30 additions & 22 deletions apps/mobile/src/features/usage/UsageLimitsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import type {
UsageProviderKind,
} from "@t3tools/contracts";
import {
elapsedShare,
evenPaceRemainingPercent,
formatAllowancePace,
formatDuration,
formatResetsIn,
limitsNotice,
paceOf,
paceDetail,
remainingPercent,
} from "@t3tools/shared/usageLimits";
import { type ReactNode, useState } from "react";
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 All @@ -40,9 +39,8 @@ function useBarColor(driver: Driver): string | null {

/**
* One window as a bar spanning its whole duration: the fill is quota left,
* the hairline is how much of the window is left, so even spending keeps the
* fill on the line. Pace sits under the left edge, the countdown under the
* right, so a row reads in one glance.
* a colored pill sits at even pace when reserve or deficit is large enough.
* The pace chip rides the header row; the countdown sits under the bar.
*/
function WindowRow(props: {
readonly window: ServerProviderUsageWindow;
Expand All @@ -51,19 +49,28 @@ function WindowRow(props: {
}) {
const { window, now } = props;
const remaining = remainingPercent(window);
const elapsed = elapsedShare(window, now);
const timeLeft = elapsed === null ? null : Math.round((1 - elapsed) * 100);
const pace = paceOf(window, now);
const detail = paceDetail(window, now);
const pace = detail ? formatAllowancePace(detail) : null;
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">
{remaining}% left
</Text>
<View className="flex-row items-center gap-1.5">
<Text className="text-sm font-t3-medium tabular-nums text-foreground">
{remaining}% left
</Text>
{pace ? (
<Text
className="text-xs tabular-nums text-foreground-tertiary"
accessibilityLabel={pace.explanation}
>
{pace.percent}
</Text>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Show the pace direction on mobile

Whenever pace is available, this renders only pace.percent, so equal-sized reserve and deficit gaps produce the same visible text—for example, both appear as 20%. The direction exists only in the accessibility label and tick color/position, leaving sighted mobile users without the arrow or wording shown by the web client; the pooled mobile card repeats the same omission. Include a visible directional icon or reserve/deficit label so the feature works consistently across clients.

AGENTS.md reference: AGENTS.md:L65-L70

Useful? React with 👍 / 👎.

) : null}
</View>
</View>
<View className="h-3 justify-center">
<View className="h-3.5 justify-center">
<View className="h-1.5 flex-row overflow-hidden rounded-full bg-subtle">
<View
className={
Expand All @@ -80,18 +87,19 @@ function WindowRow(props: {
/>
<View style={{ flex: 100 - remaining }} />
</View>
{timeLeft !== null ? (
{detail ? (
<View
className="absolute top-0 bottom-0 w-px bg-foreground"
style={{ left: `${timeLeft}%`, opacity: 0.6 }}
className={
detail.status === "deficit"
? "absolute top-0 h-3.5 w-0.5 rounded-full bg-red-500"
: "absolute top-0 h-3.5 w-0.5 rounded-full bg-emerald-500"
}
style={{ left: `${evenPaceRemainingPercent(detail)}%`, marginLeft: -1 }}
/>
) : 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>
{resetsIn ? (
<Text className="text-xs tabular-nums text-foreground-tertiary">{resetsIn}</Text>
) : null}
</View>
);
Expand Down
Loading
Loading