@@ -210,6 +210,8 @@ const PROVIDERS = [
210210 } ,
211211] as const ;
212212
213+ const CHATGPT_OAUTH_DEFAULT_MODEL = "openai-chatgpt/gpt-5.3-codex" ;
214+
213215export function Settings ( ) {
214216 const queryClient = useQueryClient ( ) ;
215217 const navigate = useNavigate ( ) ;
@@ -236,6 +238,11 @@ export function Settings() {
236238 message : string ;
237239 sample ?: string | null ;
238240 } | null > ( null ) ;
241+ const [ isPollingOpenAiBrowserOAuth , setIsPollingOpenAiBrowserOAuth ] = useState ( false ) ;
242+ const [ openAiBrowserOAuthMessage , setOpenAiBrowserOAuthMessage ] = useState < {
243+ text : string ;
244+ type : "success" | "error" ;
245+ } | null > ( null ) ;
239246 const [ message , setMessage ] = useState < {
240247 text : string ;
241248 type : "success" | "error" ;
@@ -287,6 +294,9 @@ export function Settings() {
287294 mutationFn : ( { provider, apiKey, model } : { provider : string ; apiKey : string ; model : string } ) =>
288295 api . testProviderModel ( provider , apiKey , model ) ,
289296 } ) ;
297+ const startOpenAiBrowserOAuthMutation = useMutation ( {
298+ mutationFn : ( params : { model : string } ) => api . startOpenAiOAuthBrowser ( params ) ,
299+ } ) ;
290300
291301 const removeMutation = useMutation ( {
292302 mutationFn : ( provider : string ) => api . removeProvider ( provider ) ,
@@ -347,6 +357,79 @@ export function Settings() {
347357 } ) ;
348358 } ;
349359
360+ const monitorOpenAiBrowserOAuth = async ( stateToken : string , popup : Window | null ) => {
361+ setIsPollingOpenAiBrowserOAuth ( true ) ;
362+ setOpenAiBrowserOAuthMessage ( null ) ;
363+ try {
364+ for ( let attempt = 0 ; attempt < 180 ; attempt += 1 ) {
365+ const status = await api . openAiOAuthBrowserStatus ( stateToken ) ;
366+ if ( status . done ) {
367+ if ( status . success ) {
368+ setOpenAiBrowserOAuthMessage ( {
369+ text : status . message || "ChatGPT OAuth configured." ,
370+ type : "success" ,
371+ } ) ;
372+ queryClient . invalidateQueries ( { queryKey : [ "providers" ] } ) ;
373+ setTimeout ( ( ) => {
374+ queryClient . invalidateQueries ( { queryKey : [ "agents" ] } ) ;
375+ queryClient . invalidateQueries ( { queryKey : [ "overview" ] } ) ;
376+ } , 3000 ) ;
377+ } else {
378+ setOpenAiBrowserOAuthMessage ( {
379+ text : status . message || "Browser sign-in failed." ,
380+ type : "error" ,
381+ } ) ;
382+ }
383+ return ;
384+ }
385+ await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
386+ }
387+ setOpenAiBrowserOAuthMessage ( {
388+ text : "Browser sign-in timed out. Please try again." ,
389+ type : "error" ,
390+ } ) ;
391+ } catch ( error : any ) {
392+ setOpenAiBrowserOAuthMessage ( {
393+ text : `Failed to verify browser sign-in: ${ error . message } ` ,
394+ type : "error" ,
395+ } ) ;
396+ } finally {
397+ setIsPollingOpenAiBrowserOAuth ( false ) ;
398+ if ( popup && ! popup . closed ) {
399+ popup . close ( ) ;
400+ }
401+ }
402+ } ;
403+
404+ const handleStartChatGptOAuth = async ( ) => {
405+ setOpenAiBrowserOAuthMessage ( null ) ;
406+ try {
407+ const result = await startOpenAiBrowserOAuthMutation . mutateAsync ( {
408+ model : CHATGPT_OAUTH_DEFAULT_MODEL ,
409+ } ) ;
410+ if ( ! result . success || ! result . authorization_url || ! result . state ) {
411+ setOpenAiBrowserOAuthMessage ( {
412+ text : result . message || "Failed to start browser sign-in" ,
413+ type : "error" ,
414+ } ) ;
415+ return ;
416+ }
417+
418+ const popup = window . open (
419+ result . authorization_url ,
420+ "spacebot-openai-oauth" ,
421+ "popup=true,width=560,height=780,noopener,noreferrer" ,
422+ ) ;
423+ setOpenAiBrowserOAuthMessage ( {
424+ text : "Complete sign-in in the browser window. Waiting for callback..." ,
425+ type : "success" ,
426+ } ) ;
427+ void monitorOpenAiBrowserOAuth ( result . state , popup ) ;
428+ } catch ( error : any ) {
429+ setOpenAiBrowserOAuthMessage ( { text : `Failed: ${ error . message } ` , type : "error" } ) ;
430+ }
431+ } ;
432+
350433 const handleClose = ( ) => {
351434 setEditingProvider ( null ) ;
352435 setKeyInput ( "" ) ;
@@ -419,24 +502,36 @@ export function Settings() {
419502 ) : (
420503 < div className = "flex flex-col gap-3" >
421504 { PROVIDERS . map ( ( provider ) => (
422- < ProviderCard
423- key = { provider . id }
424- provider = { provider . id }
425- name = { provider . name }
426- description = { provider . description }
427- configured = { isConfigured ( provider . id ) }
428- defaultModel = { provider . defaultModel }
429- onEdit = { ( ) => {
430- setEditingProvider ( provider . id ) ;
431- setKeyInput ( "" ) ;
432- setModelInput ( provider . defaultModel ?? "" ) ;
433- setTestedSignature ( null ) ;
434- setTestResult ( null ) ;
435- setMessage ( null ) ;
436- } }
437- onRemove = { ( ) => removeMutation . mutate ( provider . id ) }
438- removing = { removeMutation . isPending }
439- />
505+ [
506+ < ProviderCard
507+ key = { provider . id }
508+ provider = { provider . id }
509+ name = { provider . name }
510+ description = { provider . description }
511+ configured = { isConfigured ( provider . id ) }
512+ defaultModel = { provider . defaultModel }
513+ onEdit = { ( ) => {
514+ setEditingProvider ( provider . id ) ;
515+ setKeyInput ( "" ) ;
516+ setModelInput ( provider . defaultModel ?? "" ) ;
517+ setTestedSignature ( null ) ;
518+ setTestResult ( null ) ;
519+ setMessage ( null ) ;
520+ } }
521+ onRemove = { ( ) => removeMutation . mutate ( provider . id ) }
522+ removing = { removeMutation . isPending }
523+ /> ,
524+ provider . id === "openai" ? (
525+ < ChatGptOAuthCard
526+ key = "openai-chatgpt"
527+ configured = { isConfigured ( "openai-chatgpt" ) }
528+ defaultModel = { CHATGPT_OAUTH_DEFAULT_MODEL }
529+ isPolling = { isPollingOpenAiBrowserOAuth }
530+ message = { openAiBrowserOAuthMessage }
531+ onSignIn = { handleStartChatGptOAuth }
532+ />
533+ ) : null ,
534+ ]
440535 ) ) }
441536 </ div >
442537 ) }
@@ -483,6 +578,8 @@ export function Settings() {
483578 < DialogDescription >
484579 { editingProvider === "ollama"
485580 ? `Enter your ${ editingProviderData ?. name } base URL. It will be saved to your instance config.`
581+ : editingProvider === "openai"
582+ ? "Enter an OpenAI API key. The model below will be applied to routing."
486583 : `Enter your ${ editingProviderData ?. name } API key. It will be saved to your instance config.` }
487584 </ DialogDescription >
488585 </ DialogHeader >
@@ -1470,8 +1567,9 @@ function ProviderCard({ provider, name, description, configured, defaultModel, o
14701567 < div className = "flex items-center gap-2" >
14711568 < span className = "text-sm font-medium text-ink" > { name } </ span >
14721569 { configured && (
1473- < span className = "text-tiny text-green-400" >
1474- ● Configured
1570+ < span className = "inline-flex items-center" >
1571+ < span className = "h-2 w-2 rounded-full bg-green-400" aria-hidden = "true" />
1572+ < span className = "sr-only" > Configured</ span >
14751573 </ span >
14761574 ) }
14771575 </ div >
@@ -1494,3 +1592,54 @@ function ProviderCard({ provider, name, description, configured, defaultModel, o
14941592 </ div >
14951593 ) ;
14961594}
1595+
1596+ interface ChatGptOAuthCardProps {
1597+ configured : boolean ;
1598+ defaultModel : string ;
1599+ isPolling : boolean ;
1600+ message : { text : string ; type : "success" | "error" } | null ;
1601+ onSignIn : ( ) => void ;
1602+ }
1603+
1604+ function ChatGptOAuthCard ( { configured, defaultModel, isPolling, message, onSignIn } : ChatGptOAuthCardProps ) {
1605+ return (
1606+ < div className = "rounded-lg border border-app-line bg-app-box p-4" >
1607+ < div className = "flex items-center gap-3" >
1608+ < ProviderIcon provider = "openai-chatgpt" size = { 32 } />
1609+ < div className = "flex-1" >
1610+ < div className = "flex items-center gap-2" >
1611+ < span className = "text-sm font-medium text-ink" > ChatGPT Plus (OAuth)</ span >
1612+ { configured && (
1613+ < span className = "inline-flex items-center" >
1614+ < span className = "h-2 w-2 rounded-full bg-green-400" aria-hidden = "true" />
1615+ < span className = "sr-only" > Configured</ span >
1616+ </ span >
1617+ ) }
1618+ </ div >
1619+ < p className = "mt-0.5 text-sm text-ink-dull" >
1620+ Sign in with your ChatGPT Plus account in the browser.
1621+ </ p >
1622+ < p className = "mt-1 text-tiny text-ink-faint" >
1623+ Default model: < span className = "text-ink-dull" > { defaultModel } </ span >
1624+ </ p >
1625+ { message && (
1626+ < p className = { `mt-1 text-tiny ${ message . type === "success" ? "text-green-400" : "text-red-400" } ` } >
1627+ { message . text }
1628+ </ p >
1629+ ) }
1630+ </ div >
1631+ < div className = "flex gap-2" >
1632+ < Button
1633+ onClick = { onSignIn }
1634+ disabled = { isPolling }
1635+ loading = { isPolling }
1636+ variant = "outline"
1637+ size = "sm"
1638+ >
1639+ Sign in with ChatGPT Plus
1640+ </ Button >
1641+ </ div >
1642+ </ div >
1643+ </ div >
1644+ ) ;
1645+ }
0 commit comments