Skip to content

Commit 88440f1

Browse files
committed
Show claude subscription usage in /usage
1 parent 60b840e commit 88440f1

File tree

2 files changed

+152
-8
lines changed

2 files changed

+152
-8
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from 'react'
2+
3+
import { useTheme } from '../hooks/use-theme'
4+
5+
interface ProgressBarProps {
6+
/** Value from 0 to 100 */
7+
value: number
8+
/** Width in characters (default: 20) */
9+
width?: number
10+
/** Optional label to show before the bar */
11+
label?: string
12+
/** Show percentage text after the bar */
13+
showPercentage?: boolean
14+
}
15+
16+
/**
17+
* Get color based on progress percentage - muted for normal, warning/error when low
18+
*/
19+
const getProgressColor = (
20+
value: number,
21+
theme: { primary: string; muted: string; warning: string; error: string },
22+
): string => {
23+
if (value <= 10) return theme.error
24+
if (value <= 25) return theme.warning
25+
return theme.muted // Use muted for normal levels
26+
}
27+
28+
/**
29+
* Get color for the filled portion of the bar
30+
*/
31+
const getBarColor = (
32+
value: number,
33+
theme: { primary: string; warning: string; error: string },
34+
): string => {
35+
if (value <= 10) return theme.error
36+
if (value <= 25) return theme.warning
37+
return theme.primary // Use primary for the bar itself
38+
}
39+
40+
/**
41+
* Terminal progress bar component
42+
* Uses block characters for visual display
43+
*/
44+
export const ProgressBar: React.FC<ProgressBarProps> = ({
45+
value,
46+
width = 20,
47+
label,
48+
showPercentage = true,
49+
}) => {
50+
const theme = useTheme()
51+
const clampedValue = Math.max(0, Math.min(100, value))
52+
const filledWidth = Math.round((clampedValue / 100) * width)
53+
const emptyWidth = width - filledWidth
54+
55+
const filledChar = '█'
56+
const emptyChar = '░'
57+
58+
const filled = filledChar.repeat(filledWidth)
59+
const empty = emptyChar.repeat(emptyWidth)
60+
61+
const barColor = getBarColor(clampedValue, theme)
62+
const textColor = getProgressColor(clampedValue, theme)
63+
64+
return (
65+
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 0 }}>
66+
{label && (
67+
<text style={{ fg: theme.muted }}>{label} </text>
68+
)}
69+
<text style={{ fg: barColor }}>{filled}</text>
70+
<text style={{ fg: theme.muted }}>{empty}</text>
71+
{showPercentage && (
72+
<text style={{ fg: textColor }}> {Math.round(clampedValue)}%</text>
73+
)}
74+
</box>
75+
)
76+
}

cli/src/components/usage-banner.tsx

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import React, { useEffect } from 'react'
33
import open from 'open'
44

55
import { BottomBanner } from './bottom-banner'
6+
import { Button } from './button'
7+
import { ProgressBar } from './progress-bar'
8+
import { useClaudeQuotaQuery } from '../hooks/use-claude-quota-query'
69
import { usageQueryKeys, useUsageQuery } from '../hooks/use-usage-query'
710
import { useChatStore } from '../state/chat-store'
811
import {
@@ -12,16 +15,44 @@ import {
1215
} from '../utils/usage-banner-state'
1316
import { WEBSITE_URL } from '../login/constants'
1417
import { useTheme } from '../hooks/use-theme'
15-
import { Button } from './button'
18+
import { isClaudeOAuthValid } from '@codebuff/sdk'
1619

1720
const MANUAL_SHOW_TIMEOUT = 60 * 1000 // 1 minute
1821
const USAGE_POLL_INTERVAL = 30 * 1000 // 30 seconds
1922

23+
/**
24+
* Format time until reset in human-readable form
25+
*/
26+
const formatResetTime = (resetDate: Date | null): string => {
27+
if (!resetDate) return ''
28+
const now = new Date()
29+
const diffMs = resetDate.getTime() - now.getTime()
30+
if (diffMs <= 0) return 'now'
31+
32+
const diffMins = Math.floor(diffMs / (1000 * 60))
33+
const diffHours = Math.floor(diffMins / 60)
34+
const remainingMins = diffMins % 60
35+
36+
if (diffHours > 0) {
37+
return `${diffHours}h ${remainingMins}m`
38+
}
39+
return `${diffMins}m`
40+
}
41+
2042
export const UsageBanner = ({ showTime }: { showTime: number }) => {
2143
const queryClient = useQueryClient()
2244
const sessionCreditsUsed = useChatStore((state) => state.sessionCreditsUsed)
2345
const setInputMode = useChatStore((state) => state.setInputMode)
2446

47+
// Check if Claude OAuth is connected
48+
const isClaudeConnected = isClaudeOAuthValid()
49+
50+
// Fetch Claude quota data if connected
51+
const { data: claudeQuota, isLoading: isClaudeLoading } = useClaudeQuotaQuery({
52+
enabled: isClaudeConnected,
53+
refetchInterval: 30 * 1000, // Refresh every 30 seconds when banner is open
54+
})
55+
2556
const {
2657
data: apiData,
2758
isLoading,
@@ -91,13 +122,50 @@ export const UsageBanner = ({ showTime }: { showTime: number }) => {
91122
borderColorKey={isLoadingData ? 'muted' : colorLevel}
92123
onClose={() => setInputMode('default')}
93124
>
94-
<Button
95-
onClick={() => {
96-
open(WEBSITE_URL + '/usage')
97-
}}
98-
>
99-
<text style={{ fg: theme.foreground }}>{text}</text>
100-
</Button>
125+
<box style={{ flexDirection: 'column', gap: 0 }}>
126+
{/* Codebuff credits section */}
127+
<Button
128+
onClick={() => {
129+
open(WEBSITE_URL + '/usage')
130+
}}
131+
>
132+
<text style={{ fg: theme.foreground }}>{text}</text>
133+
</Button>
134+
135+
{/* Claude subscription section - only show if connected */}
136+
{isClaudeConnected && (
137+
<box style={{ flexDirection: 'column', marginTop: 1 }}>
138+
<text style={{ fg: theme.primary }}>Claude subscription</text>
139+
{isClaudeLoading ? (
140+
<text style={{ fg: theme.muted }}>Loading quota...</text>
141+
) : claudeQuota ? (
142+
<box style={{ flexDirection: 'column', gap: 0 }}>
143+
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>
144+
<text style={{ fg: theme.muted }}>5-hour:</text>
145+
<ProgressBar value={claudeQuota.fiveHourRemaining} width={15} />
146+
{claudeQuota.fiveHourResetsAt && (
147+
<text style={{ fg: theme.muted }}>
148+
(resets in {formatResetTime(claudeQuota.fiveHourResetsAt)})
149+
</text>
150+
)}
151+
</box>
152+
{/* Only show 7-day bar if the user has a 7-day limit */}
153+
{claudeQuota.sevenDayResetsAt && (
154+
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>
155+
<text style={{ fg: theme.muted }}>7-day: </text>
156+
<ProgressBar value={claudeQuota.sevenDayRemaining} width={15} />
157+
<text style={{ fg: theme.muted }}>
158+
(resets in {formatResetTime(claudeQuota.sevenDayResetsAt)})
159+
</text>
160+
</box>
161+
)}
162+
</box>
163+
) : (
164+
<text style={{ fg: theme.muted }}>Unable to fetch quota</text>
165+
)}
166+
</box>
167+
)}
168+
</box>
101169
</BottomBanner>
102170
)
103171
}

0 commit comments

Comments
 (0)