Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
75a228f
Initial backend impl
jahooma Jan 28, 2026
00af124
Review fixes
jahooma Jan 28, 2026
8e31469
Plans to tiered subscription. Don't store plan name/tier in db
jahooma Jan 28, 2026
66463e9
Extract getUserByStripeCustomerId helper
jahooma Jan 28, 2026
b807cfa
migrateUnusedCredits: remove filter on free/referral
jahooma Jan 28, 2026
8976298
Add .env.example for stripe price id
jahooma Jan 28, 2026
ed2a1d9
Remove subscription_count. Add more stripe status enums
jahooma Jan 28, 2026
31db66e
cleanup
jahooma Jan 28, 2026
458616a
Generate migration
jahooma Jan 28, 2026
c39155b
More reviewer improvments
jahooma Jan 28, 2026
cba210d
Update migrateUnusedCredits query
jahooma Jan 28, 2026
40a0b2e
Rename Flex to Strong
jahooma Jan 28, 2026
76f71c4
Add subscription tiers. Extract util getStripeId
jahooma Jan 28, 2026
9184aa2
Web routes to cancel, change tier, create subscription, or get subscr…
jahooma Jan 28, 2026
3f81504
Web subscription UI
jahooma Jan 28, 2026
5e9b314
Fix billing test to mock subscription endpoint
jahooma Jan 28, 2026
a7c6823
cli subscription changes
jahooma Jan 28, 2026
71c4d1d
Merge branch 'main' into subscription-client
jahooma Jan 29, 2026
9d79443
Fix type error
jahooma Jan 29, 2026
77c296c
Update usage multiplier
jahooma Jan 29, 2026
6770873
Merge branch 'main' into subscription-client
jahooma Jan 30, 2026
a5589d8
Handle subscription scheduled webhook events
jahooma Jan 30, 2026
e29b8cd
Simplify subscription plan to use on manage subscription button
jahooma Jan 30, 2026
3d5b4d1
Makeover for subscription panel
jahooma Jan 30, 2026
20f680c
Tweak subscription section design
jahooma Jan 30, 2026
66edcaa
Merge branch 'main' into subscription-client
jahooma Jan 30, 2026
13e8fc0
Create a credit block when you send a message
jahooma Jan 30, 2026
b9c5a92
fix 401 getting subscription
jahooma Jan 30, 2026
2a0015b
Set auth token at app startup
jahooma Jan 30, 2026
05b0321
Improve 5 hour limit banner
jahooma Jan 31, 2026
6e58594
Don't create a new block if the previous one's 5 hours is not up
jahooma Jan 31, 2026
f23f122
Show the scheduled tier in subscription panel
jahooma Jan 31, 2026
919a856
Fix: when cancelling a downgrade, scheduled_tier was not being cleared
jahooma Jan 31, 2026
07ba6f5
fix test
jahooma Feb 2, 2026
12794da
Remove bottom status bar for Strong subscription. Include subscriptio…
jahooma Feb 2, 2026
a124b3e
Improve usage banner a lot
jahooma Feb 2, 2026
12e7c01
Update /usage and subscription banner labels/ui
jahooma Feb 2, 2026
7120b0e
Revert thinking code changes
jahooma Feb 2, 2026
0a72e18
Refactor to pull out Subscription types
jahooma Feb 2, 2026
c9b56fc
Use generated updated_at for subscription table
jahooma Feb 2, 2026
a52d403
Improve stripe "phases" docs
jahooma Feb 2, 2026
fba5e79
Let you change setting for pause/spend credits for when subscription …
jahooma Feb 2, 2026
2d9cbea
Refactor so only one ensureSubscriberBlockGrant function is injected
jahooma Feb 2, 2026
631838c
Tweaks for usage banner
jahooma Feb 2, 2026
fadcc88
Clean up time formatting utils
jahooma Feb 2, 2026
f68ac73
Fetch authenticated billing portal link!
jahooma Feb 2, 2026
aedb14c
Update the pricing to advertize codebuff strong
jahooma Feb 2, 2026
e67902b
Update Codebuff strong screen
jahooma Feb 2, 2026
6f75461
Remove /strong page. Merge it into /pricing for simplicity
jahooma Feb 2, 2026
a6def1f
Tweak usage base pricing copy
jahooma Feb 2, 2026
e090f02
Tweak block limits
jahooma Feb 3, 2026
0c34f9b
Subscription success toast
jahooma Feb 3, 2026
afa0869
cli: Include link to upgrade plan when you hit limit
jahooma Feb 3, 2026
22551e6
Merge branch 'main' into subscription-client
jahooma Feb 3, 2026
38f349f
Clean up subscription limit banner
jahooma Feb 4, 2026
16bf768
align usage progress bars
jahooma Feb 4, 2026
94ec423
tweak copy in pricing page
jahooma Feb 4, 2026
2053bb5
Update pricing page styles again
jahooma Feb 4, 2026
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 @@ -1245,6 +1247,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 @@ -1436,6 +1461,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