Skip to content
Open
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
23 changes: 19 additions & 4 deletions apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
remainingPercent,
type LimitAccount,
type LimitPoolWindow,
resetCreditsForWindow,
usageShareSummary,
} from "@t3tools/shared/usageLimits";
import { Fragment, type ReactNode, useId, useState } from "react";
import { Linking, Pressable, View } from "react-native";
Expand Down Expand Up @@ -235,7 +237,10 @@ function PoolWindowCard({
);
const { account, window } = member;
const timeLeft = paceRemainingPercent(window, now);
const credits = account.limits.resetCredits?.availableCount ?? 0;
const { count: credits, scope } = resetCreditsForWindow(
account.limits.resetCredits,
window.id,
);
const resetsIn = formatResetsIn(window, now)?.replace("resets in ", "");
return (
<Pressable
Expand Down Expand Up @@ -295,6 +300,9 @@ function PoolWindowCard({
<Text className="text-xs font-t3-medium tabular-nums text-foreground">
{credits}
</Text>
{scope ? (
<Text className="text-xs text-foreground-muted">{scope}</Text>
) : null}
</>
) : null}
</View>
Expand Down Expand Up @@ -323,13 +331,16 @@ function PoolWindowCard({
{pool.columns.map(({ account, window }, index) => {
if (!window) return null;
const segmentColor = normalizeProviderAccentColor(account.accentColor) ?? color;
const credits = account.limits.resetCredits?.availableCount ?? 0;
const { count: credits, scope } = resetCreditsForWindow(
account.limits.resetCredits,
window.id,
);
const resetsIn = formatResetsIn(window, now);
return (
<Pressable
key={account.key}
accessibilityRole="button"
accessibilityLabel={`Segment ${index + 1}, ${accountName(account)}, ${remainingPercent(window)}% left${resetsIn ? `, ${resetsIn}` : ""}${credits ? `, ${credits} reset credits banked` : ""}`}
accessibilityLabel={`Segment ${index + 1}, ${accountName(account)}, ${remainingPercent(window)}% left${resetsIn ? `, ${resetsIn}` : ""}${credits ? `, ${credits} ${scope ? `${scope} ` : ""}reset ${credits === 1 ? "credit" : "credits"} banked` : ""}`}
accessibilityHint="Show account details"
onPress={() => openAccount(account)}
className="min-h-[44px] flex-row items-center gap-2 active:opacity-60"
Expand Down Expand Up @@ -373,6 +384,7 @@ function PoolWindowCard({
<Text className="text-xs font-t3-medium tabular-nums text-foreground">
{credits}
</Text>
{scope ? <Text className="text-xs text-foreground-muted">{scope}</Text> : null}
</>
) : null}
</View>
Expand Down Expand Up @@ -447,6 +459,7 @@ export function UsageLimitsSection({
{windows.map((window) => {
const details =
pool.driver === "cursor" ? cursorUsageWindowDetails(window.id, pool) : undefined;
const share = window.members.find((member) => member.window.share)?.window.share;
return (
<PoolWindowCard
key={`${window.kind}:${window.id}`}
Expand All @@ -457,7 +470,9 @@ export function UsageLimitsSection({
selectedEnvironmentIds === null ? null : [...selectedEnvironmentIds]
}
label={details?.label}
description={details?.description}
description={
details?.description ?? usageShareSummary(share, windows) ?? undefined
}
wide={wide}
/>
);
Expand Down
39 changes: 26 additions & 13 deletions apps/mobile/src/features/usage/UsageLimitsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
paceOf,
paceRemainingPercent,
remainingPercent,
sharedUsage,
usageShareSummary,
} from "@t3tools/shared/usageLimits";
import { type ReactNode, useContext, useRef, useState } from "react";
import { Atom } from "effect/reactivity";
Expand Down Expand Up @@ -50,11 +52,16 @@ function useBarColor(driver: Driver): string | null {

function WindowRow(props: {
readonly window: ServerProviderUsageWindow;
readonly windows: ReadonlyArray<ServerProviderUsageWindow>;
readonly color: string | null;
readonly now: number;
}) {
const { window, now } = props;
const remaining = remainingPercent(window);
const shared = sharedUsage(window, props.windows)?.percent ?? 0;
const shareSummary = usageShareSummary(window.share, props.windows);
const fill = remaining <= 10 ? "bg-red-500" : remaining <= 30 ? "bg-amber-500" : "bg-foreground";
const fillColor = remaining > 30 && props.color ? { backgroundColor: props.color } : null;
const timeLeft = paceRemainingPercent(window, now);
const pace = paceOf(window, now);
const resetsIn = formatResetsIn(window, now);
Expand All @@ -69,19 +76,16 @@ function WindowRow(props: {
style={barWidth > 0 ? { width: barWidth } : undefined}
>
<View
className={
remaining <= 10
? "h-full rounded-full bg-red-500"
: remaining <= 30
? "h-full rounded-full bg-amber-500"
: "h-full rounded-full bg-foreground"
}
style={[
{ flex: remaining },
remaining > 30 && props.color ? { backgroundColor: props.color } : null,
]}
className={`h-full ${shared > 0 ? "rounded-l-full" : "rounded-full"} ${window.share ? "opacity-40" : ""} ${fill}`}
style={[{ flex: remaining }, fillColor]}
/>
<View style={{ flex: 100 - remaining }} />
{shared > 0 ? (
<View
className={`h-full opacity-40 ${remaining > 0 ? "rounded-r-full" : "rounded-full"} ${fill}`}
style={[{ flex: shared }, fillColor]}
/>
) : null}
<View style={{ flex: 100 - remaining - shared }} />
</View>
);
return (
Expand All @@ -92,6 +96,9 @@ function WindowRow(props: {
{remaining}% left
</Text>
</View>
{shareSummary ? (
<Text className="text-xs text-foreground-tertiary">{shareSummary}</Text>
) : null}
<View
className="h-3 justify-center"
onLayout={(event) => setBarWidth(event.nativeEvent.layout.width)}
Expand Down Expand Up @@ -203,7 +210,13 @@ export function AccountLimits(props: {
) : (
<View className="gap-3">
{limits.windows.map((window) => (
<WindowRow key={window.id} window={window} color={color} now={now} />
<WindowRow
key={window.id}
window={window}
windows={limits.windows}
color={color}
now={now}
/>
))}
</View>
)}
Expand Down
18 changes: 12 additions & 6 deletions apps/server/src/provider/ClaudeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
type ServerProvider,
type ServerProviderModel,
type ServerProviderSlashCommand,
type ServerProviderResetCredits,
} from "@t3tools/contracts";
import * as Cause from "effect/Cause";
import * as DateTime from "effect/DateTime";
Expand Down Expand Up @@ -64,6 +63,7 @@ import {
claudeUsageResponseToLimits,
recordClaudeUsageResponse,
} from "./claudeUsageLimits.ts";
import { type ClaudeAccountUsage, withClaudeAccountUsage } from "./claudeResetCredits.ts";
import {
BUNDLED_CLAUDE_MODEL_CATALOG,
CLAUDE_EFFORT_LEVELS,
Expand Down Expand Up @@ -657,7 +657,10 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
modelCatalog: ClaudeModelCatalog = BUNDLED_CLAUDE_MODEL_CATALOG,
/** Shared with the adapter so turn events reuse the scoped-bucket names this probe saw. */
scopedLimitNames?: Ref.Ref<ClaudeScopedLimitNames>,
resolveResetCredits?: (version: string) => Effect.Effect<ServerProviderResetCredits | undefined>,
resolveAccountUsage?: (
version: string,
overageIncluded: string | undefined,
) => Effect.Effect<ClaudeAccountUsage | undefined>,
): Effect.fn.Return<
ServerProviderDraft,
never,
Expand Down Expand Up @@ -862,12 +865,15 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
response: capabilities.usage,
checkedAt: usageCheckedAt,
}).limits;
const resetCredits =
resolveResetCredits &&
const accountUsage =
resolveAccountUsage &&
capabilities.subscriptionType &&
!usageLimits.unavailable &&
parsedVersion
? yield* resolveResetCredits(parsedVersion)
? yield* resolveAccountUsage(
parsedVersion,
scopedLimitNames ? (yield* Ref.get(scopedLimitNames)).overageIncluded : undefined,
)
: undefined;
return buildServerProvider({
presentation: CLAUDE_PRESENTATION,
Expand All @@ -888,7 +894,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
...(authMetadata ? authMetadata : {}),
},
...(versionUpgradeMessage ? { message: versionUpgradeMessage } : {}),
usageLimits: resetCredits ? { ...usageLimits, resetCredits } : usageLimits,
usageLimits: withClaudeAccountUsage(usageLimits, accountUsage),
},
});
});
Expand Down
28 changes: 21 additions & 7 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
? configDir
: undefined,
);
const resetCreditsCacheFile = ClaudeResetCredits.claudeResetCreditsCacheFile(
path,
stateDir,
configDir,
);
const stampIdentity = withInstanceIdentity({
instanceId,
driverKind: DRIVER_KIND,
Expand Down Expand Up @@ -274,8 +279,13 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
stateDir,
catalog,
scopedLimitNames,
(version) =>
ClaudeResetCredits.readClaudeResetCredits(configDir, version).pipe(
(version, overageIncluded) =>
ClaudeResetCredits.readClaudeAccountUsage(
configDir,
version,
overageIncluded,
resetCreditsCacheFile,
).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Expand All @@ -297,6 +307,10 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
Effect.provideService(Path.Path, path),
);

const invalidateUsageCache = Cache.invalidateAll(capabilitiesProbeCache).pipe(
Effect.andThen(ClaudeResetCredits.forgetClaudeResetCredits(resetCreditsCacheFile)),
Effect.provideService(FileSystem.FileSystem, fileSystem),
);
const snapshotSettings = yield* makeProviderSnapshotSettingsSource(effectiveConfig);
const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<ClaudeSettings>>({
resolveMaintenance,
Expand All @@ -309,7 +323,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
Effect.map(stampIdentity),
),
checkProvider,
invalidateUsageCache: Cache.invalidateAll(capabilitiesProbeCache),
invalidateUsageCache,
enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
resolveMaintenance().pipe(
Effect.flatMap((maintenanceCapabilities) =>
Expand Down Expand Up @@ -378,13 +392,13 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
cause,
}),
),
// A failed claim may still have spent the reset.
Effect.tapError(() => invalidateUsageCache),
Effect.tap((outcome) =>
ResetCreditCoordinator.refreshAfterRedemption({
outcome,
getSnapshot: snapshot.getSnapshot,
refresh: Cache.invalidateAll(capabilitiesProbeCache).pipe(
Effect.andThen(snapshot.refresh),
),
refresh: invalidateUsageCache.pipe(Effect.andThen(snapshot.refresh)),
}).pipe(
Effect.filterOrFail(
(confirmed) => confirmed,
Expand All @@ -411,7 +425,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
accentColor,
enabled,
snapshot,
invalidateCaches: Cache.invalidateAll(capabilitiesProbeCache),
invalidateCaches: invalidateUsageCache,
snapshotForCwd: (cwd: string) =>
snapshot.getSnapshot.pipe(
Effect.flatMap((machineSnapshot) =>
Expand Down
Loading
Loading