Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,13 @@ jobs:
echo "No regular tests found in .agents"
fi
elif [ "${{ matrix.package }}" = "web" ]; then
bun run test --runInBand
# Use bun test directly to pick up bunfig.toml preloads for Request global
TEST_FILES=$(find src -name '*.test.ts' ! -name '*.integration.test.ts' ! -path 'src/__tests__/e2e/*' 2>/dev/null | sort | tr '\n' ' ')
if [ -n "$TEST_FILES" ]; then
bun test $TEST_FILES
else
echo "No tests found in web"
fi
else
# Run all non-integration tests in a single bun test invocation
# This avoids xargs exit code issues with orphaned child processes
Expand Down
2 changes: 1 addition & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ linkWorkspacePackages = true
[test]
# Exclude test repositories, integration tests, and Playwright e2e tests from test execution by default
exclude = ["evals/test-repos/**", "**/*.integration.test.*", "web/src/__tests__/e2e/**"]
preload = ["./sdk/test/setup-env.ts", "./test/setup-bigquery-mocks.ts"]
preload = ["./sdk/test/setup-env.ts", "./test/setup-bigquery-mocks.ts", "./web/test/setup-globals.ts"]
28 changes: 28 additions & 0 deletions cli/src/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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 @@ -57,6 +58,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 {
type ChatKeyboardState,
createDefaultChatKeyboardState,
Expand Down Expand Up @@ -161,6 +163,11 @@ export const Chat = ({
const { statusMessage } = useClipboard()
const { ad } = useGravityAd()

// Fetch subscription data early - needed for session credits tracking
const { data: subscriptionData } = useSubscriptionQuery({
refetchInterval: 60 * 1000,
})

// Set initial mode from CLI flag on mount
useEffect(() => {
if (initialMode) {
Expand Down Expand Up @@ -425,6 +432,7 @@ export const Chat = ({
resumeQueue,
continueChat,
continueChatId,
subscriptionData,
})

sendMessageRef.current = sendMessage
Expand Down Expand Up @@ -1278,6 +1286,26 @@ export const Chat = ({
refetchInterval: 60 * 1000, // Refetch every 60 seconds
})

// Auto-show subscription limit banner when rate limit becomes active
const subscriptionLimitShownRef = useRef(false)
const subscriptionRateLimit = subscriptionData?.hasSubscription ? subscriptionData.rateLimit : undefined
const fallbackToALaCarte = subscriptionData?.fallbackToALaCarte ?? false
useEffect(() => {
const isLimited = subscriptionRateLimit?.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 (!fallbackToALaCarte) {
useChatStore.getState().setInputMode('subscriptionLimit')
}
} else if (!isLimited) {
subscriptionLimitShownRef.current = false
if (useChatStore.getState().inputMode === 'subscriptionLimit') {
useChatStore.getState().setInputMode('default')
}
}
}, [subscriptionRateLimit?.limited, fallbackToALaCarte])

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

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 @@ -382,6 +382,14 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
clearInput(params)
},
}),
defineCommand({
name: 'subscribe',
aliases: ['strong'],
handler: (params) => {
open(WEBSITE_URL + '/pricing')
clearInput(params)
},
}),
defineCommand({
name: 'buy-credits',
handler: (params) => {
Expand Down
75 changes: 47 additions & 28 deletions cli/src/components/bottom-status-line.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface BottomStatusLineProps {

/**
* 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,
Expand All @@ -25,28 +25,28 @@ export const BottomStatusLine: React.FC<BottomStatusLineProps> = ({
}) => {
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
// Only show when Claude is connected
if (!isClaudeConnected) {
return null
}

// Determine dot color for Claude: red if exhausted, green if active, muted otherwise
const claudeDotColor = isClaudeExhausted
? theme.error
: isClaudeActive
? theme.success
Expand All @@ -59,23 +59,42 @@ export const BottomStatusLine: React.FC<BottomStatusLineProps> = ({
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 and not depleted */}
{!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 */}
{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>
)}
</box>
)
}
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
59 changes: 46 additions & 13 deletions cli/src/components/message-footer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { SUBSCRIPTION_DISPLAY_NAME } from '@codebuff/common/constants/subscription-plans'
import { pluralize } from '@codebuff/common/util/string'
import { TextAttributes } from '@opentui/core'
import React, { useCallback, useMemo } from 'react'

import { CopyButton } from './copy-button'
import { ElapsedTimer } from './elapsed-timer'
import { FeedbackIconButton } from './feedback-icon-button'
import { useSubscriptionQuery } from '../hooks/use-subscription-query'
import {
getBlockPercentRemaining,
isCoveredBySubscription,
} from '../utils/subscription'
import { useTheme } from '../hooks/use-theme'
import {
useFeedbackStore,
Expand Down Expand Up @@ -157,19 +163,7 @@ export const MessageFooter: React.FC<MessageFooterProps> = ({
if (typeof credits === 'number' && credits > 0) {
footerItems.push({
key: 'credits',
node: (
<text
attributes={TextAttributes.DIM}
style={{
wrapMode: 'none',
fg: theme.secondary,
marginTop: 0,
marginBottom: 0,
}}
>
{pluralize(credits, 'credit')}
</text>
),
node: <CreditsOrSubscriptionIndicator credits={credits} />,
})
}
if (shouldRenderFeedbackButton) {
Expand Down Expand Up @@ -222,3 +216,42 @@ export const MessageFooter: React.FC<MessageFooterProps> = ({
</box>
)
}

const CreditsOrSubscriptionIndicator: React.FC<{ credits: number }> = ({ credits }) => {
const theme = useTheme()
const { data: subscriptionData } = useSubscriptionQuery({
refetchInterval: false,
refetchOnActivity: false,
pauseWhenIdle: false,
})

const blockPercentRemaining = useMemo(
() => getBlockPercentRemaining(subscriptionData),
[subscriptionData],
)

const showSubscriptionIndicator = isCoveredBySubscription(subscriptionData)

if (showSubscriptionIndicator) {
const label = (blockPercentRemaining ?? 0) < 20
? `✓ ${SUBSCRIPTION_DISPLAY_NAME} (${blockPercentRemaining}% left)`
: `✓ ${SUBSCRIPTION_DISPLAY_NAME}`
return (
<text
attributes={TextAttributes.DIM}
style={{ wrapMode: 'none', fg: theme.success, marginTop: 0, marginBottom: 0 }}
>
{label}
</text>
)
}

return (
<text
attributes={TextAttributes.DIM}
style={{ wrapMode: 'none', fg: theme.secondary, marginTop: 0, marginBottom: 0 }}
>
{pluralize(credits, 'credit')}
</text>
)
}
2 changes: 1 addition & 1 deletion cli/src/components/progress-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const ProgressBar: React.FC<ProgressBarProps> = ({
<box style={{ flexDirection: 'row', alignItems: 'center', gap: 0 }}>
{label && <text style={{ fg: theme.muted }}>{label} </text>}
<text style={{ fg: barColor }}>{filled}</text>
<text style={{ fg: theme.muted }}>{empty}</text>
{emptyWidth > 0 && <text style={{ fg: theme.muted }}>{empty}</text>}
{showPercentage && (
<text style={{ fg: textColor }}> {Math.round(clampedValue)}%</text>
)}
Expand Down
Loading