Skip to content

Commit 0bd0e09

Browse files
committed
initial commit
1 parent 5e7fbbf commit 0bd0e09

File tree

18 files changed

+3657
-12
lines changed

18 files changed

+3657
-12
lines changed

cli/src/commands/command-registry.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,16 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
478478
clearInput(params)
479479
},
480480
}),
481+
defineCommand({
482+
name: 'connect:codex',
483+
aliases: ['codex', 'openai'],
484+
handler: (params) => {
485+
// Enter connect:codex mode to show the OAuth banner
486+
useChatStore.getState().setInputMode('connect:codex')
487+
params.saveToHistory(params.inputValue.trim())
488+
clearInput(params)
489+
},
490+
}),
481491
defineCommand({
482492
name: 'history',
483493
aliases: ['chats'],

cli/src/commands/router.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
parseCommandInput,
1717
} from './router-utils'
1818
import { handleClaudeAuthCode } from '../components/claude-connect-banner'
19+
import { handleCodexAuthCode } from '../components/codex-connect-banner'
1920
import { getProjectRoot } from '../project-files'
2021
import { useChatStore } from '../state/chat-store'
2122
import {
@@ -356,6 +357,23 @@ export async function routeUserPrompt(
356357
return
357358
}
358359

360+
// Handle connect:codex mode input (authorization code)
361+
if (inputMode === 'connect:codex') {
362+
const code = trimmed
363+
if (code) {
364+
const result = await handleCodexAuthCode(code)
365+
setMessages((prev) => [
366+
...prev,
367+
getUserMessage(trimmed),
368+
getSystemMessage(result.message),
369+
])
370+
}
371+
saveToHistory(trimmed)
372+
setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
373+
setInputMode('default')
374+
return
375+
}
376+
359377
// Handle referral mode input
360378
if (inputMode === 'referral') {
361379
// Validate the referral code (3-50 alphanumeric chars with optional dashes)
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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+
startOAuthFlowWithCallback,
8+
stopCallbackServer,
9+
exchangeCodeForTokens,
10+
disconnectCodexOAuth,
11+
getCodexOAuthStatus,
12+
} from '../utils/codex-oauth'
13+
import { useTheme } from '../hooks/use-theme'
14+
15+
type FlowState =
16+
| 'checking'
17+
| 'not-connected'
18+
| 'waiting-for-code'
19+
| 'connected'
20+
| 'error'
21+
22+
export const CodexConnectBanner = () => {
23+
const setInputMode = useChatStore((state) => state.setInputMode)
24+
const theme = useTheme()
25+
const [flowState, setFlowState] = useState<FlowState>('checking')
26+
const [error, setError] = useState<string | null>(null)
27+
const [isDisconnectHovered, setIsDisconnectHovered] = useState(false)
28+
const [isConnectHovered, setIsConnectHovered] = useState(false)
29+
30+
// Check initial connection status and auto-open browser if not connected
31+
useEffect(() => {
32+
const status = getCodexOAuthStatus()
33+
if (status.connected) {
34+
setFlowState('connected')
35+
} else {
36+
// Automatically start OAuth flow when not connected
37+
setFlowState('waiting-for-code')
38+
startOAuthFlowWithCallback((callbackStatus, message) => {
39+
if (callbackStatus === 'success') {
40+
setFlowState('connected')
41+
} else if (callbackStatus === 'error') {
42+
setError(message ?? 'Authorization failed')
43+
setFlowState('error')
44+
}
45+
}).catch((err) => {
46+
setError(err instanceof Error ? err.message : 'Failed to start OAuth flow')
47+
setFlowState('error')
48+
})
49+
}
50+
51+
// Cleanup: stop the callback server when the component unmounts
52+
return () => {
53+
stopCallbackServer()
54+
}
55+
}, [])
56+
57+
const handleConnect = async () => {
58+
try {
59+
setFlowState('waiting-for-code')
60+
await startOAuthFlowWithCallback((callbackStatus, message) => {
61+
if (callbackStatus === 'success') {
62+
setFlowState('connected')
63+
} else if (callbackStatus === 'error') {
64+
setError(message ?? 'Authorization failed')
65+
setFlowState('error')
66+
}
67+
})
68+
} catch (err) {
69+
setError(err instanceof Error ? err.message : 'Failed to start OAuth flow')
70+
setFlowState('error')
71+
}
72+
}
73+
74+
const handleDisconnect = () => {
75+
disconnectCodexOAuth()
76+
setFlowState('not-connected')
77+
}
78+
79+
const handleClose = () => {
80+
setInputMode('default')
81+
}
82+
83+
// Connected state
84+
if (flowState === 'connected') {
85+
const status = getCodexOAuthStatus()
86+
const connectedDate = status.connectedAt
87+
? new Date(status.connectedAt).toLocaleDateString()
88+
: 'Unknown'
89+
90+
return (
91+
<BottomBanner borderColorKey="success" onClose={handleClose}>
92+
<box style={{ flexDirection: 'column', gap: 0, flexGrow: 1 }}>
93+
<text style={{ fg: theme.success }}>✓ Connected to Codex</text>
94+
<box style={{ flexDirection: 'row', gap: 2, marginTop: 1 }}>
95+
<text style={{ fg: theme.muted }}>Since {connectedDate}</text>
96+
<text style={{ fg: theme.muted }}>·</text>
97+
<Button
98+
onClick={handleDisconnect}
99+
onMouseOver={() => setIsDisconnectHovered(true)}
100+
onMouseOut={() => setIsDisconnectHovered(false)}
101+
>
102+
<text
103+
style={{ fg: isDisconnectHovered ? theme.error : theme.muted }}
104+
>
105+
Disconnect
106+
</text>
107+
</Button>
108+
</box>
109+
</box>
110+
</BottomBanner>
111+
)
112+
}
113+
114+
// Error state
115+
if (flowState === 'error') {
116+
return (
117+
<BottomBanner
118+
borderColorKey="error"
119+
text={`Error: ${error}. Press Escape to close.`}
120+
onClose={handleClose}
121+
/>
122+
)
123+
}
124+
125+
// Waiting for code state
126+
if (flowState === 'waiting-for-code') {
127+
return (
128+
<BottomBanner borderColorKey="info" onClose={handleClose}>
129+
<box style={{ flexDirection: 'column', gap: 0, flexGrow: 1 }}>
130+
<text style={{ fg: theme.info }}>Waiting for authorization</text>
131+
<text style={{ fg: theme.muted, marginTop: 1 }}>
132+
Sign in with your OpenAI account in the browser. The authorization
133+
will complete automatically.
134+
</text>
135+
</box>
136+
</BottomBanner>
137+
)
138+
}
139+
140+
// Not connected / checking state - show connect button
141+
return (
142+
<BottomBanner borderColorKey="info" onClose={handleClose}>
143+
<box style={{ flexDirection: 'column', gap: 0, flexGrow: 1 }}>
144+
<text style={{ fg: theme.info }}>Connect to Codex</text>
145+
<box style={{ flexDirection: 'row', gap: 2, marginTop: 1 }}>
146+
<text style={{ fg: theme.muted }}>Use your ChatGPT Plus/Pro subscription</text>
147+
<text style={{ fg: theme.muted }}>·</text>
148+
<Button
149+
onClick={handleConnect}
150+
onMouseOver={() => setIsConnectHovered(true)}
151+
onMouseOut={() => setIsConnectHovered(false)}
152+
>
153+
<text style={{ fg: isConnectHovered ? theme.success : theme.link }}>
154+
Click to connect →
155+
</text>
156+
</Button>
157+
</box>
158+
</box>
159+
</BottomBanner>
160+
)
161+
}
162+
163+
/**
164+
* Handle the authorization code input from the user.
165+
* This is called when the user pastes their code in connect:codex mode.
166+
*/
167+
export async function handleCodexAuthCode(code: string): Promise<{
168+
success: boolean
169+
message: string
170+
}> {
171+
try {
172+
await exchangeCodeForTokens(code)
173+
return {
174+
success: true,
175+
message:
176+
'Successfully connected your Codex subscription! Codebuff will now use it for OpenAI model requests.',
177+
}
178+
} catch (err) {
179+
return {
180+
success: false,
181+
message:
182+
err instanceof Error
183+
? err.message
184+
: 'Failed to exchange authorization code',
185+
}
186+
}
187+
}

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

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

33
import { ClaudeConnectBanner } from './claude-connect-banner'
4+
import { CodexConnectBanner } from './codex-connect-banner'
45
import { HelpBanner } from './help-banner'
56
import { PendingAttachmentsBanner } from './pending-attachments-banner'
67
import { ReferralBanner } from './referral-banner'
@@ -26,6 +27,7 @@ const BANNER_REGISTRY: Record<
2627
referral: () => <ReferralBanner />,
2728
help: () => <HelpBanner />,
2829
'connect:claude': () => <ClaudeConnectBanner />,
30+
'connect:codex': () => <CodexConnectBanner />,
2931
}
3032

3133
/**

cli/src/data/slash-commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export const SLASH_COMMANDS: SlashCommand[] = [
4040
description: 'Connect your Claude Pro/Max subscription',
4141
aliases: ['claude'],
4242
},
43+
{
44+
id: 'connect:codex',
45+
label: 'connect:codex',
46+
description: 'Connect your ChatGPT Plus/Pro subscription',
47+
aliases: ['codex', 'openai'],
48+
},
4349
{
4450
id: 'ads:enable',
4551
label: 'ads:enable',

0 commit comments

Comments
 (0)