11import React , { useEffect , useState } from 'react'
22
3- import { BottomBanner } from './bottom-banner'
43import { Button } from './button'
54import { useTheme } from '../hooks/use-theme'
6- import { useChatStore } from '../state/chat-store'
75import {
86 connectChatGptOAuth ,
97 disconnectChatGptOAuth ,
108 exchangeChatGptCodeForTokens ,
119 getChatGptOAuthStatus ,
1210 stopChatGptOAuthServer ,
1311} from '../utils/chatgpt-oauth'
12+ import { BORDER_CHARS } from '../utils/ui-constants'
1413
1514type FlowState =
1615 | 'checking'
@@ -20,36 +19,40 @@ type FlowState =
2019 | 'error'
2120
2221export const ChatGptConnectBanner = ( ) => {
23- const setInputMode = useChatStore ( ( state ) => state . setInputMode )
2422 const theme = useTheme ( )
2523 const [ flowState , setFlowState ] = useState < FlowState > ( 'checking' )
2624 const [ error , setError ] = useState < string | null > ( null )
25+ const [ authUrl , setAuthUrl ] = useState < string | null > ( null )
26+ const [ hovered , setHovered ] = useState ( false )
2727
2828 useEffect ( ( ) => {
2929 const status = getChatGptOAuthStatus ( )
30- if ( status . connected ) {
30+ if ( ! status . connected ) {
31+ setFlowState ( 'waiting-for-code' )
32+ const result = connectChatGptOAuth ( )
33+ setAuthUrl ( result . authUrl )
34+ result . credentials
35+ . then ( ( ) => {
36+ setFlowState ( 'connected' )
37+ } )
38+ . catch ( ( err ) => {
39+ setError ( err instanceof Error ? err . message : 'Failed to connect' )
40+ setFlowState ( 'error' )
41+ } )
42+ } else {
3143 setFlowState ( 'connected' )
32- return
3344 }
3445
35- setFlowState ( 'waiting-for-code' )
36- connectChatGptOAuth ( )
37- . then ( ( ) => {
38- setFlowState ( 'connected' )
39- } )
40- . catch ( ( err ) => {
41- setError ( err instanceof Error ? err . message : 'Failed to connect' )
42- setFlowState ( 'error' )
43- } )
44-
4546 return ( ) => {
4647 stopChatGptOAuthServer ( )
4748 }
4849 } , [ ] )
4950
50- const handleConnect = async ( ) => {
51+ const handleConnect = ( ) => {
5152 setFlowState ( 'waiting-for-code' )
52- connectChatGptOAuth ( )
53+ const result = connectChatGptOAuth ( )
54+ setAuthUrl ( result . authUrl )
55+ result . credentials
5356 . then ( ( ) => {
5457 setFlowState ( 'connected' )
5558 } )
@@ -64,67 +67,111 @@ export const ChatGptConnectBanner = () => {
6467 setFlowState ( 'not-connected' )
6568 }
6669
67- const handleClose = ( ) => setInputMode ( 'default' )
70+ const panelStyle = {
71+ width : '100%' as const ,
72+ borderStyle : 'single' as const ,
73+ borderColor : theme . border ,
74+ customBorderChars : BORDER_CHARS ,
75+ paddingLeft : 1 ,
76+ paddingRight : 1 ,
77+ }
6878
69- if ( flowState === 'connected' ) {
70- const status = getChatGptOAuthStatus ( )
71- const connectedDate = status . connectedAt
72- ? new Date ( status . connectedAt ) . toLocaleDateString ( )
73- : 'Unknown'
79+ const actionButtonStyle = {
80+ flexDirection : 'row' as const ,
81+ alignItems : 'center' as const ,
82+ paddingLeft : 1 ,
83+ paddingRight : 1 ,
84+ borderStyle : 'single' as const ,
85+ borderColor : hovered ? theme . foreground : theme . border ,
86+ customBorderChars : BORDER_CHARS ,
87+ }
88+
89+ const escHint = (
90+ < text style = { { fg : theme . muted } } > esc</ text >
91+ )
7492
93+ if ( flowState === 'connected' ) {
7594 return (
76- < BottomBanner borderColorKey = "success" onClose = { handleClose } >
77- < box style = { { flexDirection : 'column' , gap : 0 } } >
78- < text style = { { fg : theme . success } } > ✓ Connected to ChatGPT</ text >
79- < text style = { { fg : theme . muted , marginTop : 1 } } >
80- Streaming requests for supported OpenAI models can now route directly through your ChatGPT subscription.
81- </ text >
82- < box style = { { flexDirection : 'row' , gap : 2 , marginTop : 1 } } >
83- < text style = { { fg : theme . muted } } > Since { connectedDate } </ text >
84- < text style = { { fg : theme . muted } } > ·</ text >
85- < Button onClick = { handleDisconnect } >
86- < text style = { { fg : theme . error } } > Disconnect</ text >
87- </ Button >
88- </ box >
95+ < box style = { { ...panelStyle , flexDirection : 'row' , justifyContent : 'space-between' , alignItems : 'center' } } >
96+ < text style = { { fg : theme . foreground } } > ✓ ChatGPT connected</ text >
97+ < box style = { { flexDirection : 'row' , gap : 1 , alignItems : 'center' } } >
98+ < Button
99+ style = { actionButtonStyle }
100+ onClick = { handleDisconnect }
101+ onMouseOver = { ( ) => setHovered ( true ) }
102+ onMouseOut = { ( ) => setHovered ( false ) }
103+ >
104+ < text wrapMode = "none" >
105+ < span fg = { theme . muted } > Disconnect</ span >
106+ </ text >
107+ </ Button >
108+ { escHint }
89109 </ box >
90- </ BottomBanner >
110+ </ box >
91111 )
92112 }
93113
94114 if ( flowState === 'error' ) {
95115 return (
96- < BottomBanner
97- borderColorKey = "error"
98- text = { `Error: ${ error ?? 'Unknown error' } . Press Escape to close.` }
99- onClose = { handleClose }
100- />
116+ < box style = { { ...panelStyle , flexDirection : 'row' , justifyContent : 'space-between' , alignItems : 'center' } } >
117+ < text style = { { fg : theme . error , flexShrink : 1 } } >
118+ { error ?? 'Unknown error' }
119+ </ text >
120+ < box style = { { flexDirection : 'row' , gap : 1 , alignItems : 'center' } } >
121+ < Button
122+ style = { actionButtonStyle }
123+ onClick = { handleConnect }
124+ onMouseOver = { ( ) => setHovered ( true ) }
125+ onMouseOut = { ( ) => setHovered ( false ) }
126+ >
127+ < text wrapMode = "none" >
128+ < span fg = { theme . foreground } > Retry</ span >
129+ </ text >
130+ </ Button >
131+ { escHint }
132+ </ box >
133+ </ box >
101134 )
102135 }
103136
104137 if ( flowState === 'waiting-for-code' ) {
105138 return (
106- < BottomBanner borderColorKey = "info" onClose = { handleClose } >
107- < box style = { { flexDirection : 'column' , gap : 0 } } >
108- < text style = { { fg : theme . info } } > Waiting for ChatGPT authorization</ text >
109- < text style = { { fg : theme . muted , marginTop : 1 } } >
110- Complete sign-in in your browser — it should connect automatically.
111- If not, paste the callback URL here.
112- </ text >
139+ < box style = { { ...panelStyle , flexDirection : 'column' } } >
140+ < box style = { { flexDirection : 'row' , justifyContent : 'space-between' , alignItems : 'center' } } >
141+ < text style = { { fg : theme . foreground } } > Connecting to ChatGPT...</ text >
142+ { escHint }
113143 </ box >
114- </ BottomBanner >
144+ < text style = { { fg : theme . muted } } >
145+ Sign in via your browser to connect.
146+ </ text >
147+ { authUrl ? (
148+ < text style = { { fg : theme . muted } } >
149+ { authUrl }
150+ </ text >
151+ ) : null }
152+ </ box >
115153 )
116154 }
117155
118- return (
119- < BottomBanner borderColorKey = "info" onClose = { handleClose } >
120- < box style = { { flexDirection : 'column' , gap : 0 } } >
121- < text style = { { fg : theme . info } } > Connect to ChatGPT</ text >
122- < Button onClick = { handleConnect } >
123- < text style = { { fg : theme . link , marginTop : 1 } } > Click to connect →</ text >
156+ if ( flowState === 'not-connected' ) {
157+ return (
158+ < box style = { { ...panelStyle , flexDirection : 'row' , justifyContent : 'space-between' , alignItems : 'center' } } >
159+ < Button
160+ style = { actionButtonStyle }
161+ onClick = { handleConnect }
162+ onMouseOver = { ( ) => setHovered ( true ) }
163+ onMouseOut = { ( ) => setHovered ( false ) }
164+ >
165+ < text wrapMode = "none" >
166+ < span fg = { theme . link } > Connect to ChatGPT</ span >
167+ </ text >
124168 </ Button >
169+ { escHint }
125170 </ box >
126- </ BottomBanner >
127- )
171+ )
172+ }
173+
174+ return null
128175}
129176
130177export async function handleChatGptAuthCode ( code : string ) : Promise < {
0 commit comments