Skip to content

Commit c068082

Browse files
authored
Connect claude subscription (#403)
1 parent 76b3209 commit c068082

24 files changed

+1761
-263
lines changed

bun.lock

Lines changed: 19 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/chat.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getAdsEnabled } from './commands/ads'
1515
import { routeUserPrompt, addBashMessageToHistory } from './commands/router'
1616
import { AdBanner } from './components/ad-banner'
1717
import { ChatInputBar } from './components/chat-input-bar'
18+
import { BottomStatusLine } from './components/bottom-status-line'
1819
import { areCreditsRestored } from './components/out-of-credits-banner'
1920
import { LoadPreviousButton } from './components/load-previous-button'
2021
import { MessageWithAgents } from './components/message-with-agents'
@@ -26,6 +27,7 @@ import { useAgentValidation } from './hooks/use-agent-validation'
2627
import { useAskUserBridge } from './hooks/use-ask-user-bridge'
2728
import { authQueryKeys } from './hooks/use-auth-query'
2829
import { useChatInput } from './hooks/use-chat-input'
30+
import { useClaudeQuotaQuery } from './hooks/use-claude-quota-query'
2931
import {
3032
useChatKeyboard,
3133
type ChatKeyboardHandlers,
@@ -73,6 +75,7 @@ import {
7375
getStatusIndicatorState,
7476
type AuthStatus,
7577
} from './utils/status-indicator-state'
78+
import { getClaudeOAuthStatus } from './utils/claude-oauth'
7679
import { createPasteHandler } from './utils/strings'
7780
import { computeInputLayoutMetrics } from './utils/text-layout'
7881
import { createMarkdownPalette } from './utils/theme-system'
@@ -1360,6 +1363,15 @@ export const Chat = ({
13601363
isAskUserActive: askUserState !== null,
13611364
})
13621365
const hasStatusIndicatorContent = statusIndicatorState.kind !== 'idle'
1366+
1367+
const isClaudeOAuthActive = getClaudeOAuthStatus().connected
1368+
1369+
// Fetch Claude quota when OAuth is active
1370+
const { data: claudeQuota } = useClaudeQuotaQuery({
1371+
enabled: isClaudeOAuthActive,
1372+
refetchInterval: 60 * 1000, // Refetch every 60 seconds
1373+
})
1374+
13631375
const inputBoxTitle = useMemo(() => {
13641376
const segments: string[] = []
13651377

@@ -1380,6 +1392,9 @@ export const Chat = ({
13801392
!feedbackMode &&
13811393
(hasStatusIndicatorContent || shouldShowQueuePreview || !isAtBottom)
13821394

1395+
// Determine if Claude is actively streaming/waiting
1396+
const isClaudeActive = isStreaming || isWaitingForResponse
1397+
13831398
// Track mouse movement for ad activity (throttled)
13841399
const lastMouseActivityRef = useRef<number>(0)
13851400
const handleMouseActivity = useCallback(() => {
@@ -1541,6 +1556,12 @@ export const Chat = ({
15411556
cwd: getProjectRoot() ?? process.cwd(),
15421557
})}
15431558
/>
1559+
1560+
<BottomStatusLine
1561+
isClaudeConnected={isClaudeOAuthActive}
1562+
isClaudeActive={isClaudeActive}
1563+
claudeQuota={claudeQuota}
1564+
/>
15441565
</box>
15451566
</box>
15461567
)

cli/src/commands/command-registry.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,16 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
442442
return { openPublishMode: true }
443443
},
444444
}),
445+
defineCommand({
446+
name: 'connect:claude',
447+
aliases: ['claude'],
448+
handler: (params) => {
449+
// Enter connect:claude mode to show the OAuth banner
450+
useChatStore.getState().setInputMode('connect:claude')
451+
params.saveToHistory(params.inputValue.trim())
452+
clearInput(params)
453+
},
454+
}),
445455
]
446456

447457
export function findCommand(cmd: string): CommandDefinition | undefined {

cli/src/commands/router.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
extractReferralCode,
1515
normalizeReferralCode,
1616
} from './router-utils'
17+
import { handleClaudeAuthCode } from '../components/claude-connect-banner'
1718
import { getProjectRoot } from '../project-files'
1819
import { useChatStore } from '../state/chat-store'
1920
import {
@@ -284,6 +285,23 @@ export async function routeUserPrompt(
284285
return
285286
}
286287

288+
// Handle connect:claude mode input (authorization code)
289+
if (inputMode === 'connect:claude') {
290+
const code = trimmed
291+
if (code) {
292+
const result = await handleClaudeAuthCode(code)
293+
setMessages((prev) => [
294+
...prev,
295+
getUserMessage(trimmed),
296+
getSystemMessage(result.message),
297+
])
298+
}
299+
saveToHistory(trimmed)
300+
setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
301+
setInputMode('default')
302+
return
303+
}
304+
287305
// Handle referral mode input
288306
if (inputMode === 'referral') {
289307
// Validate the referral code (3-50 alphanumeric chars with optional dashes)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from 'react'
2+
3+
import { useTheme } from '../hooks/use-theme'
4+
5+
import { formatResetTime } from '../utils/time-format'
6+
7+
import type { ClaudeQuotaData } from '../hooks/use-claude-quota-query'
8+
9+
interface BottomStatusLineProps {
10+
/** Whether Claude OAuth is connected */
11+
isClaudeConnected: boolean
12+
/** Whether Claude is actively being used (streaming/waiting) */
13+
isClaudeActive: boolean
14+
/** Quota data from Anthropic API */
15+
claudeQuota?: ClaudeQuotaData | null
16+
}
17+
18+
/**
19+
* Bottom status line component - shows below the input box
20+
* Currently displays Claude subscription status when connected
21+
*/
22+
export const BottomStatusLine: React.FC<BottomStatusLineProps> = ({
23+
isClaudeConnected,
24+
isClaudeActive,
25+
claudeQuota,
26+
}) => {
27+
const theme = useTheme()
28+
29+
// Don't render if there's nothing to show
30+
if (!isClaudeConnected) {
31+
return null
32+
}
33+
34+
// Use the more restrictive of the two quotas (5-hour window is usually the limiting factor)
35+
const displayRemaining = claudeQuota
36+
? Math.min(claudeQuota.fiveHourRemaining, claudeQuota.sevenDayRemaining)
37+
: null
38+
39+
// Check if quota is exhausted (0%)
40+
const isExhausted = displayRemaining !== null && displayRemaining <= 0
41+
42+
// Get the reset time for the limiting quota window
43+
const resetTime = claudeQuota
44+
? claudeQuota.fiveHourRemaining <= claudeQuota.sevenDayRemaining
45+
? claudeQuota.fiveHourResetsAt
46+
: claudeQuota.sevenDayResetsAt
47+
: null
48+
49+
// Determine dot color: red if exhausted, green if active, muted otherwise
50+
const dotColor = isExhausted
51+
? theme.error
52+
: isClaudeActive
53+
? theme.success
54+
: theme.muted
55+
56+
return (
57+
<box
58+
style={{
59+
width: '100%',
60+
flexDirection: 'row',
61+
justifyContent: 'flex-end',
62+
paddingRight: 1,
63+
}}
64+
>
65+
<box
66+
style={{
67+
flexDirection: 'row',
68+
alignItems: 'center',
69+
gap: 0,
70+
}}
71+
>
72+
<text style={{ fg: dotColor }}></text>
73+
<text style={{ fg: theme.muted }}> Claude subscription</text>
74+
{isExhausted && resetTime ? (
75+
<text style={{ fg: theme.muted }}>{` · resets in ${formatResetTime(resetTime)}`}</text>
76+
) : displayRemaining !== null ? (
77+
<text style={{ fg: theme.foreground }}>{` ${Math.round(displayRemaining)}%`}</text>
78+
) : null}
79+
</box>
80+
</box>
81+
)
82+
}

0 commit comments

Comments
 (0)