claude-code/components/Settings/Usage.tsx

377 lines
39 KiB
TypeScript
Raw Normal View History

import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { useEffect, useState } from 'react';
import { extraUsage as extraUsageCommand } from 'src/commands/extra-usage/index.js';
import { formatCost } from 'src/cost-tracker.js';
import { getSubscriptionType } from 'src/utils/auth.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { Box, Text } from '../../ink.js';
import { useKeybinding } from '../../keybindings/useKeybinding.js';
import { type ExtraUsage, fetchUtilization, type RateLimit, type Utilization } from '../../services/api/usage.js';
import { formatResetText } from '../../utils/format.js';
import { logError } from '../../utils/log.js';
import { jsonStringify } from '../../utils/slowOperations.js';
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js';
import { Byline } from '../design-system/Byline.js';
import { ProgressBar } from '../design-system/ProgressBar.js';
import { isEligibleForOverageCreditGrant, OverageCreditUpsell } from '../LogoV2/OverageCreditUpsell.js';
type LimitBarProps = {
title: string;
limit: RateLimit;
maxWidth: number;
showTimeInReset?: boolean;
extraSubtext?: string;
};
function LimitBar(t0) {
const $ = _c(34);
const {
title,
limit,
maxWidth,
showTimeInReset: t1,
extraSubtext
} = t0;
const showTimeInReset = t1 === undefined ? true : t1;
const {
utilization,
resets_at
} = limit;
if (utilization === null) {
return null;
}
const usedText = `${Math.floor(utilization)}% used`;
let subtext;
if (resets_at) {
let t2;
if ($[0] !== resets_at || $[1] !== showTimeInReset) {
t2 = formatResetText(resets_at, true, showTimeInReset);
$[0] = resets_at;
$[1] = showTimeInReset;
$[2] = t2;
} else {
t2 = $[2];
}
subtext = `Resets ${t2}`;
}
if (extraSubtext) {
if (subtext) {
subtext = `${extraSubtext} · ${subtext}`;
} else {
subtext = extraSubtext;
}
}
if (maxWidth >= 62) {
let t2;
if ($[3] !== title) {
t2 = <Text bold={true}>{title}</Text>;
$[3] = title;
$[4] = t2;
} else {
t2 = $[4];
}
const t3 = utilization / 100;
let t4;
if ($[5] !== t3) {
t4 = <ProgressBar ratio={t3} width={50} fillColor="rate_limit_fill" emptyColor="rate_limit_empty" />;
$[5] = t3;
$[6] = t4;
} else {
t4 = $[6];
}
let t5;
if ($[7] !== usedText) {
t5 = <Text>{usedText}</Text>;
$[7] = usedText;
$[8] = t5;
} else {
t5 = $[8];
}
let t6;
if ($[9] !== t4 || $[10] !== t5) {
t6 = <Box flexDirection="row" gap={1}>{t4}{t5}</Box>;
$[9] = t4;
$[10] = t5;
$[11] = t6;
} else {
t6 = $[11];
}
let t7;
if ($[12] !== subtext) {
t7 = subtext && <Text dimColor={true}>{subtext}</Text>;
$[12] = subtext;
$[13] = t7;
} else {
t7 = $[13];
}
let t8;
if ($[14] !== t2 || $[15] !== t6 || $[16] !== t7) {
t8 = <Box flexDirection="column">{t2}{t6}{t7}</Box>;
$[14] = t2;
$[15] = t6;
$[16] = t7;
$[17] = t8;
} else {
t8 = $[17];
}
return t8;
} else {
let t2;
if ($[18] !== title) {
t2 = <Text bold={true}>{title}</Text>;
$[18] = title;
$[19] = t2;
} else {
t2 = $[19];
}
let t3;
if ($[20] !== subtext) {
t3 = subtext && <><Text> </Text><Text dimColor={true}>· {subtext}</Text></>;
$[20] = subtext;
$[21] = t3;
} else {
t3 = $[21];
}
let t4;
if ($[22] !== t2 || $[23] !== t3) {
t4 = <Text>{t2}{t3}</Text>;
$[22] = t2;
$[23] = t3;
$[24] = t4;
} else {
t4 = $[24];
}
const t5 = utilization / 100;
let t6;
if ($[25] !== maxWidth || $[26] !== t5) {
t6 = <ProgressBar ratio={t5} width={maxWidth} fillColor="rate_limit_fill" emptyColor="rate_limit_empty" />;
$[25] = maxWidth;
$[26] = t5;
$[27] = t6;
} else {
t6 = $[27];
}
let t7;
if ($[28] !== usedText) {
t7 = <Text>{usedText}</Text>;
$[28] = usedText;
$[29] = t7;
} else {
t7 = $[29];
}
let t8;
if ($[30] !== t4 || $[31] !== t6 || $[32] !== t7) {
t8 = <Box flexDirection="column">{t4}{t6}{t7}</Box>;
$[30] = t4;
$[31] = t6;
$[32] = t7;
$[33] = t8;
} else {
t8 = $[33];
}
return t8;
}
}
export function Usage(): React.ReactNode {
const [utilization, setUtilization] = useState<Utilization | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const {
columns
} = useTerminalSize();
const availableWidth = columns - 2; // 2 for screen padding
const maxWidth = Math.min(availableWidth, 80);
const loadUtilization = React.useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const data = await fetchUtilization();
setUtilization(data);
} catch (err) {
logError(err as Error);
const axiosError = err as {
response?: {
data?: unknown;
};
};
const responseBody = axiosError.response?.data ? jsonStringify(axiosError.response.data) : undefined;
setError(responseBody ? `Failed to load usage data: ${responseBody}` : 'Failed to load usage data');
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
void loadUtilization();
}, [loadUtilization]);
useKeybinding('settings:retry', () => {
void loadUtilization();
}, {
context: 'Settings',
isActive: !!error && !isLoading
});
if (error) {
return <Box flexDirection="column" gap={1}>
<Text color="error">Error: {error}</Text>
<Text dimColor>
<Byline>
<ConfigurableShortcutHint action="settings:retry" context="Settings" fallback="r" description="retry" />
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />
</Byline>
</Text>
</Box>;
}
if (!utilization) {
return <Box flexDirection="column" gap={1}>
<Text dimColor>Loading usage data</Text>
<Text dimColor>
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />
</Text>
</Box>;
}
// Only Max and Team plans have a Sonnet limit that differs from the weekly
// limit (see rateLimitMessages.ts). For other plans the bar is redundant.
// Show for null (unknown plan) to stay consistent with rateLimitMessages.ts,
// which labels it "Sonnet limit" in that case.
const subscriptionType = getSubscriptionType();
const showSonnetBar = subscriptionType === 'max' || subscriptionType === 'team' || subscriptionType === null;
const limits = [{
title: 'Current session',
limit: utilization.five_hour
}, {
title: 'Current week (all models)',
limit: utilization.seven_day
}, ...(showSonnetBar ? [{
title: 'Current week (Sonnet only)',
limit: utilization.seven_day_sonnet
}] : [])];
return <Box flexDirection="column" gap={1} width="100%">
{limits.some(({
limit
}) => limit) || <Text dimColor>/usage is only available for subscription plans.</Text>}
{limits.map(({
title,
limit: limit_0
}) => limit_0 && <LimitBar key={title} title={title} limit={limit_0} maxWidth={maxWidth} />)}
{utilization.extra_usage && <ExtraUsageSection extraUsage={utilization.extra_usage} maxWidth={maxWidth} />}
{isEligibleForOverageCreditGrant() && <OverageCreditUpsell maxWidth={maxWidth} />}
<Text dimColor>
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />
</Text>
</Box>;
}
type ExtraUsageSectionProps = {
extraUsage: ExtraUsage;
maxWidth: number;
};
const EXTRA_USAGE_SECTION_TITLE = 'Extra usage';
function ExtraUsageSection(t0) {
const $ = _c(20);
const {
extraUsage,
maxWidth
} = t0;
const subscriptionType = getSubscriptionType();
const isProOrMax = subscriptionType === "pro" || subscriptionType === "max";
if (!isProOrMax) {
return false;
}
if (!extraUsage.is_enabled) {
if (extraUsageCommand.isEnabled()) {
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box flexDirection="column"><Text bold={true}>{EXTRA_USAGE_SECTION_TITLE}</Text><Text dimColor={true}>Extra usage not enabled · /extra-usage to enable</Text></Box>;
$[0] = t1;
} else {
t1 = $[0];
}
return t1;
}
return null;
}
if (extraUsage.monthly_limit === null) {
let t1;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box flexDirection="column"><Text bold={true}>{EXTRA_USAGE_SECTION_TITLE}</Text><Text dimColor={true}>Unlimited</Text></Box>;
$[1] = t1;
} else {
t1 = $[1];
}
return t1;
}
if (typeof extraUsage.used_credits !== "number" || typeof extraUsage.utilization !== "number") {
return null;
}
const t1 = extraUsage.used_credits / 100;
let t2;
if ($[2] !== t1) {
t2 = formatCost(t1, 2);
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
const formattedUsedCredits = t2;
const t3 = extraUsage.monthly_limit / 100;
let t4;
if ($[4] !== t3) {
t4 = formatCost(t3, 2);
$[4] = t3;
$[5] = t4;
} else {
t4 = $[5];
}
const formattedMonthlyLimit = t4;
let T0;
let t5;
let t6;
let t7;
if ($[6] !== extraUsage.utilization) {
const now = new Date();
const oneMonthReset = new Date(now.getFullYear(), now.getMonth() + 1, 1);
T0 = LimitBar;
t7 = EXTRA_USAGE_SECTION_TITLE;
t5 = extraUsage.utilization;
t6 = oneMonthReset.toISOString();
$[6] = extraUsage.utilization;
$[7] = T0;
$[8] = t5;
$[9] = t6;
$[10] = t7;
} else {
T0 = $[7];
t5 = $[8];
t6 = $[9];
t7 = $[10];
}
let t8;
if ($[11] !== t5 || $[12] !== t6) {
t8 = {
utilization: t5,
resets_at: t6
};
$[11] = t5;
$[12] = t6;
$[13] = t8;
} else {
t8 = $[13];
}
const t9 = `${formattedUsedCredits} / ${formattedMonthlyLimit} spent`;
let t10;
if ($[14] !== T0 || $[15] !== maxWidth || $[16] !== t7 || $[17] !== t8 || $[18] !== t9) {
t10 = <T0 title={t7} limit={t8} showTimeInReset={false} extraSubtext={t9} maxWidth={maxWidth} />;
$[14] = T0;
$[15] = maxWidth;
$[16] = t7;
$[17] = t8;
$[18] = t9;
$[19] = t10;
} else {
t10 = $[19];
}
return t10;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsInVzZUVmZmVjdCIsInVzZVN0YXRlIiwiZXh0cmFVc2FnZSIsImV4dHJhVXNhZ2VDb21tYW5kIiwiZm9ybWF0Q29zdCIsImdldFN1YnNjcmlwdGlvblR5cGUiLCJ1c2VUZXJtaW5hbFNpemUiLCJCb3giLCJUZXh0IiwidXNlS2V5YmluZGluZyIsIkV4dHJhVXNhZ2UiLCJmZXRjaFV0aWxpemF0aW9uIiwiUmF0ZUxpbWl0IiwiVXRpbGl6YXRpb24iLCJmb3JtYXRSZXNldFRleHQiLCJsb2dFcnJvciIsImpzb25TdHJpbmdpZnkiLCJDb25maWd1cmFibGVTaG9ydGN1dEhpbnQiLCJCeWxpbmUiLCJQcm9ncmVzc0JhciIsImlzRWxpZ2libGVGb3JPdmVyYWdlQ3JlZGl0R3JhbnQiLCJPdmVyYWdlQ3JlZGl0VXBzZWxsIiwiTGltaXRCYXJQcm9wcyIsInRpdGxlIiwibGltaXQiLCJtYXhXaWR0aCIsInNob3dUaW1lSW5SZXNldCIsImV4dHJhU3VidGV4dCIsIkxpbWl0QmFyIiwidDAiLCIkIiwiX2MiLCJ0MSIsInVuZGVmaW5lZCIsInV0aWxpemF0aW9uIiwicmVzZXRzX2F0IiwidXNlZFRleHQiLCJNYXRoIiwiZmxvb3IiLCJzdWJ0ZXh0IiwidDIiLCJ0MyIsInQ0IiwibWF4QmFyV2lkdGgiLCJ0NSIsInQ2IiwidDciLCJ0OCIsIlVzYWdlIiwiUmVhY3ROb2RlIiwic2V0VXRpbGl6YXRpb24iLCJlcnJvciIsInNldEVycm9yIiwiaXNMb2FkaW5nIiwic2V0SXNMb2FkaW5nIiwiY29sdW1ucyIsImF2YWlsYWJsZVdpZHRoIiwibWluIiwibG9hZFV0aWxpemF0aW9uIiwidXNlQ2FsbGJhY2siLCJkYXRhIiwiZXJyIiwiRXJyb3IiLCJheGlvc0Vycm9yIiwicmVzcG9uc2UiLCJyZXNwb25zZUJvZHkiLCJjb250ZXh0IiwiaXNBY3RpdmUiLCJzdWJzY3JpcHRpb25UeXBlIiwic2hvd1Nvbm5ldEJhciIsImxpbWl0cyIsImZpdmVfaG91ciIsInNldmVuX2RheSIsInNldmVuX2RheV9zb25uZXQiLCJzb21lIiwibWFwIiwiZXh0cmFfdXNhZ2UiLCJFeHRyYVVzYWdlU2VjdGlvblByb3BzIiwiRVhUUkFfVVNBR0VfU0VDVElPTl9USVRMRSIsIkV4dHJhVXNhZ2VTZWN0aW9uIiwiaXNQcm9Pck1heCIsImlzX2VuYWJsZWQiLCJpc0VuYWJsZWQiLCJTeW1ib2wiLCJmb3IiLCJtb250aGx5X2xpbWl0IiwidXNlZF9jcmVkaXRzIiwiZm9ybWF0dGVkVXNlZENyZWRpdHMiLCJmb3JtYXR0ZWRNb250aGx5TGltaXQiLCJUMCIsIm5vdyIsIkRhdGUiLCJvbmVNb250aFJlc2V0IiwiZ2V0RnVsbFllYXIiLCJnZXRNb250aCIsInRvSVNPU3RyaW5nIiwidDkiLCJ0MTAiXSwic291cmNlcyI6WyJVc2FnZS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyB1c2VFZmZlY3QsIHVzZVN0YXRlIH0gZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBleHRyYVVzYWdlIGFzIGV4dHJhVXNhZ2VDb21tYW5kIH0gZnJvbSAnc3JjL2NvbW1hbmRzL2V4dHJhLXVzYWdlL2luZGV4LmpzJ1xuaW1wb3J0IHsgZm9ybWF0Q29zdCB9IGZyb20gJ3NyYy9jb3N0LXRyYWNrZXIuanMnXG5pbXBvcnQgeyBnZXRTdWJzY3JpcHRpb25UeXBlIH0gZnJvbSAnc3JjL3V0aWxzL2F1dGguanMnXG5pbXBvcnQgeyB1c2VUZXJtaW5hbFNpemUgfSBmcm9tICcuLi8uLi9ob29rcy91c2VUZXJtaW5hbFNpemUuanMnXG5pbXBvcnQgeyBCb3gsIFRleHQgfSBmcm9tICcuLi8uLi9pbmsuanMnXG5pbXBvcnQgeyB1c2VLZXliaW5kaW5nIH0gZnJvbSAnLi4vLi4va2V5YmluZGluZ3MvdXNlS2V5YmluZGluZy5qcydcbmltcG9ydCB7XG4gIHR5cGUgRXh0cmFVc2FnZSxcbiAgZmV0Y2hVdGlsaXphdGlvbixcbiAgdHlwZSBSYXRlTGltaXQsXG4gIHR5cGUgVXRpbGl6YXRpb24sXG59IGZyb20gJy4uLy4uL3NlcnZpY2VzL2FwaS91c2FnZS5qcydcbmltcG9ydCB7IGZvcm1hdFJlc2V0VGV4dCB9IGZyb20gJy4uLy4uL3V0aWxzL2Zvcm1hdC5qcydcbmltcG9ydCB7IGxvZ0Vycm9yIH0gZnJvbSAnLi4vLi4vdXRpbHMvbG9nLmpzJ1xuaW1wb3J0IHsganNvblN0cmluZ2lmeSB9IGZyb20gJy4uLy4uL3V0aWxzL3Nsb3dPcGVyYXRpb25zLmpzJ1xuaW1wb3J0IHsgQ29uZmlndXJhYmxlU2hvcnRjdXRIaW50IH0gZnJvbSAnLi4vQ29uZmlndXJhYmxlU2hvcnRjdXRIaW50LmpzJ1xuaW1wb3J0IHsgQnlsaW5lIH0gZnJvbSAnLi4vZGVzaWduLXN5c3RlbS9CeWxpbmUuanMnXG5pbXBvcnQgeyBQcm9ncmVzc0JhciB9IGZyb20gJy4uL2Rlc2lnbi1zeXN0ZW0vUHJvZ3Jlc3NCYXIuanMnXG5pbXBvcnQge1xuICBpc0VsaWdpYmxlRm9yT3ZlcmFnZUNyZWRpdEdyYW50LFxuICBPdmVyYWdlQ3JlZGl0VXBzZWxsLFxufSBmcm9tICcuLi9Mb2dvVjIvT3ZlcmFnZUNyZWRpdFVwc2VsbC5qcydcblxudHlwZSBMaW1pdEJhclByb3BzID0ge1xuICB0aXRsZTogc3RyaW5nXG4gIGxpbWl0OiBSYXRlTGltaXRcbiAgbWF4V2lkdGg6IG51bWJlclxuICBzaG93VGltZUluUmVzZXQ/OiBib29sZWFuXG4gIGV4dHJhU3VidGV4dD86IHN0cmluZ1xufVxuXG5mdW5jdGlvbiBMaW1pdEJhcih7XG4gIHRpdGxlLFxuICBsaW1pdCxcbiAgbWF4V2lkdGgsXG4gIHNob3dUaW1lSW5SZXNldCA9IHRydWUsXG4gIGV4dHJhU3VidGV4dCxcbn06IExpbWl0QmFyUHJvcHMpOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCB7IHV0aWxpemF0aW9uLCByZXNldHNfYXQgfSA9IGxpbWl0XG4gIGlmICh1dGlsaXphdGlvbiA9PT0gbnVsbCkge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICAvLyBDYWxjdWxhdGUgdXNhZ2UgcGVyY2VudGFnZVxuICBjb25zdCB1c2VkVGV4dCA9IGAke01hdGguZmxvb3IodXRpbGl6YXRpb24pfSUgdXNlZGBcblxuICBsZXQgc3VidGV4dDogc3RyaW5nIHwgdW5kZWZpbmVkXG4gIGlmIChyZXNldHNfYXQpIHtcbiAgICBzdWJ0ZXh0ID0gYFJlc2V0cyAke2Zvcm1hdFJlc2V0VGV4dChyZXNldHNfYXQsIHRydWUsIHNob3dUaW1lSW5SZXNldCl9YFxuICB9XG5cbiAgaWYgKGV4dHJhU3VidGV4dCkge1xuICAgIGlmIChzdWJ0ZXh0KSB7XG4gICAgICBzdWJ0ZXh0ID0gYCR7ZXh0cmFTdWJ0ZXh0fSDCtyAke3N1YnRleHR9YFx