Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions cli/src/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { useChatState } from './hooks/use-chat-state'
import { useChatStreaming } from './hooks/use-chat-streaming'
import { useChatUI } from './hooks/use-chat-ui'
import { useClaudeQuotaQuery } from './hooks/use-claude-quota-query'
import { useSubscriptionQuery } from './hooks/use-subscription-query'
import { useClipboard } from './hooks/use-clipboard'
import { useEvent } from './hooks/use-event'
import { useGravityAd } from './hooks/use-gravity-ad'
Expand All @@ -55,6 +56,7 @@ import { getClaudeOAuthStatus } from './utils/claude-oauth'
import { showClipboardMessage } from './utils/clipboard'
import { readClipboardImage } from './utils/clipboard-image'
import { getInputModeConfig } from './utils/input-modes'
import { getAlwaysUseALaCarte } from './utils/settings'
import {
type ChatKeyboardState,
createDefaultChatKeyboardState,
Expand Down Expand Up @@ -1236,6 +1238,29 @@ export const Chat = ({
refetchInterval: 60 * 1000, // Refetch every 60 seconds
})

// Fetch subscription data
const { data: subscriptionData } = useSubscriptionQuery({
refetchInterval: 60 * 1000,
})

// Auto-show subscription limit banner when rate limit becomes active
const subscriptionLimitShownRef = useRef(false)
useEffect(() => {
const isLimited = subscriptionData?.rateLimit?.limited === true
if (isLimited && !subscriptionLimitShownRef.current) {
subscriptionLimitShownRef.current = true
// Skip showing the banner if user prefers to always fall back to a-la-carte
if (!getAlwaysUseALaCarte()) {
useChatStore.getState().setInputMode('subscriptionLimit')
}
} else if (!isLimited) {
subscriptionLimitShownRef.current = false
if (useChatStore.getState().inputMode === 'subscriptionLimit') {
useChatStore.getState().setInputMode('default')
}
}
}, [subscriptionData?.rateLimit?.limited])

const inputBoxTitle = useMemo(() => {
const segments: string[] = []

Expand Down Expand Up @@ -1427,6 +1452,8 @@ export const Chat = ({
isClaudeConnected={isClaudeOAuthActive}
isClaudeActive={isClaudeActive}
claudeQuota={claudeQuota}
hasSubscription={subscriptionData?.hasSubscription ?? false}
subscriptionRateLimit={subscriptionData?.rateLimit}
/>
</box>
</box>
Expand Down
8 changes: 8 additions & 0 deletions cli/src/commands/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,14 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
clearInput(params)
},
}),
defineCommand({
name: 'subscribe',
aliases: ['strong'],
handler: (params) => {
open(WEBSITE_URL + '/strong')
clearInput(params)
},
}),
defineCommand({
name: 'buy-credits',
handler: (params) => {
Expand Down
128 changes: 100 additions & 28 deletions cli/src/components/bottom-status-line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTheme } from '../hooks/use-theme'
import { formatResetTime } from '../utils/time-format'

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

interface BottomStatusLineProps {
/** Whether Claude OAuth is connected */
Expand All @@ -12,70 +13,141 @@ interface BottomStatusLineProps {
isClaudeActive: boolean
/** Quota data from Anthropic API */
claudeQuota?: ClaudeQuotaData | null
/** Whether the user has an active Codebuff Strong subscription */
hasSubscription: boolean
/** Rate limit data for the subscription */
subscriptionRateLimit?: SubscriptionRateLimit | null
}

/**
* Bottom status line component - shows below the input box
* Currently displays Claude subscription status when connected
* Displays Claude subscription status and/or Codebuff Strong status
*/
export const BottomStatusLine: React.FC<BottomStatusLineProps> = ({
isClaudeConnected,
isClaudeActive,
claudeQuota,
hasSubscription,
subscriptionRateLimit,
}) => {
const theme = useTheme()

// Don't render if there's nothing to show
if (!isClaudeConnected) {
return null
}

// Use the more restrictive of the two quotas (5-hour window is usually the limiting factor)
const displayRemaining = claudeQuota
const claudeDisplayRemaining = claudeQuota
? Math.min(claudeQuota.fiveHourRemaining, claudeQuota.sevenDayRemaining)
: null

// Check if quota is exhausted (0%)
const isExhausted = displayRemaining !== null && displayRemaining <= 0
// Check if Claude quota is exhausted (0%)
const isClaudeExhausted = claudeDisplayRemaining !== null && claudeDisplayRemaining <= 0

// Get the reset time for the limiting quota window
const resetTime = claudeQuota
// Get the reset time for the limiting Claude quota window
const claudeResetTime = claudeQuota
? claudeQuota.fiveHourRemaining <= claudeQuota.sevenDayRemaining
? claudeQuota.fiveHourResetsAt
: claudeQuota.sevenDayResetsAt
: null

// Determine dot color: red if exhausted, green if active, muted otherwise
const dotColor = isExhausted
// Show Claude when connected and not depleted (takes priority over Strong)
const showClaude = isClaudeConnected && !isClaudeExhausted
// Show Strong when subscribed AND (no Claude connected OR Claude depleted)
const showStrong = hasSubscription && (!isClaudeConnected || isClaudeExhausted)

// Don't render if there's nothing to show
if (!showClaude && !showStrong && !(isClaudeConnected && isClaudeExhausted)) {
return null
}

// Determine dot color for Claude: red if exhausted, green if active, muted otherwise
const claudeDotColor = isClaudeExhausted
? theme.error
: isClaudeActive
? theme.success
: theme.muted

// Subscription remaining percentage (based on weekly)
const subscriptionRemaining = subscriptionRateLimit
? 100 - subscriptionRateLimit.weeklyPercentUsed
: null
const isSubscriptionLimited = subscriptionRateLimit?.limited === true

// Get subscription reset time
const subscriptionResetTime = subscriptionRateLimit
? subscriptionRateLimit.reason === 'block_exhausted' && subscriptionRateLimit.blockResetsAt
? new Date(subscriptionRateLimit.blockResetsAt)
: subscriptionRateLimit.weeklyResetsAt
? new Date(subscriptionRateLimit.weeklyResetsAt)
: null
: null

// Determine dot color for Strong: red if limited, green if has remaining credits, muted otherwise
const strongDotColor = isSubscriptionLimited
? theme.error
: subscriptionRemaining !== null && subscriptionRemaining > 0
? theme.success
: theme.muted

return (
<box
style={{
width: '100%',
flexDirection: 'row',
justifyContent: 'flex-end',
paddingRight: 1,
gap: 2,
}}
>
<box
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 0,
}}
>
<text style={{ fg: dotColor }}>●</text>
<text style={{ fg: theme.muted }}> Claude subscription</text>
{isExhausted && resetTime ? (
<text style={{ fg: theme.muted }}>{` · resets in ${formatResetTime(resetTime)}`}</text>
) : displayRemaining !== null ? (
<BatteryIndicator value={displayRemaining} theme={theme} />
) : null}
</box>
{/* Show Claude subscription when connected (even when depleted, to show reset time) */}
{isClaudeConnected && !isClaudeExhausted && (
<box
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 0,
}}
>
<text style={{ fg: claudeDotColor }}>●</text>
<text style={{ fg: theme.muted }}> Claude subscription</text>
{claudeDisplayRemaining !== null ? (
<BatteryIndicator value={claudeDisplayRemaining} theme={theme} />
) : null}
</box>
)}

{/* Show Claude as depleted when exhausted */}
{isClaudeConnected && isClaudeExhausted && (
<box
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 0,
}}
>
<text style={{ fg: theme.error }}>●</text>
<text style={{ fg: theme.muted }}> Claude</text>
{claudeResetTime && (
<text style={{ fg: theme.muted }}>{` · resets in ${formatResetTime(claudeResetTime)}`}</text>
)}
</box>
)}

{/* Show Codebuff Strong when subscribed and Claude not healthy */}
{showStrong && (
<box
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 0,
}}
>
<text style={{ fg: strongDotColor }}>●</text>
<text style={{ fg: theme.muted }}> Codebuff Strong</text>
{isSubscriptionLimited && subscriptionResetTime ? (
<text style={{ fg: theme.muted }}>{` · resets in ${formatResetTime(subscriptionResetTime)}`}</text>
) : subscriptionRemaining !== null ? (
<BatteryIndicator value={subscriptionRemaining} theme={theme} />
) : null}
</box>
)}
</box>
)
}
Expand Down
6 changes: 6 additions & 0 deletions cli/src/components/chat-input-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FeedbackContainer } from './feedback-container'
import { InputModeBanner } from './input-mode-banner'
import { MultilineInput, type MultilineInputHandle } from './multiline-input'
import { OutOfCreditsBanner } from './out-of-credits-banner'
import { SubscriptionLimitBanner } from './subscription-limit-banner'
import { PublishContainer } from './publish-container'
import { SuggestionMenu, type SuggestionItem } from './suggestion-menu'
import { useAskUserBridge } from '../hooks/use-ask-user-bridge'
Expand Down Expand Up @@ -187,6 +188,11 @@ export const ChatInputBar = ({
return <OutOfCreditsBanner />
}

// Subscription limit mode: replace entire input with subscription limit banner
if (inputMode === 'subscriptionLimit') {
return <SubscriptionLimitBanner />
}

// Handle input changes with special mode entry detection
const handleInputChange = (value: InputValue) => {
// Detect entering bash mode: user typed exactly '!' when in default mode
Expand Down
2 changes: 2 additions & 0 deletions cli/src/components/input-mode-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ClaudeConnectBanner } from './claude-connect-banner'
import { HelpBanner } from './help-banner'
import { PendingAttachmentsBanner } from './pending-attachments-banner'
import { ReferralBanner } from './referral-banner'
import { SubscriptionLimitBanner } from './subscription-limit-banner'
import { UsageBanner } from './usage-banner'
import { useChatStore } from '../state/chat-store'

Expand All @@ -26,6 +27,7 @@ const BANNER_REGISTRY: Record<
referral: () => <ReferralBanner />,
help: () => <HelpBanner />,
'connect:claude': () => <ClaudeConnectBanner />,
subscriptionLimit: () => <SubscriptionLimitBanner />,
}

/**
Expand Down
Loading