feat(cli): always show quota remaining in /stats

This commit is contained in:
Spencer
2026-02-20 21:22:22 +00:00
parent 9d195f0f31
commit 62841eea35
4 changed files with 260 additions and 172 deletions
@@ -65,21 +65,6 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
([, metrics]) => metrics.api.totalRequests > 0,
);
if (activeModels.length === 0) {
return (
<Box
borderStyle="round"
borderColor={theme.border.default}
paddingTop={1}
paddingX={2}
>
<Text color={theme.text.primary}>
No API calls have been made in this session.
</Text>
</Box>
);
}
const modelNames = activeModels.map(([name]) => name);
const hasThoughts = activeModels.some(
@@ -354,19 +339,20 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
<Text color={theme.text.primary}>{tier}</Text>
</Box>
)}
{isAuto &&
pooledRemaining !== undefined &&
pooledLimit !== undefined &&
pooledLimit > 0 && (
<QuotaStatsInfo
remaining={pooledRemaining}
limit={pooledLimit}
resetTime={pooledResetTime}
/>
)}
<QuotaStatsInfo
remaining={pooledRemaining}
limit={pooledLimit}
resetTime={pooledResetTime}
/>
{(showUserIdentity || isAuto) && <Box height={1} />}
<Table data={rows} columns={columns} />
{activeModels.length === 0 ? (
<Text color={theme.text.primary}>
No API calls have been made in this session.
</Text>
) : (
<Table data={rows} columns={columns} />
)}
</Box>
);
};
+134 -146
View File
@@ -162,15 +162,7 @@ const ModelUsageTable: React.FC<{
? `${getDisplayString(currentModel)} Usage`
: `Model Usage`;
const hasPooledQuota =
(pooledRemaining !== undefined && pooledRemaining !== null) ||
(pooledLimit !== undefined && pooledLimit !== null && pooledLimit > 0);
if (rows.length === 0 && !hasPooledQuota && !isAuto) {
return null;
}
const showQuotaColumn = !!quotas && rows.some((row) => !!row.bucket);
const showQuotaColumn = !!quotas;
const nameWidth = 22;
const requestsWidth = 7;
@@ -202,28 +194,100 @@ const ModelUsageTable: React.FC<{
</Box>
</Box>
{(hasPooledQuota || isAuto) && (
<Box flexDirection="column" marginTop={0} marginBottom={1}>
<QuotaStatsInfo
remaining={pooledRemaining}
limit={pooledLimit}
resetTime={pooledResetTime}
showDetails={true}
/>
{isAuto && (
<Text color={theme.text.primary}>
For a full token breakdown, run `/stats model`.
</Text>
)}
</Box>
)}
<Box flexDirection="column" marginTop={0} marginBottom={1}>
<QuotaStatsInfo
remaining={pooledRemaining}
limit={pooledLimit}
resetTime={pooledResetTime}
showDetails={true}
/>
{isAuto && (
<Text color={theme.text.primary}>
For a full token breakdown, run `/stats model`.
</Text>
)}
</Box>
{rows.length > 0 && (
<>
<Box alignItems="flex-end">
<Box width={nameWidth}>
<Box alignItems="flex-end">
<Box width={nameWidth}>
<Text bold color={theme.text.primary}>
Model
</Text>
</Box>
<Box
width={requestsWidth}
flexDirection="column"
alignItems="flex-end"
flexShrink={0}
>
<Text bold color={theme.text.primary}>
Reqs
</Text>
</Box>
{!showQuotaColumn && (
<>
<Box
width={uncachedWidth}
flexDirection="column"
alignItems="flex-end"
flexShrink={0}
>
<Text bold color={theme.text.primary}>
Model
Input Tokens
</Text>
</Box>
<Box
width={cachedWidth}
flexDirection="column"
alignItems="flex-end"
flexShrink={0}
>
<Text bold color={theme.text.primary}>
Cache Reads
</Text>
</Box>
<Box
width={outputTokensWidth}
flexDirection="column"
alignItems="flex-end"
flexShrink={0}
>
<Text bold color={theme.text.primary}>
Output Tokens
</Text>
</Box>
</>
)}
{showQuotaColumn && (
<Box width={usageLimitWidth} justifyContent="flex-end">
<Text bold color={theme.text.primary}>
Usage left
</Text>
</Box>
)}
</Box>
{/* Divider */}
<Box
borderStyle="round"
borderBottom={true}
borderTop={false}
borderLeft={false}
borderRight={false}
borderColor={theme.border.default}
width={totalWidth}
></Box>
{rows.length > 0 ? (
rows.map((row) => (
<Box key={row.key}>
<Box width={nameWidth}>
<Text
color={row.isActive ? theme.text.primary : theme.text.secondary}
wrap="truncate-end"
>
{row.modelName}
</Text>
</Box>
<Box
@@ -232,11 +296,12 @@ const ModelUsageTable: React.FC<{
alignItems="flex-end"
flexShrink={0}
>
<Text bold color={theme.text.primary}>
Reqs
<Text
color={row.isActive ? theme.text.primary : theme.text.secondary}
>
{row.requests}
</Text>
</Box>
{!showQuotaColumn && (
<>
<Box
@@ -245,8 +310,12 @@ const ModelUsageTable: React.FC<{
alignItems="flex-end"
flexShrink={0}
>
<Text bold color={theme.text.primary}>
Input Tokens
<Text
color={
row.isActive ? theme.text.primary : theme.text.secondary
}
>
{row.inputTokens}
</Text>
</Box>
<Box
@@ -255,9 +324,7 @@ const ModelUsageTable: React.FC<{
alignItems="flex-end"
flexShrink={0}
>
<Text bold color={theme.text.primary}>
Cache Reads
</Text>
<Text color={theme.text.secondary}>{row.cachedTokens}</Text>
</Box>
<Box
width={outputTokensWidth}
@@ -265,122 +332,43 @@ const ModelUsageTable: React.FC<{
alignItems="flex-end"
flexShrink={0}
>
<Text bold color={theme.text.primary}>
Output Tokens
<Text
color={
row.isActive ? theme.text.primary : theme.text.secondary
}
>
{row.outputTokens}
</Text>
</Box>
</>
)}
{showQuotaColumn && (
<Box width={usageLimitWidth} justifyContent="flex-end">
<Text bold color={theme.text.primary}>
Usage left
</Text>
</Box>
)}
</Box>
{/* Divider */}
<Box
borderStyle="round"
borderBottom={true}
borderTop={false}
borderLeft={false}
borderRight={false}
borderColor={theme.border.default}
width={totalWidth}
></Box>
{rows.map((row) => (
<Box key={row.key}>
<Box width={nameWidth}>
<Text
color={
row.isActive ? theme.text.primary : theme.text.secondary
}
wrap="truncate-end"
>
{row.modelName}
</Text>
</Box>
<Box
width={requestsWidth}
flexDirection="column"
alignItems="flex-end"
flexShrink={0}
>
<Text
color={
row.isActive ? theme.text.primary : theme.text.secondary
}
>
{row.requests}
</Text>
</Box>
{!showQuotaColumn && (
<>
<Box
width={uncachedWidth}
flexDirection="column"
alignItems="flex-end"
flexShrink={0}
>
<Text
color={
row.isActive ? theme.text.primary : theme.text.secondary
}
>
{row.inputTokens}
<Box width={usageLimitWidth} justifyContent="flex-end">
{row.bucket && row.bucket.remainingFraction != null && (
<Text color={theme.text.secondary}>
{(row.bucket.remainingFraction * 100).toFixed(1)}%
{row.bucket.resetTime && (
<Text color={theme.text.secondary}>
{' '}
(
{(function (t) {
const formatted = formatResetTime(t);
return formatted === 'Resetting...' ||
formatted === '< 1m'
? formatted
: `Resets in ${formatted}`;
})(row.bucket.resetTime)}
)
</Text>
</Box>
<Box
width={cachedWidth}
flexDirection="column"
alignItems="flex-end"
flexShrink={0}
>
<Text color={theme.text.secondary}>{row.cachedTokens}</Text>
</Box>
<Box
width={outputTokensWidth}
flexDirection="column"
alignItems="flex-end"
flexShrink={0}
>
<Text
color={
row.isActive ? theme.text.primary : theme.text.secondary
}
>
{row.outputTokens}
</Text>
</Box>
</>
)}
</Text>
)}
<Box width={usageLimitWidth} justifyContent="flex-end">
{row.bucket && row.bucket.remainingFraction != null && (
<Text color={theme.text.secondary}>
{(row.bucket.remainingFraction * 100).toFixed(1)}%
{row.bucket.resetTime && (
<Text color={theme.text.secondary}>
{' '}
(
{(function (t) {
const formatted = formatResetTime(t);
return formatted === 'Resetting...' ||
formatted === '< 1m'
? formatted
: `Resets in ${formatted}`;
})(row.bucket.resetTime)}
)
</Text>
)}
</Text>
)}
</Box>
</Box>
))}
</>
</Box>
))
) : (
<Box>
<Text color={theme.text.secondary}>No model usage recorded.</Text>
</Box>
)}
{cacheEfficiency > 0 && !showQuotaColumn && rows.length > 0 && (
@@ -5,6 +5,8 @@ exports[`<ModelStatsDisplay /> > should display a single model correctly 1`] = `
│ │
│ Model Stats For Nerds │
│ │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Metric gemini-2.5-pro │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
@@ -28,6 +30,8 @@ exports[`<ModelStatsDisplay /> > should display conditional rows if at least one
│ │
│ Model Stats For Nerds │
│ │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Metric gemini-2.5-pro gemini-2.5-flash │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
@@ -51,6 +55,8 @@ exports[`<ModelStatsDisplay /> > should display role breakdown correctly 1`] = `
│ │
│ Model Stats For Nerds │
│ │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Metric gemini-2.5-pro │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
@@ -78,6 +84,8 @@ exports[`<ModelStatsDisplay /> > should display stats for multiple models correc
│ │
│ Model Stats For Nerds │
│ │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Metric gemini-2.5-pro gemini-2.5-flash │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
@@ -101,6 +109,8 @@ exports[`<ModelStatsDisplay /> > should filter out invalid role names 1`] = `
│ │
│ Model Stats For Nerds │
│ │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Metric gemini-2.5-pro │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
@@ -121,6 +131,8 @@ exports[`<ModelStatsDisplay /> > should handle large values without wrapping or
│ │
│ Model Stats For Nerds │
│ │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Metric gemini-2.5-pro │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
@@ -144,6 +156,8 @@ exports[`<ModelStatsDisplay /> > should handle long role name layout 1`] = `
│ │
│ Model Stats For Nerds │
│ │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Metric gemini-2.5-pro │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
@@ -170,6 +184,8 @@ exports[`<ModelStatsDisplay /> > should not display conditional rows if no model
│ │
│ Model Stats For Nerds │
│ │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Metric gemini-2.5-pro │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
@@ -188,6 +204,11 @@ exports[`<ModelStatsDisplay /> > should not display conditional rows if no model
exports[`<ModelStatsDisplay /> > should render "no API calls" message when there are no active models 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Model Stats For Nerds │
│ │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ No API calls have been made in this session. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
@@ -17,6 +17,14 @@ exports[`<StatsDisplay /> > Code Changes Display > displays Code Changes when li
│ » API Time: 0s (0.0%) │
│ » Tool Time: 100ms (100.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -37,6 +45,14 @@ exports[`<StatsDisplay /> > Code Changes Display > hides Code Changes when no li
│ » API Time: 0s (0.0%) │
│ » Tool Time: 100ms (100.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -57,6 +73,14 @@ exports[`<StatsDisplay /> > Conditional Color Tests > renders success rate in gr
│ » API Time: 0s (0.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -77,6 +101,14 @@ exports[`<StatsDisplay /> > Conditional Color Tests > renders success rate in re
│ » API Time: 0s (0.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -97,6 +129,14 @@ exports[`<StatsDisplay /> > Conditional Color Tests > renders success rate in ye
│ » API Time: 0s (0.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -118,6 +158,9 @@ exports[`<StatsDisplay /> > Conditional Rendering Tests > hides Efficiency secti
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ gemini-2.5-pro 1 100 0 100 │
@@ -142,6 +185,14 @@ exports[`<StatsDisplay /> > Conditional Rendering Tests > hides User Agreement w
│ » API Time: 0s (0.0%) │
│ » Tool Time: 123ms (100.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -163,6 +214,9 @@ exports[`<StatsDisplay /> > Quota Display > renders model table quota percentage
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Usage left │
│ ───────────────────────────────────────────────────────────────────── │
│ gemini-2.5-flash - 50.0% │
@@ -193,6 +247,9 @@ exports[`<StatsDisplay /> > Quota Display > renders pooled quota even when no mo
│ /auth to upgrade or switch to API key. │
│ For a full token breakdown, run \`/stats model\`. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
@@ -246,6 +303,9 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information for unused
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Usage left │
│ ───────────────────────────────────────────────────────────────────── │
│ gemini-2.5-flash - 50.0% (Resets in 2h) │
@@ -271,6 +331,9 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information when quota
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Usage left │
│ ───────────────────────────────────────────────────────────────────── │
│ gemini-2.5-pro 1 75.0% (Resets in 1h 30m) │
@@ -295,6 +358,14 @@ exports[`<StatsDisplay /> > Title Rendering > renders the custom title when a ti
│ » API Time: 0s (0.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -315,6 +386,14 @@ exports[`<StatsDisplay /> > Title Rendering > renders the default title when no
│ » API Time: 0s (0.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -336,6 +415,9 @@ exports[`<StatsDisplay /> > renders a table with two models correctly 1`] = `
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ gemini-2.5-pro 3 500 500 2,000 │
@@ -365,6 +447,9 @@ exports[`<StatsDisplay /> > renders all sections when all data is present 1`] =
│ » Tool Time: 123ms (55.2%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ gemini-2.5-pro 1 50 50 100 │
@@ -391,6 +476,14 @@ exports[`<StatsDisplay /> > renders only the Performance section in its zero sta
│ » API Time: 0s (0.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
│ Usage limits span all sessions and reset daily. │
│ /auth to upgrade or switch to API key. │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ───────────────────────────────────────────────────────────────────────── │
│ No model usage recorded. │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;