Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
95 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
836a937
Merge branch 'main' into subscription-client
jahooma Feb 4, 2026
4064c46
fix tests
jahooma Feb 4, 2026
6d68248
Merge branch 'main' into subscription-client
jahooma Feb 4, 2026
1c6f346
Enable invoice creation and tax id collection in stripe checkout
jahooma Feb 4, 2026
8677629
Revert "Enable invoice creation and tax id collection in stripe check…
jahooma Feb 4, 2026
b971584
fix(db): split referral_legacy migration to handle PostgreSQL enum li…
brandonkachen Feb 4, 2026
9c027aa
refactor(db): Switch from drizzle-kit push to migrate for safer produ…
brandonkachen Feb 4, 2026
3c54a96
fix(db): Remove backfill migration to fix PostgreSQL enum transaction…
brandonkachen Feb 4, 2026
d83f365
chore: Remove backfill script (already applied manually)
brandonkachen Feb 4, 2026
ca4ea4b
Enable invoice creation and tax id collection in stripe checkout
jahooma Feb 4, 2026
18bb92f
fix(db): Remove trailing comma in migration journal JSON
brandonkachen Feb 4, 2026
1f8ae74
Revert "refactor(db): Switch from drizzle-kit push to migrate for saf…
brandonkachen Feb 5, 2026
ce513ea
feat: Add fallbackToALaCarte server-side preference
brandonkachen Feb 5, 2026
666ec05
fix: address code review feedback for fallbackToALaCarte feature
brandonkachen Feb 5, 2026
09bdb58
style: remove max-width constraint from UsageDisplay card
brandonkachen Feb 5, 2026
9f8e9d0
style: update SubscriptionCta with acid-green button and cleaner copy
brandonkachen Feb 5, 2026
85a0022
style: use acid-green for SubscriptionCta card border and icon backgr…
brandonkachen Feb 5, 2026
dd71ccf
fix: add warning log when subscription not found in handleSubscriptio…
brandonkachen Feb 5, 2026
d407cdf
Revert "Enable invoice creation and tax id collection in stripe check…
brandonkachen Feb 5, 2026
8c65530
Don't include subscription credits in /usage stats
jahooma Feb 5, 2026
16702f8
/usage: Don't add to session credits if credits spent are part of sub…
jahooma Feb 5, 2026
7eedfa4
fix: address code review feedback for subscription-client branch
brandonkachen Feb 5, 2026
020121f
Add back some stripe checkout fields that are mildly beneficial
jahooma Feb 5, 2026
9047000
fix: enforce fallback_to_a_la_carte preference and move block grant a…
brandonkachen Feb 5, 2026
7a1531b
test: add unit tests for subscription limit enforcement in chat compl…
brandonkachen Feb 5, 2026
c226108
Merge main into subscription-client
brandonkachen Feb 5, 2026
164abc5
feat: add dedicated billing-portal endpoint for on-demand portal URL …
brandonkachen Feb 5, 2026
71c2641
test: add unit tests for billing-portal endpoint using dependency inj…
brandonkachen Feb 5, 2026
5617bac
feat: use on-demand billing portal fetch for all Billing Portal buttons
brandonkachen Feb 5, 2026
810d33f
refactor: consolidate billing portal buttons into single button in Us…
brandonkachen Feb 5, 2026
4461825
test: add unit tests for org billing portal endpoint using dependency…
brandonkachen Feb 5, 2026
fe5324a
Let users upgrade/downgrade from pricing page (linked from hitting li…
jahooma Feb 5, 2026
2e64cfb
fix: add NextRequest import to billing portal tests to fix Request po…
brandonkachen Feb 5, 2026
77525d4
fix: add web globals preload for Bun tests to fix Request polyfill issue
brandonkachen Feb 5, 2026
9a974c6
fix: remove unnecessary ts-expect-error directives from setup-globals.ts
brandonkachen Feb 5, 2026
a5c6adf
fix: add web/bunfig.toml with preload for Request global in tests
brandonkachen Feb 5, 2026
e36530c
fix: use bun test directly for web tests to pick up bunfig.toml preloads
brandonkachen Feb 5, 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
Comment thread
jahooma marked this conversation as resolved.
Outdated
? 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 */}
Comment thread
jahooma marked this conversation as resolved.
Outdated
{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
Loading