Skip to content

Commit 3c10d26

Browse files
committed
Initial impl!
1 parent 153a249 commit 3c10d26

File tree

14 files changed

+958
-145
lines changed

14 files changed

+958
-145
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/commands/command-registry.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,16 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
423423
return { openPublishMode: true }
424424
},
425425
}),
426+
defineCommand({
427+
name: 'connect:claude',
428+
aliases: ['claude'],
429+
handler: (params) => {
430+
// Enter connect:claude mode to show the OAuth banner
431+
useChatStore.getState().setInputMode('connect:claude')
432+
params.saveToHistory(params.inputValue.trim())
433+
clearInput(params)
434+
},
435+
}),
426436
]
427437

428438
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 {
@@ -282,6 +283,23 @@ export async function routeUserPrompt(
282283
return
283284
}
284285

286+
// Handle connect:claude mode input (authorization code)
287+
if (inputMode === 'connect:claude') {
288+
const code = trimmed
289+
if (code) {
290+
const result = await handleClaudeAuthCode(code)
291+
setMessages((prev) => [
292+
...prev,
293+
getUserMessage(trimmed),
294+
getSystemMessage(result.message),
295+
])
296+
}
297+
saveToHistory(trimmed)
298+
setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
299+
setInputMode('default')
300+
return
301+
}
302+
285303
// Handle referral mode input
286304
if (inputMode === 'referral') {
287305
// Validate the referral code (3-50 alphanumeric chars with optional dashes)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React, { useState, useEffect } from 'react'
2+
3+
import { BottomBanner } from './bottom-banner'
4+
import { Button } from './button'
5+
import { useChatStore } from '../state/chat-store'
6+
import {
7+
openOAuthInBrowser,
8+
exchangeCodeForTokens,
9+
disconnectClaudeOAuth,
10+
getClaudeOAuthStatus,
11+
} from '../utils/claude-oauth'
12+
import { useTheme } from '../hooks/use-theme'
13+
14+
type FlowState = 'checking' | 'not-connected' | 'waiting-for-code' | 'connected' | 'error'
15+
16+
export const ClaudeConnectBanner = () => {
17+
const setInputMode = useChatStore((state) => state.setInputMode)
18+
const theme = useTheme()
19+
const [flowState, setFlowState] = useState<FlowState>('checking')
20+
const [error, setError] = useState<string | null>(null)
21+
const [isDisconnectHovered, setIsDisconnectHovered] = useState(false)
22+
23+
// Check initial connection status
24+
useEffect(() => {
25+
const status = getClaudeOAuthStatus()
26+
if (status.connected) {
27+
setFlowState('connected')
28+
} else {
29+
setFlowState('not-connected')
30+
}
31+
}, [])
32+
33+
const handleConnect = async () => {
34+
try {
35+
setFlowState('waiting-for-code')
36+
await openOAuthInBrowser()
37+
} catch (err) {
38+
setError(err instanceof Error ? err.message : 'Failed to open browser')
39+
setFlowState('error')
40+
}
41+
}
42+
43+
const handleDisconnect = () => {
44+
disconnectClaudeOAuth()
45+
setFlowState('not-connected')
46+
}
47+
48+
const handleClose = () => {
49+
setInputMode('default')
50+
}
51+
52+
// Connected state
53+
if (flowState === 'connected') {
54+
const status = getClaudeOAuthStatus()
55+
const connectedDate = status.connectedAt
56+
? new Date(status.connectedAt).toLocaleDateString()
57+
: 'Unknown'
58+
59+
return (
60+
<BottomBanner borderColorKey="success" onClose={handleClose}>
61+
<box style={{ flexDirection: 'row', justifyContent: 'space-between', width: '100%' }}>
62+
<text style={{ fg: theme.success }}>
63+
✓ Connected to Claude (since {connectedDate})
64+
</text>
65+
<Button
66+
onClick={handleDisconnect}
67+
onMouseOver={() => setIsDisconnectHovered(true)}
68+
onMouseOut={() => setIsDisconnectHovered(false)}
69+
>
70+
<text style={{ fg: isDisconnectHovered ? theme.error : theme.muted }}>
71+
disconnect
72+
</text>
73+
</Button>
74+
</box>
75+
</BottomBanner>
76+
)
77+
}
78+
79+
// Error state
80+
if (flowState === 'error') {
81+
return (
82+
<BottomBanner
83+
borderColorKey="error"
84+
text={`Error: ${error}. Press Escape to close.`}
85+
onClose={handleClose}
86+
/>
87+
)
88+
}
89+
90+
// Waiting for code state
91+
if (flowState === 'waiting-for-code') {
92+
return (
93+
<BottomBanner borderColorKey="info" onClose={handleClose}>
94+
<box style={{ flexDirection: 'column', gap: 0 }}>
95+
<text style={{ fg: theme.info }}>
96+
Browser opened. Sign in with your Claude account, then paste the authorization code below.
97+
</text>
98+
<text style={{ fg: theme.muted, marginTop: 1 }}>
99+
Type the code in the input box above and press Enter.
100+
</text>
101+
</box>
102+
</BottomBanner>
103+
)
104+
}
105+
106+
// Not connected / checking state - show connect button
107+
return (
108+
<BottomBanner borderColorKey="info" onClose={handleClose}>
109+
<box style={{ flexDirection: 'row', justifyContent: 'space-between', width: '100%' }}>
110+
<text style={{ fg: theme.info }}>
111+
Connect your Claude Pro/Max subscription to use Claude models directly.
112+
</text>
113+
<Button onClick={handleConnect}>
114+
<text style={{ fg: theme.link }}>Connect →</text>
115+
</Button>
116+
</box>
117+
</BottomBanner>
118+
)
119+
}
120+
121+
/**
122+
* Handle the authorization code input from the user.
123+
* This is called when the user pastes their code in connect:claude mode.
124+
*/
125+
export async function handleClaudeAuthCode(code: string): Promise<{
126+
success: boolean
127+
message: string
128+
}> {
129+
try {
130+
await exchangeCodeForTokens(code)
131+
return {
132+
success: true,
133+
message: 'Successfully connected to Claude! Your Claude models will now use your subscription.',
134+
}
135+
} catch (err) {
136+
return {
137+
success: false,
138+
message: err instanceof Error ? err.message : 'Failed to exchange authorization code',
139+
}
140+
}
141+
}

cli/src/components/input-mode-banner.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react'
22

3+
import { ClaudeConnectBanner } from './claude-connect-banner'
34
import { HelpBanner } from './help-banner'
45
import { PendingImagesBanner } from './pending-images-banner'
56
import { ReferralBanner } from './referral-banner'
@@ -24,6 +25,7 @@ const BANNER_REGISTRY: Record<
2425
usage: ({ showTime }) => <UsageBanner showTime={showTime} />,
2526
referral: () => <ReferralBanner />,
2627
help: () => <HelpBanner />,
28+
'connect:claude': () => <ClaudeConnectBanner />,
2729
}
2830

2931
/**

cli/src/data/slash-commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,11 @@ export const SLASH_COMMANDS: SlashCommand[] = [
9393
label: 'publish',
9494
description: 'Publish agents to the agent store',
9595
},
96+
{
97+
id: 'connect:claude',
98+
label: 'connect:claude',
99+
description: 'Connect your Claude Pro/Max subscription',
100+
aliases: ['claude'],
101+
},
96102
...MODE_COMMANDS,
97103
]

0 commit comments

Comments
 (0)