Skip to content

Commit a7c6823

Browse files
committed
cli subscription changes
1 parent 5e9b314 commit a7c6823

13 files changed

+554
-31
lines changed

cli/src/chat.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { useChatState } from './hooks/use-chat-state'
3434
import { useChatStreaming } from './hooks/use-chat-streaming'
3535
import { useChatUI } from './hooks/use-chat-ui'
3636
import { useClaudeQuotaQuery } from './hooks/use-claude-quota-query'
37+
import { useSubscriptionQuery } from './hooks/use-subscription-query'
3738
import { useClipboard } from './hooks/use-clipboard'
3839
import { useEvent } from './hooks/use-event'
3940
import { useGravityAd } from './hooks/use-gravity-ad'
@@ -55,6 +56,7 @@ import { getClaudeOAuthStatus } from './utils/claude-oauth'
5556
import { showClipboardMessage } from './utils/clipboard'
5657
import { readClipboardImage } from './utils/clipboard-image'
5758
import { getInputModeConfig } from './utils/input-modes'
59+
import { getAlwaysUseALaCarte } from './utils/settings'
5860
import {
5961
type ChatKeyboardState,
6062
createDefaultChatKeyboardState,
@@ -1236,6 +1238,29 @@ export const Chat = ({
12361238
refetchInterval: 60 * 1000, // Refetch every 60 seconds
12371239
})
12381240

1241+
// Fetch subscription data
1242+
const { data: subscriptionData } = useSubscriptionQuery({
1243+
refetchInterval: 60 * 1000,
1244+
})
1245+
1246+
// Auto-show subscription limit banner when rate limit becomes active
1247+
const subscriptionLimitShownRef = useRef(false)
1248+
useEffect(() => {
1249+
const isLimited = subscriptionData?.rateLimit?.limited === true
1250+
if (isLimited && !subscriptionLimitShownRef.current) {
1251+
subscriptionLimitShownRef.current = true
1252+
// Skip showing the banner if user prefers to always fall back to a-la-carte
1253+
if (!getAlwaysUseALaCarte()) {
1254+
useChatStore.getState().setInputMode('subscriptionLimit')
1255+
}
1256+
} else if (!isLimited) {
1257+
subscriptionLimitShownRef.current = false
1258+
if (useChatStore.getState().inputMode === 'subscriptionLimit') {
1259+
useChatStore.getState().setInputMode('default')
1260+
}
1261+
}
1262+
}, [subscriptionData?.rateLimit?.limited])
1263+
12391264
const inputBoxTitle = useMemo(() => {
12401265
const segments: string[] = []
12411266

@@ -1427,6 +1452,8 @@ export const Chat = ({
14271452
isClaudeConnected={isClaudeOAuthActive}
14281453
isClaudeActive={isClaudeActive}
14291454
claudeQuota={claudeQuota}
1455+
hasSubscription={subscriptionData?.hasSubscription ?? false}
1456+
subscriptionRateLimit={subscriptionData?.rateLimit}
14301457
/>
14311458
</box>
14321459
</box>

cli/src/commands/command-registry.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,14 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
380380
clearInput(params)
381381
},
382382
}),
383+
defineCommand({
384+
name: 'subscribe',
385+
aliases: ['strong'],
386+
handler: (params) => {
387+
open(WEBSITE_URL + '/strong')
388+
clearInput(params)
389+
},
390+
}),
383391
defineCommand({
384392
name: 'buy-credits',
385393
handler: (params) => {

cli/src/components/bottom-status-line.tsx

Lines changed: 100 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useTheme } from '../hooks/use-theme'
44
import { formatResetTime } from '../utils/time-format'
55

66
import type { ClaudeQuotaData } from '../hooks/use-claude-quota-query'
7+
import type { SubscriptionRateLimit } from '../hooks/use-subscription-query'
78

89
interface BottomStatusLineProps {
910
/** Whether Claude OAuth is connected */
@@ -12,70 +13,141 @@ interface BottomStatusLineProps {
1213
isClaudeActive: boolean
1314
/** Quota data from Anthropic API */
1415
claudeQuota?: ClaudeQuotaData | null
16+
/** Whether the user has an active Codebuff Strong subscription */
17+
hasSubscription: boolean
18+
/** Rate limit data for the subscription */
19+
subscriptionRateLimit?: SubscriptionRateLimit | null
1520
}
1621

1722
/**
1823
* Bottom status line component - shows below the input box
19-
* Currently displays Claude subscription status when connected
24+
* Displays Claude subscription status and/or Codebuff Strong status
2025
*/
2126
export const BottomStatusLine: React.FC<BottomStatusLineProps> = ({
2227
isClaudeConnected,
2328
isClaudeActive,
2429
claudeQuota,
30+
hasSubscription,
31+
subscriptionRateLimit,
2532
}) => {
2633
const theme = useTheme()
2734

28-
// Don't render if there's nothing to show
29-
if (!isClaudeConnected) {
30-
return null
31-
}
32-
3335
// Use the more restrictive of the two quotas (5-hour window is usually the limiting factor)
34-
const displayRemaining = claudeQuota
36+
const claudeDisplayRemaining = claudeQuota
3537
? Math.min(claudeQuota.fiveHourRemaining, claudeQuota.sevenDayRemaining)
3638
: null
3739

38-
// Check if quota is exhausted (0%)
39-
const isExhausted = displayRemaining !== null && displayRemaining <= 0
40+
// Check if Claude quota is exhausted (0%)
41+
const isClaudeExhausted = claudeDisplayRemaining !== null && claudeDisplayRemaining <= 0
4042

41-
// Get the reset time for the limiting quota window
42-
const resetTime = claudeQuota
43+
// Get the reset time for the limiting Claude quota window
44+
const claudeResetTime = claudeQuota
4345
? claudeQuota.fiveHourRemaining <= claudeQuota.sevenDayRemaining
4446
? claudeQuota.fiveHourResetsAt
4547
: claudeQuota.sevenDayResetsAt
4648
: null
4749

48-
// Determine dot color: red if exhausted, green if active, muted otherwise
49-
const dotColor = isExhausted
50+
// Show Claude when connected and not depleted (takes priority over Strong)
51+
const showClaude = isClaudeConnected && !isClaudeExhausted
52+
// Show Strong when subscribed AND (no Claude connected OR Claude depleted)
53+
const showStrong = hasSubscription && (!isClaudeConnected || isClaudeExhausted)
54+
55+
// Don't render if there's nothing to show
56+
if (!showClaude && !showStrong && !(isClaudeConnected && isClaudeExhausted)) {
57+
return null
58+
}
59+
60+
// Determine dot color for Claude: red if exhausted, green if active, muted otherwise
61+
const claudeDotColor = isClaudeExhausted
5062
? theme.error
5163
: isClaudeActive
5264
? theme.success
5365
: theme.muted
5466

67+
// Subscription remaining percentage (based on weekly)
68+
const subscriptionRemaining = subscriptionRateLimit
69+
? 100 - subscriptionRateLimit.weeklyPercentUsed
70+
: null
71+
const isSubscriptionLimited = subscriptionRateLimit?.limited === true
72+
73+
// Get subscription reset time
74+
const subscriptionResetTime = subscriptionRateLimit
75+
? subscriptionRateLimit.reason === 'block_exhausted' && subscriptionRateLimit.blockResetsAt
76+
? new Date(subscriptionRateLimit.blockResetsAt)
77+
: subscriptionRateLimit.weeklyResetsAt
78+
? new Date(subscriptionRateLimit.weeklyResetsAt)
79+
: null
80+
: null
81+
82+
// Determine dot color for Strong: red if limited, green if has remaining credits, muted otherwise
83+
const strongDotColor = isSubscriptionLimited
84+
? theme.error
85+
: subscriptionRemaining !== null && subscriptionRemaining > 0
86+
? theme.success
87+
: theme.muted
88+
5589
return (
5690
<box
5791
style={{
5892
width: '100%',
5993
flexDirection: 'row',
6094
justifyContent: 'flex-end',
6195
paddingRight: 1,
96+
gap: 2,
6297
}}
6398
>
64-
<box
65-
style={{
66-
flexDirection: 'row',
67-
alignItems: 'center',
68-
gap: 0,
69-
}}
70-
>
71-
<text style={{ fg: dotColor }}></text>
72-
<text style={{ fg: theme.muted }}> Claude subscription</text>
73-
{isExhausted && resetTime ? (
74-
<text style={{ fg: theme.muted }}>{` · resets in ${formatResetTime(resetTime)}`}</text>
75-
) : displayRemaining !== null ? (
76-
<BatteryIndicator value={displayRemaining} theme={theme} />
77-
) : null}
78-
</box>
99+
{/* Show Claude subscription when connected (even when depleted, to show reset time) */}
100+
{isClaudeConnected && !isClaudeExhausted && (
101+
<box
102+
style={{
103+
flexDirection: 'row',
104+
alignItems: 'center',
105+
gap: 0,
106+
}}
107+
>
108+
<text style={{ fg: claudeDotColor }}></text>
109+
<text style={{ fg: theme.muted }}> Claude subscription</text>
110+
{claudeDisplayRemaining !== null ? (
111+
<BatteryIndicator value={claudeDisplayRemaining} theme={theme} />
112+
) : null}
113+
</box>
114+
)}
115+
116+
{/* Show Claude as depleted when exhausted */}
117+
{isClaudeConnected && isClaudeExhausted && (
118+
<box
119+
style={{
120+
flexDirection: 'row',
121+
alignItems: 'center',
122+
gap: 0,
123+
}}
124+
>
125+
<text style={{ fg: theme.error }}></text>
126+
<text style={{ fg: theme.muted }}> Claude</text>
127+
{claudeResetTime && (
128+
<text style={{ fg: theme.muted }}>{` · resets in ${formatResetTime(claudeResetTime)}`}</text>
129+
)}
130+
</box>
131+
)}
132+
133+
{/* Show Codebuff Strong when subscribed and Claude not healthy */}
134+
{showStrong && (
135+
<box
136+
style={{
137+
flexDirection: 'row',
138+
alignItems: 'center',
139+
gap: 0,
140+
}}
141+
>
142+
<text style={{ fg: strongDotColor }}></text>
143+
<text style={{ fg: theme.muted }}> Codebuff Strong</text>
144+
{isSubscriptionLimited && subscriptionResetTime ? (
145+
<text style={{ fg: theme.muted }}>{` · resets in ${formatResetTime(subscriptionResetTime)}`}</text>
146+
) : subscriptionRemaining !== null ? (
147+
<BatteryIndicator value={subscriptionRemaining} theme={theme} />
148+
) : null}
149+
</box>
150+
)}
79151
</box>
80152
)
81153
}

cli/src/components/chat-input-bar.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { FeedbackContainer } from './feedback-container'
66
import { InputModeBanner } from './input-mode-banner'
77
import { MultilineInput, type MultilineInputHandle } from './multiline-input'
88
import { OutOfCreditsBanner } from './out-of-credits-banner'
9+
import { SubscriptionLimitBanner } from './subscription-limit-banner'
910
import { PublishContainer } from './publish-container'
1011
import { SuggestionMenu, type SuggestionItem } from './suggestion-menu'
1112
import { useAskUserBridge } from '../hooks/use-ask-user-bridge'
@@ -187,6 +188,11 @@ export const ChatInputBar = ({
187188
return <OutOfCreditsBanner />
188189
}
189190

191+
// Subscription limit mode: replace entire input with subscription limit banner
192+
if (inputMode === 'subscriptionLimit') {
193+
return <SubscriptionLimitBanner />
194+
}
195+
190196
// Handle input changes with special mode entry detection
191197
const handleInputChange = (value: InputValue) => {
192198
// Detect entering bash mode: user typed exactly '!' when in default mode

cli/src/components/input-mode-banner.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ClaudeConnectBanner } from './claude-connect-banner'
44
import { HelpBanner } from './help-banner'
55
import { PendingAttachmentsBanner } from './pending-attachments-banner'
66
import { ReferralBanner } from './referral-banner'
7+
import { SubscriptionLimitBanner } from './subscription-limit-banner'
78
import { UsageBanner } from './usage-banner'
89
import { useChatStore } from '../state/chat-store'
910

@@ -26,6 +27,7 @@ const BANNER_REGISTRY: Record<
2627
referral: () => <ReferralBanner />,
2728
help: () => <HelpBanner />,
2829
'connect:claude': () => <ClaudeConnectBanner />,
30+
subscriptionLimit: () => <SubscriptionLimitBanner />,
2931
}
3032

3133
/**

0 commit comments

Comments
 (0)