Skip to content

Commit 913702d

Browse files
amDosionclaude
andauthored
feat: built-in status line with usage quota display (claude-code-best#89)
* feat: built-in status line with usage quota display Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 86d2c8f commit 913702d

2 files changed

Lines changed: 188 additions & 298 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { formatCost } from '../cost-tracker.js';
3+
import { Box, Text } from '../ink.js';
4+
import { formatTokens } from '../utils/format.js';
5+
import { ProgressBar } from './design-system/ProgressBar.js';
6+
import { useTerminalSize } from '../hooks/useTerminalSize.js';
7+
8+
type RateLimitBucket = {
9+
utilization: number;
10+
resets_at: number;
11+
};
12+
13+
type BuiltinStatusLineProps = {
14+
modelName: string;
15+
contextUsedPct: number;
16+
usedTokens: number;
17+
contextWindowSize: number;
18+
totalCostUsd: number;
19+
rateLimits: {
20+
five_hour?: RateLimitBucket;
21+
seven_day?: RateLimitBucket;
22+
};
23+
};
24+
25+
/**
26+
* Format a countdown from now until the given epoch time (in seconds).
27+
* Returns a compact human-readable string like "3h12m", "5d20h", "45m", or "now".
28+
*/
29+
export function formatCountdown(epochSeconds: number): string {
30+
const diff = epochSeconds - Date.now() / 1000;
31+
if (diff <= 0) return 'now';
32+
33+
const days = Math.floor(diff / 86400);
34+
const hours = Math.floor((diff % 86400) / 3600);
35+
const minutes = Math.floor((diff % 3600) / 60);
36+
37+
if (days >= 1) return `${days}d${hours}h`;
38+
if (hours >= 1) return `${hours}h${minutes}m`;
39+
return `${minutes}m`;
40+
}
41+
42+
function Separator() {
43+
return <Text dimColor>{' \u2502 '}</Text>;
44+
}
45+
46+
function BuiltinStatusLineInner({
47+
modelName,
48+
contextUsedPct,
49+
usedTokens,
50+
contextWindowSize,
51+
totalCostUsd,
52+
rateLimits,
53+
}: BuiltinStatusLineProps) {
54+
const { columns } = useTerminalSize();
55+
56+
// Force re-render every 60s so countdowns stay current
57+
const [tick, setTick] = useState(0);
58+
useEffect(() => {
59+
const hasResetTime = rateLimits.five_hour?.resets_at || rateLimits.seven_day?.resets_at;
60+
if (!hasResetTime) return;
61+
const id = setInterval(() => setTick(t => t + 1), 60_000);
62+
return () => clearInterval(id);
63+
}, [rateLimits.five_hour?.resets_at, rateLimits.seven_day?.resets_at]);
64+
65+
// Suppress unused-variable lint for tick (it exists only to trigger re-renders)
66+
void tick;
67+
68+
// Model display: use first two words (e.g. "Opus 4.6") instead of just first word
69+
const modelParts = modelName.split(' ');
70+
const shortModel = modelParts.length >= 2 ? `${modelParts[0]} ${modelParts[1]}` : modelName;
71+
72+
const wide = columns >= 100;
73+
const narrow = columns < 60;
74+
75+
const hasFiveHour = rateLimits.five_hour != null;
76+
const hasSevenDay = rateLimits.seven_day != null;
77+
78+
const fiveHourPct = hasFiveHour ? Math.round(rateLimits.five_hour!.utilization * 100) : 0;
79+
const sevenDayPct = hasSevenDay ? Math.round(rateLimits.seven_day!.utilization * 100) : 0;
80+
81+
// Token display: "50k/1M"
82+
const tokenDisplay = `${formatTokens(usedTokens)}/${formatTokens(contextWindowSize)}`;
83+
84+
return (
85+
<Box wrap="truncate">
86+
{/* Model name */}
87+
<Text>{shortModel}</Text>
88+
89+
{/* Context usage with token counts */}
90+
<Separator />
91+
<Text dimColor>Context </Text>
92+
<Text>{contextUsedPct}%</Text>
93+
{!narrow && <Text dimColor> ({tokenDisplay})</Text>}
94+
95+
{/* 5-hour session rate limit */}
96+
{hasFiveHour && (
97+
<>
98+
<Separator />
99+
<Text dimColor>Session </Text>
100+
{wide && (
101+
<>
102+
<ProgressBar
103+
ratio={rateLimits.five_hour!.utilization}
104+
width={10}
105+
fillColor="rate_limit_fill"
106+
emptyColor="rate_limit_empty"
107+
/>
108+
<Text> </Text>
109+
</>
110+
)}
111+
<Text>{fiveHourPct}%</Text>
112+
{!narrow && rateLimits.five_hour!.resets_at > 0 && (
113+
<Text dimColor> {formatCountdown(rateLimits.five_hour!.resets_at)}</Text>
114+
)}
115+
</>
116+
)}
117+
118+
{/* 7-day weekly rate limit */}
119+
{hasSevenDay && (
120+
<>
121+
<Separator />
122+
<Text dimColor>Weekly </Text>
123+
{wide && (
124+
<>
125+
<ProgressBar
126+
ratio={rateLimits.seven_day!.utilization}
127+
width={10}
128+
fillColor="rate_limit_fill"
129+
emptyColor="rate_limit_empty"
130+
/>
131+
<Text> </Text>
132+
</>
133+
)}
134+
<Text>{sevenDayPct}%</Text>
135+
{!narrow && rateLimits.seven_day!.resets_at > 0 && (
136+
<Text dimColor> {formatCountdown(rateLimits.seven_day!.resets_at)}</Text>
137+
)}
138+
</>
139+
)}
140+
141+
{/* Cost */}
142+
{totalCostUsd > 0 && (
143+
<>
144+
<Separator />
145+
<Text>{formatCost(totalCostUsd)}</Text>
146+
</>
147+
)}
148+
</Box>
149+
);
150+
}
151+
152+
export const BuiltinStatusLine = React.memo(BuiltinStatusLineInner);

0 commit comments

Comments
 (0)